reachweb/laravel-wallet-passes

Laravel package for generating Apple Wallet (.pkpass) and Google Wallet digital passes

Maintainers

Package info

github.com/reachweb/laravel-wallet-passes

pkg:composer/reachweb/laravel-wallet-passes

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.0 2026-04-11 11:35 UTC

This package is auto-updated.

Last update: 2026-04-11 11:56:00 UTC


README

A Laravel package for generating, signing, and managing Apple Wallet (.pkpass) and Google Wallet digital passes behind a single, fluent API.

Requirements

  • PHP 8.3+
  • Laravel 12 or 13
  • ext-openssl (Apple pass signing)
  • ext-zip (Apple .pkpass packaging)

Installation

composer require reach/laravel-wallet-passes

Publish the config file:

php artisan vendor:publish --tag=wallet-passes-config

Publish and run the migrations (required for Apple pass-update web service):

php artisan vendor:publish --tag=wallet-passes-migrations
php artisan migrate

Configuration

Add the following to your .env file. Only configure the providers you plan to use.

Apple Wallet

APPLE_PASS_CERTIFICATE_PATH=/path/to/pass-certificate.p12
APPLE_PASS_CERTIFICATE_PASSWORD=your-certificate-password
APPLE_WWDR_CERTIFICATE_PATH=/path/to/AppleWWDRCAG4.pem
APPLE_PASS_TYPE_IDENTIFIER=pass.com.example.mypass
APPLE_TEAM_IDENTIFIER=ABCDE12345
APPLE_ORGANIZATION_NAME="Your Company"
APPLE_WEB_SERVICE_URL=https://example.com/wallet
APPLE_APNS_ENVIRONMENT=production
Variable Description
APPLE_PASS_CERTIFICATE_PATH Absolute path to the .p12 Pass Type ID certificate exported from Keychain Access
APPLE_PASS_CERTIFICATE_PASSWORD Password used when exporting the .p12 file
APPLE_WWDR_CERTIFICATE_PATH Absolute path to Apple's WWDR G4 intermediate certificate (.pem)
APPLE_PASS_TYPE_IDENTIFIER The Pass Type ID registered in your Apple Developer account (e.g. pass.com.example.mypass)
APPLE_TEAM_IDENTIFIER Your 10-character Apple Developer Team ID
APPLE_ORGANIZATION_NAME Displayed on the pass lock screen
APPLE_WEB_SERVICE_URL Base URL for pass-update callbacks (must match the webServiceURL in your passes)
APPLE_APNS_ENVIRONMENT production or sandbox (defaults to production)

Google Wallet

GOOGLE_WALLET_SERVICE_ACCOUNT_PATH=/path/to/service-account.json
GOOGLE_WALLET_ISSUER_ID=3388000000012345678
GOOGLE_WALLET_APPLICATION_NAME=MyApp
Variable Description
GOOGLE_WALLET_SERVICE_ACCOUNT_PATH Absolute path to the Google Cloud service account JSON key file
GOOGLE_WALLET_ISSUER_ID Your Google Wallet API Issuer ID (found in the Google Pay & Wallet Console)
GOOGLE_WALLET_APPLICATION_NAME Application name sent with API requests (defaults to WalletPasses)

Additional Config

The published config/wallet-passes.php file also supports:

'default' => env('WALLET_PASS_PROVIDER', 'apple'), // 'apple', 'google', or 'both'

'routes' => [
    'enabled' => true,       // Enable Apple web service routes
    'prefix'  => 'wallet',   // URL prefix for all package routes
    'middleware' => [],       // Additional middleware to apply
],

'logging' => [
    'enabled' => env('WALLET_PASS_LOGGING', false),
    'channel' => env('WALLET_PASS_LOG_CHANNEL', 'stack'),
],

Usage

Using the Facade

The WalletPass facade is the main entry point. It returns pre-configured builders for each provider.

use Reach\WalletPasses\Facades\WalletPass;

// Get an Apple builder
$builder = WalletPass::apple();

// Get a Google builder
$builder = WalletPass::google();

// Attach a Passable model (auto-populates builder fields)
$builder = WalletPass::forModel($order)->apple();

Creating an Apple Wallet Pass

use Reach\WalletPasses\Facades\WalletPass;
use Reach\WalletPasses\Apple\Enums\PassStyle;
use Reach\WalletPasses\Apple\Enums\BarcodeFormat;

