passlet 0.2.1 → 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 CHANGED
@@ -3,12 +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
- **[Passlet](https://github.com/oscartrevio/passlet)** is a library for generating Apple Wallet and Google Wallet passes from a single TypeScript API.
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
- [![npm version](https://img.shields.io/npm/v/passlet)](https://www.npmjs.com/package/passlet)
9
- [![downloads](https://img.shields.io/npm/dm/passlet)](https://www.npmjs.com/package/passlet)
10
- [![license](https://img.shields.io/npm/l/passlet)](https://www.npmjs.com/package/passlet)
11
- [![GitHub stars](https://img.shields.io/github/stars/oscartrevio/passlet)](https://github.com/oscartrevio/passlet/stargazers)
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.
12
25
 
13
26
  ## Install
14
27
 
@@ -46,28 +59,119 @@ const pass = wallet.loyalty({
46
59
  });
47
60
 
48
61
  const { apple, google } = await pass.create({ serialNumber: "user-123" });
49
- // apple → Uint8Array (.pkpass file, serve with Content-Type: application/vnd.apple.pkpass)
50
- // google → JWT string (save link: https://pay.google.com/gp/v/save/<jwt>)
51
62
  ```
52
63
 
53
- Omit `apple` or `google` from the credentials to skip that provider.
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.
54
69
 
55
70
  ## Pass types
56
71
 
57
72
  ```ts
58
- wallet.loyalty(config);
59
- wallet.event(config);
60
- wallet.flight(config);
61
- wallet.coupon(config);
62
- wallet.giftCard(config);
63
- 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
64
79
  ```
65
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
+
66
141
  ## Credentials
67
142
 
68
- **Apple** — requires an Apple Developer account with a Pass Type ID. [Create one in the Developer portal](https://developer.apple.com/account/resources/identifiers/list/passTypeId), then download your signing certificate and convert it to PEM.
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
69
173
 
70
- **Google** — requires a Google Wallet issuer account. [Set one up in the Pay & Wallet Console](https://pay.google.com/business/console), create a service account, and download the JSON key.
174
+ PRs welcome. If you've dealt with wallet pass APIs before, you know why this needs to exist.
71
175
 
72
176
  ## License
73
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
- /** Top-right of the pass. Compact — typically one field. */
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
- /** Large, prominent area. Apple: primaryFields. Google: subheader (label) + header (value). */
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
- /** Below primary. Apple: secondaryFields. Google: textModulesData. */
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
- /** Below secondary. Supports two rows via { row: 0 | 1 }. Apple: auxiliaryFields. Google: textModulesData. */
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
- /** Back of the pass — hidden by default. Apple: backFields. Google: infoModuleData. */
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
- // Update an existing pass. For Google: PATCHes the object via the Wallet REST API.
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
- // Delete a pass. For Google: permanently removes the object via the Wallet REST API.
1334
- // Apple passes cannot be remotely deleted — use expire() to invalidate them instead.
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
- // Expire a pass. For Google: sets object state to EXPIRED via the Wallet REST API.
1345
- // Apple passes expire automatically when expiresAt is reached.
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
  }