sbcwallet 0.0.2 β†’ 0.0.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
@@ -1,191 +1,148 @@
1
- # 🎟️ sbcwallet
1
+ # sbcwallet
2
2
 
3
- Unified Wallet-Pass SDK for Real-World Credentials
3
+ Unified wallet-pass SDK for Apple Wallet (.pkpass) and Google Wallet.
4
4
 
5
- sbcwallet is a TypeScript SDK for generating, signing, and managing verifiable passes on Apple Wallet and Google Wallet.
6
- Built on @sbcwallet, it bridges cryptographic truth and real-world credentials β€” enabling secure, interoperable workflows for logistics, healthcare, and beyond.
7
-
8
- βΈ»
9
-
10
- ## ✨ Overview
11
-
12
- sbcwallet provides a unified abstraction layer for issuing and updating wallet passes across multiple ecosystems.
13
- It standardizes claim flows (like PES β†’ TO) and status pipelines (ISSUED β†’ PRESENCE β†’ OPS β†’ EXITED) while maintaining verifiable hashes, signatures, and anchor integrity via sbcwallet Core.
14
-
15
- βΈ»
16
-
17
- ## πŸš€ Quickstart
5
+ ## Install
18
6
 
19
7
  ```sh
20
8
  npm install sbcwallet
21
9
  ```
22
10
 
23
- ```js
11
+ ## Quickstart (Loyalty)
24
12
 
25
- import { createParentSchedule, createChildTicket, getPkpassBuffer } from 'sbcwallet'
13
+ Multi-tenant loyalty is designed for a real-world setup:
14
+ - Each business (tenant) defines its own card design (logo, colors, issuer name).
15
+ - Users add a card using their own `memberId`.
16
+ - Points can be updated for an existing issued card.
26
17
 
27
- // 1️⃣ Create a parent PES schedule
28
- const pes = await createParentSchedule({
29
- profile: 'logistics',
30
- programName: 'Morning Yard Veracruz',
31
- site: 'Patio Gate 3'
32
- })
18
+ ### Define a business (per-tenant theme) and create its program
33
19
 
34
- // 2️⃣ Claim a child Transport Order
35
- const to = await createChildTicket({
36
- parentId: pes.id,
37
- plate: 'ABC123A',
38
- carrier: 'Transportes Golfo'
20
+ ```ts
21
+ import { createBusiness, createLoyaltyProgram } from 'sbcwallet'
22
+
23
+ const biz = createBusiness({
24
+ name: 'X Cafe',
25
+ programName: 'Spirit Rewards',
26
+ pointsLabel: 'Points',
27
+ wallet: {
28
+ googleWallet: {
29
+ issuerName: 'X Cafe',
30
+ backgroundColor: '#111827',
31
+ logoUrl: 'https://example.com/logo.png',
32
+
33
+ // Advanced passthrough: merged into the Google loyaltyClass payload
34
+ classOverrides: {
35
+ reviewStatus: 'UNDER_REVIEW'
36
+ }
37
+ },
38
+ appleWallet: {
39
+ organizationName: 'X Cafe',
40
+ logoText: 'X',
41
+ backgroundColor: 'rgb(17, 24, 39)',
42
+
43
+ // Advanced passthrough: merged into the Apple pass.json payload
44
+ passOverrides: {
45
+ userInfo: { tenant: 'spirit-hub' }
46
+ }
47
+ }
48
+ }
39
49
  })
40
50
 
41
- // 3️⃣ Generate Apple Wallet pass
42
- const buf = await getPkpassBuffer('child', to)
43
- await fs.promises.writeFile('ticket.pkpass', buf)
51
+ await createLoyaltyProgram({
52
+ businessId: biz.id,
53
+ locations: [
54
+ { latitude: 35.6892, longitude: 51.389 },
55
+ { latitude: 35.7, longitude: 51.4 }
56
+ ],
57
+ // Apple Wallet: shown when the pass becomes relevant (e.g., near a location)
58
+ relevantText: 'Welcome back β€” show this card at checkout',
59
+ countryCode: 'OM',
60
+ homepageUrl: 'https://example.com'
61
+ })
44
62
  ```
45
63
 
46
- ## 🎁 Loyalty Cards (Multi-tenant)
47
-
48
- Each business defines its own loyalty program, customers create accounts, and each customer gets a loyalty card that shows:
49
- - A QR/barcode identifier (`memberId`)
50
- - Current points (`points`) which can be updated
64
+ ### Issue a card and generate a Save URL
51
65
 
52
66
  ```ts
53
67
  import {
54
- createBusiness,
55
68
  createCustomerAccount,
56
- createLoyaltyProgram,
57
69
  issueLoyaltyCard,
58
70
  updateLoyaltyPoints,
59
71
  getGoogleObject
60
72
  } from 'sbcwallet'
61
73
 
62
- const biz = createBusiness({ name: 'SBC', pointsLabel: 'points' })
63
- await createLoyaltyProgram({ businessId: biz.id })
74
+ const memberId = 'USER-123'
75
+
76
+ const customer = createCustomerAccount({
77
+ businessId: biz.id,
78
+ fullName: 'Alice',
79
+ memberId
80
+ })
81
+
82
+ const card = await issueLoyaltyCard({
83
+ businessId: biz.id,
84
+ customerId: customer.id,
85
+ initialPoints: 10,
86
+ metadata: {
87
+ googleWallet: {
88
+ objectOverrides: {
89
+ linksModuleData: {
90
+ uris: [{ uri: 'https://example.com', description: 'Website' }]
91
+ }
92
+ }
93
+ }
94
+ }
95
+ })
64
96
 
65
- const customer = createCustomerAccount({ businessId: biz.id, fullName: 'Alice' })
66
- const card = await issueLoyaltyCard({ businessId: biz.id, customerId: customer.id, initialPoints: 10 })
67
97
  await updateLoyaltyPoints({ cardId: card.id, delta: 5 })
68
98
 
69
99
  const { saveUrl } = await getGoogleObject('child', card)
70
100
  console.log(saveUrl)
