Skip to content

Commit 714df81

Browse files
committed
Allow single-vertical Builder
1 parent 3830f5e commit 714df81

22 files changed

Lines changed: 697 additions & 270 deletions

README.md

Lines changed: 7 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,16 @@ Wallet Kit helps you build the **JSON payloads** wallet platforms expect. It foc
1818
- **PHP** 8.5+
1919
- **symfony/serializer** ^8.0
2020

21-
## Dual-platform builder
21+
## 🛠️ Builder
2222

23-
The **`Jolicode\WalletKit\Builder`** namespace provides a **fluent API** that builds **both** an Apple [`Pass`](src/Pass/Apple/Model/Pass.php) and the matching Google Wallet **class + object** in one go. Entry point: [`WalletPass`](src/Builder/WalletPass.php) (`generic`, `offer`, `loyalty`, `eventTicket`, `flight`, `transit`, `giftCard`).
23+
The **`Jolicode\WalletKit\Builder`** namespace exposes a fluent API around [`WalletPass`](src/Builder/WalletPass.php). For **Apple and Google together**, use [`WalletPlatformContext::both(...)`](src/Builder/WalletPlatformContext.php) then `build()`[`BuiltWalletPass`](src/Builder/BuiltWalletPass.php) (`apple()`, `google()`). Normalize the models with Symfony Serializer and this package’s normalizers.
2424

25-
1. Create a [`WalletPlatformContext`](src/Builder/WalletPlatformContext.php) with your Apple identifiers (team, pass type, serial, organization, description) and Google IDs (`classId`, `objectId`).
26-
2. Chain portable options (barcodes, colors, grouping, validity, web service URL, …) via [`CommonWalletBuilderTrait`](src/Builder/CommonWalletBuilderTrait.php).
27-
3. Call **`build()`**[`BuiltWalletPass`](src/Builder/BuiltWalletPass.php): `apple()` for `pass.json`, `google()` for the [`GoogleWalletPair`](src/Builder/GoogleWalletPair.php) (issuer class + holder object).
25+
**Cookbook** (single-store `appleOnly` / `googleOnly`, every vertical, shared options, exceptions): [docs/builder-examples.md](docs/builder-examples.md).
2826

29-
**Full cookbook:** [docs/builder-examples.md](docs/builder-examples.md)**one real-world example per vertical** (generic, offer, loyalty, event ticket, flight, transit, gift card).
30-
31-
### Example A — coupon / offer (both stores)
27+
### Example — dual platform
3228

3329
```php
34-
$context = new WalletPlatformContext(
30+
$context = WalletPlatformContext::both(
3531
appleTeamIdentifier: 'ABCDE12345',
3632
applePassTypeIdentifier: 'pass.com.example.coupon',
3733
appleSerialNumber: 'COUPON-001',
@@ -63,39 +59,6 @@ $built = WalletPass::offer(
6359
// Then normalize with Symfony Serializer + this library’s normalizers.
6460
```
6561

66-
### Example B — flight boarding (both stores)
67-
68-
```php
69-
$flightContext = new WalletPlatformContext(
70-
appleTeamIdentifier: 'ABCDE12345',
71-
applePassTypeIdentifier: 'pass.com.example.boarding',
72-
appleSerialNumber: 'BP-8844',
73-
appleOrganizationName: 'Example Airways',
74-
appleDescription: 'SFO → LAX',
75-
googleClassId: '3388000000012345.example_flight_class',
76-
googleObjectId: '3388000000012345.example_flight_object',
77-
defaultGoogleReviewStatus: ReviewStatusEnum::APPROVED,
78-
defaultGoogleObjectState: StateEnum::ACTIVE,
79-
);
80-
81-
$built = WalletPass::flight(
82-
$flightContext,
83-
passengerName: 'Taylor Lee',
84-
reservationInfo: new ReservationInfo(confirmationCode: 'ABC123'),
85-
flightHeader: new FlightHeader(
86-
carrier: new FlightCarrier(carrierIataCode: 'ZZ'),
87-
flightNumber: '101',
88-
),
89-
origin: new AirportInfo(airportIataCode: 'SFO'),
90-
destination: new AirportInfo(airportIataCode: 'LAX'),
91-
)
92-
->withGrouping('trip-sfo-lax-2026', 0)
93-
->build();
94-
95-
// $built->apple() → boardingPass + PKTransitTypeAir
96-
// $built->google() → FlightClass + FlightObject
97-
```
98-
9962
### 🍏 Apple Wallet
10063

