├── tlint.json ├── ecs.php ├── rector.php ├── src ├── PassBuilder.php ├── LaravelPassesServiceProvider.php ├── Domains │ ├── AppleDomain.php │ └── GoogleDomain.php └── Google │ └── GoogleClient.php ├── CHANGELOG.md ├── .php-cs-fixer.dist.php ├── grumphp.yml ├── LICENSE.md ├── composer.json ├── README.md └── config └── passes.php /tlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "Chiiya\\LaravelCodeStyle\\TLintPreset" 3 | } 4 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | import(CodeStyle::ECS); 8 | $config->paths([ 9 | __DIR__.'/src', 10 | __DIR__.'/config', 11 | ]); 12 | }; 13 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 8 | __DIR__.'/src', 9 | __DIR__.'/config', 10 | ]); 11 | $config->importNames(); 12 | $config->import(CodeStyle::RECTOR); 13 | }; 14 | -------------------------------------------------------------------------------- /src/PassBuilder.php: -------------------------------------------------------------------------------- 1 | apple; 18 | } 19 | 20 | public function google(): GoogleDomain 21 | { 22 | return $this->google; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-passes` will be documented in this file. 4 | 5 | ## v1.1.0 - 2025-02-20 6 | 7 | ### What's Changed 8 | 9 | * Added Laravel 12 compatibility 10 | * Bump dependabot/fetch-metadata from 1.6.0 to 2.2.0 by @dependabot in https://github.com/chiiya/laravel-passes/pull/27 11 | * Bump ramsey/composer-install from 2 to 3 by @dependabot in https://github.com/chiiya/laravel-passes/pull/26 12 | 13 | **Full Changelog**: https://github.com/chiiya/laravel-passes/compare/1.0.2...1.1.0 14 | 15 | ## 1.0.0 - 202X-XX-XX 16 | 17 | - initial release 18 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setFinder(Finder::create()->in(__DIR__.'/src')) 11 | ->setRules([ 12 | '@Chiiya' => true, 13 | '@Chiiya:risky' => true, 14 | CommentedOutFunctionFixer::name() => [ 15 | 'functions' => ['dd', 'dump', 'ini_set', 'print_r', 'var_dump', 'var_export'], 16 | ], 17 | ]) 18 | ->setRiskyAllowed(true); 19 | -------------------------------------------------------------------------------- /grumphp.yml: -------------------------------------------------------------------------------- 1 | grumphp: 2 | hooks_dir: ~ 3 | hooks_preset: local 4 | stop_on_failure: false 5 | ignore_unstaged_changes: false 6 | hide_circumvention_tip: false 7 | process_timeout: 60 8 | ascii: 9 | failed: failed.txt 10 | succeeded: succeeded.txt 11 | parallel: 12 | enabled: false 13 | max_workers: 32 14 | fixer: 15 | enabled: true 16 | fix_by_default: true 17 | tasks: 18 | composer: ~ 19 | phpcsfixer: 20 | config: '.php-cs-fixer.dist.php' 21 | ecs: ~ 22 | rector: ~ 23 | tlint: ~ 24 | extensions: 25 | - Chiiya\LaravelCodeStyle\GrumPHPExtensionLoader 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) chiiya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/LaravelPassesServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-passes') 18 | ->hasConfigFile(); 19 | } 20 | 21 | public function packageRegistered(): void 22 | { 23 | $this->app->bind(ClientInterface::class, GoogleClient::class); 24 | $this->app->bind(PassFactory::class, fn () => new PassFactory([ 25 | 'temp_dir' => config('passes.apple.temp_dir'), 26 | 'output' => config('passes.apple.temp_dir'), 27 | 'certificate' => config('passes.apple.certificate'), 28 | 'password' => config('passes.apple.password'), 29 | 'wwdr' => config('passes.apple.wwdr'), 30 | ])); 31 | } 32 | 33 | public function bootingPackage(): void 34 | { 35 | Http::macro('isFaking', fn () => $this->recording); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chiiya/laravel-passes", 3 | "description": "Laravel library for creating iOS and Android Wallet Passes", 4 | "keywords": [ 5 | "chiiya", 6 | "laravel", 7 | "passes", 8 | "wallet", 9 | "apple", 10 | "google", 11 | "android", 12 | "ios" 13 | ], 14 | "homepage": "https://github.com/chiiya/laravel-passes", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Elisha Witte", 19 | "email": "github@chiiya.moe", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": { 24 | "php": "^8.2", 25 | "chiiya/passes": "^1.2", 26 | "illuminate/contracts": "^9.0|^10.0|^11.0|^12.0", 27 | "spatie/laravel-package-tools": "^1.11" 28 | }, 29 | "require-dev": { 30 | "chiiya/laravel-code-style": "^3.0", 31 | "nunomaduro/collision": "^6.1|^7.0|^8.0", 32 | "orchestra/testbench": "^7.1|^8.0|^9.0|^10.0", 33 | "phpunit/phpunit": "^9.5|^10.0|^11.0" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Chiiya\\LaravelPasses\\": "src" 38 | } 39 | }, 40 | "config": { 41 | "sort-packages": true, 42 | "allow-plugins": { 43 | "dealerdirect/phpcodesniffer-composer-installer": true, 44 | "phpro/grumphp": true 45 | } 46 | }, 47 | "extra": { 48 | "laravel": { 49 | "providers": [ 50 | "Chiiya\\LaravelPasses\\LaravelPassesServiceProvider" 51 | ] 52 | } 53 | }, 54 | "minimum-stability": "dev", 55 | "prefer-stable": true 56 | } 57 | -------------------------------------------------------------------------------- /src/Domains/AppleDomain.php: -------------------------------------------------------------------------------- 1 | exists($path); 24 | } 25 | 26 | /** 27 | * If the given pass file exists, returns storage path. Otherwise 28 | * returns null. 29 | */ 30 | public function location(string $path): ?string 31 | { 32 | $path = Str::finish($path, PassFactory::PASS_EXTENSION); 33 | 34 | if (! $this->exists($path)) { 35 | return null; 36 | } 37 | 38 | return $path; 39 | } 40 | 41 | /** 42 | * Create new pass file and store it in the configured storage. 43 | * Returns storage path including file extension. 44 | */ 45 | public function create(Pass $pass, ?string $path = null): string 46 | { 47 | $path = Str::finish($path ?? $pass->serialNumber, PassFactory::PASS_EXTENSION); 48 | $file = $this->factory->create($pass, basename($path, PassFactory::PASS_EXTENSION)); 49 | 50 | // Move file to storage disk 51 | $handle = fopen($file->getRealPath(), 'r'); 52 | Storage::disk(config('passes.apple.disk'))->writeStream($path, $handle); 53 | fclose($handle); 54 | unlink($file->getRealPath()); 55 | 56 | return $path; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Google/GoogleClient.php: -------------------------------------------------------------------------------- 1 | evaluate($this->getClient()->get($url)); 24 | } 25 | 26 | /** 27 | * Create a resource. 28 | * 29 | * @throws RequestException 30 | */ 31 | public function post(string $url, JsonSerializable $data): array 32 | { 33 | return $this->evaluate($this->getClient()->post($url, $data->jsonSerialize())); 34 | } 35 | 36 | /** 37 | * Update a resource. 38 | * 39 | * @throws RequestException 40 | */ 41 | public function put(string $url, JsonSerializable $data): array 42 | { 43 | return $this->evaluate($this->getClient()->put($url, $data->jsonSerialize())); 44 | } 45 | 46 | /** 47 | * Check for errors and return JSON decoded response. 48 | * 49 | * @throws RequestException 50 | */ 51 | protected function evaluate(Response $response): array 52 | { 53 | $response->throw(); 54 | 55 | return $response->json(); 56 | } 57 | 58 | /** 59 | * Get the configured base client. 60 | */ 61 | protected function getClient(): PendingRequest 62 | { 63 | $client = Http::withHeaders([ 64 | 'Accept' => 'application/json', 65 | 'Content-type' => 'application/json', 66 | ]); 67 | 68 | if (! Http::isFaking()) { 69 | $credentials = ServiceCredentials::parse(config('passes.google.credentials')); 70 | $client->withMiddleware(GoogleAuthMiddleware::createAuthTokenMiddleware($credentials)); 71 | $client->withOptions([ 72 | 'auth' => 'google_auth', 73 | ]); 74 | } 75 | 76 | return $client; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Passes 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/chiiya/laravel-passes.svg?style=flat-square)](https://packagist.org/packages/chiiya/laravel-passes) 4 | [![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/chiiya/laravel-passes/lint?label=code%20style)](https://github.com/chiiya/laravel-passes/actions?query=workflow%3Alint+branch%3Amaster) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/chiiya/laravel-passes.svg?style=flat-square)](https://packagist.org/packages/chiiya/laravel-passes) 6 | 7 | Laravel package for creating iOS and Android Wallet Passes. 8 | 9 | ## Installation 10 | 11 | You can install the package via composer: 12 | 13 | ```bash 14 | composer require chiiya/laravel-passes 15 | ``` 16 | 17 | Publish the configuration files with: 18 | 19 | ```bash 20 | php artisan vendor:publish --tag="passes-config" 21 | ``` 22 | 23 | ## Usage 24 | 25 | This package is a thin wrapper around [chiiya/passes](https://github.com/chiiya/passes), that allows you to directly 26 | inject the Google repositories or Apple `PassFactory` in your application: 27 | 28 | ```php 29 | public function __construct( 30 | private OfferClassRepository $offers, 31 | private PassFactory $apple, 32 | ) 33 | 34 | public function handle(): void 35 | { 36 | $this->apple->create(...); 37 | $this->offers->get(...); 38 | } 39 | ``` 40 | 41 | You may also use the `PassBuilder` class, which is an entry point to all pass building functionalities and contains 42 | a helper method for creating a signed Google JWT: 43 | 44 | ```php 45 | use Chiiya\LaravelPasses\PassBuilder; 46 | 47 | public function __construct( 48 | private PassBuilder $builder, 49 | ) 50 | 51 | public function handle(): void 52 | { 53 | $this->builder->apple()->create(...); 54 | $this->builder->google()->offerClasses()->create(...); 55 | $this->builder->google()->createJWT()->addOfferObject(...)->sign(); 56 | } 57 | ``` 58 | 59 | For documentation on method signatures, check out [chiiya/passes](https://github.com/chiiya/passes). 60 | 61 | ## Testing 62 | 63 | Since this package uses the Laravel HTTP Client under the hood to perform API requests, 64 | you may simply call `Http::fake()` to fake responses in your tests. For mocking specific responses, 65 | check out the [example responses](https://github.com/chiiya/passes/tree/master/tests/Google/Fixtures/responses). 66 | 67 | ## Changelog 68 | 69 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 70 | 71 | ## Contributing 72 | 73 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 74 | 75 | ## License 76 | 77 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 78 | -------------------------------------------------------------------------------- /config/passes.php: -------------------------------------------------------------------------------- 1 | [ 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Certificate Path 8 | |-------------------------------------------------------------------------- 9 | | Path to the .p12 Apple pass type certificate. 10 | | Example: storage_path('app/credentials/certificate.p12'). 11 | | See https://github.com/chiiya/passes/documentation/requirements.md 12 | */ 13 | 'certificate' => env('PASSES_APPLE_CERT'), 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | WWDR Path 18 | |-------------------------------------------------------------------------- 19 | | Path to the WWDR intermediate certificate. 20 | | Example: storage_path('app/credentials/wwdr.pem'). 21 | | See https://www.apple.com/certificateauthority/ 22 | */ 23 | 'wwdr' => env('PASSES_APPLE_WWDR'), 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Certificate Password 28 | |-------------------------------------------------------------------------- 29 | | Password for the .p12 Apple pass type certificate. 30 | | See https://github.com/chiiya/passes/documentation/requirements.md 31 | */ 32 | 'password' => env('PASSES_APPLE_PASSWORD'), 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Storage Disk 37 | |-------------------------------------------------------------------------- 38 | | Disk where the generated .pkpass file for Apple Wallet is stored. You may 39 | | use any disk configured under filesystems.disks in your Laravel config. 40 | */ 41 | 'disk' => env('MEDIA_DISK', 'public'), 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Location of a temporary directory 46 | |-------------------------------------------------------------------------- 47 | | The directory specified must be writeable by the webserver process. 48 | | The temporary directory is required to build the pass bundle. 49 | */ 50 | 'temp_dir' => sys_get_temp_dir(), 51 | ], 52 | 53 | 'google' => [ 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Service Credentials Path 57 | |-------------------------------------------------------------------------- 58 | | Path to the service account credentials JSON file. 59 | | See https://github.com/chiiya/passes/documentation/requirements.md 60 | */ 61 | 'credentials' => env('PASSES_GOOGLE_CREDENTIALS'), 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | Origins 66 | |-------------------------------------------------------------------------- 67 | | Valid domains for the Save to Wallet button. 68 | */ 69 | 'origins' => [env('PASSES_GOOGLE_ORIGINS', env('APP_URL'))], 70 | ], 71 | ]; 72 | -------------------------------------------------------------------------------- /src/Domains/GoogleDomain.php: -------------------------------------------------------------------------------- 1 | eventTicketClassRepository instanceof EventTicketClassRepository) { 44 | $this->eventTicketClassRepository = new EventTicketClassRepository($this->client); 45 | } 46 | 47 | return $this->eventTicketClassRepository; 48 | } 49 | 50 | public function eventTicketObjects(): EventTicketObjectRepository 51 | { 52 | if (! $this->eventTicketObjectRepository instanceof EventTicketObjectRepository) { 53 | $this->eventTicketObjectRepository = new EventTicketObjectRepository($this->client); 54 | } 55 | 56 | return $this->eventTicketObjectRepository; 57 | } 58 | 59 | public function flightClasses(): FlightClassRepository 60 | { 61 | if (! $this->flightClassRepository instanceof FlightClassRepository) { 62 | $this->flightClassRepository = new FlightClassRepository($this->client); 63 | } 64 | 65 | return $this->flightClassRepository; 66 | } 67 | 68 | public function flightObjects(): FlightObjectRepository 69 | { 70 | if (! $this->flightObjectRepository instanceof FlightObjectRepository) { 71 | $this->flightObjectRepository = new FlightObjectRepository($this->client); 72 | } 73 | 74 | return $this->flightObjectRepository; 75 | } 76 | 77 | public function giftCardClasses(): GiftCardClassRepository 78 | { 79 | if (! $this->giftCardClassRepository instanceof GiftCardClassRepository) { 80 | $this->giftCardClassRepository = new GiftCardClassRepository($this->client); 81 | } 82 | 83 | return $this->giftCardClassRepository; 84 | } 85 | 86 | public function giftCardObjects(): GiftCardObjectRepository 87 | { 88 | if (! $this->giftCardObjectRepository instanceof GiftCardObjectRepository) { 89 | $this->giftCardObjectRepository = new GiftCardObjectRepository($this->client); 90 | } 91 | 92 | return $this->giftCardObjectRepository; 93 | } 94 | 95 | public function loyaltyClasses(): LoyaltyClassRepository 96 | { 97 | if (! $this->loyaltyClassRepository instanceof LoyaltyClassRepository) { 98 | $this->loyaltyClassRepository = new LoyaltyClassRepository($this->client); 99 | } 100 | 101 | return $this->loyaltyClassRepository; 102 | } 103 | 104 | public function loyaltyObjects(): LoyaltyObjectRepository 105 | { 106 | if (! $this->loyaltyObjectRepository instanceof LoyaltyObjectRepository) { 107 | $this->loyaltyObjectRepository = new LoyaltyObjectRepository($this->client); 108 | } 109 | 110 | return $this->loyaltyObjectRepository; 111 | } 112 | 113 | public function offerClasses(): OfferClassRepository 114 | { 115 | if (! $this->offerClassRepository instanceof OfferClassRepository) { 116 | $this->offerClassRepository = new OfferClassRepository($this->client); 117 | } 118 | 119 | return $this->offerClassRepository; 120 | } 121 | 122 | public function offerObjects(): OfferObjectRepository 123 | { 124 | if (! $this->offerObjectRepository instanceof OfferObjectRepository) { 125 | $this->offerObjectRepository = new OfferObjectRepository($this->client); 126 | } 127 | 128 | return $this->offerObjectRepository; 129 | } 130 | 131 | public function transitClasses(): TransitClassRepository 132 | { 133 | if (! $this->transitClassRepository instanceof TransitClassRepository) { 134 | $this->transitClassRepository = new TransitClassRepository($this->client); 135 | } 136 | 137 | return $this->transitClassRepository; 138 | } 139 | 140 | public function transitObjects(): TransitObjectRepository 141 | { 142 | if (! $this->transitObjectRepository instanceof TransitObjectRepository) { 143 | $this->transitObjectRepository = new TransitObjectRepository($this->client); 144 | } 145 | 146 | return $this->transitObjectRepository; 147 | } 148 | 149 | /** 150 | * Create a new JWT. 151 | */ 152 | public function createJWT(array $payload = []): JWT 153 | { 154 | if (! $this->credentials instanceof ServiceCredentials) { 155 | $this->credentials = ServiceCredentials::parse(config('passes.google.credentials')); 156 | } 157 | 158 | return new JWT( 159 | iss: $this->credentials->client_email, 160 | key: $this->credentials->private_key, 161 | origins: config('passes.google.origins'), 162 | payload: $payload, 163 | ); 164 | } 165 | } 166 | --------------------------------------------------------------------------------