From 139d55ea99a97e1e2dd15393e527a173f9928a38 Mon Sep 17 00:00:00 2001 From: Craig Newbury Date: Mon, 5 Feb 2024 21:05:26 +0000 Subject: [PATCH 1/2] Add support for Wallet Orders --- src/FinanceOrder.php | 114 +++++++++++++++++++++++++++++++++++++++++++ src/PKPass.php | 11 +++-- 2 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 src/FinanceOrder.php diff --git a/src/FinanceOrder.php b/src/FinanceOrder.php new file mode 100644 index 0000000..33dc427 --- /dev/null +++ b/src/FinanceOrder.php @@ -0,0 +1,114 @@ +json); + + // Creates SHA hashes for string files in each project. + foreach ($this->locales as $language => $strings) { + $sha[$language . '.lproj/' . self::FILE_TYPE . '.strings'] = hash(self::HASH_ALGO, $strings); + } + + foreach ($this->files as $name => $path) { + $sha[$name] = hash(self::HASH_ALGO, file_get_contents($path)); + } + + foreach ($this->remote_file_urls as $name => $url) { + $sha[$name] = hash(self::HASH_ALGO, file_get_contents($url)); + } + + foreach ($this->files_content as $name => $content) { + $sha[$name] = hash(self::HASH_ALGO, $content); + } + + return json_encode((object)$sha); + } + + /** + * Creates .pkpass zip archive. + * + * @param string $manifest + * @param string $signature + * @return string + * @throws PKPassException + */ + protected function createZip($manifest, $signature) + { + // Package file in Zip (as .order) + $zip = new ZipArchive(); + $filename = tempnam($this->tempPath, self::FILE_TYPE); + if (!$zip->open($filename, ZipArchive::OVERWRITE)) { + throw new PKPassException('Could not open ' . basename($filename) . ' with ZipArchive extension.'); + } + $zip->addFromString('signature', $signature); + $zip->addFromString('manifest.json', $manifest); + $zip->addFromString(self::PAYLOAD_FILE, $this->json); + + // Add translation dictionary + foreach ($this->locales as $language => $strings) { + if (!$zip->addEmptyDir($language . '.lproj')) { + throw new PKPassException('Could not create ' . $language . '.lproj folder in zip archive.'); + } + $zip->addFromString($language . '.lproj/' . self::FILE_TYPE . '.strings', $strings); + } + + foreach ($this->files as $name => $path) { + $zip->addFile($path, $name); + } + + foreach ($this->remote_file_urls as $name => $url) { + $download_file = file_get_contents($url); + $zip->addFromString($name, $download_file); + } + + foreach ($this->files_content as $name => $content) { + $zip->addFromString($name, $content); + } + + $zip->close(); + + if (!file_exists($filename) || filesize($filename) < 1) { + @unlink($filename); + throw new PKPassException('Error while creating order.order. Check your ZIP extension.'); + } + + $content = file_get_contents($filename); + unlink($filename); + + return $content; + } +} diff --git a/src/PKPass.php b/src/PKPass.php index b8e1e45..f46ade5 100644 --- a/src/PKPass.php +++ b/src/PKPass.php @@ -23,6 +23,9 @@ */ class PKPass { + const FILE_TYPE = 'pass'; + const FILE_EXT = 'pkpass'; + const MIME_TYPE = 'application/vnd.apple.pkpass'; /** * Holds the path to the certificate. * @var string @@ -55,7 +58,7 @@ class PKPass /** * Holds the JSON payload. - * @var object|array + * @var string */ protected $json; @@ -312,7 +315,7 @@ public function create($output = false) // Output pass header('Content-Description: File Transfer'); - header('Content-Type: application/vnd.apple.pkpass'); + header('Content-Type: ' . self::MIME_TYPE); header('Content-Disposition: attachment; filename="' . $this->getName() . '"'); header('Content-Transfer-Encoding: binary'); header('Connection: Keep-Alive'); @@ -332,9 +335,9 @@ public function create($output = false) */ public function getName() { - $name = $this->name ?: 'pass'; + $name = $this->name ?: self::FILE_TYPE; if (!strstr($name, '.')) { - $name .= '.pkpass'; + $name .= '.' . self::FILE_EXT; } return $name; From 8a063754b441d33bcc24f9d2fe51b01bae9ffd1e Mon Sep 17 00:00:00 2001 From: Craig Newbury Date: Tue, 6 Feb 2024 22:29:57 +0000 Subject: [PATCH 2/2] Add unit test as fixture files --- tests/FinanceOrderTest.php | 100 +++++++++++++++++++++++++++ tests/fixtures/order/logo.png | Bin 0 -> 3254 bytes tests/fixtures/order/ws03-xs-red.jpg | Bin 0 -> 7442 bytes 3 files changed, 100 insertions(+) create mode 100644 tests/FinanceOrderTest.php create mode 100644 tests/fixtures/order/logo.png create mode 100644 tests/fixtures/order/ws03-xs-red.jpg diff --git a/tests/FinanceOrderTest.php b/tests/FinanceOrderTest.php new file mode 100644 index 0000000..98296a4 --- /dev/null +++ b/tests/FinanceOrderTest.php @@ -0,0 +1,100 @@ +assertIsString($orer); + $this->assertGreaterThan(100, strlen($orer)); + $this->assertStringContainsString('logo.png', $orer); + $this->assertStringContainsString('ws03-xs-red.jpg', $orer); + $this->assertStringContainsString('manifest.json', $orer); + + // try to read the ZIP file + $temp_name = tempnam(sys_get_temp_dir(), 'pkpass'); + file_put_contents($temp_name, $orer); + $zip = new ZipArchive(); + $res = $zip->open($temp_name); + $this->assertTrue($res, 'Invalid ZIP file.'); + $this->assertEquals(count($expected_files), $zip->numFiles); + + // extract zip to temp dir + $temp_dir = $temp_name . '_dir'; + mkdir($temp_dir); + $zip->extractTo($temp_dir); + $zip->close(); + echo $temp_dir; + foreach ($expected_files as $file) { + $this->assertFileExists($temp_dir . DIRECTORY_SEPARATOR . $file); + } + } + + public function testBasicGeneration() + { + $pass = new FinanceOrder(__DIR__ . '/fixtures/example-certificate.p12', 'password'); + $pass->setData([ + "createdAt" => "2024-02-01T19:45:50+00:00", + "merchant" => [ + "displayName" => "Luma", + "merchantIdentifier" => "merchant.com.pkpass.unit-test", + "url" => "https://demo-store.test/", + 'logo' => 'logo.png', + ], + "orderIdentifier" => "1", + "orderManagementURL" => "https://demo-store.test/sales/order/view", + 'orderNumber' => '#000000001', + "orderType" => "ecommerce", + "orderTypeIdentifier" => "order.com.pkpass.unit-test", + 'payment' => [ + 'summaryItems' => [ + [ + 'label' => 'Shipping & Handling', + 'value' => [ + 'amount' => '5.00', + 'currency' => 'USD', + ] + ], + ], + 'total' => [ + 'amount' => '36.39', + 'currency' => 'USB', + ], + 'status' => 'paid' + ], + "status" => "open", + "updatedAt" => "2024-02-01T19:45:50+00:00", + 'customer' => [ + 'emailAddress' => 'roni_cost@example.com', + 'familyName' => 'Veronica', + 'givenName' => 'Costello', + ], + 'lineItems' => [ + [ + 'image' => 'ws03-xs-red.jpg', + 'price' => [ + 'amount' => '31.39', + 'currency' => 'USD', + ], + 'quantity' => 1, + 'title' => 'Iris Workout Top', + 'sku' => 'WS03-XS-Red', + ], + ], + "schemaVersion" => 1, + ]); + $pass->addFile(__DIR__ . '/fixtures/order/logo.png'); + $pass->addFile(__DIR__ . '/fixtures/order/ws03-xs-red.jpg'); + $value = $pass->create(); + $this->validateOrder($value, [ + 'logo.png', + 'ws03-xs-red.jpg', + 'manifest.json', + 'order.json', + 'signature', + ]); + } +} \ No newline at end of file diff --git a/tests/fixtures/order/logo.png b/tests/fixtures/order/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9841d93d13781156435d41b413a0a83a232ccbde GIT binary patch literal 3254 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/order/ws03-xs-red.jpg b/tests/fixtures/order/ws03-xs-red.jpg new file mode 100644 index 0000000000000000000000000000000000000000..972fbe5bfb73a3364527608646ce6b4f382fbe1c GIT binary patch literal 7442 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 C&Rs|V literal 0 HcmV?d00001