$pkpass = WalletPass::apple()
    ->setStyle(PassStyle::EventTicket)
    ->setSerialNumber('E-001')
    ->setDescription('Concert Ticket')
    ->setHeader('Live Concert')
    ->setColors(
        background: '#1E3A5F',
        foreground: '#FFFFFF',
        label: '#AABBCC',
    )
    ->setIcon(storage_path('passes/icon.png'))
    ->setLogo(storage_path('passes/logo.png'))
    ->setStrip(storage_path('passes/strip.png'))
    ->addHeaderField('date', 'Date', 'Jun 15, 2025')
    ->addPrimaryField('event', 'Event', 'Summer Music Festival')
    ->addSecondaryField('location', 'Location', 'Main Stage')
    ->addAuxiliaryField('seat', 'Seat', 'GA-042')
    ->addBackField('terms', 'Terms', 'No refunds.')
    ->setBarcode('E-001', BarcodeFormat::QR)
    ->addLocation(37.3349, -122.0090, 'Venue')
    ->setRelevantDate(new DateTime('2025-06-15T18:00:00-07:00'))
    ->setWebServiceUrl(config('wallet-passes.apple.web_service_url'))
    ->setAuthenticationToken('your-auth-token')
    ->generate(); // Returns binary .pkpass content

Returning a download response

// In a controller:
return WalletPass::apple()
    ->setSerialNumber('E-001')
    // ... other fields ...
    ->download('concert-ticket.pkpass');

Creating a Google Wallet Pass

use Reach\WalletPasses\Facades\WalletPass;
use Reach\WalletPasses\Apple\Enums\BarcodeFormat;

// Option 1: Generate a save URL via JWT (no server-side API call)
$saveUrl = WalletPass::google()
    ->setClassId('concert-class')
    ->setClassName('Live Concerts')
    ->setObjectId('ticket-001')
    ->setHeader('Summer Music Festival')
    ->setSubheader('Main Stage')
    ->setHeroImage('https://example.com/hero.png', 'Concert banner')
    ->setLogo('https://example.com/logo.png', 'Logo')
    ->setBackgroundColor('#1E3A5F')
    ->addField('date', 'Date', 'Jun 15, 2025')
    ->addField('seat', 'Seat', 'GA-042')
    ->addLink('https://example.com/event', 'Event Details')
    ->setBarcode('ticket-001', BarcodeFormat::QR)
    ->addLocation(37.3349, -122.0090)
    ->setState('ACTIVE')
    ->setExpiration(new DateTime('2025-06-16T00:00:00Z'))
    ->generateSaveUrl(); // Returns "Add to Google Wallet" URL

// Option 2: Create via REST API first, then generate save URL
$saveUrl = WalletPass::google()
    ->setClassId('concert-class')
    ->setObjectId('ticket-001')
    // ... other fields ...
    ->createViaApi(); // Creates class/object via API, returns save URL

Model Integration with the Passable Interface

For models that represent passes (e.g. orders, tickets, memberships), implement the Passable interface and use the HasWalletPass trait:

use Illuminate\Database\Eloquent\Model;
use Reach\WalletPasses\Contracts\Passable;
use Reach\WalletPasses\Traits\HasWalletPass;
use Reach\WalletPasses\DTOs\PassField;
use Reach\WalletPasses\DTOs\PassLocation;

class Order extends Model implements Passable
{
    use HasWalletPass;

    public function getPassSerialNumber(): string
    {
        return 'ORDER-' . $this->id;
    }

    public function getPassDescription(): string
    {
        return 'Order #' . $this->id;
    }

    public function getPassFields(): array
    {
        return [
            new PassField('item', 'Item', $this->item_name),
            new PassField('qty', 'Quantity', (string) $this->quantity),
        ];
    }

    public function getPassBarcodeMessage(): string
    {
        return 'ORDER-' . $this->id;
    }

    public function getPassLocations(): array
    {
        return [
            new PassLocation(
                latitude: $this->store_lat,
                longitude: $this->store_lng,
                relevantText: 'Pickup location',
            ),
        ];
    }
}

Then use the trait methods:

$order = Order::find(1);

// Generate an Apple .pkpass binary
$pkpass = $order->generateApplePass();

// Return a download response
return $order->getApplePassResponse('order-pass.pkpass');

// Generate a Google Wallet save URL
$url = $order->generateGooglePassUrl();