71
101
  ```
72
102
 
73
- βΈ»
74
-
75
- ## 🧠 Architecture
76
- ```bash
77
- sbcwallet
78
- β”œβ”€β”€ adapters/ # Apple + Google Wallet adapters
79
- β”œβ”€β”€ api/ # Unified issuance/update API
80
- β”œβ”€β”€ profiles/ # Domain-specific field maps
81
- β”œβ”€β”€ templates/ # JSON templates for passes
82
- └── types.ts # Shared types and validation
83
- ```
84
- ### Key Components
85
- ```bash
86
- Module Description
87
- adapters/apple.ts Builds and signs .pkpass files using passkit-generator.
88
- adapters/google.ts Creates Google Wallet class/object JSON payloads.
89
- api/unified.ts Unified functions: createParentSchedule, createChildTicket, updatePassStatus.
90
- profiles/ Domain-specific mappings (logistics, healthcare, etc.).
91
- templates/ JSON templates for field mapping and layout.
92
- ```
93
-
94
- βΈ»
95
-
96
- ## 🧩 Profiles
103
+ ## Location-based surfacing and notifications
97
104
 
98
- ### Logistics (default)
105
+ This SDK supports two related concepts:
99
106
 
100
- Entity Description Example
101
- Parent (PES) Program Entry Schedule Gate window, site, available slots
102
- Child (TO) Transport Order Plate, carrier, client, status
103
- Statuses ISSUED β†’ PRESENCE β†’ SCALE β†’ OPS β†’ EXITED
107
+ 1) Location-based surfacing (no server required)
108
+ - Apple Wallet: setting `locations` and `relevantText` in pass.json can surface the pass on the lock screen when the user is near the business.
109
+ - Google Wallet: setting `locations` on the class/object helps Wallet surface the pass contextually.
104
110
 
105
- ### Healthcare (reference)
111
+ 2) Push-style notifications (server required)
112
+ - Google Wallet supports sending a message via the `addMessage` API. Your system decides *when* to send the message (for example, after your app detects the user is near the business).
106
113
 
107
- Entity Description Example
108
- Parent Appointment Batch Doctor, location, date
109
- Child Patient Visit Ticket Patient, procedure, status
110
- Statuses SCHEDULED β†’ CHECKIN β†’ PROCEDURE β†’ DISCHARGED
114
+ ```ts
115
+ import { pushLoyaltyMessage } from 'sbcwallet'
111
116
 
112
- Switch profiles dynamically:
113
- ```js
114
- await createChildTicket({ profile: 'healthcare', ... })
117
+ await pushLoyaltyMessage({
118
+ cardId: card.id,
119
+ header: 'X',
120
+ body: 'You are nearby β€” show this card to earn points.',
121
+ messageType: 'TEXT_AND_NOTIFY'
122
+ })
115
123
  ```
116
124
 
117
- βΈ»
118
-
119
- ## πŸ” Integration with sbcwallet Core
120
-
121
- sbcwallet Pass automatically uses:
122
- β€’ hashEvent() for deterministic hashes
123
- β€’ signCredential() for ECDSA signatures
124
- β€’ dailyMerkle() for anchoring batches
125
-
126
- This ensures every pass is cryptographically verifiable and compatible with sbcwallet’s event audit trail.
127
-
128
- βΈ»
129
-
130
- ## πŸ§ͺ Testing
131
-
132
- `npm run test`
133
-
134
- Tests include:
135
- β€’ Apple .pkpass field mapping
136
- β€’ Google Wallet JSON validity
137
- β€’ Cross-profile field validation
138
- β€’ Core integration (hash + sign + verify)
125
+ ## Demo server (multi-tenant)
139
126
 
140
- βΈ»
141
-
142
- ## βš™οΈ Environment Variables (Apple Wallet)
143
-
144
- ```sh
145
- APPLE_TEAM_ID=ABCD1234
146
- APPLE_PASS_TYPE_ID=pass.com.sbcwallet.logistics
147
- APPLE_CERT_PATH=./certs/pass.p12
148
- APPLE_CERT_PASSWORD=yourpassword
149
- APPLE_WWDR_PATH=./certs/wwdr.pem
150
- ```
151
-
152
- For Google Wallet, include:
153
127
  ```sh
154
- GOOGLE_ISSUER_ID=issuer-id
155
- GOOGLE_SA_JSON=./google/credentials.json
128
+ npm run loyalty:server:multi
156
129
  ```
157
130
 
158
- βΈ»
131
+ Open `http://localhost:5190`.
159
132
 
160
- ## 🧾 License
133
+ ## Configuration
161
134
 
162
- Apache License 2.0
163
- Β© 2025 sbcwallet β€” open and extensible.
135
+ For Google Wallet Save URLs to work on-device you must set:
136
+ - `GOOGLE_ISSUER_ID`
137
+ - `GOOGLE_SA_JSON`
164
138
 
165
- βΈ»
139
+ For Apple Wallet signing, see APPLE_WALLET_SETUP.md.
166
140
 
167
- ## 🀝 Contributing
168
- 1. Fork the repo
169
- 2. Run npm install
170
- 3. Add or improve a profile under src/profiles/
171
- 4. Write tests in tests/
172
- 5. Submit a PR using conventional commits
141
+ ## Development
173
142
 
174
- βΈ»
175
-
176
- ## 🧭 Part of the sbcwallet Ecosystem
177
-
178
- Repo Purpose
179
143
  ```sh
180
- sbcwallet/core Verifiable event SDK β€” hashing, signing, Merkle trees
181
- sbcwallet/pass Wallet-pass abstraction over Core (this repo)
182
- sbcwallet/wallet Reference logistics PWA & API
183
- sbcwallet/id Hosted identity & orchestration layer (SaaS)
144
+ npm run build
145
+ npm test
146
+ npm pack --dry-run
184
147
  ```
185
148
 
