Skip to main content

Integration from Apple Wallet using Wallet Extensions

Overview

Apple mandates that you must integrate with Apple's Wallet Extensions before distributing an app that supports push provisioning.

These extensions enable the Apple Wallet to discover potential cards in your app to add the wallet, and provisions them from within Apple Wallet without launching your app.

To implement this, you'll need to provide two extensions:

  • Non-UI extension: An extension that is queried from Apple Wallet to fetch the required card metadata, and to provision the card.
  • UI Extension: An extension to allow the user to sign-in to their account within your app, if it's necessary.
note

The user must launch your app at least once before the extensions are recognized by Apple Wallet.

The following diagram outlines how Apple Wallet and your app interact through these extensions:

Set up your project for Apple Wallet Extensions

Create the App Extensions

Navigate to your project's file on Xcode, and click on the plus icon tagged "Add a target" In the dialog that appears, select Intents Extension as the template for your target.

Create intents extension

Upon tapping next, you'll need to set the following values:

Create intents extension

  • Product Name: WalletExtension
  • Team: Select your Apple developer organisation
  • Language: Select Swift
  • Starting point: Leave Messaging
  • Include UI Extension: Keep it selected
  • Project: Ensure your project is selected
  • Embed in Application: Ensure your app is selected

After clicking Finish, you should see two new folders within your project containing the code for the Extensions.

Once the Extensions are created, please record your app, and Extensions bundle identifiers, and share them with our support team so we can add them to the allow-list for provisioning. For example:

Bundle Identifier
ABCDE12345.com.weavr.app
ABCDE12345.com.weavr.app.WalletExtension
ABCDE12345.com.weavr.app.WalletExtensionUI
tip

Note that the prefix ABCDE12345 is your team's developer ID.

Configure the App Extensions

With the Extensions created you'll need to configure them to work as Apple Wallet extensions rather than Messages extensions. To do so, you'll need to ensure they have the following as dependencies:

  • PassKit.framework
  • For the Non-UI Extension:
    • The Weavr Provisioning SDK, via the .xcframework file provided
    • Phyre's SDK, by adding the pod 'ApplePayProvisioning', '5.0.0' to the Non-UI Extension target
    • MeaWallet's SDK, via the .xcframework file provided

Notice you'll need to remove Intents.framework and IntentsUI.framework from the dependency list as these are added by the Messaging template.

Once the dependencies are set up, you need to ensure that you have the correct capabilities configured on each Extension. The following tables outline the capabilities needed for each Extension:

App

CapabilitySet upReason
App groupsgroup.your.apps.bundle.idTo share storage between your app and extensions so they can read each other's data.
In-App ProvisioningSo the card can be provisioned from your Extensions.
WalletAllow all team pass typesSo the app can check if a card is already added.

Non-UI Extension

CapabilitySet upReason
App groupsgroup.your.apps.bundle.idTo share storage between your app and extensions so they can read each other's data.
In-App ProvisioningSo the card can be provisioned from your Extensions.

UI Extension

CapabilitySet upReason
App groupsgroup.your.apps.bundle.idTo share storage between your app and extensions so they can read each other's data.
In-App ProvisioningSo the card can be provisioned from your Extensions.

Configure the Non-UI Extension

Update the Info.plist file of the Extension as per below:

Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.PassKit.issuer-provisioning</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).IntentHandler</string>
</dict>
</dict>
</plist>

This informs Apple Wallet that the Extension integrates with it via the NSExtensionPointIdentifier, and specifies which class provides the implementation; in the example this is the IntentHandler.

In order for the IntentHandler to be compatible, you'll need it to implement the PKIssuerProvisioningExtensionHandler protocol.

Configure the UI Extension

Similar to the Non-UI Extension, update the Info.plist as follows:

Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.PassKit.issuer-provisioning.authorization</string>
</dict>
</dict>
</plist>

In this case, rather than declaring NSExtensionPrincipalClass, you are declaring NSExtensionMainStoryboard to be MainInterface. As a result, when the Extension triggers, Apple launches the initial view controller found inside the relevant storyboard.

In order for the initial view controller to be compatible, you'll need it to implement the PKIssuerProvisioningExtensionAuthorizationProviding protocol.

Configure the App Extensions in Apple Developer portal

Next you'll need to register both App Extension bundle identifiers in the Apple Developer portal Identifiers section. Create provisioning profiles accordingly, with the capabilities required for each Extension.

If you have not already done so, create a new App Group, so that the app and Extensions can share storage between them. Normally, group.team-id.bundleid is the format used to identify a new group. You must ensure this value matches the App Groups of both the App, the Non-UI Extension, and the UI Extension.

Implement the App Extensions

Shared storage between app and Extensions