10164
Apple’s model maps to a **single** tree: either use the **builder** above or build a `Pass` manually (see `src/Pass/Apple/`) and normalize it to the structure that becomes **`pass.json`** inside a pass package. Images, manifest, and cryptographic signing are still your responsibility.
@@ -114,7 +77,8 @@ composer require jolicode/wallet-kit
11477

11578
- `Jolicode\WalletKit\Pass\Apple` — Apple Wallet `pass.json` payloads
11679
- `Jolicode\WalletKit\Pass\Android` — Google Wallet class and object payloads
117-
- `Jolicode\WalletKit\Builder` — Fluent dual-platform builders (`WalletPass`, …)
80+
- `Jolicode\WalletKit\Builder` — Fluent builders (`WalletPass`, …) for Apple, Google, or both
81+
- `Jolicode\WalletKit\Exception` — Builder context and `BuiltWalletPass` accessor exceptions
11882

11983
## API spec checks (with Castor)
12084

docs/builder-examples.md

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
# Wallet Kit — Builder examples
22

3-
This page shows one **end-to-end example** per vertical supported by [`WalletPass`](../src/Builder/WalletPass.php). Each example assumes you already configured a [`WalletPlatformContext`](../src/Builder/WalletPlatformContext.php) with your real Apple and Google identifiers.
3+
This page shows one **end-to-end example** per vertical supported by [`WalletPass`](../src/Builder/WalletPass.php). Examples below use a **dual-platform** context; you can also use [`WalletPlatformContext::appleOnly`](../src/Builder/WalletPlatformContext.php) or [`::googleOnly`](../src/Builder/WalletPlatformContext.php) when you only target one store.
44

55
After `build()`, you get a [`BuiltWalletPass`](../src/Builder/BuiltWalletPass.php):
66

7-
- `$built->apple()` → Apple [`Pass`](../src/Pass/Apple/Model/Pass.php) for `pass.json`
8-
- `$built->google()->issuerClass` / `$built->google()->passObject` → Google class and object for the Wallet API
7+
- `$built->apple()` → Apple [`Pass`](../src/Pass/Apple/Model/Pass.php) for `pass.json` (throws [`ApplePassNotAvailableException`](../src/Exception/ApplePassNotAvailableException.php) if the context had no Apple slice)
8+
- `$built->google()->issuerClass` / `$built->google()->passObject` → Google class and object (throws [`GoogleWalletPairNotAvailableException`](../src/Exception/GoogleWalletPairNotAvailableException.php) if there was no Google slice)
99

1010
Serialize with **Symfony Serializer** and the normalizers from this package (see [`tests/Builder/BuilderTestSerializerFactory.php`](../tests/Builder/BuilderTestSerializerFactory.php) for a full list).
1111

1212
---
1313

14-
## Shared context (all examples)
14+
## Shared context (dual-platform examples)
1515