// Generate a signed download URL (expires in 24 hours)
$downloadUrl = $order->getPassDownloadUrl();

// Notify registered devices of a pass update (Apple)
$notified = $order->notifyPassUpdate();

Apple Wallet Setup Guide

1. Create a Pass Type ID

  1. Go to Apple Developer > Certificates, Identifiers & Profiles
  2. Under Identifiers, click + and select Pass Type IDs
  3. Enter a description and an identifier (e.g. pass.com.yourcompany.mypass)
  4. Register the identifier

2. Create and Export the Pass Certificate

  1. In the Identifiers list, select your Pass Type ID and click Create Certificate
  2. Follow the prompts to create a Certificate Signing Request (CSR) via Keychain Access
  3. Upload the CSR and download the certificate (.cer file)
  4. Double-click the .cer to install it in Keychain Access
  5. In Keychain Access, find the certificate under My Certificates, right-click, and select Export
  6. Save as a .p12 file with a password
  7. Place the .p12 file in a secure location on your server (outside the web root)
  8. Set APPLE_PASS_CERTIFICATE_PATH and APPLE_PASS_CERTIFICATE_PASSWORD in .env

3. Download the WWDR Intermediate Certificate

  1. Download the Apple Worldwide Developer Relations (WWDR) G4 certificate from Apple PKI
  2. Convert to PEM if needed:
    openssl x509 -inform der -in AppleWWDRCAG4.cer -out AppleWWDRCAG4.pem
  3. Place the .pem file alongside your .p12 and set APPLE_WWDR_CERTIFICATE_PATH

4. Find Your Team Identifier

Your 10-character Team ID is shown at the top right of the Apple Developer Membership page. Set it as APPLE_TEAM_IDENTIFIER.

Google Wallet Setup Guide

1. Create a Google Cloud Project

  1. Go to Google Cloud Console
  2. Create a new project or select an existing one
  3. Enable the Google Wallet API under APIs & Services > Library

2. Create a Service Account

  1. Go to IAM & Admin > Service Accounts
  2. Click Create Service Account
  3. Give it a name and grant no specific roles (the Wallet API uses its own permissions)
  4. On the service account detail page, go to Keys > Add Key > Create new key
  5. Select JSON format and download the key file
  6. Place the JSON file in a secure location and set GOOGLE_WALLET_SERVICE_ACCOUNT_PATH

3. Set Up a Wallet API Issuer Account

  1. Go to the Google Pay & Wallet Console
  2. Sign up for the Wallet API and create an Issuer account
  3. Copy your Issuer ID (a numeric string) and set GOOGLE_WALLET_ISSUER_ID
  4. Under Manage > API Access, add your service account email as a user with Developer permissions

4. Demo Mode

New Wallet API issuer accounts start in demo mode. Passes can only be saved by users added as test users in the Google Pay & Wallet Console. Apply for production access once your integration is ready.

Apple Web Service (Pass Updates)

This package implements Apple's web service protocol for pass updates. When enabled, the following endpoints are registered automatically:

Method Endpoint Description
POST {prefix}/v1/devices/{deviceId}/registrations/{passTypeId}/{serial} Register a device for push updates
DELETE {prefix}/v1/devices/{deviceId}/registrations/{passTypeId}/{serial} Unregister a device
GET {prefix}/v1/devices/{deviceId}/registrations/{passTypeId} List serial numbers for a device
GET {prefix}/v1/passes/{passTypeId}/{serial} Get the latest version of a pass
POST {prefix}/v1/log Receive log messages from Apple Wallet
GET {prefix}/download/{serial} Download a pass via signed URL

Implementing the PassRetriever

When Apple requests the latest version of a pass, the package needs to know how to build it. Create a class implementing PassRetriever and register it in your config:

// app/Services/MyPassRetriever.php
use Reach\WalletPasses\Apple\ApplePassBuilder;
use Reach\WalletPasses\Contracts\PassRetriever;
use Reach\WalletPasses\Facades\WalletPass;

class MyPassRetriever implements PassRetriever
{
    public function retrieve(string $passTypeIdentifier, string $serialNumber): ?ApplePassBuilder
    {
        $order = Order::where('serial', $serialNumber)->first();

        if (! $order) {
            return null; // Returns 404 to Apple
        }

        return WalletPass::forModel($order)->apple()
            ->setIcon(storage_path('passes/icon.png'))
            ->setAuthenticationToken($order->pass_token);
    }
}