186
- βΈ»
187
-
188
- β€œsbcwallet Pass connects cryptographic truth with human experience β€”
189
- turning every credential into a verifiable story.”
190
-
191
- Reflection: evidence βœ“ logic consistent brevity optimized
@@ -27,26 +27,28 @@ export class AppleWalletAdapter {
27
27
  const template = this.mergeTemplates(baseTemplate, profileTemplate);
28
28
  // Apply pass data to template
29
29
  const populatedTemplate = this.populateTemplate(template, passData, profile, passType);
30
+ // Optional per-pass overrides via metadata (useful for per-business theming)
31
+ const appleWallet = passData?.metadata?.appleWallet || {};
30
32
  // Build pass props from populated template
31
33
  const passProps = {
32
34
  serialNumber: passData.id,
33
- description: populatedTemplate.description || 'sbcwallet Pass',
34
- organizationName: populatedTemplate.organizationName || 'sbcwallet',
35
+ description: appleWallet.description || populatedTemplate.description || 'sbcwallet Pass',
36
+ organizationName: appleWallet.organizationName || populatedTemplate.organizationName || 'sbcwallet',
35
37
  passTypeIdentifier: this.config.passTypeId,
36
38
  teamIdentifier: this.config.teamId
37
39
  };
38
40
  // Add colors
39
- if (populatedTemplate.backgroundColor) {
40
- passProps.backgroundColor = populatedTemplate.backgroundColor;
41
+ if (appleWallet.backgroundColor || populatedTemplate.backgroundColor) {
42
+ passProps.backgroundColor = appleWallet.backgroundColor || populatedTemplate.backgroundColor;
41
43
  }
42
- if (populatedTemplate.foregroundColor) {
43
- passProps.foregroundColor = populatedTemplate.foregroundColor;
44
+ if (appleWallet.foregroundColor || populatedTemplate.foregroundColor) {
45
+ passProps.foregroundColor = appleWallet.foregroundColor || populatedTemplate.foregroundColor;
44
46
  }
45
- if (populatedTemplate.labelColor) {
46
- passProps.labelColor = populatedTemplate.labelColor;
47
+ if (appleWallet.labelColor || populatedTemplate.labelColor) {
48
+ passProps.labelColor = appleWallet.labelColor || populatedTemplate.labelColor;
47
49
  }
48
- if (populatedTemplate.logoText) {
49
- passProps.logoText = populatedTemplate.logoText;
50
+ if (appleWallet.logoText || populatedTemplate.logoText) {
51
+ passProps.logoText = appleWallet.logoText || populatedTemplate.logoText;
50
52
  }
51
53
  // Add barcodes
52
54
  if (populatedTemplate.barcodes && populatedTemplate.barcodes.length > 0) {
@@ -56,6 +58,11 @@ export class AppleWalletAdapter {
56
58
  if (populatedTemplate.generic) {
57
59
  passProps.generic = populatedTemplate.generic;
58
60
  }
61
+ // Advanced passthrough: allow issuers to supply any PassKit fields.
62
+ // Example: webServiceURL, authenticationToken, appLaunchURL, userInfo, beacons, nfc, etc.
63
+ if (appleWallet.passOverrides && typeof appleWallet.passOverrides === 'object') {
64
+ Object.assign(passProps, appleWallet.passOverrides);
65
+ }
59
66
  // Create pass
60
67
  const pass = new PKPass({}, {
61
68
  wwdr: this.config.wwdrPath,
@@ -13,6 +13,11 @@ export declare class GoogleWalletAdapter {
13
13
  private getFieldValue;
14
14
  private generateSaveUrl;
15
15
  private upsertInAPI;
16
+ addMessageToLoyaltyObject(loyaltyObjectId: string, message: {
17
+ header: string;
18
+ body: string;
19
+ messageType?: string;
20
+ }): Promise<void>;
16
21
  /**
17
22
  * Generate and add hero image with progress bar to the pass object
18
23
  *
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'url';
3
3
  import { dirname, join } from 'path';
4
4
  import { GoogleAuth } from 'google-auth-library';
5
5
  import { generateLogisticsHeroImage, generateHealthcareHeroImage } from '../utils/progress-image.js';
6
+ import { logDebug, logWarn, logError } from '../utils/logger.js';
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = dirname(__filename);
8
9
  export class GoogleWalletAdapter {
@@ -32,14 +33,14 @@ export class GoogleWalletAdapter {
32
33
  await this.upsertInAPI('loyaltyClass', classPayload);
33
34
  }
34
35
  catch (error) {
35
- console.warn('⚠️ Google Wallet API loyaltyClass create/update failed');
36
- console.warn(error);
36
+ logWarn('Google Wallet API loyaltyClass create/update failed');
37
+ logWarn(error);
37
38
  }
38
39
  }
39
40
  else {
40
- console.log('⚠️ No Google Auth - class not created in API (will not work on device)');
41
+ logWarn('No Google Auth - class not created in API (will not work on device)');
41
42
  }
42
- console.log('Google Wallet Class:', JSON.stringify(classPayload, null, 2));
43
+ logDebug('Google Wallet Class:', JSON.stringify(classPayload, null, 2));
43
44
  // Save URL is not applicable for classes.
44
45
  return { object: classPayload, saveUrl: '' };
45
46
  }
@@ -85,20 +86,20 @@ export class GoogleWalletAdapter {
85
86
  }
86
87
  catch (error) {
87
88
  // Keep going so we can still generate a signed Save URL for debugging/testing.
88
- console.warn('⚠️ Google Wallet API object create/update failed; continuing to Save URL generation');
89
- console.warn(error);
89
+ logWarn('Google Wallet API object create/update failed; continuing to Save URL generation');
90
+ logWarn(error);
90
91
  }
91
92
  }
92
93
  else {
93
- console.log('⚠️ No Google Auth - object not created in API (will not work on device)');
94
+ logWarn('No Google Auth - object not created in API (will not work on device)');
94
95
  }
95
96
  // Generate save URL with signed JWT
96
97
  // Prefer embedding the full object in the JWT payload so the Save URL can work
97
98
  // even if the object was not pre-created in the API.
98
99
  const saveUrl = await this.generateSaveUrl(populatedObject, isLoyalty ? 'loyaltyObjects' : 'genericObjects');
99
100
  // Log the object
100
- console.log('Google Wallet Object:', JSON.stringify(populatedObject, null, 2));
101
- console.log('Save URL:', saveUrl);
101
+ logDebug('Google Wallet Object:', JSON.stringify(populatedObject, null, 2));
102
+ logDebug('Save URL:', saveUrl);
102
103
  return {
103
104
  object: populatedObject,
104
105
  saveUrl
@@ -120,10 +121,16 @@ export class GoogleWalletAdapter {
120
121
  ...baseTemplate,
121
122
  ...profileTemplate,
122
123
  id: classId,
123
- issuerName: profileTemplate.issuerName || googleWallet.issuerName || 'sbcwallet',
124
+ // Prefer business/program-provided metadata over template defaults.
125
+ issuerName: googleWallet.issuerName || profileTemplate.issuerName || baseTemplate.issuerName || 'sbcwallet',
124
126
  programName: passData.programName || googleWallet.programName || 'Loyalty',
125
127
  hexBackgroundColor: googleWallet.backgroundColor || baseTemplate.hexBackgroundColor || '#111827'
126
128
  };
129
+ // Geo locations (latitude/longitude pairs) for location-based surfacing.
130
+ const locations = googleWallet.locations || metadata.locations;
131
+ if (Array.isArray(locations) && locations.length > 0) {
132
+ payload.locations = locations;
133
+ }
127
134
  if (googleWallet.countryCode)
128
135
  payload.countryCode = googleWallet.countryCode;
129
136
  if (googleWallet.homepageUrl) {
@@ -280,7 +287,7 @@ export class GoogleWalletAdapter {
280
287
  const baseUrl = 'https://pay.google.com/gp/v/save';
281
288
  const objectId = passObject.id;
282
289
  if (!this.config.serviceAccountPath) {
283
- console.warn('⚠️ No service account - returning unsigned URL (will not work)');
290
+ logWarn('No service account - returning unsigned URL (will not work)');
284
291
  return `${baseUrl}/${encodeURIComponent(objectId)}`;
285
292
  }
286
293
  try {
@@ -306,7 +313,7 @@ export class GoogleWalletAdapter {
306
313
  return `${baseUrl}/${token}`;
307
314
  }
308
315
  catch (error) {
309
- console.error('Error generating signed JWT:', error);
316
+ logError('Error generating signed JWT:', error);
310
317
  return `${baseUrl}/${encodeURIComponent(objectId)}`;
311
318
  }
312
319
  }
@@ -324,12 +331,12 @@ export class GoogleWalletAdapter {
324
331
  method: 'POST',
325
332
  data: payload
326
333
  });
327
- console.log(`βœ… ${kind} created in Google Wallet API`);
334
+ logDebug(`βœ… ${kind} created in Google Wallet API`);
328
335
  }
329
336
  catch (error) {
330
337
  if (error.response?.status === 409) {
331
338
  // Object already exists, try to update it
332
- console.log('ℹ️ Resource exists, updating...');
339
+ logDebug('ℹ️ Resource exists, updating...');
333
340
  try {
334
341
  const client = await this.auth.getClient();
335
342
  const baseUrl = 'https://walletobjects.googleapis.com/walletobjects/v1';
@@ -340,19 +347,37 @@ export class GoogleWalletAdapter {
340
347
  method: 'PUT',
341
348
  data: payload
342
349
  });
343
- console.log(`βœ… ${kind} updated in Google Wallet API`);
350
+ logDebug(`βœ… ${kind} updated in Google Wallet API`);
344
351
  }
345
352
  catch (updateError) {
346
- console.error(`❌ Error updating ${kind}:`, updateError);
353
+ logError(`❌ Error updating ${kind}:`, updateError);
347
354
  throw updateError;
348
355
  }
349
356
  }
350
357
  else {
351
- console.error(`❌ Error creating ${kind}:`, error.response?.data || error.message);
358
+ logError(`❌ Error creating ${kind}:`, error.response?.data || error.message);
352
359
  throw error;
353
360
  }
354
361
  }
355
362
  }
363
+ async addMessageToLoyaltyObject(loyaltyObjectId, message) {
364
+ if (!this.auth) {
365
+ throw new Error('Google Auth not initialized');
366
+ }
367
+ const client = await this.auth.getClient();
368
+ const baseUrl = 'https://walletobjects.googleapis.com/walletobjects/v1';
369
+ await client.request({
370
+ url: `${baseUrl}/loyaltyObject/${encodeURIComponent(loyaltyObjectId)}/addMessage`,
371
+ method: 'POST',
372
+ data: {
373
+ message: {
374
+ header: message.header,
375
+ body: message.body,
376
+ messageType: message.messageType || 'TEXT_AND_NOTIFY'
377
+ }
378
+ }
379
+ });
380
+ }
356
381
  /**
357
382
  * Generate and add hero image with progress bar to the pass object
358
383
  *
@@ -381,11 +406,11 @@ export class GoogleWalletAdapter {
381
406
  // Ensure directory exists
382
407
  try {
383
408
  await writeFile(imagePath, imageBuffer);
384
- console.log(`✨ Hero image saved: ${imagePath}`);
409
+ logDebug(`✨ Hero image saved: ${imagePath}`);
385
410
  }
386
411
  catch (err) {
387
412
  // Directory might not exist, that's OK - just skip for now
388
- console.log(`ℹ️ Hero image generated (not uploaded - requires public URL)`);
413
+ logDebug('ℹ️ Hero image generated (not uploaded - requires public URL)');
389
414
  }
390
415
  // TODO: Upload to cloud storage and get public URL
391
416
  // For now, we'll skip adding the hero image to the pass object
@@ -410,10 +435,10 @@ export class GoogleWalletAdapter {
410
435
  DISCHARGED: '#7ED321'
411
436
  };
412
437
  passObject.hexBackgroundColor = statusColors[passData.status] || '#4A90E2';
413
- console.log(`✨ Dynamic color applied for status: ${passData.status} (${passObject.hexBackgroundColor})`);
438
+ logDebug(`✨ Dynamic color applied for status: ${passData.status} (${passObject.hexBackgroundColor})`);
414
439
  }
415
440
  catch (error) {
416
- console.error('⚠️ Failed to generate hero image:', error);
441
+ logError('⚠️ Failed to generate hero image:', error);
417
442
  // Continue without hero image if generation fails
418
443
  }
419
444
  }
@@ -421,9 +446,9 @@ export class GoogleWalletAdapter {
421
446
  // Stub for creating a Google Wallet class
422
447
  // In a real implementation, this would call the Google Wallet API
423
448
  const classId = `${this.config.issuerId}.${profile.name}_${passType}`;
424
- console.log(`Creating Google Wallet Class: ${classId}`);
425
- console.log('Profile:', profile.name);
426
- console.log('Type:', passType);
449
+ logDebug(`Creating Google Wallet Class: ${classId}`);
450
+ logDebug('Profile:', profile.name);
451
+ logDebug('Type:', passType);
427
452
  // This would normally make an API call to:
428
453
  // POST https://walletobjects.googleapis.com/walletobjects/v1/genericClass
429
454
  }
@@ -1,4 +1,4 @@
1
- import type { CreateParentInput, CreateChildInput, CreateBusinessInput, CreateCustomerAccountInput, CreateLoyaltyProgramInput, IssueLoyaltyCardInput, UpdateLoyaltyPointsInput, LoyaltyBusiness, LoyaltyCustomerAccount, ParentPassData, ChildPassData, PassData, PassStatus, ProfileType, ProfileConfig, PassGenerationResult } from '../types.js';
1
+ import type { CreateParentInput, CreateChildInput, CreateBusinessInput, CreateCustomerAccountInput, CreateLoyaltyProgramInput, IssueLoyaltyCardInput, UpdateLoyaltyPointsInput, PushLoyaltyMessageInput, LoyaltyBusiness, LoyaltyCustomerAccount, ParentPassData, ChildPassData, PassData, PassStatus, ProfileType, ProfileConfig, PassGenerationResult } from '../types.js';
2
2
  /**
3
3
  * Get a profile by name
4
4
  */
@@ -31,6 +31,17 @@ export declare function issueLoyaltyCard(input: IssueLoyaltyCardInput): Promise<
31
31
  * Update points on a loyalty card.
32
32
  */
33
33
  export declare function updateLoyaltyPoints(input: UpdateLoyaltyPointsInput): Promise<PassData>;
34
+ /**
35
+ * Send a message to a Google Wallet loyalty object.
36
+ *
37
+ * Notes:
38
+ * - This uses the Google Wallet API addMessage endpoint (requires service account credentials).
39
+ * - Location-based surfacing is controlled by the pass locations; your system decides WHEN to send messages.
40
+ */
41
+ export declare function pushLoyaltyMessage(input: PushLoyaltyMessageInput): Promise<{
42
+ ok: true;
43
+ objectId: string;
44
+ }>;
34
45
  /**
35
46
  * Create a parent schedule (PES or AppointmentBatch)
36
47
  */
@@ -1,6 +1,7 @@
1
- import { CreateParentInputSchema, CreateChildInputSchema, CreateBusinessInputSchema, CreateCustomerAccountInputSchema, CreateLoyaltyProgramInputSchema, IssueLoyaltyCardInputSchema, UpdateLoyaltyPointsInputSchema } from '../types.js';
1
+ import { CreateParentInputSchema, CreateChildInputSchema, CreateBusinessInputSchema, CreateCustomerAccountInputSchema, CreateLoyaltyProgramInputSchema, IssueLoyaltyCardInputSchema, UpdateLoyaltyPointsInputSchema, PushLoyaltyMessageInputSchema } from '../types.js';
2
2
  import { AppleWalletAdapter } from '../adapters/apple.js';
3
3
  import { GoogleWalletAdapter } from '../adapters/google.js';
4
+ import { logWarn } from '../utils/logger.js';
4
5
  import logisticsProfile from '../profiles/logistics/index.js';
5
6
  import healthcareProfile from '../profiles/healthcare/index.js';
6
7
  import loyaltyProfile from '../profiles/loyalty/index.js';
@@ -78,6 +79,7 @@ export function createBusiness(input) {
78
79
  name: validated.name,
79
80
  programName: validated.programName || `${validated.name} Loyalty`,
80
81
  pointsLabel: validated.pointsLabel || 'Points',
82
+ wallet: validated.wallet,
81
83
  createdAt: now,
82
84
  updatedAt: now
83
85
  };
@@ -122,6 +124,38 @@ export async function createLoyaltyProgram(input) {
122
124
  if (!business) {
123
125
  throw new Error(`Business not found: ${validated.businessId}`);
124
126
  }
127
+ const businessWallet = business.wallet || {};
128
+ const inputMetadata = (validated.metadata || {});
129
+ const mergedGoogleWallet = {
130
+ ...(businessWallet.googleWallet || {}),
131
+ ...(inputMetadata.googleWallet || {}),
132
+ locations: validated.locations,
133
+ countryCode: validated.countryCode,
134
+ homepageUrl: validated.homepageUrl
135
+ };
136
+ // If caller didn't set issuerName explicitly, default to the business name
137
+ // so the card shows the tenant brand instead of template defaults.
138
+ if (!mergedGoogleWallet.issuerName) {
139
+ mergedGoogleWallet.issuerName = business.name;
140
+ }
141
+ const mergedAppleWallet = {
142
+ ...(businessWallet.appleWallet || {}),
143
+ ...(inputMetadata.appleWallet || {})
144
+ };
145
+ // Apple Wallet: location-based surfacing is controlled via pass.json fields.
146
+ // We attach them at the program level so issued cards inherit the behavior.
147
+ if (validated.locations && Array.isArray(validated.locations) && validated.locations.length > 0) {
148
+ mergedAppleWallet.passOverrides ||= {};
149
+ if (!mergedAppleWallet.passOverrides.locations) {
150
+ mergedAppleWallet.passOverrides.locations = validated.locations;
151
+ }
152
+ }
153
+ if (validated.relevantText) {
154
+ mergedAppleWallet.passOverrides ||= {};
155
+ if (!mergedAppleWallet.passOverrides.relevantText) {
156
+ mergedAppleWallet.passOverrides.relevantText = validated.relevantText;
157
+ }
158
+ }
125
159
  const program = await createParentSchedule({
126
160
  id: validated.programId,
127
161
  profile: 'loyalty',
@@ -132,12 +166,8 @@ export async function createLoyaltyProgram(input) {
132
166
  businessId: business.id,
133
167
  businessName: business.name,
134
168
  pointsLabel: business.pointsLabel,
135
- googleWallet: {
136
- ...validated.metadata?.googleWallet,
137
- locations: validated.locations,
138
- countryCode: validated.countryCode,
139
- homepageUrl: validated.homepageUrl
140
- }
169
+ googleWallet: mergedGoogleWallet,
170
+ appleWallet: mergedAppleWallet
141
171
  }
142
172
  });
143
173
  business.loyaltyProgramId = program.id;
@@ -164,6 +194,7 @@ export async function issueLoyaltyCard(input) {
164
194
  }
165
195
  const program = passStore.get(business.loyaltyProgramId);
166
196
  const programGoogleWallet = program && program.type === 'parent' ? program.metadata?.googleWallet : undefined;
197
+ const programAppleWallet = program && program.type === 'parent' ? program.metadata?.appleWallet : undefined;
167
198
  const card = await createChildTicket({
168
199
  id: validated.cardId,
169
200
  profile: 'loyalty',
@@ -180,6 +211,10 @@ export async function issueLoyaltyCard(input) {
180
211
  googleWallet: {
181
212
  ...(programGoogleWallet || {}),
182
213
  ...(validated.metadata?.googleWallet || {})
214
+ },
215
+ appleWallet: {
216
+ ...(programAppleWallet || {}),
217
+ ...(validated.metadata?.appleWallet || {})
183
218
  }
184
219
  }
185
220
  });
@@ -214,6 +249,25 @@ export async function updateLoyaltyPoints(input) {
214
249
  passStore.set(pass.id, pass);
215
250
  return pass;
216
251
  }
252
+ /**
253
+ * Send a message to a Google Wallet loyalty object.
254
+ *
255
+ * Notes:
256
+ * - This uses the Google Wallet API addMessage endpoint (requires service account credentials).
257
+ * - Location-based surfacing is controlled by the pass locations; your system decides WHEN to send messages.
258
+ */
259
+ export async function pushLoyaltyMessage(input) {
260
+ const validated = PushLoyaltyMessageInputSchema.parse(input);
261
+ const issuerId = process.env.GOOGLE_ISSUER_ID || 'test-issuer';
262
+ const objectId = validated.objectId || `${issuerId}.${validated.cardId}`;
263
+ const adapter = new GoogleWalletAdapter();
264
+ await adapter.addMessageToLoyaltyObject(objectId, {
265
+ header: validated.header,
266
+ body: validated.body,
267
+ messageType: validated.messageType
268
+ });
269
+ return { ok: true, objectId };
270
+ }
217
271
  /**
218
272
  * Create a parent schedule (PES or AppointmentBatch)
219
273
  */
@@ -358,7 +412,7 @@ export async function generatePass(passData, options = { includeApple: true, inc
358
412
  result.applePkpass = await getPkpassBuffer(passType, passData);
359
413
  }
360
414
  catch (error) {
361
- console.warn('Failed to generate Apple Wallet pass:', error);
415
+ logWarn('Failed to generate Apple Wallet pass:', error);
362
416
  }
363
417
  }
364
418
  if (options.includeGoogle) {
@@ -368,7 +422,7 @@ export async function generatePass(passData, options = { includeApple: true, inc
368
422
  result.googleSaveUrl = googleResult.saveUrl;
369
423
  }
370
424
  catch (error) {
371
- console.warn('Failed to generate Google Wallet object:', error);
425
+ logWarn('Failed to generate Google Wallet object:', error);
372
426
  }
373
427
  }
374
428
  return result;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- export { createParentSchedule, createChildTicket, updatePassStatus, createBusiness, getBusiness, createCustomerAccount, getCustomerAccount, createLoyaltyProgram, issueLoyaltyCard, updateLoyaltyPoints, getPkpassBuffer, getGoogleObject, listProfiles, getProfile, getPass, generatePass } from './api/unified.js';
1
+ export { createParentSchedule, createChildTicket, updatePassStatus, createBusiness, getBusiness, createCustomerAccount, getCustomerAccount, createLoyaltyProgram, issueLoyaltyCard, updateLoyaltyPoints, pushLoyaltyMessage, getPkpassBuffer, getGoogleObject, listProfiles, getProfile, getPass, generatePass } from './api/unified.js';
2
2
  export { AppleWalletAdapter } from './adapters/apple.js';
3
3
  export { GoogleWalletAdapter } from './adapters/google.js';
4
4
  export { default as logisticsProfile } from './profiles/logistics/index.js';
5
5
  export { default as healthcareProfile } from './profiles/healthcare/index.js';
6
6
  export { default as loyaltyProfile } from './profiles/loyalty/index.js';
7
- export type { ProfileType, PassStatus, LogisticsStatus, HealthcareStatus, LoyaltyStatus, GeoLocation, TimeWindow, BasePassData, ParentPassData, ChildPassData, PassData, CreateParentInput, CreateChildInput, LoyaltyBusiness, LoyaltyCustomerAccount, CreateBusinessInput, CreateCustomerAccountInput, CreateLoyaltyProgramInput, IssueLoyaltyCardInput, UpdateLoyaltyPointsInput, ApplePassConfig, ApplePassField, ApplePassTemplate, GooglePassConfig, GoogleTextField, GooglePassClass, GooglePassObject, ProfileFieldMap, ProfileConfig, PassGenerationResult } from './types.js';
8
- export { CreateParentInputSchema, CreateChildInputSchema, TimeWindowSchema, CreateBusinessInputSchema, CreateCustomerAccountInputSchema, CreateLoyaltyProgramInputSchema, IssueLoyaltyCardInputSchema, UpdateLoyaltyPointsInputSchema } from './types.js';
7
+ export type { ProfileType, PassStatus, LogisticsStatus, HealthcareStatus, LoyaltyStatus, GeoLocation, TimeWindow, BasePassData, ParentPassData, ChildPassData, PassData, CreateParentInput, CreateChildInput, LoyaltyBusiness, LoyaltyCustomerAccount, CreateBusinessInput, CreateCustomerAccountInput, CreateLoyaltyProgramInput, IssueLoyaltyCardInput, UpdateLoyaltyPointsInput, PushLoyaltyMessageInput, ApplePassConfig, ApplePassField, ApplePassTemplate, GooglePassConfig, GoogleTextField, GooglePassClass, GooglePassObject, ProfileFieldMap, ProfileConfig, PassGenerationResult } from './types.js';
8
+ export { CreateParentInputSchema, CreateChildInputSchema, TimeWindowSchema, CreateBusinessInputSchema, CreateCustomerAccountInputSchema, CreateLoyaltyProgramInputSchema, IssueLoyaltyCardInputSchema, UpdateLoyaltyPointsInputSchema, PushLoyaltyMessageInputSchema } from './types.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Main exports
2
- export { createParentSchedule, createChildTicket, updatePassStatus, createBusiness, getBusiness, createCustomerAccount, getCustomerAccount, createLoyaltyProgram, issueLoyaltyCard, updateLoyaltyPoints, getPkpassBuffer, getGoogleObject, listProfiles, getProfile, getPass, generatePass } from './api/unified.js';
2
+ export { createParentSchedule, createChildTicket, updatePassStatus, createBusiness, getBusiness, createCustomerAccount, getCustomerAccount, createLoyaltyProgram, issueLoyaltyCard, updateLoyaltyPoints, pushLoyaltyMessage, getPkpassBuffer, getGoogleObject, listProfiles, getProfile, getPass, generatePass } from './api/unified.js';
3
3
  // Adapter exports
4
4
  export { AppleWalletAdapter } from './adapters/apple.js';
5
5
  export { GoogleWalletAdapter } from './adapters/google.js';
@@ -8,4 +8,4 @@ export { default as logisticsProfile } from './profiles/logistics/index.js';
8
8
  export { default as healthcareProfile } from './profiles/healthcare/index.js';
9
9
  export { default as loyaltyProfile } from './profiles/loyalty/index.js';
10
10
  // Schema exports
11
- export { CreateParentInputSchema, CreateChildInputSchema, TimeWindowSchema, CreateBusinessInputSchema, CreateCustomerAccountInputSchema, CreateLoyaltyProgramInputSchema, IssueLoyaltyCardInputSchema, UpdateLoyaltyPointsInputSchema } from './types.js';
11
+ export { CreateParentInputSchema, CreateChildInputSchema, TimeWindowSchema, CreateBusinessInputSchema, CreateCustomerAccountInputSchema, CreateLoyaltyProgramInputSchema, IssueLoyaltyCardInputSchema, UpdateLoyaltyPointsInputSchema, PushLoyaltyMessageInputSchema } from './types.js';
package/dist/types.d.ts CHANGED
@@ -154,6 +154,10 @@ export interface LoyaltyBusiness {
154
154
  programName: string;
155
155
  pointsLabel: string;
156
156
  loyaltyProgramId?: string;
157
+ wallet?: {
158
+ googleWallet?: Record<string, any>;
159
+ appleWallet?: Record<string, any>;
160
+ };
157
161
  createdAt: string;
158
162
  updatedAt: string;
159
163
  }
@@ -170,16 +174,34 @@ export declare const CreateBusinessInputSchema: z.ZodObject<{
170
174
  name: z.ZodString;
171
175
  programName: z.ZodOptional<z.ZodString>;
172
176
  pointsLabel: z.ZodOptional<z.ZodString>;
177
+ wallet: z.ZodOptional<z.ZodObject<{
178
+ googleWallet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
179
+ appleWallet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
180
+ }, "strip", z.ZodTypeAny, {
181
+ googleWallet?: Record<string, any> | undefined;
182
+ appleWallet?: Record<string, any> | undefined;
183
+ }, {
184
+ googleWallet?: Record<string, any> | undefined;
185
+ appleWallet?: Record<string, any> | undefined;
186
+ }>>;
173
187
  }, "strip", z.ZodTypeAny, {
174
188
  name: string;
175
189
  id?: string | undefined;
176
190
  programName?: string | undefined;
177
191
  pointsLabel?: string | undefined;
192
+ wallet?: {
193
+ googleWallet?: Record<string, any> | undefined;
194
+ appleWallet?: Record<string, any> | undefined;
195
+ } | undefined;
178
196
  }, {
179
197
  name: string;
180
198
  id?: string | undefined;
181
199
  programName?: string | undefined;
182
200
  pointsLabel?: string | undefined;
201
+ wallet?: {
202
+ googleWallet?: Record<string, any> | undefined;
203
+ appleWallet?: Record<string, any> | undefined;
204
+ } | undefined;
183
205
  }>;
184
206
  export type CreateBusinessInput = z.infer<typeof CreateBusinessInputSchema>;
185
207
  export declare const CreateCustomerAccountInputSchema: z.ZodObject<{
@@ -214,6 +236,7 @@ export declare const CreateLoyaltyProgramInputSchema: z.ZodObject<{
214
236
  latitude: number;
215
237
  longitude: number;
216
238
  }>, "many">>;
239
+ relevantText: z.ZodOptional<z.ZodString>;
217
240
  countryCode: z.ZodOptional<z.ZodString>;
218
241
  homepageUrl: z.ZodOptional<z.ZodString>;
219
242
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
@@ -227,6 +250,7 @@ export declare const CreateLoyaltyProgramInputSchema: z.ZodObject<{
227
250
  latitude: number;
228
251
  longitude: number;
229
252
  }[] | undefined;
253
+ relevantText?: string | undefined;
230
254
  countryCode?: string | undefined;
231
255
  homepageUrl?: string | undefined;
232
256
  }, {
@@ -239,6 +263,7 @@ export declare const CreateLoyaltyProgramInputSchema: z.ZodObject<{
239
263
  latitude: number;
240
264
  longitude: number;
241
265
  }[] | undefined;
266
+ relevantText?: string | undefined;
242
267
  countryCode?: string | undefined;
243
268
  homepageUrl?: string | undefined;
244
269
  }>;
@@ -285,6 +310,38 @@ export declare const UpdateLoyaltyPointsInputSchema: z.ZodEffects<z.ZodObject<{
285
310
  delta?: number | undefined;
286
311
  }>;
287
312
  export type UpdateLoyaltyPointsInput = z.infer<typeof UpdateLoyaltyPointsInputSchema>;
313
+ export declare const PushLoyaltyMessageInputSchema: z.ZodEffects<z.ZodObject<{
314
+ cardId: z.ZodOptional<z.ZodString>;
315
+ objectId: z.ZodOptional<z.ZodString>;
316
+ header: z.ZodString;
317
+ body: z.ZodString;
318
+ messageType: z.ZodOptional<z.ZodString>;
319
+ }, "strip", z.ZodTypeAny, {
320
+ header: string;
321
+ body: string;
322
+ cardId?: string | undefined;
323
+ objectId?: string | undefined;
324
+ messageType?: string | undefined;
325
+ }, {
326
+ header: string;
327
+ body: string;
328
+ cardId?: string | undefined;
329
+ objectId?: string | undefined;
330
+ messageType?: string | undefined;
331
+ }>, {
332
+ header: string;
333
+ body: string;
334
+ cardId?: string | undefined;
335
+ objectId?: string | undefined;
336
+ messageType?: string | undefined;
337
+ }, {
338
+ header: string;
339
+ body: string;
340
+ cardId?: string | undefined;
341
+ objectId?: string | undefined;
342
+ messageType?: string | undefined;
343
+ }>;
344
+ export type PushLoyaltyMessageInput = z.infer<typeof PushLoyaltyMessageInputSchema>;
288
345
  export type GeoLocation = {
289
346
  latitude: number;
290
347
  longitude: number;
package/dist/types.js CHANGED
@@ -40,7 +40,12 @@ export const CreateBusinessInputSchema = z.object({
40
40
  id: z.string().min(1).optional(),
41
41
  name: z.string().min(1),
42
42
  programName: z.string().min(1).optional(),
43
- pointsLabel: z.string().min(1).optional()
43
+ pointsLabel: z.string().min(1).optional(),
44
+ // Per-business theming / design knobs (optional)
45
+ wallet: z.object({
46
+ googleWallet: z.record(z.any()).optional(),
47
+ appleWallet: z.record(z.any()).optional()
48
+ }).optional()
44
49
  });
45
50
  export const CreateCustomerAccountInputSchema = z.object({
46
51
  id: z.string().min(1).optional(),
@@ -59,6 +64,8 @@ export const CreateLoyaltyProgramInputSchema = z.object({
59
64
  latitude: z.number().min(-90).max(90),
60
65
  longitude: z.number().min(-180).max(180)
61
66
  })).optional(),
67
+ // Apple Wallet: text shown when the pass becomes relevant (e.g., near a location)
68
+ relevantText: z.string().min(1).optional(),
62
69
  countryCode: z.string().length(2).optional(),
63
70
  homepageUrl: z.string().url().optional(),
64
71
  metadata: z.record(z.any()).optional()
@@ -78,3 +85,12 @@ export const UpdateLoyaltyPointsInputSchema = z.object({
78
85
  }).refine(v => v.setPoints !== undefined || v.delta !== undefined, {
79
86
  message: 'Provide either setPoints or delta'
80
87
  });
88
+ export const PushLoyaltyMessageInputSchema = z.object({
89
+ cardId: z.string().min(1).optional(),
90
+ objectId: z.string().min(1).optional(),
91
+ header: z.string().min(1),
92
+ body: z.string().min(1),
93
+ messageType: z.string().min(1).optional()
94
+ }).refine(v => v.cardId !== undefined || v.objectId !== undefined, {
95
+ message: 'Provide either cardId or objectId'
96
+ });
@@ -0,0 +1,4 @@
1
+ export declare function logDebug(...args: unknown[]): void;
2
+ export declare function logInfo(...args: unknown[]): void;
3
+ export declare function logWarn(...args: unknown[]): void;
4
+ export declare function logError(...args: unknown[]): void;
@@ -0,0 +1,24 @@
1
+ const isTestEnv = process.env.NODE_ENV === 'test' ||
2
+ process.env.VITEST === 'true' ||
3
+ process.env.VITEST === '1';
4
+ const isDebugEnabled = /^(1|true|yes)$/i.test(process.env.SBCWALLET_DEBUG || '');
5
+ export function logDebug(...args) {
6
+ if (!isDebugEnabled)
7
+ return;
8
+ console.log(...args);
9
+ }
10
+ export function logInfo(...args) {
11
+ if (!isDebugEnabled)
12
+ return;
13
+ console.log(...args);
14
+ }
15
+ export function logWarn(...args) {
16
+ if (isTestEnv && !isDebugEnabled)
17
+ return;
18
+ console.warn(...args);
19
+ }
20
+ export function logError(...args) {
21
+ if (isTestEnv && !isDebugEnabled)
22
+ return;
23
+ console.error(...args);
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbcwallet",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Unified Wallet-Pass SDK for Real-World Credentials",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -31,7 +31,8 @@
31
31
  "publish:npm": "npm publish",
32
32
  "loyalty:issue": "npm run build && node examples/loyalty-google-issue.js",
33
33
  "loyalty:server": "npm run build && node examples/loyalty-admin-server.js",
34
- "loyalty:server:fixed": "npm run build && node examples/loyalty-fixed-card-server.js"
34
+ "loyalty:server:fixed": "npm run build && node examples/loyalty-fixed-card-server.js",
35
+ "loyalty:server:multi": "npm run build && node examples/loyalty-multi-tenant-server.js"
35
36
  },
36
37
  "keywords": [
37
38
  "wallet",