laulamanapps/apple-passbook-laravel 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

laulamanapps/apple-passbook-laravel

Composer 安装命令:

composer require laulamanapps/apple-passbook-laravel

包简介

Generate Apple Wallet passes (PassKit) from your Laravel application

README 文档

README

This package provides Laravel integration for the LauLamanApps Apple Passbook Package: generate Apple Wallet passes (.pkpass files) and serve the Apple PassKit Web Service endpoints from your Laravel application.

Requirements

  • PHP 8.1+
  • Laravel 11.x, 12.x or 13.x

Installation

composer require laulamanapps/apple-passbook-laravel

The service provider (LauLamanApps\ApplePassbookLaravel\ApplePassbookServiceProvider) is registered automatically via Laravel package auto-discovery.

Run Tests

composer install
./bin/phpunit

Configuration

Publish the config file:

php artisan vendor:publish --tag=apple-passbook-config

This creates config/apple-passbook.php:

Key Default Description
certificate env('APPLE_PASSBOOK_CERTIFICATE') Path to your .p12 Pass Type ID certificate
certificate_password env('APPLE_PASSBOOK_CERTIFICATE_PASSWORD') Password protecting the .p12 file
apple_wwdr_ca env('APPLE_PASSBOOK_WWDR_CA') (null) Optional path to an Apple WWDR CA .pem; null uses the library default
web_service.enabled env('APPLE_PASSBOOK_WEB_SERVICE_ENABLED', true) Whether the PassKit web service routes are registered
web_service.route_prefix env('APPLE_PASSBOOK_WEB_SERVICE_ROUTE_PREFIX', '/v1') Route prefix for the web service endpoints

Add the ENV variables to your .env file:

###> laulamanapps/apple-passbook-laravel ###
APPLE_PASSBOOK_CERTIFICATE=/path/to/certificates/pass.p12
APPLE_PASSBOOK_CERTIFICATE_PASSWORD=password
###< laulamanapps/apple-passbook-laravel ###

Get Certificate

Head over to the Apple Developer Portal to get yourself a certificate to sign your passbooks with.

Export the certificate and key to a .p12 file using Keychain Access.

Generating a Pass

The package registers LauLamanApps\ApplePassbook\Build\Compiler as a singleton, configured with your certificate. Inject it anywhere:

namespace App\Http\Controllers;

use LauLamanApps\ApplePassbook\Build\Compiler;
use LauLamanApps\ApplePassbook\GenericPassbook;
use LauLamanApps\ApplePassbook\MetaData\Barcode;
use LauLamanApps\ApplePassbook\Style\BarcodeFormat;

final class PassbookController
{
    public function __construct(
        private readonly Compiler $compiler,
    ) {
    }

    public function download()
    {
        $passbook = new GenericPassbook('8j23fm3');
        $passbook->setTeamIdentifier('<TeamId>');
        $passbook->setPassTypeIdentifier('<PassTypeId>');
        $passbook->setOrganizationName('Toy Town');
        $passbook->setDescription('Toy Town Membership');

        $barcode = new Barcode();
        $barcode->setFormat(BarcodeFormat::Pdf417);
        $barcode->setMessage('123456789');
        $passbook->setBarcode($barcode);

        return response($this->compiler->compile($passbook), 200, [
            'Content-Description' => 'File Transfer',
            'Content-Type' => 'application/vnd.apple.pkpass',
            'Content-Disposition' => 'filename="passbook.pkpass"',
        ]);
    }
}

See the core package documentation for all pass types (event ticket, coupon, boarding pass, store card, generic), fields, images, colors and locations.

PassKit Web Service

When web_service.enabled is true the package registers all Apple PassKit Web Service endpoints under the configured prefix (default /v1):

Method URI Purpose
POST /v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber} Register a device for pass updates
DELETE /v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber} Unregister a device
GET /v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier} Serial numbers of updated passes
GET /v1/passes/{passTypeIdentifier}/{serialNumber} Retrieve the latest version of a pass
POST /v1/log Device diagnostic logging

For the web service to work, the pass itself must point at your application:

$passbook->setWebService('https://example.com/', $authenticationToken);

Events

The controllers delegate all persistence decisions to your application through mutable event objects. Each event starts with Status::Unhandled — your listener must set the appropriate status, otherwise a LogicException is thrown.