Register it in config/wallet-passes.php:

'apple' => [
    // ...
    'pass_retrieval_handler' => \App\Services\MyPassRetriever::class,
],

Sending Pass Update Notifications

When a pass's data changes, notify registered devices to fetch the updated version:

use Reach\WalletPasses\Apple\ApplePassUpdater;

$updater = new ApplePassUpdater(config('wallet-passes.apple'));

// Notify devices registered for a specific pass
$count = $updater->notifyDevices('pass.com.example.mypass', 'ORDER-42');

// Notify all devices for a pass type
$count = $updater->notifyAllDevices('pass.com.example.mypass');

Or via the HasWalletPass trait:

$order->notifyPassUpdate();

Events

The package dispatches the following events. Listen for them in your EventServiceProvider or with Event::listen():

Event Properties When
PassCreated provider, serialNumber, passTypeIdentifier A pass is generated
PassUpdated provider, serialNumber, passTypeIdentifier, updateType A pass is updated
PassRetrieved serialNumber, deviceLibraryIdentifier A device fetches the latest pass
DeviceRegistered deviceLibraryIdentifier, serialNumber, pushToken A device registers for updates
DeviceUnregistered deviceLibraryIdentifier, serialNumber A device unregisters

All events live under Reach\WalletPasses\Events\.

use Reach\WalletPasses\Events\DeviceRegistered;

Event::listen(DeviceRegistered::class, function (DeviceRegistered $event) {
    Log::info("Device {$event->deviceLibraryIdentifier} registered for pass {$event->serialNumber}");
});

Exceptions

All package exceptions extend Reach\WalletPasses\Exceptions\WalletPassException, so you can catch every package error with one type:

Exception Thrown When
CertificateNotFoundException .p12 or WWDR certificate file is missing or unreadable
PassSigningException OpenSSL signing fails (bad cert, wrong password, etc.)
InvalidPassDataException Required fields are missing when calling generate() or generateJwt()
GoogleApiException Google Wallet REST API returns an error
use Reach\WalletPasses\Exceptions\WalletPassException;
use Reach\WalletPasses\Exceptions\CertificateNotFoundException;

try {
    $pkpass = WalletPass::apple()->setSerialNumber('001')->generate();
} catch (CertificateNotFoundException $e) {
    // Handle missing certificate
} catch (WalletPassException $e) {
    // Handle any other package error
}

Troubleshooting

Apple Wallet

"Invalid certificate" or signing errors

  • Ensure the .p12 was exported from a Pass Type ID certificate, not a development/distribution certificate
  • Verify the password matches what was set during export
  • Make sure you're using the WWDR G4 certificate (not G1/G2/G3)
  • Check that ext-openssl is installed: php -m | grep openssl

Pass doesn't appear on device

  • The icon image is required. Ensure setIcon() is called with a valid PNG path
  • Serial number, pass type identifier, and team identifier are all required
  • Check that the .pkpass MIME type is application/vnd.apple.pkpass

Pass updates not working

  • Verify webServiceURL in the pass matches your APPLE_WEB_SERVICE_URL
  • The authentication token must be at least 16 characters
  • Ensure routes are enabled in config (wallet-passes.routes.enabled)
  • Check that your PassRetriever implementation is registered in config
  • For sandbox testing, set APPLE_APNS_ENVIRONMENT=sandbox

Google Wallet

"Permission denied" API errors

  • Ensure the service account email has Developer access in the Google Pay & Wallet Console
  • Verify the JSON key file path is correct and readable

"Class not found" errors

  • When using generateSaveUrl(), the class and object are created inline via JWT -- no API call needed
  • When using createViaApi(), the class is created first; ensure your issuer ID is correct

Passes only visible to test users

  • New issuer accounts are in demo mode. Add test user emails in the Google Pay & Wallet Console
  • Apply for production access to make passes available to all users

General

"Class not found" PHP errors

  • Run composer dump-autoload after installation
  • Ensure the service provider is auto-discovered (check composer.json extra.laravel.providers)

Missing migrations

  • Run php artisan vendor:publish --tag=wallet-passes-migrations then php artisan migrate

Testing

The package includes a comprehensive Pest test suite. To run the tests:

php artisan test --compact

Test certificates and Google service account fixtures are generated on the fly -- no real credentials are needed to run the test suite.

License

MIT -- see LICENSE.