To ensure the proper functioning of the Wallet Extensions, your app and its Extensions must share access to certain data, such as authentication tokens and card information. This shared storage is essential for the provisioning flow, as it allows Extensions to access the data fetched or stored by your app.

Apple provides App Groups as a mechanism to enable shared storage between an app and its Extensions. By configuring App Groups, you can ensure that both your app and Extensions can read and write to the same storage location.

The implementation of shared storage varies from app to app, so the specific details are not be covered in this guide. However, it's recommend you consider the following steps when implementing this storage:

  1. Configure Shared Storage:

    • Update your storage or database layer to access storage provided by App Groups.
    • Ensure that the App Group identifier (e.g., group.your.app.group) is correctly set up in your app and Extensions.
  2. Data Migration (if needed):

    • If your app is already storing data in a different location, consider implementing a migration process to transfer it to the shared storage.
  3. Reuse Domain Models and Storage Classes:

    • Make your domain models and storage classes accessible to both your app and Extensions. This ensures consistency in how data is serialized and deserialized.
  4. Test Shared Storage in the App:

    • Verify that your app can write to and read from the shared storage without issues.
  5. Test Shared Storage in Extensions:

    • Confirm that your Extensions can access the data written by the app and perform their required operations.

Below are reference examples of how to use UserDefaults and FileManager to interact with shared storage:

// Using UserDefaults for Shared Storage
let sharedDefaults = UserDefaults(suiteName: "group.your.app.group")
sharedDefaults?.set("value", forKey: "key")
let value = sharedDefaults?.string(forKey: "key")

// Using FileManager for Shared Storage
if let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.your.app.group") {
let fileURL = containerURL.appendingPathComponent("sharedData.json")
try? "data".write(to: fileURL, atomically: true, encoding: .utf8)
let data = try? String(contentsOf: fileURL, encoding: .utf8)
}

By following these steps and leveraging shared storage, you can ensure seamless communication and data sharing between your app and its Extensions.

Implement the UI Extension

The overarching requirement of the UI Extension is that you need to have (as the initial view controller of the Storyboard) a class that implements PKIssuerProvisioningExtensionAuthorizationProviding.

The main requirement of this protocol is to include a completionHandler as follows:

MyViewController.swift
import PassKit
import UIKit

class MyViewController: UIViewController, PKIssuerProvisioningExtensionAuthorizationProviding {

var completionHandler: ((PKIssuerProvisioningExtensionAuthorizationResult) -> Void)?

// ...
}

As part of this implementation, you'll need to:

  1. Display your App's login flow
  2. Await for the user to enter their credentials
  3. Sign-in the user
  4. Store your token in shared storage
  5. Download the list of cards for the user
  6. Store the cards in shared storage
  7. Call the completion handler and inform Apple Wallet of the result of the login flow - Either .canceled, or .authorized.
danger

App UI extension has a memory limit of 60 MB, developers are responsible to optimize the code and libraries to fit this requirement.

Implement the Non-UI Extension

The Non-UI Extension requires a class to be provided that implements PKIssuerProvisioningExtensionHandler.

danger

The Non-UI extension has a memory limit of 55 MB, developers are responsible to optimize the code and libraries to fit this requirement.

There is also a time limit to complete each extension call by Apple.

PKIssuerProvisioningExtensionHandler consists of 4 methods, implemented in the next snippet. Part of the implementation depends on how you store data and what your domain models look like. The parts that need implementing have been extracted and highlighted with a // MARK: tag. As a result, you can copy this code into your IntentHandler, complete the missing parts, and start provisioning.

IntentHandler.swift
import PassKit
import WeavrPushProvisioning

