@calf/wallet
v0.0.1-alpha.1
Published
Apple Wallet and Google Wallet toolkit module of Calf framework.
Readme
@calf/wallet
Technical toolkit for Apple Wallet and Google Wallet integrations.
The package does not handle database persistence, routes, permissions, customer models, share tokens, device registration storage, or business decisions about when a pass should be updated. Applications map their own domain data to the inputs exported by this package.
Entry points
import { AppleWallet } from "@calf/wallet/apple";
import { GoogleWallet } from "@calf/wallet/google";
import { IWalletTextModule } from "@calf/wallet/shared";The root entry point also re-exports all Apple, Google and shared APIs.
Apple Wallet
import { AppleWallet } from "@calf/wallet/apple";
const appleWallet = AppleWallet.create({
passTypeIdentifier,
teamIdentifier,
organizationName,
certificate,
privateKey,
wwdrCertificate,
privateKeyPassphrase,
webServiceURL,
environment: "production"
});environment controls the APNs endpoint used by update notifications. Use "sandbox" for development passes signed with development credentials and "production" for production passes. The older production: false flag is still accepted for backward compatibility, but environment is preferred.
Create pass.json without packaging:
const passJson = appleWallet.passes.createJson({
serialNumber,
authenticationToken,
description: "Loyalty card",
logoText: "Company",
colors: {
backgroundColor: "rgb(198,15,31)",
foregroundColor: "rgb(255,255,255)",
labelColor: "rgb(255,255,255)"
},
barcode: {
format: "PKBarcodeFormatQR",
message: "123456"
},
fields: {
primary: [{ key: "points", label: "Points", value: "120" }],
secondary: [{ key: "name", label: "Name", value: "Jan Novak" }],
back: []
}
});Create a signed .pkpass package:
const passPackage = await appleWallet.passes.create({
serialNumber,
authenticationToken,
description: "Loyalty card",
colors: {
backgroundColor: "#c60f1f",
foregroundColor: "#ffffff",
labelColor: "#ffffff"
},
barcode: {
format: "PKBarcodeFormatQR",
message: "123456"
},
fields: {
primary: [{ key: "points", label: "Points", value: "120" }],
secondary: [{ key: "name", label: "Name", value: "Jan Novak" }],
back: []
},
assets: {
icon: iconBuffer,
logo: logoBuffer,
logo2x: logo2xBuffer,
logo3x: logo3xBuffer
}
});Apple asset keys are mapped to Wallet package file names:
icon->icon.pngicon2x->[email protected]icon3x->[email protected]logo->logo.pnglogo2x->[email protected]logo3x->[email protected]strip,thumbnailandfooterfollow the same@2xand@3xnaming patterncustomFilescan be used for additional package files keyed by exact file name
Send Apple Wallet update push notifications:
const results = await appleWallet.notifications.notifyPassUpdated({
pushTokens: ["..."]
});notifyPassUpdated uses passTypeIdentifier from AppleWallet.create(...) as the APNs topic. Pass passTypeIdentifier to notifyPassUpdated only when a single instance needs to notify a different pass type.
await appleWallet.notifications.notifyPassUpdated({
passTypeIdentifier: "pass.com.example.other",
pushTokens: ["..."],
environment: "sandbox"
});The application remains responsible for implementing Apple's Wallet web service routes and storing device registrations. This package only sends APNs update notifications for push tokens passed to it.
Create a field carrying the latest visible notification message:
const notificationField = appleWallet.notifications.createNotificationField({
message: {
id: "lunch",
title: "Lunch",
body: "We have free tables today."
},
label: "Last notification"
});
const passPackage = await appleWallet.passes.create({
...passInput,
fields: {
...passInput.fields,
back: [
...(passInput.fields.back || []),
notificationField
]
}
});
await appleWallet.notifications.notifyPassUpdated({
pushTokens
});Apple Wallet APNs update pushes do not carry a custom notification text. They tell Wallet to fetch the updated pass from the application's Wallet web service. The visible notification comes from the pass update itself, usually from a changed field with changeMessage.
Google Wallet
import { GoogleWallet } from "@calf/wallet/google";
const googleWallet = GoogleWallet.create({
issuerId,
serviceAccount,
origins: ["https://example.com"]
});serviceAccount is the parsed Google service account JSON object. The package also accepts serviceAccountPath when the application wants the library to read the JSON file:
const googleWallet = GoogleWallet.create({
issuerId,
serviceAccountPath: "/secure/google-wallet-service-account.json",
origins: ["https://example.com"]
});The service account must contain at least client_email and private_key and must have Wallet Object issuer permissions.
Create or patch a loyalty class:
await googleWallet.loyaltyClasses.upsert({
classId,
issuerName: "Company",
programName: "Loyalty card",
reviewStatus: "UNDER_REVIEW",
hexBackgroundColor: "#c60f1f",
logo: {
uri: "https://example.com/logo.png",
description: "Logo"
}
});classId and objectId must be fully qualified Google Wallet IDs, for example issuerId.my-class and issuerId.my-object. The package does not prefix local IDs with issuerId, because applications usually need stable control over ID generation and persistence.
Create or patch a loyalty object:
await googleWallet.loyaltyObjects.upsert({
objectId,
classId,
state: "ACTIVE",
accountId: "123456",
accountName: "Jan Novak",
barcode: {
type: "QR_CODE",
value: "123456"
},
textModules: [
{ id: "points", header: "Points", body: "120" }
]
});Create an Add to Google Wallet save URL:
const saveLink = googleWallet.saveLinks.create({
loyaltyObjects: [{ id: objectId, classId }]
});
console.log(saveLink.url);Google must be able to fetch image URLs over public HTTPS. This package does not host assets or validate that an image URL is publicly reachable.
Send a Google Wallet message and trigger a push notification:
await googleWallet.notifications.notifyObject({
objectId,
message: {
id: "lunch",
title: "Lunch",
body: "We have free tables today."
}
});For class-level notifications:
await googleWallet.notifications.notifyClass({
classId,
message: {
id: "campaign-2026-06",
title: "June offer",
body: "Show your card today and get an extra reward."
}
});Google Wallet notifications use the Add Message API with messageType: "TEXT_AND_NOTIFY" by default. Google controls the lock-screen notification appearance, users must have Wallet notifications enabled, and Google documents a limit of 3 notification-triggering messages per pass in 24 hours. Use messageType: "TEXT" when you want to add a message without triggering a push notification.
Updating Existing Passes
The application decides when a pass should be updated and persists its own timestamps. A typical loyalty points update looks like this:
// 1. Application updates its own domain data.
client.clientPoints = 180;
await ClientModel.updateOne({ _id: client._id }, { $set: { clientPoints: client.clientPoints } });
// 2. Application loads its own Wallet metadata.
const pushTokens = client.appleWallet.registrations.map((registration) => registration.pushToken);
const objectId = client.googleWallet.objectId;
const classId = client.googleWallet.classId;
// 3. Apple Wallet devices are told to fetch the updated pass from the app's web service.
await appleWallet.notifications.notifyPassUpdated({
pushTokens
});
// 4. Google Wallet object is patched or upserted with mapped data.
await googleWallet.loyaltyObjects.upsert({
objectId,
classId,
state: "ACTIVE",
accountId: client.clientCardIdentifier,
accountName: client.name || "-",
loyaltyPoints: {
label: "Points",
balance: { string: String(client.clientPoints || 0) }
},
textModules: [
{ id: "points", header: "Points", body: String(client.clientPoints || 0) }
]
});
// 5. Application stores its own update metadata.
await ClientModel.updateOne({ _id: client._id }, { $set: { "wallet.lastUpdatedAt": new Date() } });The model names above are only an application-side example. They are not part of this package and are not runtime dependencies.
Migration/reference mapping
The implementation was derived from the existing backend Wallet services, but the public API is generalized:
IAppleWalletPassConfigbecameIAppleWalletConfigfor platform credentials and identifiers.IAppleWalletPassJson, Apple barcode, field, file map and package concepts were kept as generic Apple Wallet types.- Apple color normalization from customer settings was generalized: both
#rrggbbandrgb(r,g,b)are accepted. - Google environment config became
IGoogleWalletConfig, withissuerId,serviceAccountorserviceAccountPath, andorigins. - Google JWT claims and save link response became
IGoogleWalletJwtClaimsandIGoogleWalletSaveLink. - Google loyalty class/object payload creation is now driven by explicit
IGoogleWalletLoyaltyClassInputandIGoogleWalletLoyaltyObjectInput.
Application responsibility remains outside the package:
- customer/client models such as
IClient - Mongo/Mongoose documents and
_id clientCardIdentifier,clientPoints,walletShare- local Apple authentication tokens and serial number storage
- Apple Wallet device registration routes and storage
- pass status, download/update timestamps and device counts
- deciding when a pass should be updated
- serving Google Wallet image assets
Example domain mapping in an application:
const appleInput = {
serialNumber: client.appleWallet.serialNumber,
authenticationToken: client.appleWallet.authenticationToken,
description: settings.description,
colors: {
backgroundColor: settings.backgroundColor,
foregroundColor: settings.textColor,
labelColor: settings.textColor
},
barcode: {
format: "PKBarcodeFormatCode128" as const,
message: client.clientCardIdentifier,
altText: client.clientCardIdentifier
},
fields: {
header: [{ key: "points", label: "Points", value: String(client.clientPoints || 0) }],
secondary: [{ key: "name", label: "Customer", value: client.name || "-" }]
},
assets: {
icon: defaultIcon,
logo: settingsAppleLogo
}
};
const googleObjectInput = {
objectId: client.googleWallet.objectId,
classId: client.googleWallet.classId,
state: "ACTIVE" as const,
accountId: client.clientCardIdentifier,
accountName: client.name || "-",
barcode: {
type: "CODE_128" as const,
value: client.clientCardIdentifier,
alternateText: client.clientCardIdentifier
},
loyaltyPoints: {
label: "Points",
balance: { string: String(client.clientPoints || 0) }
}
};