passlet 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -14
- package/dist/index.cjs +75 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -16
- package/dist/index.d.ts +117 -16
- package/dist/index.js +75 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,10 +3,25 @@
|
|
|
3
3
|
<img src="https://raw.githubusercontent.com/oscartrevio/passlet/main/.github/assets/header.svg" alt="Passlet" width="100%">
|
|
4
4
|
</picture>
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://img.shields.io/npm/v/passlet"><img src="https://img.shields.io/npm/v/passlet?style=flat-square&color=cb3837" alt="npm"></a>
|
|
8
|
+
<a href="https://img.shields.io/npm/l/passlet"><img src="https://img.shields.io/npm/l/passlet?style=flat-square" alt="license"></a>
|
|
9
|
+
</p>
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
**[Passlet](https://github.com/oscartrevio/passlet)** is a library for generating Apple Wallet and Google Wallet passes from a single API.
|
|
14
|
+
|
|
15
|
+
## The problem
|
|
16
|
+
|
|
17
|
+
Creating wallet passes is painful.
|
|
18
|
+
|
|
19
|
+
Apple Wallet and Google Wallet have nothing in common.
|
|
20
|
+
Apple uses .pkpass files — a signed ZIP bundle with a JSON manifest, PKCS#7 certificates, and SHA hashes for every asset. Google uses a REST API with service accounts, JWTs, and a completely different data model. Different field names. Different auth flows. Different everything.
|
|
21
|
+
|
|
22
|
+
If your app supports both, you're building two separate systems.
|
|
23
|
+
|
|
24
|
+
Passlet fixes that. One API that handles the signing, formatting, and platform translation for you — whether you need one wallet or both.
|
|
10
25
|
|
|
11
26
|
## Install
|
|
12
27
|
|
|
@@ -44,28 +59,119 @@ const pass = wallet.loyalty({
|
|
|
44
59
|
});
|
|
45
60
|
|
|
46
61
|
const { apple, google } = await pass.create({ serialNumber: "user-123" });
|
|
47
|
-
// apple → Uint8Array (.pkpass file, serve with Content-Type: application/vnd.apple.pkpass)
|
|
48
|
-
// google → JWT string (save link: https://pay.google.com/gp/v/save/<jwt>)
|
|
49
62
|
```
|
|
50
63
|
|
|
51
|
-
|
|
64
|
+
`apple` is a `Uint8Array` — the `.pkpass` file, ready to serve with `Content-Type: application/vnd.apple.pkpass`.
|
|
65
|
+
|
|
66
|
+
`google` is a JWT string — build the save link as `https://pay.google.com/gp/v/save/{jwt}`.
|
|
67
|
+
|
|
68
|
+
Only need one platform? Omit `apple` or `google` from the wallet config and Passlet skips it.
|
|
52
69
|
|
|
53
70
|
## Pass types
|
|
54
71
|
|
|
55
72
|
```ts
|
|
56
|
-
wallet.loyalty(config);
|
|
57
|
-
wallet.event(config);
|
|
58
|
-
wallet.flight(config);
|
|
59
|
-
wallet.coupon(config);
|
|
60
|
-
wallet.giftCard(config);
|
|
61
|
-
wallet.generic(config);
|
|
73
|
+
wallet.loyalty(config); // Rewards cards, memberships, point systems
|
|
74
|
+
wallet.event(config); // Concerts, conferences, sports, any ticketed event
|
|
75
|
+
wallet.flight(config); // Boarding passes with gate, seat, departure info
|
|
76
|
+
wallet.coupon(config); // Discounts, promos, limited-time offers
|
|
77
|
+
wallet.giftCard(config); // Prepaid cards with balance tracking
|
|
78
|
+
wallet.generic(config); // Anything else — credentials, IDs, parking, keys
|
|
62
79
|
```
|
|
63
80
|
|
|
81
|
+
Every type maps automatically to the correct Apple pass style and Google Wallet class. You don't touch platform-specific schemas.
|
|
82
|
+
|
|
83
|
+
## Fields
|
|
84
|
+
|
|
85
|
+
Fields define what shows on the pass. Passlet handles the translation to each platform's layout model.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { field } from "passlet";
|
|
89
|
+
|
|
90
|
+
const pass = wallet.event({
|
|
91
|
+
id: "summer-fest",
|
|
92
|
+
name: "Summer Fest",
|
|
93
|
+
fields: [
|
|
94
|
+
field.primary("event", "Event", "Summer Fest"),
|
|
95
|
+
field.secondary("date", "Date", "Aug 29, 2026"),
|
|
96
|
+
field.secondary("location", "Location", "Monterrey, MX"),
|
|
97
|
+
field.auxiliary("door", "Doors Open", "6:00 PM"),
|
|
98
|
+
field.auxiliary("seat", "Seat", "GA"),
|
|
99
|
+
],
|
|
100
|
+
barcode: {
|
|
101
|
+
format: "QR",
|
|
102
|
+
value: "TICKET-2444",
|
|
103
|
+
altText: "TICKET-2444",
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Field groups (`primary`, `secondary`, `auxiliary`, `back`) map to Apple's layout zones and Google's equivalent row structure. Primary fields render large and prominent. Secondary and auxiliary fill the detail rows. Back fields go on the reverse side (Apple) or expandable section (Google).
|
|
109
|
+
|
|
110
|
+
## Barcodes
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
barcode: {
|
|
114
|
+
format: "QR", // "QR" | "PDF417" | "AZTEC" | "CODE128"
|
|
115
|
+
value: "ABC-12345",
|
|
116
|
+
altText: "ABC-12345", // Text shown below the barcode
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Passlet normalizes barcode formats across platforms. If a format isn't supported on one platform, it falls back to the closest equivalent.
|
|
121
|
+
|
|
122
|
+
## Visual customization
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
wallet.loyalty({
|
|
126
|
+
id: "my-card",
|
|
127
|
+
name: "My Card",
|
|
128
|
+
backgroundColor: "#1c1917",
|
|
129
|
+
foregroundColor: "#fafaf9",
|
|
130
|
+
labelColor: "#a8a29e",
|
|
131
|
+
icon: readFileSync("./assets/icon.png"),
|
|
132
|
+
logo: readFileSync("./assets/logo.png"),
|
|
133
|
+
fields: [
|
|
134
|
+
/* ... */
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Colors accept any hex value. Images accept `Uint8Array` or `Buffer`. Passlet handles the asset bundling for Apple and image hosting references for Google.
|
|
140
|
+
|
|
64
141
|
## Credentials
|
|
65
142
|
|
|
66
|
-
|
|
143
|
+
### Apple
|
|
144
|
+
|
|
145
|
+
Requires an Apple Developer account with a Pass Type ID.
|
|
146
|
+
|
|
147
|
+
1. [Create a Pass Type ID](https://developer.apple.com/account/resources/identifiers/list/passTypeId) in the Apple Developer portal
|
|
148
|
+
2. Create and download the signing certificate
|
|
149
|
+
3. Export as `.p12`, convert to PEM:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
openssl pkcs12 -in certificate.p12 -clcerts -nokeys -out signerCert.pem
|
|
153
|
+
openssl pkcs12 -in certificate.p12 -nocerts -out signerKey.pem
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
4. Download the [Apple WWDR certificate](https://www.apple.com/certificateauthority/) (G4)
|
|
157
|
+
|
|
158
|
+
### Google
|
|
159
|
+
|
|
160
|
+
Requires a Google Wallet issuer account.
|
|
161
|
+
|
|
162
|
+
1. [Sign up for the Google Pay & Wallet Console](https://pay.google.com/business/console)
|
|
163
|
+
2. Create a service account with the Google Wallet API enabled
|
|
164
|
+
3. Download the JSON key — use `client_email` and `private_key` from the file
|
|
165
|
+
|
|
166
|
+
## Roadmap
|
|
167
|
+
|
|
168
|
+
- [ ] Pass updates and push notifications (APNs + Google API)
|
|
169
|
+
- [ ] CLI for quick pass generation
|
|
170
|
+
- [ ] Pass playground
|
|
171
|
+
|
|
172
|
+
## Contributing
|
|
67
173
|
|
|
68
|
-
|
|
174
|
+
PRs welcome. If you've dealt with wallet pass APIs before, you know why this needs to exist.
|
|
69
175
|
|
|
70
176
|
## License
|
|
71
177
|
|
package/dist/index.cjs
CHANGED
|
@@ -581,6 +581,12 @@ function validateGoogleRequirements(pass) {
|
|
|
581
581
|
"Google Wallet logo must be a URL string, not bytes"
|
|
582
582
|
);
|
|
583
583
|
}
|
|
584
|
+
if (pass.type === "loyalty" && !pass.logo) {
|
|
585
|
+
throw new WalletError(
|
|
586
|
+
"GOOGLE_MISSING_LOGO",
|
|
587
|
+
"Google Wallet loyalty passes require a logo URL (programLogo)"
|
|
588
|
+
);
|
|
589
|
+
}
|
|
584
590
|
if (pass.type === "flight") {
|
|
585
591
|
const { carrier, flightNumber, origin, destination } = pass;
|
|
586
592
|
if (!(carrier && flightNumber && origin && destination)) {
|
|
@@ -1239,35 +1245,58 @@ function resolveOptions(arg) {
|
|
|
1239
1245
|
return typeof arg === "string" ? { value: arg } : arg;
|
|
1240
1246
|
}
|
|
1241
1247
|
var field = {
|
|
1242
|
-
/**
|
|
1248
|
+
/**
|
|
1249
|
+
* Top-right corner of the pass. Space is limited — use at most one field.
|
|
1250
|
+
*
|
|
1251
|
+
* Apple → `headerFields`. Google → `textModulesData`.
|
|
1252
|
+
*/
|
|
1243
1253
|
header: (key, label, arg) => ({
|
|
1244
1254
|
slot: "header",
|
|
1245
1255
|
key,
|
|
1246
1256
|
label,
|
|
1247
1257
|
...resolveOptions(arg)
|
|
1248
1258
|
}),
|
|
1249
|
-
/**
|
|
1259
|
+
/**
|
|
1260
|
+
* Large, prominent area below the logo.
|
|
1261
|
+
*
|
|
1262
|
+
* Apple → `primaryFields`. Google → `subheader` (label) + `header` (value).
|
|
1263
|
+
* On boarding passes, Apple renders a transit icon between the two primary fields,
|
|
1264
|
+
* so place departure on the left and arrival on the right.
|
|
1265
|
+
*/
|
|
1250
1266
|
primary: (key, label, arg) => ({
|
|
1251
1267
|
slot: "primary",
|
|
1252
1268
|
key,
|
|
1253
1269
|
label,
|
|
1254
1270
|
...resolveOptions(arg)
|
|
1255
1271
|
}),
|
|
1256
|
-
/**
|
|
1272
|
+
/**
|
|
1273
|
+
* Row below the primary area.
|
|
1274
|
+
*
|
|
1275
|
+
* Apple → `secondaryFields`. Google → `textModulesData`.
|
|
1276
|
+
*/
|
|
1257
1277
|
secondary: (key, label, arg) => ({
|
|
1258
1278
|
slot: "secondary",
|
|
1259
1279
|
key,
|
|
1260
1280
|
label,
|
|
1261
1281
|
...resolveOptions(arg)
|
|
1262
1282
|
}),
|
|
1263
|
-
/**
|
|
1283
|
+
/**
|
|
1284
|
+
* Row below secondary. Supports two rows — pass `{ row: 0 }` or `{ row: 1 }` to assign.
|
|
1285
|
+
*
|
|
1286
|
+
* Apple → `auxiliaryFields`. Google → `textModulesData`.
|
|
1287
|
+
*/
|
|
1264
1288
|
auxiliary: (key, label, arg) => ({
|
|
1265
1289
|
slot: "auxiliary",
|
|
1266
1290
|
key,
|
|
1267
1291
|
label,
|
|
1268
1292
|
...resolveOptions(arg)
|
|
1269
1293
|
}),
|
|
1270
|
-
/**
|
|
1294
|
+
/**
|
|
1295
|
+
* Back of the pass — visible only when the user flips it over.
|
|
1296
|
+
* Good for terms, redemption instructions, or contact info.
|
|
1297
|
+
*
|
|
1298
|
+
* Apple → `backFields`. Google → `infoModuleData`.
|
|
1299
|
+
*/
|
|
1271
1300
|
back: (key, label, arg) => ({
|
|
1272
1301
|
slot: "back",
|
|
1273
1302
|
key,
|
|
@@ -1301,6 +1330,16 @@ var Pass = class {
|
|
|
1301
1330
|
this.config = config;
|
|
1302
1331
|
this.credentials = credentials;
|
|
1303
1332
|
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Issue a pass to a recipient. Runs both providers in parallel.
|
|
1335
|
+
*
|
|
1336
|
+
* Returns an {@link IssuedPass} with:
|
|
1337
|
+
* - `apple` — a `.pkpass` `Uint8Array` ready to serve, or `null` if Apple credentials were omitted.
|
|
1338
|
+
* - `google` — a signed JWT for the Google Wallet save link, or `null` if Google credentials were omitted.
|
|
1339
|
+
* - `warnings` — non-fatal notices (e.g. a missing optional image).
|
|
1340
|
+
*
|
|
1341
|
+
* @throws {WalletError} `CREATE_CONFIG_INVALID` if `createConfig` fails validation.
|
|
1342
|
+
*/
|
|
1304
1343
|
async create(createConfig) {
|
|
1305
1344
|
validateCreateConfig(createConfig);
|
|
1306
1345
|
const [appleResult, googleResult] = await Promise.allSettled([
|
|
@@ -1319,7 +1358,13 @@ var Pass = class {
|
|
|
1319
1358
|
warnings: [...appleResult.value.warnings, ...googleResult.value.warnings]
|
|
1320
1359
|
};
|
|
1321
1360
|
}
|
|
1322
|
-
|
|
1361
|
+
/**
|
|
1362
|
+
* Push updated field values to an already-issued pass.
|
|
1363
|
+
*
|
|
1364
|
+
* Google: PATCHes the object via the Wallet REST API (the pass must have been
|
|
1365
|
+
* saved to a wallet first). Apple: no-op — Apple passes update when the holder
|
|
1366
|
+
* re-downloads the pass via your web service.
|
|
1367
|
+
*/
|
|
1323
1368
|
async update(createConfig) {
|
|
1324
1369
|
validateCreateConfig(createConfig);
|
|
1325
1370
|
if (this.credentials.google) {
|
|
@@ -1330,8 +1375,12 @@ var Pass = class {
|
|
|
1330
1375
|
);
|
|
1331
1376
|
}
|
|
1332
1377
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1378
|
+
/**
|
|
1379
|
+
* Permanently delete a pass.
|
|
1380
|
+
*
|
|
1381
|
+
* Google: removes the object via the Wallet REST API.
|
|
1382
|
+
* Apple: no-op — Apple passes cannot be remotely deleted; use {@link expire} to invalidate them.
|
|
1383
|
+
*/
|
|
1335
1384
|
async delete(serialNumber) {
|
|
1336
1385
|
if (this.credentials.google) {
|
|
1337
1386
|
await deleteGooglePass(
|
|
@@ -1341,8 +1390,13 @@ var Pass = class {
|
|
|
1341
1390
|
);
|
|
1342
1391
|
}
|
|
1343
1392
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1393
|
+
/**
|
|
1394
|
+
* Mark a pass as expired / invalid.
|
|
1395
|
+
*
|
|
1396
|
+
* Google: transitions the object state to `EXPIRED` via the Wallet REST API.
|
|
1397
|
+
* Apple: no-op — Apple passes expire automatically when `expiresAt` is reached,
|
|
1398
|
+
* or you can set `apple.voided: true` at issue time via {@link create}.
|
|
1399
|
+
*/
|
|
1346
1400
|
async expire(serialNumber) {
|
|
1347
1401
|
if (this.credentials.google) {
|
|
1348
1402
|
await expireGooglePass(
|
|
@@ -1360,21 +1414,32 @@ var Wallet = class {
|
|
|
1360
1414
|
constructor(credentials) {
|
|
1361
1415
|
this.credentials = credentials;
|
|
1362
1416
|
}
|
|
1417
|
+
/** Create a loyalty / rewards card pass. */
|
|
1363
1418
|
loyalty(config) {
|
|
1364
1419
|
return new Pass({ ...config, type: "loyalty" }, this.credentials);
|
|
1365
1420
|
}
|
|
1421
|
+
/** Create an event ticket pass. */
|
|
1366
1422
|
event(config) {
|
|
1367
1423
|
return new Pass({ ...config, type: "event" }, this.credentials);
|
|
1368
1424
|
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Create a boarding pass. Covers air, train, bus, and boat transit.
|
|
1427
|
+
*
|
|
1428
|
+
* Apple boarding passes render a transit icon between the two `primary` fields,
|
|
1429
|
+
* so use `field.primary()` for the departure and arrival locations.
|
|
1430
|
+
*/
|
|
1369
1431
|
flight(config) {
|
|
1370
1432
|
return new Pass({ ...config, type: "flight" }, this.credentials);
|
|
1371
1433
|
}
|
|
1434
|
+
/** Create a coupon / offer pass. */
|
|
1372
1435
|
coupon(config) {
|
|
1373
1436
|
return new Pass({ ...config, type: "coupon" }, this.credentials);
|
|
1374
1437
|
}
|
|
1438
|
+
/** Create a gift card pass. */
|
|
1375
1439
|
giftCard(config) {
|
|
1376
1440
|
return new Pass({ ...config, type: "giftCard" }, this.credentials);
|
|
1377
1441
|
}
|
|
1442
|
+
/** Create a generic pass for anything that doesn't fit the other types. */
|
|
1378
1443
|
generic(config) {
|
|
1379
1444
|
return new Pass({ ...config, type: "generic" }, this.credentials);
|
|
1380
1445
|
}
|