class IntentHandler: PKIssuerProvisioningExtensionHandler {

// Determines if there is a pass available and if authentication is required before adding it.

// The completion block return value, PKIssuerProvisioningExtensionStatus, has the following properties:
// requiresAuthentication: Bool - Whether authorization is required before passes can be added.
// passEntriesAvailable: Bool - Whether at least one pass is available to add in the iPhone.
// remotePassEntriesAvailable: Bool - Whether at least one pass is available to add in the remote device (Apple Watch).

// The completion callback should be invoked within 100ms. The extension is not displayed to the user
// in Apple Wallet if you exceed this time.
override func status(completion: @escaping (PKIssuerProvisioningExtensionStatus) -> Void) {
let status = PKIssuerProvisioningExtensionStatus()

// Requires Authentication is true if there's not token, or if it is expired
let token = loadToken()
status.requiresAuthentication = token?.isExpired() ?? true

let storedCardData = loadCards()

// If one card can be found that can be added to the phone, set passEntriesAvailable to true.
status.passEntriesAvailable = storedCardData.first { card in
return WPPComponents.getCardStatusInWallet(
forCardWithLastFourDigits: card.cardNumberLastFour,
deviceType: .phone
) == .notAdded
} != nil

// If one card can be found that can be added to the phone, set remotePassEntriesAvailable to true.
status.remotePassEntriesAvailable = storedCardData.first { card in
return WPPComponents.getCardStatusInWallet(
forCardWithLastFourDigits: card.cardNumberLastFour,
deviceType: .watch
) == .notAdded
} != nil

completion(status)
}

// Finds the list of passes (cards) that can be added to an iPhone.

// The result expected by Apple is a `[PKIssuerProvisioningExtensionPassEntry]` representing
// the cards (passes in Apple's language) that are available to add to Wallet.

// PKIssuerProvisioningExtensionPaymentPassEntry has the following properties:
// art: CGImage - image representing the card displayed to the user. The image must have square corners and should not include personally identifiable information like user name or account number.
// title: String - a name for the pass that the system displays to the user when they add or select the card.
// identifier: String - The ID of the card
// addRequestConfiguration: PKAddPaymentPassRequestConfiguration - the configuration data used for setting up and displaying a view controller that lets the user add a payment pass.

// You have to avoid returning cards that are already present in the user’s Wallet, therefore you must ensure their status is `notAdded`.
// The method should return within 20 seconds or the attempt will be halted and treated as a failure.
override func passEntries() async -> [PKIssuerProvisioningExtensionPassEntry] {
guard let token = loadToken()?.rawToken else {
return []
}

return await generatePassEntries(
authenticationToken: token,
cards: loadCards(),
deviceType: .phone
)
}

// Remote pass entries has the same behaviour as passEntries, but targets cards that can be
// Added to the Apple Watch.
override func remotePassEntries() async -> [PKIssuerProvisioningExtensionPassEntry] {
guard let token = loadToken()?.rawToken else {
return []
}

return await generatePassEntries(
authenticationToken: token,
cards: loadCards(),
deviceType: .watch
)
}

// identifier: String - an internal value the issuer uses to identify the card.
// configuration: PKAddPaymentPassRequestConfiguration - the configuration the system uses to add a secure pass. This configuration is prepared in methods passEntriesWithCompletion: and remotePassEntriesWithCompletion:.
// certificates, nonce, nonceSignature - parameters are generated by Apple Pay identically to PKAddPaymentPassViewControllerDelegate methods.

// The completion handler is called by the system for the data needed to add a card to Apple Pay.
// This handler takes a parameter request of type PKAddPaymentPassRequestConfiguration that contains the card data the system needs to add a card to Apple Pay.

// The continuation handler must be called within 20 seconds or an error is displayed.
// Subsequent to timeout, the continuation handler is invalid and invocations is ignored.
override func generateAddPaymentPassRequestForPassEntryWithIdentifier(
_ identifier: String,
configuration: PKAddPaymentPassRequestConfiguration,
certificateChain certificates: [Data],
nonce: Data,
nonceSignature: Data,
completionHandler completion: @escaping (PKAddPaymentPassRequest?) -> Void) {

let authenticationToken = "authenticationToken"

WPPComponents.addPaymentPass(authenticationToken: authenticationToken, clientPaymentCardId: identifier, certificates: certificates, nonce: nonce, nonceSignature: nonceSignature, completionHandler: completion)
}

// MARK: Helper functions

// You can leave this function as is, it's just extracting common code
// between passEntries and remotePassEntries
func generatePassEntries(
authenticationToken: String,
cards: [Card],
deviceType: DeviceType
) async -> Array<PKIssuerProvisioningExtensionPassEntry> {
let entries = cards.compactMap { card -> PassEntryInfo? in
let canAddCard = .notAdded == WPPComponents.getCardStatusInWallet(
forCardWithLastFourDigits: card.cardNumberLastFour,
deviceType: deviceType
)

// Compact map removes nil from the array
guard canAddCard else {
return nil
}

return PassEntryInfo(
cardId: card.id,
cardholderName: card.cardholder,
panLastFour: card.cardNumberLastFour,
cardDescription: card.friendlyName,
cardImageURL: URL(string: card.digitalArtworkUrl)
)
}

return await WPPComponents.makePassEntriesForCardsWith(
authenticationToken: authenticationToken,
entries: entries.compactMap { $0 },
// TODO Provide your own default card. Consider handling nil more elegantly
imageFallback: UIImage(named: "default-card")!.cgImage!
)
}

// MARK: To be implemented

// TODO Replace with your own models
struct Card {
let id: String
let cardNumberLastFour: String
let cardholder: String
let friendlyName: String
let digitalArtworkUrl: String
}

// TODO Replace with your own models
struct Token {
let rawToken: String

func isExpired() -> Bool {
return false
}
}

func loadCards() -> [Card] {
// TODO implement loading cards from your shared storage
return []
}

func loadToken() -> Token? {
// TODO implement loading cards from your shared storage
return Token(rawToken: "faked token")
}
}