Event Dispatched when
DeviceRegisteredEvent A device registers for pass updates
DeviceUnregisteredEvent A device unregisters from pass updates
DeviceRequestUpdatedPassesEvent A device requests the serial numbers of updated passes (passesUpdatedSince is available via getPassesUpdatedSince())
RetrieveUpdatedPassbookEvent A device requests an updated pass (If-Modified-Since is available via getUpdatedSince())

The /v1/log endpoint needs no listener: messages are written to the application logger at info level.

Authenticating requests

Devices authenticate with an Authorization: ApplePass <token> header carrying the token you set on the pass with setWebService(). The token-carrying events expose isAuthenticatedBy(string $expectedToken): bool.

Always use $event->isAuthenticatedBy(...) — never compare $event->getAuthenticationToken() with !==. isAuthenticatedBy() uses PHP's hash_equals(), which compares the two tokens in constant time. A plain string comparison short-circuits on the first differing byte, which leaks timing information an attacker can use to reconstruct a valid token byte by byte.

Example listeners

namespace App\Listeners;

use App\Models\Pass;
use DateTimeImmutable;
use LauLamanApps\ApplePassbookLaravel\Events\DeviceRegisteredEvent;

final class RegisterDevice
{
    public function handle(DeviceRegisteredEvent $event): void
    {
        $pass = Pass::where('serial_number', $event->getSerialNumber())->first();

        if ($pass === null || !$event->isAuthenticatedBy($pass->getAuthToken())) {
            $event->notAuthorized();

            return;
        }

        $registration = $pass->registrations()->firstOrCreate(
            ['device_library_identifier' => $event->getDeviceLibraryIdentifier()],
            ['push_token' => $event->getPushToken()],
        );

        if (!$registration->wasRecentlyCreated) {
            $event->alreadyRegistered();

            return;
        }

        $event->deviceRegistered();
    }
}
namespace App\Listeners;

use App\Models\Pass;
use LauLamanApps\ApplePassbookLaravel\Events\DeviceUnregisteredEvent;

final class UnregisterDevice
{
    public function handle(DeviceUnregisteredEvent $event): void
    {
        $pass = Pass::where('serial_number', $event->getSerialNumber())->first();

        if ($pass === null || !$event->isAuthenticatedBy($pass->getAuthToken())) {
            $event->notAuthorized();

            return;
        }

        $pass->registrations()
            ->where('device_library_identifier', $event->getDeviceLibraryIdentifier())
            ->delete();

        $event->deviceUnregistered();
    }
}
namespace App\Listeners;

use App\Models\Pass;
use LauLamanApps\ApplePassbookLaravel\Events\DeviceRequestUpdatedPassesEvent;

final class ListUpdatedPasses
{
    public function handle(DeviceRequestUpdatedPassesEvent $event): void
    {
        $passes = Pass::registeredTo($event->getDeviceLibraryIdentifier())
            ->where('pass_type_identifier', $event->getPassTypeIdentifier())
            ->when($event->getPassesUpdatedSince(), fn ($query, $since) => $query->where('updated_at', '>', $since))
            ->get();

        if ($passes->isEmpty()) {
            $event->notFound();

            return;
        }

        $event->setSerialNumbers(
            $passes->pluck('serial_number')->all(),
            $passes->max('updated_at')->toDateTimeImmutable(),
        );
    }
}
namespace App\Listeners;

use App\Models\Pass;
use LauLamanApps\ApplePassbookLaravel\Events\RetrieveUpdatedPassbookEvent;

final class RetrieveUpdatedPassbook
{
    public function handle(RetrieveUpdatedPassbookEvent $event): void
    {
        $pass = Pass::where('serial_number', $event->getSerialNumber())->first();

        if ($pass === null) {
            $event->notFound();

            return;
        }

        if (!$event->isAuthenticatedBy($pass->getAuthToken())) {
            $event->notAuthorized();

            return;
        }

        $updatedAt = $pass->updated_at->toDateTimeImmutable();

        if ($event->getUpdatedSince() !== null && $updatedAt <= $event->getUpdatedSince()) {
            $event->notModified();

            return;
        }

        $event->setPassbook($pass->toPassbook(), $updatedAt);
    }
}

Since Laravel 11, listeners in app/Listeners are auto-discovered — no manual registration is required. The compiled pass is returned to the device as application/vnd.apple.pkpass with a Last-Modified header, and 304 Not Modified is honored via the If-Modified-Since request header.

Credits

This package has been developed by LauLaman.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 1
  • 依赖项目数: 1
  • 推荐数: 2

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-08

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固