1616
```php
1717
<?php
@@ -22,7 +22,7 @@ use Jolicode\WalletKit\Builder\WalletPlatformContext;
2222
use Jolicode\WalletKit\Pass\Android\Model\Shared\ReviewStatusEnum;
2323
use Jolicode\WalletKit\Pass\Android\Model\Shared\StateEnum;
2424

25-
$context = new WalletPlatformContext(
25+
$context = WalletPlatformContext::both(
2626
appleTeamIdentifier: 'YOUR_TEAM_ID',
2727
applePassTypeIdentifier: 'pass.com.example.app',
2828
appleSerialNumber: 'UNIQUE-SERIAL-001',
@@ -37,6 +37,67 @@ $context = new WalletPlatformContext(
3737

3838
---
3939

40+
## Apple-only and Google-only snippets
41+
42+
**Apple-only** (no Google IDs required). After `build()`, use only `$built->apple()`; `$built->google()` throws.
43+
44+
```php
45+
use Jolicode\WalletKit\Builder\WalletPlatformContext;
46+
use Jolicode\WalletKit\Builder\WalletPass;
47+
use Jolicode\WalletKit\Pass\Apple\Model\Field;
48+
use Jolicode\WalletKit\Pass\Apple\Model\PassStructure;
49+
use Jolicode\WalletKit\Pass\Android\Model\Generic\GenericTypeEnum;
50+
51+
$appleContext = WalletPlatformContext::appleOnly(
52+
appleTeamIdentifier: 'YOUR_TEAM_ID',
53+
applePassTypeIdentifier: 'pass.com.example.app',
54+
appleSerialNumber: 'SN-APPLE-ONLY',
55+
appleOrganizationName: 'Example Org',
56+
appleDescription: 'Membership',
57+
);
58+
59+
$built = WalletPass::generic($appleContext)
60+
->withPassStructure(new PassStructure(
61+
primaryFields: [new Field(key: 'member', value: 'Jane', label: 'Member')],
62+
))
63+
->withGenericType(GenericTypeEnum::GYM_MEMBERSHIP)
64+
->build();
65+
66+
$pass = $built->apple();
67+
```
68+
69+
**Google-only** (requires `issuerName` on the context for class payloads). After `build()`, use `$built->google()`; `$built->apple()` throws. You can still call `addAppleBarcode()` to supply a barcode image for the Google object.
70+
71+
```php
72+
use Jolicode\WalletKit\Builder\WalletPlatformContext;
73+
use Jolicode\WalletKit\Builder\WalletPass;
74+
use Jolicode\WalletKit\Pass\Android\Model\Offer\RedemptionChannelEnum;
75+
use Jolicode\WalletKit\Pass\Apple\Model\Barcode;
76+
use Jolicode\WalletKit\Pass\Apple\Model\BarcodeFormatEnum;
77+
78+
$googleContext = WalletPlatformContext::googleOnly(
79+
googleClassId: '3388000000012345.example_offer_class',
80+
googleObjectId: '3388000000012345.example_offer_object',
81+
issuerName: 'Example Shop',
82+
);
83+
84+
$built = WalletPass::offer(
85+
$googleContext,
86+
title: '10% off',
87+
provider: 'Example Shop',
88+
redemptionChannel: RedemptionChannelEnum::INSTORE,
89+
)->addAppleBarcode(new Barcode(
90+
altText: 'Promo',
91+
format: BarcodeFormatEnum::QR,
92+
message: 'SAVE10',
93+
messageEncoding: 'utf-8',
94+
))->build();
95+
96+
$pair = $built->google();
97+
```
98+
99+
---
100+
40101
## 1. Generic pass
41102

42103
**Use case:** membership card, insurance card, or any pass that does not fit a specialized vertical.
@@ -273,3 +334,4 @@ These methods come from [`CommonWalletBuilderTrait`](../src/Builder/CommonWallet
273334

274335
- The library does **not** sign `.pkpass` bundles or call Google Wallet REST APIs.
275336
- Apple and Google models differ: not every field exists on both sides. Use `mutateApple` or adjust the returned Google class/object after `build()` for platform-specific details.
337+
- A [`WalletPlatformContext`](../src/Builder/WalletPlatformContext.php) with **no** Apple and **no** Google slice throws [`InvalidWalletPlatformContextException`](../src/Exception/InvalidWalletPlatformContextException.php). Google-only contexts must include a non-empty `issuerName` (or use `::googleOnly(...)`, which enforces it).

src/Builder/AppleWalletContext.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Jolicode\WalletKit\Builder;
6+
7+
/**
8+
* Apple Wallet identifiers and defaults for builder output.
9+
*/
10+
final class AppleWalletContext
11+
{
12+
public function __construct(
13+
public readonly string $teamIdentifier,
14+
public readonly string $passTypeIdentifier,
15+
public readonly string $serialNumber,
16+
public readonly string $organizationName,
17+
public readonly string $description,
18+
public readonly int $formatVersion = 1,
19+
) {
20+
}
21+
}

src/Builder/BuiltWalletPass.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,38 @@
44

55
namespace Jolicode\WalletKit\Builder;
66

7+
use Jolicode\WalletKit\Exception\ApplePassNotAvailableException;
8+
use Jolicode\WalletKit\Exception\GoogleWalletPairNotAvailableException;
79
use Jolicode\WalletKit\Pass\Apple\Model\Pass;
810

911
final class BuiltWalletPass
1012
{
1113
public function __construct(
12-
private readonly Pass $apple,
13-
private readonly GoogleWalletPair $google,
14+
private readonly ?Pass $apple,
15+
private readonly ?GoogleWalletPair $google,
1416
) {
1517
}
1618

1719
public function apple(): Pass
1820
{
21+
if (null === $this->apple) {
22+
throw new ApplePassNotAvailableException();
23+
}
24+
1925
return $this->apple;
2026
}
2127

2228
public function google(): GoogleWalletPair
2329
{
30+
if (null === $this->google) {
31+
throw new GoogleWalletPairNotAvailableException();
32+
}
33+
2434
return $this->google;
2535
}
2636

2737
public function googleVertical(): GoogleVerticalEnum
2838
{
29-
return $this->google->vertical;
39+
return $this->google()->vertical;
3040
}
3141
}

src/Builder/CommonWalletBuilderTrait.php

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use Jolicode\WalletKit\Builder\Internal\BarcodeMapper;
88
use Jolicode\WalletKit\Builder\Internal\ColorMapper;
99
use Jolicode\WalletKit\Builder\Internal\CommonWalletState;
10+
use Jolicode\WalletKit\Exception\ApplePlatformContextRequiredException;
11+
use Jolicode\WalletKit\Exception\GooglePlatformContextRequiredException;
1012
use Jolicode\WalletKit\Pass\Android\Model\Shared\AppLinkData;
1113
use Jolicode\WalletKit\Pass\Android\Model\Shared\Barcode as GoogleBarcode;
1214
use Jolicode\WalletKit\Pass\Android\Model\Shared\GoogleDateTime;
@@ -200,12 +202,22 @@ protected function resolvedGoogleHex(): ?string
200202

201203
protected function resolvedGoogleReviewStatus(): ReviewStatusEnum
202204
{
203-
return $this->common->googleReviewStatus ?? $this->context->defaultGoogleReviewStatus;
205+
$google = $this->context->google;
206+
if (null === $google) {
207+
throw new GooglePlatformContextRequiredException('resolvedGoogleReviewStatus() requires a Google context.');
208+
}
209+
210+
return $this->common->googleReviewStatus ?? $google->defaultReviewStatus;
204211
}
205212

206213
protected function resolvedGoogleObjectState(): StateEnum
207214
{
208-
return $this->common->googleObjectState ?? $this->context->defaultGoogleObjectState;
215+
$google = $this->context->google;
216+
if (null === $google) {
217+
throw new GooglePlatformContextRequiredException('resolvedGoogleObjectState() requires a Google context.');
218+
}
219+
220+
return $this->common->googleObjectState ?? $google->defaultObjectState;
209221
}
210222

211223
protected function resolvedGoogleGrouping(): ?GroupingInfo
@@ -231,13 +243,18 @@ protected function finishApplePass(Pass $pass): Pass
231243
*/
232244
protected function createApplePass(\Jolicode\WalletKit\Pass\Apple\Model\PassTypeEnum $passType, \Jolicode\WalletKit\Pass\Apple\Model\PassStructure $structure): Pass
233245
{
246+
$apple = $this->context->apple;
247+
if (null === $apple) {
248+
throw new ApplePlatformContextRequiredException('createApplePass() requires an Apple context.');
249+
}
250+
234251
$pass = new Pass(
235-
description: $this->context->appleDescription,
236-
organizationName: $this->context->appleOrganizationName,
237-
teamIdentifier: $this->context->appleTeamIdentifier,
238-
passTypeIdentifier: $this->context->applePassTypeIdentifier,
239-
formatVersion: $this->context->appleFormatVersion,
240-
serialNumber: $this->context->appleSerialNumber,
252+
description: $apple->description,
253+
organizationName: $apple->organizationName,
254+
teamIdentifier: $apple->teamIdentifier,
255+
passTypeIdentifier: $apple->passTypeIdentifier,
256+
formatVersion: $apple->formatVersion,
257+
serialNumber: $apple->serialNumber,
241258
passType: $passType,
242259
structure: $structure,
243260
barcodes: $this->common->appleBarcodes,

src/Builder/EventTicket/EventTicketWalletBuilder.php

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -59,35 +59,41 @@ public function build(): BuiltWalletPass
5959
secondaryFields: $secondaryFields,
6060
);
6161

62-
$applePass = $this->createApplePass(PassTypeEnum::EVENT_TICKET, $structure);
63-
64-
$eventClass = new EventTicketClass(
65-
id: $this->context->googleClassId,
66-
issuerName: $this->context->appleOrganizationName,
67-
eventName: $this->eventName,
68-
reviewStatus: $this->resolvedGoogleReviewStatus(),
69-
hexBackgroundColor: $this->resolvedGoogleHex(),
70-
linksModuleData: $this->common->linksModuleData,
71-
appLinkData: $this->common->appLinkData,
72-
);
73-
74-
$eventObject = new EventTicketObject(
75-
id: $this->context->googleObjectId,
76-
classId: $this->context->googleClassId,
77-
state: $this->resolvedGoogleObjectState(),
78-
ticketHolderName: $this->ticketHolderName,
79-
ticketNumber: $this->ticketNumber,
80-
barcode: $this->primaryGoogleBarcode(),
81-
hexBackgroundColor: $this->resolvedGoogleHex(),
82-
validTimeInterval: $this->common->validTimeInterval,
83-
linksModuleData: $this->common->linksModuleData,
84-
appLinkData: $this->common->appLinkData,
85-
groupingInfo: $this->resolvedGoogleGrouping(),
86-
);
62+
$applePass = $this->context->hasApple()
63+
? $this->createApplePass(PassTypeEnum::EVENT_TICKET, $structure)
64+
: null;
65+
66+
$googlePair = null;
67+
if ($this->context->hasGoogle()) {
68+
$g = $this->context->google;
69+
70+
$eventClass = new EventTicketClass(
71+
id: $g->classId,
72+
issuerName: $this->context->googleIssuerName(),
73+
eventName: $this->eventName,
74+
reviewStatus: $this->resolvedGoogleReviewStatus(),
75+
hexBackgroundColor: $this->resolvedGoogleHex(),
76+
linksModuleData: $this->common->linksModuleData,
77+
appLinkData: $this->common->appLinkData,
78+
);
79+
80+
$eventObject = new EventTicketObject(
81+
id: $g->objectId,
82+
classId: $g->classId,
83+
state: $this->resolvedGoogleObjectState(),
84+
ticketHolderName: $this->ticketHolderName,
85+
ticketNumber: $this->ticketNumber,
86+
barcode: $this->primaryGoogleBarcode(),
87+
hexBackgroundColor: $this->resolvedGoogleHex(),
88+
validTimeInterval: $this->common->validTimeInterval,
89+
linksModuleData: $this->common->linksModuleData,
90+
appLinkData: $this->common->appLinkData,
91+
groupingInfo: $this->resolvedGoogleGrouping(),
92+
);
93+
94+
$googlePair = new GoogleWalletPair(GoogleVerticalEnum::EVENT_TICKET, $eventClass, $eventObject);
95+
}
8796

88-
return new BuiltWalletPass(
89-
$applePass,
90-
new GoogleWalletPair(GoogleVerticalEnum::EVENT_TICKET, $eventClass, $eventObject),
91-
);
97+
return new BuiltWalletPass($applePass, $googlePair);
9298
}
9399
}

0 commit comments

Comments
 (0)