sbcwallet 0.0.1
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/.env.example +10 -0
- package/.github/workflows/build.yml +66 -0
- package/.github/workflows/release.yml +57 -0
- package/APPLE_WALLET_SETUP.md +318 -0
- package/GOOGLE_WALLET_SETUP.md +473 -0
- package/LICENSE +201 -0
- package/README.md +187 -0
- package/dist/adapters/apple.d.ts +10 -0
- package/dist/adapters/apple.js +153 -0
- package/dist/adapters/google.d.ts +26 -0
- package/dist/adapters/google.js +431 -0
- package/dist/api/unified.d.ts +67 -0
- package/dist/api/unified.js +375 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +11 -0
- package/dist/profiles/healthcare/index.d.ts +91 -0
- package/dist/profiles/healthcare/index.js +151 -0
- package/dist/profiles/logistics/index.d.ts +91 -0
- package/dist/profiles/logistics/index.js +152 -0
- package/dist/profiles/loyalty/index.d.ts +91 -0
- package/dist/profiles/loyalty/index.js +81 -0
- package/dist/templates/apple/child.json +59 -0
- package/dist/templates/apple/parent.json +54 -0
- package/dist/templates/google/child_object.json +38 -0
- package/dist/templates/google/loyalty_class.json +7 -0
- package/dist/templates/google/loyalty_object.json +29 -0
- package/dist/templates/google/parent_class.json +10 -0
- package/dist/templates/google/parent_object.json +33 -0
- package/dist/types.d.ts +422 -0
- package/dist/types.js +80 -0
- package/dist/utils/progress-image.d.ts +23 -0
- package/dist/utils/progress-image.js +94 -0
- package/examples/.loyalty-fixed-state.json +10 -0
- package/examples/claim-flow.ts +163 -0
- package/examples/loyalty-admin-server.js +207 -0
- package/examples/loyalty-admin.html +260 -0
- package/examples/loyalty-fixed-card-server.js +288 -0
- package/examples/loyalty-flow.ts +78 -0
- package/examples/loyalty-google-issue.js +115 -0
- package/package.json +51 -0
- package/scripts/copy-assets.js +35 -0
- package/scripts/smoke-dist-import.js +39 -0
- package/setup-google-class.js +97 -0
- package/setup-google-class.ts +105 -0
- package/src/adapters/apple.ts +193 -0
- package/src/adapters/google.ts +521 -0
- package/src/api/unified.ts +487 -0
- package/src/index.ts +74 -0
- package/src/profiles/healthcare/index.ts +157 -0
- package/src/profiles/logistics/index.ts +158 -0
- package/src/profiles/loyalty/index.ts +87 -0
- package/src/templates/apple/child.json +59 -0
- package/src/templates/apple/parent.json +54 -0
- package/src/templates/google/child_object.json +38 -0
- package/src/templates/google/loyalty_class.json +7 -0
- package/src/templates/google/loyalty_object.json +29 -0
- package/src/templates/google/parent_class.json +10 -0
- package/src/templates/google/parent_object.json +33 -0
- package/src/types.ts +324 -0
- package/src/utils/progress-image.ts +130 -0
- package/test-google-wallet.js +78 -0
- package/test-google-wallet.ts +94 -0
- package/tests/adapters.test.ts +244 -0
- package/tests/loyalty.test.ts +39 -0
- package/tests/unified.test.ts +388 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { GoogleAuth } from 'google-auth-library';
|
|
5
|
+
import { generateLogisticsHeroImage, generateHealthcareHeroImage } from '../utils/progress-image.js';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
export class GoogleWalletAdapter {
|
|
9
|
+
config;
|
|
10
|
+
auth = null;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = {
|
|
13
|
+
issuerId: config?.issuerId || process.env.GOOGLE_ISSUER_ID || 'test-issuer',
|
|
14
|
+
serviceAccountPath: config?.serviceAccountPath || process.env.GOOGLE_SA_JSON
|
|
15
|
+
};
|
|
16
|
+
// Initialize Google Auth if credentials are available
|
|
17
|
+
if (this.config.serviceAccountPath) {
|
|
18
|
+
this.auth = new GoogleAuth({
|
|
19
|
+
keyFile: this.config.serviceAccountPath,
|
|
20
|
+
scopes: ['https://www.googleapis.com/auth/wallet_object.issuer']
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async generatePassObject(passData, profile, passType) {
|
|
25
|
+
try {
|
|
26
|
+
const isLoyalty = profile.name === 'loyalty';
|
|
27
|
+
// Loyalty uses a different Google Wallet schema (loyaltyClass/loyaltyObject)
|
|
28
|
+
if (isLoyalty && passType === 'parent' && passData.type === 'parent') {
|
|
29
|
+
const classPayload = await this.generateLoyaltyClass(passData, profile);
|
|
30
|
+
if (this.auth) {
|
|
31
|
+
try {
|
|
32
|
+
await this.upsertInAPI('loyaltyClass', classPayload);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.warn('⚠️ Google Wallet API loyaltyClass create/update failed');
|
|
36
|
+
console.warn(error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
console.log('⚠️ No Google Auth - class not created in API (will not work on device)');
|
|
41
|
+
}
|
|
42
|
+
console.log('Google Wallet Class:', JSON.stringify(classPayload, null, 2));
|
|
43
|
+
// Save URL is not applicable for classes.
|
|
44
|
+
return { object: classPayload, saveUrl: '' };
|
|
45
|
+
}
|
|
46
|
+
// Load template
|
|
47
|
+
const templateFilename = isLoyalty
|
|
48
|
+
? 'loyalty_object.json'
|
|
49
|
+
: `${passType}_object.json`;
|
|
50
|
+
const templatePath = join(__dirname, '..', 'templates', 'google', templateFilename);
|
|
51
|
+
const templateContent = await readFile(templatePath, 'utf-8');
|
|
52
|
+
const baseTemplate = JSON.parse(templateContent);
|
|
53
|
+
// Get profile-specific template
|
|
54
|
+
const profileTemplate = passType === 'parent'
|
|
55
|
+
? profile.defaultTemplates.google.parentObject
|
|
56
|
+
: profile.defaultTemplates.google.childObject;
|
|
57
|
+
// Merge templates
|
|
58
|
+
const template = { ...baseTemplate, ...profileTemplate };
|
|
59
|
+
// Populate template with pass data
|
|
60
|
+
const populatedObject = this.populateObject(template, passData, profile, passType);
|
|
61
|
+
// Generate class ID and object ID
|
|
62
|
+
const classId = isLoyalty && passData.type === 'child'
|
|
63
|
+
? `${this.config.issuerId}.${passData.parentId}`
|
|
64
|
+
: `${this.config.issuerId}.${profile.name}_${passType}`;
|
|
65
|
+
const objectId = `${this.config.issuerId}.${passData.id}`;
|
|
66
|
+
populatedObject.classId = classId;
|
|
67
|
+
populatedObject.id = objectId;
|
|
68
|
+
// Set barcode
|
|
69
|
+
if (populatedObject.barcode) {
|
|
70
|
+
const barcodeValue = passData.memberId || passData.id;
|
|
71
|
+
populatedObject.barcode.value = barcodeValue;
|
|
72
|
+
}
|
|
73
|
+
if (isLoyalty) {
|
|
74
|
+
;
|
|
75
|
+
populatedObject.accountId = passData.memberId || passData.id;
|
|
76
|
+
populatedObject.accountName = passData.customerName || '';
|
|
77
|
+
this.applyLoyaltyExtras(populatedObject, passData);
|
|
78
|
+
}
|
|
79
|
+
// Generate and add hero image with progress bar
|
|
80
|
+
await this.addHeroImage(populatedObject, passData, profile);
|
|
81
|
+
// Create the object in Google Wallet API if auth is available
|
|
82
|
+
if (this.auth) {
|
|
83
|
+
try {
|
|
84
|
+
await this.upsertInAPI(isLoyalty ? 'loyaltyObject' : 'genericObject', populatedObject);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
// 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);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.log('⚠️ No Google Auth - object not created in API (will not work on device)');
|
|
94
|
+
}
|
|
95
|
+
// Generate save URL with signed JWT
|
|
96
|
+
// Prefer embedding the full object in the JWT payload so the Save URL can work
|
|
97
|
+
// even if the object was not pre-created in the API.
|
|
98
|
+
const saveUrl = await this.generateSaveUrl(populatedObject, isLoyalty ? 'loyaltyObjects' : 'genericObjects');
|
|
99
|
+
// Log the object
|
|
100
|
+
console.log('Google Wallet Object:', JSON.stringify(populatedObject, null, 2));
|
|
101
|
+
console.log('Save URL:', saveUrl);
|
|
102
|
+
return {
|
|
103
|
+
object: populatedObject,
|
|
104
|
+
saveUrl
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw new Error(`Failed to generate Google Wallet object: ${error instanceof Error ? error.message : String(error)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async generateLoyaltyClass(passData, profile) {
|
|
112
|
+
const templatePath = join(__dirname, '..', 'templates', 'google', 'loyalty_class.json');
|
|
113
|
+
const templateContent = await readFile(templatePath, 'utf-8');
|
|
114
|
+
const baseTemplate = JSON.parse(templateContent);
|
|
115
|
+
const profileTemplate = profile.defaultTemplates.google.parentClass || {};
|
|
116
|
+
const classId = `${this.config.issuerId}.${passData.id}`;
|
|
117
|
+
const metadata = passData.metadata || {};
|
|
118
|
+
const googleWallet = metadata.googleWallet || {};
|
|
119
|
+
const payload = {
|
|
120
|
+
...baseTemplate,
|
|
121
|
+
...profileTemplate,
|
|
122
|
+
id: classId,
|
|
123
|
+
issuerName: profileTemplate.issuerName || googleWallet.issuerName || 'sbcwallet',
|
|
124
|
+
programName: passData.programName || googleWallet.programName || 'Loyalty',
|
|
125
|
+
hexBackgroundColor: googleWallet.backgroundColor || baseTemplate.hexBackgroundColor || '#111827'
|
|
126
|
+
};
|
|
127
|
+
if (googleWallet.countryCode)
|
|
128
|
+
payload.countryCode = googleWallet.countryCode;
|
|
129
|
+
if (googleWallet.homepageUrl) {
|
|
130
|
+
payload.homepageUri = {
|
|
131
|
+
uri: googleWallet.homepageUrl,
|
|
132
|
+
description: googleWallet.homepageLabel || 'Website'
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// Images: require public URLs. Only include if provided.
|
|
136
|
+
if (googleWallet.logoUrl) {
|
|
137
|
+
payload.programLogo = { sourceUri: { uri: googleWallet.logoUrl } };
|
|
138
|
+
}
|
|
139
|
+
if (googleWallet.heroImageUrl) {
|
|
140
|
+
payload.heroImage = { sourceUri: { uri: googleWallet.heroImageUrl } };
|
|
141
|
+
}
|
|
142
|
+
if (googleWallet.wordMarkUrl) {
|
|
143
|
+
payload.wordMark = { sourceUri: { uri: googleWallet.wordMarkUrl } };
|
|
144
|
+
}
|
|
145
|
+
// Callback settings
|
|
146
|
+
if (googleWallet.updateRequestUrl) {
|
|
147
|
+
payload.callbackOptions = { updateRequestUrl: googleWallet.updateRequestUrl };
|
|
148
|
+
}
|
|
149
|
+
// Advanced/optional: allow raw overrides to be merged in.
|
|
150
|
+
if (googleWallet.classOverrides && typeof googleWallet.classOverrides === 'object') {
|
|
151
|
+
Object.assign(payload, googleWallet.classOverrides);
|
|
152
|
+
}
|
|
153
|
+
return payload;
|
|
154
|
+
}
|
|
155
|
+
populateObject(template, passData, profile, passType) {
|
|
156
|
+
const populated = { ...template };
|
|
157
|
+
// Set card title based on pass type (Google Wallet format)
|
|
158
|
+
if (passType === 'parent' && passData.type === 'parent') {
|
|
159
|
+
const headerText = profile.name === 'logistics'
|
|
160
|
+
? 'Program Entry Schedule'
|
|
161
|
+
: profile.name === 'healthcare'
|
|
162
|
+
? 'Appointment Batch'
|
|
163
|
+
: 'Loyalty Program';
|
|
164
|
+
const bodyText = passData.programName;
|
|
165
|
+
// Backward-compatible shape used by existing tests/consumers
|
|
166
|
+
populated.cardTitle = { header: headerText, body: bodyText };
|
|
167
|
+
populated.header = { header: 'Schedule', body: passData.id };
|
|
168
|
+
populated.cardTitle.defaultValue = { language: 'en-US', value: headerText };
|
|
169
|
+
populated.header.defaultValue = { language: 'en-US', value: bodyText };
|
|
170
|
+
}
|
|
171
|
+
else if (passData.type === 'child') {
|
|
172
|
+
const headerText = profile.name === 'logistics'
|
|
173
|
+
? 'Transport Order'
|
|
174
|
+
: profile.name === 'healthcare'
|
|
175
|
+
? 'Patient Visit'
|
|
176
|
+
: 'Loyalty Card';
|
|
177
|
+
const bodyText = passData.customerName || passData.memberId || passData.plate || passData.patientName || passData.id;
|
|
178
|
+
// Backward-compatible shape used by existing tests/consumers
|
|
179
|
+
populated.cardTitle = { header: headerText, body: bodyText };
|
|
180
|
+
populated.header = { header: 'Order', body: passData.id };
|
|
181
|
+
populated.cardTitle.defaultValue = { language: 'en-US', value: headerText };
|
|
182
|
+
populated.header.defaultValue = { language: 'en-US', value: bodyText };
|
|
183
|
+
}
|
|
184
|
+
// Populate text modules
|
|
185
|
+
if (populated.textModulesData) {
|
|
186
|
+
populated.textModulesData = populated.textModulesData.map(module => {
|
|
187
|
+
const value = this.getFieldValue(module.id, passData);
|
|
188
|
+
return {
|
|
189
|
+
...module,
|
|
190
|
+
body: value || module.body
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
// Add additional fields based on pass data
|
|
194
|
+
if (passData.type === 'parent' && passData.window) {
|
|
195
|
+
const windowModule = populated.textModulesData.find(m => m.id === 'window');
|
|
196
|
+
if (windowModule) {
|
|
197
|
+
windowModule.body = `${new Date(passData.window.from).toLocaleString()} - ${new Date(passData.window.to).toLocaleString()}`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Add status field
|
|
201
|
+
const statusModule = populated.textModulesData.find(m => m.id === 'status');
|
|
202
|
+
if (statusModule) {
|
|
203
|
+
statusModule.body = passData.status;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return populated;
|
|
207
|
+
}
|
|
208
|
+
applyLoyaltyExtras(passObject, passData) {
|
|
209
|
+
const metadata = passData.metadata || {};
|
|
210
|
+
const googleWallet = metadata.googleWallet || {};
|
|
211
|
+
// Geo locations
|
|
212
|
+
const locations = googleWallet.locations || metadata.locations;
|
|
213
|
+
if (Array.isArray(locations) && locations.length > 0) {
|
|
214
|
+
passObject.locations = locations;
|
|
215
|
+
}
|
|
216
|
+
// Links
|
|
217
|
+
if (Array.isArray(googleWallet.links) && googleWallet.links.length > 0) {
|
|
218
|
+
passObject.linksModuleData = {
|
|
219
|
+
uris: googleWallet.links.map((l, idx) => ({
|
|
220
|
+
id: l.id || String(idx + 1),
|
|
221
|
+
description: l.label || l.description,
|
|
222
|
+
uri: l.url || l.uri
|
|
223
|
+
})).filter((u) => u.uri)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
// Images
|
|
227
|
+
if (Array.isArray(googleWallet.imageModules) && googleWallet.imageModules.length > 0) {
|
|
228
|
+
passObject.imageModulesData = googleWallet.imageModules
|
|
229
|
+
.map((img) => img?.imageUrl || img?.uri)
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
.map((uri) => ({
|
|
232
|
+
mainImage: { sourceUri: { uri } }
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
// Messages
|
|
236
|
+
if (Array.isArray(googleWallet.messages) && googleWallet.messages.length > 0) {
|
|
237
|
+
passObject.messages = googleWallet.messages
|
|
238
|
+
.map((m, idx) => ({
|
|
239
|
+
id: m.id || String(idx + 1),
|
|
240
|
+
header: m.header,
|
|
241
|
+
body: m.body,
|
|
242
|
+
messageType: m.messageType
|
|
243
|
+
}))
|
|
244
|
+
.filter((m) => m.header && m.body);
|
|
245
|
+
}
|
|
246
|
+
// Allow raw overrides
|
|
247
|
+
if (googleWallet.objectOverrides && typeof googleWallet.objectOverrides === 'object') {
|
|
248
|
+
Object.assign(passObject, googleWallet.objectOverrides);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
getFieldValue(key, passData) {
|
|
252
|
+
// Handle nested keys
|
|
253
|
+
const keys = key.split('.');
|
|
254
|
+
let value = passData;
|
|
255
|
+
for (const k of keys) {
|
|
256
|
+
if (value && typeof value === 'object' && k in value) {
|
|
257
|
+
value = value[k];
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
return '';
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Map common fields
|
|
264
|
+
if (key === 'site' && passData.type === 'parent') {
|
|
265
|
+
return passData.site || '';
|
|
266
|
+
}
|
|
267
|
+
if (key === 'carrier' && passData.type === 'child') {
|
|
268
|
+
return passData.carrier || '';
|
|
269
|
+
}
|
|
270
|
+
if (key === 'client' && passData.type === 'child') {
|
|
271
|
+
return passData.client || '';
|
|
272
|
+
}
|
|
273
|
+
if (key === 'status') {
|
|
274
|
+
return passData.status;
|
|
275
|
+
}
|
|
276
|
+
return value !== undefined && value !== null ? String(value) : '';
|
|
277
|
+
}
|
|
278
|
+
async generateSaveUrl(passObject, payloadKey) {
|
|
279
|
+
// Generate signed JWT for Google Wallet save URL
|
|
280
|
+
const baseUrl = 'https://pay.google.com/gp/v/save';
|
|
281
|
+
const objectId = passObject.id;
|
|
282
|
+
if (!this.config.serviceAccountPath) {
|
|
283
|
+
console.warn('⚠️ No service account - returning unsigned URL (will not work)');
|
|
284
|
+
return `${baseUrl}/${encodeURIComponent(objectId)}`;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
// Read service account
|
|
288
|
+
const serviceAccount = JSON.parse(await readFile(this.config.serviceAccountPath, 'utf-8'));
|
|
289
|
+
// Create JWT claims
|
|
290
|
+
const claims = {
|
|
291
|
+
iss: serviceAccount.client_email,
|
|
292
|
+
aud: 'google',
|
|
293
|
+
origins: [],
|
|
294
|
+
typ: 'savetowallet',
|
|
295
|
+
payload: {
|
|
296
|
+
// Embedding the full object enables a true end-to-end Save URL flow.
|
|
297
|
+
// (Class must still exist and your account must be allowed if issuer/class is in test mode.)
|
|
298
|
+
[payloadKey]: [passObject]
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
// Sign the JWT
|
|
302
|
+
const { default: jwt } = await import('jsonwebtoken');
|
|
303
|
+
const token = jwt.sign(claims, serviceAccount.private_key, {
|
|
304
|
+
algorithm: 'RS256'
|
|
305
|
+
});
|
|
306
|
+
return `${baseUrl}/${token}`;
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
console.error('Error generating signed JWT:', error);
|
|
310
|
+
return `${baseUrl}/${encodeURIComponent(objectId)}`;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async upsertInAPI(kind, payload) {
|
|
314
|
+
if (!this.auth) {
|
|
315
|
+
throw new Error('Google Auth not initialized');
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const client = await this.auth.getClient();
|
|
319
|
+
const baseUrl = 'https://walletobjects.googleapis.com/walletobjects/v1';
|
|
320
|
+
const resource = kind;
|
|
321
|
+
const url = `${baseUrl}/${resource}`;
|
|
322
|
+
await client.request({
|
|
323
|
+
url,
|
|
324
|
+
method: 'POST',
|
|
325
|
+
data: payload
|
|
326
|
+
});
|
|
327
|
+
console.log(`✅ ${kind} created in Google Wallet API`);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
if (error.response?.status === 409) {
|
|
331
|
+
// Object already exists, try to update it
|
|
332
|
+
console.log('ℹ️ Resource exists, updating...');
|
|
333
|
+
try {
|
|
334
|
+
const client = await this.auth.getClient();
|
|
335
|
+
const baseUrl = 'https://walletobjects.googleapis.com/walletobjects/v1';
|
|
336
|
+
const resource = kind;
|
|
337
|
+
const resourceId = payload.id;
|
|
338
|
+
await client.request({
|
|
339
|
+
url: `${baseUrl}/${resource}/${resourceId}`,
|
|
340
|
+
method: 'PUT',
|
|
341
|
+
data: payload
|
|
342
|
+
});
|
|
343
|
+
console.log(`✅ ${kind} updated in Google Wallet API`);
|
|
344
|
+
}
|
|
345
|
+
catch (updateError) {
|
|
346
|
+
console.error(`❌ Error updating ${kind}:`, updateError);
|
|
347
|
+
throw updateError;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
console.error(`❌ Error creating ${kind}:`, error.response?.data || error.message);
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Generate and add hero image with progress bar to the pass object
|
|
358
|
+
*
|
|
359
|
+
* Note: Google Wallet API requires publicly accessible URLs for images.
|
|
360
|
+
* This method generates the hero image and saves it locally.
|
|
361
|
+
* For production, upload images to a CDN/cloud storage and use those URLs.
|
|
362
|
+
*/
|
|
363
|
+
async addHeroImage(passObject, passData, profile) {
|
|
364
|
+
try {
|
|
365
|
+
if (profile.name !== 'logistics' && profile.name !== 'healthcare') {
|
|
366
|
+
// Loyalty (and any future profiles) skip hero images by default.
|
|
367
|
+
passObject.hexBackgroundColor = '#111827';
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// Generate hero image based on profile and status
|
|
371
|
+
let imageBuffer;
|
|
372
|
+
if (profile.name === 'logistics') {
|
|
373
|
+
imageBuffer = await generateLogisticsHeroImage(passData.status);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
imageBuffer = await generateHealthcareHeroImage(passData.status);
|
|
377
|
+
}
|
|
378
|
+
// Save image to local file system
|
|
379
|
+
const { writeFile } = await import('fs/promises');
|
|
380
|
+
const imagePath = join(__dirname, '..', '..', 'hero-images', `${passData.id}.png`);
|
|
381
|
+
// Ensure directory exists
|
|
382
|
+
try {
|
|
383
|
+
await writeFile(imagePath, imageBuffer);
|
|
384
|
+
console.log(`✨ Hero image saved: ${imagePath}`);
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
// Directory might not exist, that's OK - just skip for now
|
|
388
|
+
console.log(`ℹ️ Hero image generated (not uploaded - requires public URL)`);
|
|
389
|
+
}
|
|
390
|
+
// TODO: Upload to cloud storage and get public URL
|
|
391
|
+
// For now, we'll skip adding the hero image to the pass object
|
|
392
|
+
// since Google Wallet requires a publicly accessible URL
|
|
393
|
+
// If you have a public image hosting URL, uncomment and use:
|
|
394
|
+
// const publicUrl = `https://your-cdn.com/hero-images/${passData.id}.png`
|
|
395
|
+
// passObject.heroImage = {
|
|
396
|
+
// sourceUri: {
|
|
397
|
+
// uri: publicUrl
|
|
398
|
+
// }
|
|
399
|
+
// }
|
|
400
|
+
// Add background color based on status
|
|
401
|
+
const statusColors = {
|
|
402
|
+
ISSUED: '#4A90E2',
|
|
403
|
+
PRESENCE: '#F5A623',
|
|
404
|
+
SCALE: '#7B68EE',
|
|
405
|
+
OPS: '#50E3C2',
|
|
406
|
+
EXITED: '#7ED321',
|
|
407
|
+
SCHEDULED: '#4A90E2',
|
|
408
|
+
CHECKIN: '#F5A623',
|
|
409
|
+
PROCEDURE: '#E94B3C',
|
|
410
|
+
DISCHARGED: '#7ED321'
|
|
411
|
+
};
|
|
412
|
+
passObject.hexBackgroundColor = statusColors[passData.status] || '#4A90E2';
|
|
413
|
+
console.log(`✨ Dynamic color applied for status: ${passData.status} (${passObject.hexBackgroundColor})`);
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
console.error('⚠️ Failed to generate hero image:', error);
|
|
417
|
+
// Continue without hero image if generation fails
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async createClass(profile, passType) {
|
|
421
|
+
// Stub for creating a Google Wallet class
|
|
422
|
+
// In a real implementation, this would call the Google Wallet API
|
|
423
|
+
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);
|
|
427
|
+
// This would normally make an API call to:
|
|
428
|
+
// POST https://walletobjects.googleapis.com/walletobjects/v1/genericClass
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
export default GoogleWalletAdapter;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { CreateParentInput, CreateChildInput, CreateBusinessInput, CreateCustomerAccountInput, CreateLoyaltyProgramInput, IssueLoyaltyCardInput, UpdateLoyaltyPointsInput, LoyaltyBusiness, LoyaltyCustomerAccount, ParentPassData, ChildPassData, PassData, PassStatus, ProfileType, ProfileConfig, PassGenerationResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Get a profile by name
|
|
4
|
+
*/
|
|
5
|
+
export declare function getProfile(profileType: ProfileType): ProfileConfig;
|
|
6
|
+
/**
|
|
7
|
+
* List all available profiles
|
|
8
|
+
*/
|
|
9
|
+
export declare function listProfiles(): ProfileType[];
|
|
10
|
+
/**
|
|
11
|
+
* Create a business (tenant) that owns a loyalty program.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createBusiness(input: CreateBusinessInput): LoyaltyBusiness;
|
|
14
|
+
export declare function getBusiness(businessId: string): LoyaltyBusiness | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Create a customer account under a business.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createCustomerAccount(input: CreateCustomerAccountInput): LoyaltyCustomerAccount;
|
|
19
|
+
export declare function getCustomerAccount(customerId: string): LoyaltyCustomerAccount | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Define (or update) the loyalty program pass for a business.
|
|
22
|
+
* This creates a parent pass with profile=loyalty.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createLoyaltyProgram(input: CreateLoyaltyProgramInput): Promise<ParentPassData>;
|
|
25
|
+
/**
|
|
26
|
+
* Issue a loyalty card (child pass) for a customer.
|
|
27
|
+
* QR/barcode value uses memberId.
|
|
28
|
+
*/
|
|
29
|
+
export declare function issueLoyaltyCard(input: IssueLoyaltyCardInput): Promise<ChildPassData>;
|
|
30
|
+
/**
|
|
31
|
+
* Update points on a loyalty card.
|
|
32
|
+
*/
|
|
33
|
+
export declare function updateLoyaltyPoints(input: UpdateLoyaltyPointsInput): Promise<PassData>;
|
|
34
|
+
/**
|
|
35
|
+
* Create a parent schedule (PES or AppointmentBatch)
|
|
36
|
+
*/
|
|
37
|
+
export declare function createParentSchedule(input: CreateParentInput): Promise<ParentPassData>;
|
|
38
|
+
/**
|
|
39
|
+
* Create a child ticket (TO or PatientVisit)
|
|
40
|
+
*/
|
|
41
|
+
export declare function createChildTicket(input: CreateChildInput): Promise<ChildPassData>;
|
|
42
|
+
/**
|
|
43
|
+
* Update the status of a pass
|
|
44
|
+
*/
|
|
45
|
+
export declare function updatePassStatus(passId: string, newStatus: PassStatus): Promise<PassData>;
|
|
46
|
+
/**
|
|
47
|
+
* Get a pass by ID
|
|
48
|
+
*/
|
|
49
|
+
export declare function getPass(passId: string): PassData | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Get Apple Wallet .pkpass buffer for a pass
|
|
52
|
+
*/
|
|
53
|
+
export declare function getPkpassBuffer(passType: 'parent' | 'child', passData: PassData): Promise<Buffer>;
|
|
54
|
+
/**
|
|
55
|
+
* Get Google Wallet object for a pass
|
|
56
|
+
*/
|
|
57
|
+
export declare function getGoogleObject(passType: 'parent' | 'child', passData: PassData): Promise<{
|
|
58
|
+
object: any;
|
|
59
|
+
saveUrl: string;
|
|
60
|
+
}>;
|
|
61
|
+
/**
|
|
62
|
+
* Generate a complete pass with both Apple and Google wallet data
|
|
63
|
+
*/
|
|
64
|
+
export declare function generatePass(passData: PassData, options?: {
|
|
65
|
+
includeApple?: boolean;
|
|
66
|
+
includeGoogle?: boolean;
|
|
67
|
+
}): Promise<PassGenerationResult>;
|