Apple Wallet Pass Series (Part 3): Setting up Updates
Learn how to implement dynamic updates for Apple Wallet passes using Node.js and web services
Apple Wallet Pass Series (Part 3): Setting up Updates
In the previous parts of this series, we navigated the labyrinth of certificate generation and crafted our first digital Apple Wallet pass. Now, it's time to take things a step further by enabling our passes to update dynamically. This means that once a user has added your pass to their wallet, you can push updates to it without any manual intervention from the user. Whether it's updating flight times, changing event details, or tweaking adding points to their loyalty card, dynamic updates enable you to make your passes relevant and useful.
But how do we make this magic happen? Let's dive into the world of web services, push notifications, and a bit of server-side wizardry.
Understanding Pass Updates
Before we get our hands dirty with code, let's understand how pass updates work as its not really an intuitive process.
-
User Installs the Pass: The user adds your pass to their Apple Wallet.
-
Device Registers for Updates: If your apple wallet has a
webServiceURL
, then the device registers itself with your server to receive updates for that pass. This event is triggered when the pass is added to the wallet for the first time. -
Pass Information Changes: Something changes on your end—perhaps a flight is delayed or a special offer is updated, this isn't immediatly reflected on the card, you must inititate the following process.
-
Server Sends Push Notification: Your server sends a push notification to the user's device, indicating that the pass has been updated.
-
Device Fetches Updated Pass: The device receives the notification and contacts your server to fetch the updated pass.
-
Pass is Updated in Wallet: The updated pass replaces the old one in the user's wallet.
In essence, updating passes is a collaborative dance between the device, your server, and Apple's push notification service.
Modifying Pass Generation
To start, we need to modify how we generated our pass in the previous post examine the code below:
const pass = await PKPass.from({
model: path.join(__dirname, "passModels", "BoardingPass.pass"),
certificates: {
wwdr,
signerCert,
signerKey,
// signerKeyPassphrase, // Include if your key is passphrase protected
},
}, {
serialNumber:serialNumber,
description: "Boarding Pass for Flight 815",
});
In this boarding pass we have the data being added to the pass as the serialNumber and the description. We need to add to the pass on creation a new property called the webServiceURL. This tells the pass what is the base url it should be calling when updating, registering and unregistering the pass. So, a modified version of the above code would look something like this. You may also want to setup the authenticaiton token to prevent your pass being updated by any other malicous servers.
const pass = await PKPass.from({
model: path.join(__dirname, "passModels", "BoardingPass.pass"),
certificates: {
wwdr,
signerCert,
signerKey,
// signerKeyPassphrase, // Include if your key is passphrase protected
},
}, {
serialNumber:serialNumber,
description: "Boarding Pass for Flight 815",
webServiceURL: "https://yourbackend.com/api",
authenticationToken: "somerandomlongstring",
});
Setting Up the Web Service
To enable pass updates, we'll need to set up a web service at our webServiceURL that handles:
- Registering and Unregistering Devices: Keeping track of which devices have your pass installed.
- Providing Updated Passes: Serving the latest version of the pass when requested.
According to Apple's Wallet Web Service Reference, we'll need to implement the following endpoints:
- Register a Device to Receive Push Notifications:
POST /v1/devices/<deviceLibraryIdentifier>/registrations/<passTypeIdentifier>/<serialNumber>
- Unregister a Device:
DELETE /v1/devices/<deviceLibraryIdentifier>/registrations/<passTypeIdentifier>/<serialNumber>
- Get the Serial Numbers for Passes Associated with a Device:
GET /v1/devices/<deviceLibraryIdentifier>/registrations/<passTypeIdentifier>?passesUpdatedSince=<tag>
- Get the Latest Version of a Pass:
GET /v1/passes/<passTypeIdentifier>/<serialNumber>
Endpoint Implementations
Device Registration
When a user adds your pass to their wallet, their device will attempt to register with your server for updates.
router.post(
"/v1/devices/:deviceLibraryIdentifier/registrations/:passTypeIdentifier/:serialNumber",
async (req, res) => {
const { deviceLibraryIdentifier, passTypeIdentifier, serialNumber } = req.params;
const authHeader = req.headers.authorization;
const { pushToken } = req.body;
// Replace with your actual authentication token on your backend
const AUTH_TOKEN = "ApplePass YOUR_AUTHENTICATION_TOKEN";
// Verify the authentication token
if (authHeader !== AUTH_TOKEN) {
return res.status(401).send("Unauthorized");
}
try {
// Store device and pass registration in MongoDB
const collection = db.collection("registrations");
await collection.updateOne(
{ deviceLibraryIdentifier },
{
$set: {
deviceLibraryIdentifier,
pushToken,
serialNumber,
passTypeIdentifier,
}
},
{ upsert: true }
);
res.sendStatus(201);
} catch (error) {
console.error("Error registering device:", error);
res.sendStatus(500);
}
}
);
Device Unregistration
When a user removes your pass from their wallet, their device will notify your server to unregister from updates.
router.delete(
"/v1/devices/:deviceLibraryIdentifier/registrations/:passTypeIdentifier/:serialNumber",
async (req, res) => {
const { deviceLibraryIdentifier } = req.params;
const authHeader = req.headers.authorization;
const AUTH_TOKEN = "ApplePass YOUR_AUTHENTICATION_TOKEN";
if (authHeader !== AUTH_TOKEN) {
return res.status(401).send("Unauthorized");
}
try {
const collection = db.collection("registrations");
const result = await collection.deleteOne({ deviceLibraryIdentifier });
if (result.deletedCount === 0) {
return res.status(404).send("No matching registration found");
}
res.sendStatus(200);
} catch (error) {
console.error("Error unregistering device:", error);
res.sendStatus(500);
}
}
);
Get Updated Passes
When you send a push notification indicating that passes have been updated, devices will call this endpoint to get the serial numbers of the passes that need updating.
router.get(
"/v1/devices/:deviceLibraryIdentifier/registrations/:passTypeIdentifier",
async (req, res) => {
const { deviceLibraryIdentifier } = req.params;
const passesUpdatedSince = req.query.passesUpdatedSince;
try {
const collection = db.collection("registrations");
const registration = await collection.findOne({ deviceLibraryIdentifier });
if (!registration) {
return res.status(404).send("No matching passes found for this device");
}
const { serialNumber, lastUpdated } = registration;
// Check if the pass has been updated since the provided tag
if (passesUpdatedSince && lastUpdated <= passesUpdatedSince) {
// No updates
return res.status(204).send();
}
res.status(200).json({
serialNumbers: [serialNumber],
lastUpdated: Date.now().toString(),
});
} catch (error) {
console.error("Error fetching updated passes:", error);
res.sendStatus(500);
}
}
);
Get Latest Pass Version
When a device knows which passes have been updated, it will call this endpoint to fetch the latest version.
router.get(
"/v1/passes/:passTypeIdentifier/:serialNumber",
async (req, res) => {
const { serialNumber } = req.params;
const authHeader = req.headers.authorization;
const AUTH_TOKEN = "ApplePass YOUR_AUTHENTICATION_TOKEN";
if (authHeader !== AUTH_TOKEN) {
return res.status(401).send("Unauthorized");
}
try {
// Generate the updated pass
const passBuffer = await generatePassBuffer(serialNumber);
res.setHeader("Content-Type", "application/vnd.apple.pkpass");
res.send(passBuffer);
} catch (error) {
console.error("Error generating pass:", error);
res.status(500).send("Error generating pass");
}
}
);
Pass Generation Helper
In the /v1/passes/:passTypeIdentifier/:serialNumber
endpoint, we called a function generatePassBuffer(serialNumber)
. Let's implement this function using the passkit-generator
library we used in the previous part.
const { PKPass } = require("passkit-generator");
const path = require("path");
const generatePassBuffer = async (serialNumber) => {
// Load your pass model and certificates
const pass = await PKPass.from({
model: path.join(__dirname, "passModels", "YourPassModel.pass"),
certificates: {
wwdr: await fs.readFile(
path.join(__dirname, "certs", "wwdr.pem"),
"utf8"
),
signerCert: await fs.readFile(
path.join(__dirname, "certs", "signerCert.pem"),
"utf8"
),
signerKey: await fs.readFile(
path.join(__dirname, "certs", "signerKey.key"),
"utf8"
),
},
});
// Update the pass data as needed
pass.serialNumber = serialNumber;
pass.barcode = {
format: "PKBarcodeFormatQR",
message: `Updated pass for serial number ${serialNumber}`,
messageEncoding: "iso-8859-1",
};
// Return the pass buffer
return await pass.getAsBuffer();
};
This is the preamble to allow passes to update at all, but this does not enable us to update them from the servers end. In essence, this allows a pass to update if the user manually updates it but doesn't allow the server to trigger a pass update on the iPhone. To trigger a manual update and test if this is working for you or not go to your apple wallet app on the phone the pass is installed on, then click the pass then the three dots on the top right hand corner, then pass details and on this page that shows the details swipe down and you will see a loader pop up and it should query the above endpoints and update the pass.