Enhancing User Experience: Push Notifications in .NET for Apple Wallet Pass Updates
Apple Wallet Passes Passes are dynamic and they reflect real-world state. Information in Apple Wallet passes can be dynamically updated. Learn how to use Apple Push Notification Service and Web Service endpoint to keep Wallet Pass updated.
Table of Contents
Apple Wallet Passes Passes are dynamic and they reflect real-world state. Information in Apple Wallet passes can be dynamically updated.
Updating a Pass is easily done by distributing a new version of the Pass with the same identifier and serial number.
Alternatively, you can add an webServiceURL
and authentication Token keys in the initial Wallet Pass added to the device. The device then uses the service endpoint to communicate with the server about the changes to passes and to fetch the latest version of the Pass as it changes.
In this blog post, let's learn how to set up Push Notification updates to Apple Wallet Passes from a .NET application.
The high-level flow remains the same regardless of whichever programming language you use to build the application.
I will use the Lambda Annotations Framework to build these API endpoints. However, you can use your existing application hosting mechanism for this.
This article is sponsored by AWS and is part of my AWS Series.
Updating a Wallet Pass
The Device the Pass is added and the Pass Provider's web server works together to keep the Wallet Pass updated.
Below are the key communication steps between the device and the server to keep the Wallet Pass updated.
- The device registers for Pass Updates to the
webServiceURL
provided in the pass, as soon as the Pass is added. - On changes to the Pass on the Web server, it uses the device information obtained in the registration request to send a push notification for the Pass Type
- On receiving the Push Notification, the user device reaches out to the
webServiceURL
to get a list of passes that has changed - For each Pass that has been updated, it asks the server for the latest version of the Pass.
The above diagram summarizes the high-level request/response flow between the user device and the web server delivering the Apple Wallet Pass updates.
Implementing the Web Service Endpoint
The Apple PassKit Web Service Reference provides the API Endpoint specifications required to support Wallet Pass updates.
When creating a Pass file, we can set the WebServiceUrl
property to set the callback service endpoint.
var request = new PassGeneratorRequest
{
...
WebServiceUrl = "https://my-apple-wallet-pass-web-service-url.com/",
AuthenticationToken = "authentication-token-used-to-authenticate-calls-to-service"
...
}
You can set an AuthenticationToken
on the pass request, that will be passed back to the Web Service Url when the device calls it to get the pass updates. You can use this to authenticate requests coming to your endpoint.
Check out the post below on details on how to create an Apple Wallet Pass from a .NET application.
Below are the 5 different endpoint capabilities that the Apple PassKit Web Service Reference expects from the server implementing the web service.
Register Device
The Register Device endpoint is POST
method with the URL format as webServiceURL
/version
/devices/deviceLibraryIdentifier
/registrations/passTypeIdentifier
/serialNumber
Below is an API Endpoint using the Lambda Annotations framework
[LambdaFunction(Policies = "AWSLambdaBasicExecutionRole")]
[HttpApi(LambdaHttpMethod.Post, "/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber}")]
public async Task RegisterDeviceForPushNotificationForAPass(
string deviceLibraryIdentifier, string passTypeIdentifier, string serialNumber,
[FromBody] RegisterRequest pushToken)
{
// Save Device Registration Info to data store
}
Get Passes For Device
The Get Serial Numbers for Passes associated with a device is a GET
request to webServiceURL
/version
/devices/deviceLibraryIdentifier
/registrations/passTypeIdentifier
?passesUpdatedSince=tag
The GetPassesForDevice
method uses the deviceLibraryIdentifier and the passesUpdatesSince
(from query parameters) to fetch the Passes that has been updated since the specified date.
It returns an array of SerialNumber strings that the Apple device then uses to enumerate and call the GetLatestPass
method on.
[LambdaFunction(Policies = "AWSLambdaBasicExecutionRole")]
[HttpApi(LambdaHttpMethod.Get, "/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}")]
public async Task<GetPassesForDeviceResponse> GetPassesForDevice(
string deviceLibraryIdentifier, string passTypeIdentifier, [FromQuery] string passesUpdatedSince, ILambdaContext context)
{
var updatedSerialNumbers = await GetPassesUpdatedForDeviceSince(
deviceLibraryIdentifier, passesUpdatedSince);
return new GetPassesForDeviceResponse()
{
SerialNumbers = updatedSerialNumbers,
LastUpdated = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(),
};
}
public class GetPassesForDeviceResponse
{
public string LastUpdated { get; set; }
public string[] SerialNumbers { get; set; }
}
We can use any value for the LastUpdated
property when returning the result. In the example here I am using the UnixTimeMilliseconds as the value returned.
The very first time the Apple Device calls this endpoint it will pass in a numm value for the query parameter. Once we return back a valid response with the LastUpdated
property specified, the Apple Device sends that in the subsequent request.
Get Latest Pass
The Get Latest Pass is a GET
request to webServiceURL
/version
/passes/passTypeIdentifier
/serialNumber
The Getpass
endpoint method returns the latest pass information if it has been updated. The endpoint uses the header 'If-Modified-Since' value to determine if the server content is modified or not.
[LambdaFunction(Policies = "AWSLambdaBasicExecutionRole")]
[HttpApi(LambdaHttpMethod.Get, "/v1/passes/{passTypeIdentifier}/{serialNumber}")]
public async Task<APIGatewayHttpApiV2ProxyResponse> GetPass(
string passTypeIdentifier, string serialNumber,
[FromHeader(Name = "If-Modified-Since")] string ifModifiedSince, ILambdaContext context)
{
var pass = await GetPass(serialNumber, passTypeIdentifier);
if (DateTimeOffset.TryParse(ifModifiedSince, out var lastUpdated) && lastUpdated < pass.LastUpdated)
return new APIGatewayHttpApiV2ProxyResponse()
{
Body = Convert.ToBase64String(pass.PassContent),
IsBase64Encoded = true,
StatusCode = (int)HttpStatusCode.OK,
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/vnd.apple.pkpass" },
{ "Last-Modified", pass.LastUpdated.ToString("o") },
{ "Content-Disposition", "attachment; filename=ticket.pkpass; filename*=UTF-8''ticket.pkpass" }
}
};
else
return new APIGatewayHttpApiV2ProxyResponse()
{
StatusCode = (int)HttpStatusCode.NotModified
};
}
If the pass content is updated it returns the new pass information along with the Last-Modified
header value, which the Apple Device will send in as the 'If-Modified-Since' header value in the subsequent request.
For unmodified passes we just return a HttpResponse of NotModified
.
Unregister Device
The Unregister Device for Pass updates is a DELETE
request to webServiceURL
/version
/devices/deviceLibraryIdentifier
/registrations/passTypeIdentifier
/serialNumber
The Unregister Device endpoint is very similar to the Register endpoint, only that it uses the Delete
Http verb.
[LambdaFunction(Policies = "AWSLambdaBasicExecutionRole")]
[HttpApi(LambdaHttpMethod.Delete, "/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber}")]
public async Task UnRegisterDeviceForPushNotificationForAPass(
string deviceLibraryIdentifier, string passTypeIdentifier, string serialNumber,
[FromBody] RegisterRequest pushToken)
{
// Remove Device from the registered list of devices for the Serial Number
}
This is usually called when the pass is manually removed from the device and it no longer requires an update to be sent for that particular serial number.
In cases where the Pass is added to multiple devices, even if the Pass is removed from one device, there can still be other devices that are still interested in the pass updates.
Logging Errors
The Log endpoint to catch any errors in the other endpoints is a POST
request to webServiceURL/version/log
The Log endpoint is intended to help you debug your web service implementation. Any errors happening in the other Endpoints are sent to this generic endpoint so that we can debug and fix the issues in the endpoint.
[LambdaFunction(Policies = "AWSLambdaBasicExecutionRole")]
[HttpApi(LambdaHttpMethod.Post, "/v1/log")]
public async Task Log([FromBody] LogRequest logRequest)
{
// Log to standard logging
}
You can log this to your standard logging console or endpoint and track it from there for errors in the API Endpoint.
Apple Push Notification on Pass Updates
Any time the information associated with Wallet Pass changes on our server side, we need to notify the Devices that have the Passes added to their Wallet.
To notify devices we will use the Apple Push Notification Service (APNS).
dotAPNS is a NuGet library used to send pushes to Apple devices via the HTTP/2 APNs API, which is an officially recommended way of interacting with APNs (Apple Push Notification service).
The same Pass can be added to multiple devices - if the user decides to share the passes or has multiple devices of their own.
For any Pass update, we need to push notify all the devices that have that pass added, to update it in all of them.
Using the IApnsService
instance from the dotAPNS NuGet library and the PushToken received as part of the Register Device API endpoint, we can notify all the devices that have the updated pass added to the wallet.
[LambdaFunction(Policies = "AWSLambdaBasicExecutionRole")]
[HttpApi(LambdaHttpMethod.Post, "/apple-wallet/push-updates/{serialNumber}")]
public async Task PushUpdatesForPass(string serialNumber)
{
var deviceRegistrations = GetDeviceRegistrationForSerialNumber(serialNumber);
var applePushes = deviceRegistrations
.Select(device =>
new ApplePush(ApplePushType.Background)
.AddToken(device.PushToken)
.AddContentAvailable());
var apnsResponses = await applePushes
.Select(ap => _apnsService.SendPushes(ap.ToArray(),
_appleWalletConfiguration.PassbookCertificate()))
.WhenAll();
}
We use the Certificate-based authentication with APNS in this case.
Note that no serial number or other information is passed along in the Push Notification to the devices.
Since the certificate has the information on the Wallet Pass Type, it passes that on to the devices automatically when delivering the Push Notification.
The Push Notification triggers the device to call back to the WebServiceUrl
configured for the Pass to get a list of updated Passes and the individual Pass.
Using push notifications we can notify the devices any time a Pass changes in the background (on our server) and have the device call back to our server endpoint to get the latest Pass details.
Most of the data exchange happens directly between the user device and our Web Server.
Rahul Nath Newsletter
Join the newsletter to receive the latest updates in your inbox.