Wallet Extensions

Learn how to set up Wallet Extensions for an iOS issuer app.

Übersicht

This page offers a guide on implementing the Wallet Extensions feature for an iOS issuer app (such as a mobile banking application). Wallet Extensions allow the user to see a list of all payment passes available for provisioning from an issuer application installed on their device, directly within Apple Wallet.

Users can start the provisioning process with a single tap and add a card to Apple Wallet without entering details manually. This experience of Wallet Extensions makes it easier for users to add a payment pass, granted by an issuer, to Apple Pay—directly within Apple Wallet.

There are multiple ways a user can add credit or debit cards to Apple Pay. However, this guide will focus on Wallet Extensions and the prerequisites for the feature (such as In-App Provisioning).

This guide will, also, offer instructions on configuring both the entitlements and app extensions required for Wallet Extensions. The Wallet Extensions feature is an iOS feature that can be implemented using Swift or Objective-C, but this guide focuses on a Swift implementation. This guide assumes that you understand the basics of programming in Swift.

Requirements

  • iOS devices running iOS 14 or later.
  • Xcode 15 or later.

Sample Application Code

The sample code referenced throughout this document is a complete implementation of Wallet Extensions. You can download the full sample Xcode project from the Implementing Wallet Extensions page on the Apple Developer Portal.

Note that because Wallet Extensions require an entitlement from Apple, you can’t compile this sample source as-is, until you have received this entitlement.

Provisioning

Apple Pay offers multiple provisioning features, including In-App Provisioning and Wallet Extensions. These two provisioning features can streamline your users’ experience of adding cards to Apple Wallet.

In-App Provisioning

Apple Pay In-App Provisioning enables issuers of transit, access, and payment cards to provision into Apple Wallet directly from the issuer’s iOS app. Users find In-App Provisioning an especially convenient method to add cards to Apple Wallet on iOS devices, as provisioning avoids users having to enter details manually.

Issuers find In-App Provisioning an effective component of a seamless iOS experience that creates a popular interface for card provisioning, while also offering a unified experience with their other services.

Another important use case is enabling provisioning for cards that have account details that are not embossed on the physical card. In-App Provisioning is the only way to initiate the provisioning of such cards into Apple Wallet for accounts that the issuer provides directly to a user’s iOS device.

In-App Provisioning relies on the user to add a payment pass from within the issuer’s iOS app. The user needs to be intentionally, already in the issuer app to initiate the In-App Provisioning experience via a button tap. Since this provisioning process starts and finishes within the issuer app, this process provides a seamless flow for the user to add a card to Apple Wallet.

Below is the In-App Provisioning experience from a sample issuer app for payment cards, where the user taps the "Add to Apple Wallet" button and the provisioning process begins. The In-App Provisioning process surfaces a user’s details to provide a convenient experience of adding their cards to Apple Pay.

In-App Provisioning

Wallet Extensions

Apple Pay In-App Provisioning relies on the user to add a payment pass from within the issuer app, and the user needs to already be in the issuer app with the intention of adding the pass. To make it easier for users to add a payment pass to Apple Pay directly within Apple Wallet, Apple Pay offers extensions for In-App Provisioning, called Wallet Extensions. Wallet Extensions improve discoverability of the option to add a payment pass to Apple Wallet, as the user can tap a button within Apple Wallet to begin the provisioning flow.

The Wallet Extensions feature relies on two extensions, a UI extension and a non-UI extension. The issuer app needs a non-UI extension to report on the status of the extension, passes the app has available to add, and to perform the card data lookup—just like when adding payment passes to Apple Wallet from within the issuer app.

As for the UI extension, the issuer app needs a UI extension to perform authentication of the user if the non-UI extension reports that authentication is required. The UI extension isn’t a redirect to the issuer app, but a separate screen that uses the same issuer app login credentials.

The Wallet Extensions experience starts in Apple Wallet. Note: A user has to open and log in to the issuer app at least once for Apple Wallet to detect the extensions. First, a user taps the add button (+) in the top right corner of Apple Wallet. The next screen Wallet displays to the user includes a "From Apps on Your iPhone" section. Issuer apps that have implemented Wallet Extensions will appear in a list under the "From Apps on Your iPhone" section if the issuer app has at least one available payment pass that is not currently in Wallet. A user can, then, tap on a listed issuer to begin the provisioning experience using the issuer’s Wallet Extension. The extension will return user to Wallet automatically once the provisioning is complete.

Wallet Extensions

Documentation for In-App Provisioning

The implementation of In-App Provisioning is a prerequisite to the configuration of Wallet Extensions. This demo guide only offers documentation on configuring Wallet Extensions. Please contact your Apple project contact to request In-App Provisioning documentation and instructions.

Wallet Extensions Flow

This section provides a top-level guide of the Wallet extensions process. Apple Wallet triggers all the methods. The ability of the extensions to trigger behavior is based solely on completion handlers and return values. Scroll through the steps below to learn more.

Wallet Extensions flow
  1. The user initiates provisioning from Apple Wallet: The user initiates adding a payment pass in Apple Wallet by tapping the add button (+) in the top right of Apple Wallet. The user must open and log in to the issuer app at least once for Apple Wallet to detect the extensions.
  2. Apple Wallet to the issuer app: The principal class of the non-UI extension needs to be a subclass of PKIssuerProvisioningExtensionHandler. Apple Wallet uses PKIssuerProvisioningExtensionStatus, which is passed to the extension handler, to interrogate the issuer app. The interrogation determines the availability of the extension, whether any payment passes are available to add, and whether adding a payment pass requires authentication.
  3. The issuer app to Apple Wallet: The status(completion:) method within PKIssuerProvisioningExtensionHandler indicates whether a payment pass is available to add to Apple Pay and whether adding the pass requires authentication. The issuer app icon displays in Apple Wallet under "From Apps on Your iPhone" if the user logged in the issuer app at least once and there is at least one payment pass available to add to Apple Pay.
    1. passEntriesAvailable: The issuer app declares whether a payment pass is available to add to an iPhone.
    2. remotePassEntriesAvailable: The issuer app declares whether a payment pass is available to add to an Apple Watch.
    3. requiresAuthentication: The issuer app declares whether adding a payment pass requires the user to authenticate in the authorization UI extension that the issuer app provides.
  4. The user selects the issuer app in Apple Wallet: The user can proceed with the issuer app’s extension by tapping the listed issuer under the "From Apps on Your iPhone" section of Apple Wallet.
  5. Apple Wallet to the issuer app: Apple Wallet uses PKIssuerProvisioningExtensionAuthorizationResult to interrogate the issuer app for determining the user’s authorization status. The authorization UI extension extends a UIViewController from the issuer app to perform authentication. The UI extension needs to be a subclass of PKIssuerProvisioningExtensionAuthorizationProviding.
  6. The issuer app to the user: The issuer app performs authentication of the user as part of their existing security framework, such as Face ID, Touch ID, or another authentication method subject to issuer requirements. Many issuer apps rely on biometric authentication like Face ID and Touch ID to provide the most seamless experience for the user.
  7. The issuer app to Apple Wallet: The result of the completionHandler, an instance property of PKIssuerProvisioningExtensionAuthorizationProviding, indicates whether the user has authorization to add a payment pass.
    1. authorized: The issuer declares that the user successfully authorized adding a payment pass.
    2. canceled: The issuer declares that the user canceled authorization or doesn’t have authorization to add the payment pass.
  8. Apple Wallet to the issuer app: After authorization, Apple Wallet uses PKIssuerProvisioningExtensionPaymentPassEntry to interrogate the issuer app for determining the list of payment passes available to add to the user’s iPhone and Apple Watch.
  9. The issuer app to Apple Wallet: The completion handlers return an array of payment passes that are available to add to the user’s iPhone ( passEntries(completion:) ) and Apple Watch ( remotePassEntries(completion:) ). This list of eligible payment passes displays to the user.
    1. art: The image representing the card that displays to the user. The image needs to have square corners and can’t include personally identifiable information like user’s name or account number. See the card art requirements available in the Functional Requirements for Apple Pay and Direct NFC Access. Please contact your Apple project contact, PNO, or service provider relationship manager for documentation on Functional Requirements for Apple Pay and Direct NFC Access.
    2. title: The name of the payment pass that displays to the user.
    3. identifier: An internal value the issuer uses to identify the card. This identifier needs to be unique.
    4. PKAddPaymentPassRequestConfiguration: The configuration data for setting up and displaying a view controller that lets the user add a payment pass.
  10. The user selects the passes to add: The user selects one or more cards to provision.
  11. Apple Wallet to the issuer app: Apple Wallet uses the completion handler generateAddPaymentPassRequestForPassEntryWithIdentifier to interrogate the issuer app for the PKAddPaymentPassRequest and supplies the identifier, configuration data, certificates, nonce, and nonce signatures for the selected payment passes.
  12. The issuer app to Apple Wallet: The completion handler, then, returns a PKAddPaymentPassRequest, which Apple Wallet uses to provision the pass.

Allow Listing and Entitlements

An entitlement is a right or privilege that grants an executable permission to use a service or technology. For example, the entitlement for In-App Provisioning grants issuers of transit, access, and payment cards the permission to provision into Apple Wallet. Entitlements are stored as key-value pairs that are embedded in the code signature of an app’s binary executable. When code signing your app, Xcode combines the project’s entitlements file, information from your developer account, and other project information to apply a final set of entitlements to your app.

Request Submission

The Wallet Extensions feature requires the same private entitlement as In-App Provisioning:

com.apple.developer.payment-pass-provisioning

This private entitlement is issued by Apple and the issuer app must include this entitlement within the Wallet Extensions process. Issuer apps that have already implemented In-App Provisioning successfully do not need to request this entitlement again for Wallet Extensions, but do need to ensure that both the In-App Provisioning and Wallet Extensions processes have a copy of the entitlement.

To request the private entitlement, as well as allow listing for the issuer app, send the following information to [email protected].


Subject

Apple Pay Entitlement and Allow List Request - Issuer Name - [Country Code]
Such as Apple Pay Entitlement and Allow List Request - MyBank - [DE]

Body
  • Issuer Name: such as MyBank
  • Country [Country Code]: such as Germany [DE]
  • Team ID: such as 1234ABCD
  • Adam ID: such as 1234567890
  • App Name: such as MyBanking

Team ID

Apple assigns developers their Team ID, which is a unique ID of a specific development team. The issuer’s Team ID value is alphanumeric, and is available on your Account page in Developer Portal.

Adam ID

Apple assigns the Adam ID, which is the unique ID of the issuer app. The issuer’s Adam ID value is numeric, and is available in the “General: App Information” tab on your App page in App Store Connect.

Create Extension Targets

To configure entitlements, you first need to create new targets for the Non-UI and UI extensions.

  1. To add a new target to your Xcode app project, choose File > New > Target.

  2. In the Multiplatform bar at the top of the new target dialog, choose iOS. In the center pane of the dialog, Xcode displays the templates you can choose. For example, the following figure shows the templates you can use to create an iOS app extension. There is not a template that exists specifically for Wallet Extensions, however, you can select the Action Extension template and click Next to get started.

    New Target Options Dialog
  3. In the next dialog, enter a Product Name of your choice and set the Action Type to Presents User Interface, which will create a new UI extension. The Product Name should represent the extension for which you are creating a target. Notice the Product Name is also used as a suffix to create the target’s bundle ID.

    UI Extension Target Settings
  4. After clicking Finish, repeat steps 1-3 above to create an additional target for the Non-UI Extension, setting the Action Type to No User Interface.

    Non-UI Extension Target Settings

Once a target is created, you can access the target and its settings in Xcode’s project editor. The newly created extension targets will appear in a list under the Targets pane.

Xcode Project Editor

App Groups Entitlement

An App Group is an entitlement that allows multiple apps developed by the same team to access one or more shared containers and communicate using interprocess communication (IPC). Apps may belong to one or more app groups, as each developer account can register a maximum of 1000 App Groups. You can also use an App Group to share data between app extensions, such as Wallet Extensions, and their containing app.

For Wallet Extensions, Apple Wallet is the extensions’ host app and the issuer app is considered the extensions’ containing app. There is no direct communication between an app extension and its containing app. Typically, the containing app is not even running while a contained extension is running. An app extension’s containing app and host app also do not communicate. For more information, see Understand How an App Extension Works.

In a typical request and response transaction, the system opens an app extension on behalf of a host app, conveying data in an extension context provided by the host. Behind the scenes, the system uses IPC to ensure that the host app and an app extension can work together to enable a cohesive experience. The system allows Wallet Extensions, via IPC, to communicate with the Apple Wallet host app on iOS.

An App Group can be used to share data between Wallet Extensions and its containing issuer app, via a shared container. Before you can create an App Group, you have to add the App Groups capability to your app’s target. A capability grants your app access to an app service provided by Apple, such as App Groups, Apple Pay, HomeKit, or In-App Purchase. To use some app services, you must add a capability with Xcode’s project editor that configures the app service correctly for you. Xcode stores the project Entitlements in a YourModuleName.entitlements file (named after your project module), edits the Information Property List (Info.plist) file, adds related frameworks, and configures your signing assets.

Follow the steps in Add a capability to add the App Groups capability to your app’s target in Xcode.

Capabilities Dialog

Once you have added the App Groups capability to your iOS issuer app, Xcode will retrieve any existing groups from your developer account and display the groups in the capabilities section. Enable one or more groups in the list by using their check-boxes to add your app as a member of those groups. Alternatively, to revoke your app’s membership from a group, uncheck the group’s checkbox. Use the Refresh button below the App Groups list to re-fetch your account’s groups at any time.

Create a new App Group

To create a new App Group with Xcode for the iOS issuer app, follow the steps below:

  1. Click the add button (+) below the App Groups list.

    Empty App Groups List
  2. Enter a container ID in the dialog that appears. A container ID must begin with group. and then a custom string in reverse DNS notation.

    New Container Dialog
  3. Click OK to save the new App Group. Xcode automatically selects the new App Group to indicate that your app is now a member of that group.

    A Checked App Group

When creating an App Group to support the Wallet Extensions feature, ensure that the new App Group is added to both “non-UI extension” and “UI extension” targets in Xcode.

Common App Group membership allows the main, containing app to share data with the extensions. If either extension target (or both) are not members of the same App Group, the extensions cannot share data with each other or the main app using the App Group’s shared container.

PNO Pass Metadata Configuration

During the implementation of In-App Provisioning, a prerequisite to Wallet Extensions, the developer is responsible for configuring the following mandatory key-value pairs on the payment network operator (PNO) system.


PNO Pass Metadata

contactName

The issuer name.

bank_app

The name of the iOS app to use for In-App Verification.

associatedApplicationIdentifiers

The issuer’s App ID. This key allows the respective apps to see, access, and activate the issuer’s payment passes.

associatedStoreIdentifiers

The issuer uses this key to link to the issuer app or to redirect users to download their app from the App Store (if it isn’t installed on the user’s device). The value for this key is the issuer app’s Adam ID, which is available from App Store Connect.

appLaunchURL

The system passes this key to the issuer app when it opens from Apple Wallet. The value contains a launch URL of the issuer’s choice, formatted as scheme://path. For more information, see Defining a custom URL scheme for your app.


For Wallet Extensions, the associatedApplicationIdentifiers key needs to be updated on the PNO system to include the App IDs of the extension targets.

An App ID is a two-part string that identifies one or more apps from a single development team. The string consists of a Team ID (which Apple supplies and is unique to a specific development team) and a bundle ID search string (which the developer supplies), with a period separating the two parts: <team_id>.<bundle_id>.

In Xcode, an app extension’s bundle ID is listed under the Signing and Capabilities pane within the project editor of the extension target; the bundle ID value is labeled as Bundle Identifier. An issuer’s Team ID is available from their Account page in Developer Portal. The example App IDs below are the result of combining a Team ID and bundle ID.


ID Example
Team ID 1234ABCD
bundle ID com.example.IssuerApp
App ID 1234ABCD.com.example.IssuerApp
Non-UI Extension App ID 1234ABCD.com.example.IssuerApp.WNonUIExt
UI Extension App ID 1234ABCD.com.example.IssuerApp.WUIExt

Entitlement Configurations

In-App Provisioning and Wallet Extensions use the same private entitlement:

com.apple.developer.payment-pass-provisioning

You will need to add the In-App Provisioning capability to each extension target to include this entitlement in the Wallet Extensions process. An entitlement is a key-value pair that grants an executable permission to use a service or technology, such as Wallet Extensions.

Follow the steps in Add a capability to add the In-App Provisioning capability to your app’s target in Xcode.

In-App Provisioning Capability

Once you have added the In-App Provisioning capability to your extension targets, the In-App Provisioning entitlement’s key-value pair will be automatically added to the Entitlements file, which can be accessed via the Project navigator. If the Entitlements file does not already exist, Xcode will create it, and the file will have the same name as your extension’s module name.

Entitlements File

NSExtension Properties

An Information Property List, also called a plist, is a resource that contains key-value pairs that identify and configure a bundle. Each target in Xcode is considered a bundle; thus, an extension target is assigned a unique bundle ID upon creation. For more information, see Create Extension Targets. During the creation of an extension target, Xcode automatically includes an Information Property List file named Info.plist, which will require editing to update properties related to NSExtension.

An extension target’s plist contains an NSExtension key, which is a dictionary that identifies the extension point and may specify some details about your extension. In the NSExtension dictionary property for Wallet Extensions, you will need to update the NSExtensionPointIdentifier and NSExtensionPrincipalClass keys.

The value of the required NSExtensionPointIdentifier key is the extension point’s reverse DNS name, such as com.apple.widget-extension. As for the NSExtensionPrincipalClass key, its value is the name of the extension’s principal class, which is a custom class that implements an app extension’s primary view or functionality. When the host app—Apple Wallet—invokes an issuer app’s extension, the extension point instantiates the principal class.

Information Property List File

You will need to manually edit the source code of the Info.plist file to update the NSExtensionPointIdentifier and NSExtensionPrincipalClass keys. To view the source code of a property list, control-click a property list file in the Project navigator, then choose Open As > Source Code from the pop-up menu.

Property List Source Code Setting
Non-UI Extension Properties

For the Non-UI extension’s plist, set the following key-value pairs in the NSExtension dictionary property. The principal class of the extension needs to be a subclass of PKIssuerProvisioningExtensionHandler with all nondeprecated methods overridden without calling super.


NSExtensionPointIdentifier:


<key>NSExtensionPointIdentifier</key>
<string>com.apple.PassKit.issuer-provisioning</string>
   

NSExtensionPrincipalClass:


<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).WNonUIExtHandler</string>
    

Non-UI Extension Property List
UI Extension Properties

For the UI extension’s plist, set the following key-value pairs in the NSExtension dictionary property. The principal class of the extension needs to be a subclass of UIViewController that adopts the protocol PKIssuerProvisioningExtensionAuthorizationProviding.


NSExtensionPointIdentifier:


<key>NSExtensionPointIdentifier</key>
<string>com.apple.PassKit.issuer-provisioning.authorization</string>
   

NSExtensionPrincipalClass:


<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).WUIExtHandler</string>
    

UI Extension Property List

Configuring Extensions

To configure Wallet Extensions, you will need to implement Swift classes to support the Non-UI extension and UI extension. This section will walk you through sample configurations of the Non-UI extension and UI extension.

Non-UI Extension

The issuer app needs a non-UI extension to report on the status of the extension, passes the app has available to add, and to perform the card data lookup—just like when adding payment passes to Apple Wallet from within the issuer app.

First, delete the JavaScript file Action.js, which was automatically included in the Non-UI extension target’s module upon creation. Then, remove the code content within the Non-UI extension’s principal class.

Within the WNonUIExt module, create a Swift file that imports the Foundation framework, which will support the instantiation of objects that can be used to access the container of the extension’s App Group. For more information, see App Groups Entitlement.

A FileManager can access or change the contents of an App Group container’s file system. Use a file manager object to access a container URL indicating the location of the group’s shared directory in the file system. The UserDefaults class provides a programmatic interface for interacting with the user’s defaults database. You can store key-value pairs persistently across the launches of the issuer app within the user’s defaults database.

Reference the following code to create instances of FileManager and UserDefaults using your extension targets’ App Group ID:


// AppGroupManager.swift

import Foundation

let appGroupID: String = "group.com.example.IssuerApp"
let appGroupSharedContainerDirectory: URL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID)!
let appGroupSharedDefaults: UserDefaults = UserDefaults(suiteName: appGroupID)!
    

To determine the presence of a paired Apple Watch, use the WatchConnectivity framework. This framework provides the WCSession class, which can be used to check if an iPhone has a paired Apple Watch. To use an instance of WCSession to check for an Apple Watch pairing, create a Swift class named WatchConnectivitySession that subclasses NSObject and WCSessionDelegate. Then configure and activate a session within the class. For more information, see Configuring and Activating the Session with WatchConnectivity framework.

Once a session is configured and activated, you can use the isPaired instance property of the WCSession to determine if an iPhone has a paired Apple Watch. Reference the following code to implement a class that configures a WCSession:


// WatchConnectivitySession.swift

import WatchConnectivity

class WatchConnectivitySession: NSObject, WCSessionDelegate {
    let session = WCSession.default
    
    override init() {
        
        // Initialize the superclass.
        super.init()
        
        // Activate the session if the current iPhone is able to use a
        // session object.
        if WCSession.isSupported() {
            session.delegate = self
            session.activate()
        }
    }
    
    // A Boolean indicating whether the current iPhone has a paired Apple Watch.
    var isPaired: Bool {
        if session.activationState == .activated {
            return session.isPaired
        } else {
            return false
        }
    }
    
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        if let error {
            print("WCSession activation failed with error: \(error.localizedDescription)")
        }
    }
    
    func sessionDidBecomeInactive(_ session: WCSession) { }
    
    func sessionDidDeactivate(_ session: WCSession) {
        session.activate()
    }
}
    

Import the PassKit framework into the Swift file for the Non-UI extension’s principal class. This framework contains PKIssuerProvisioningExtensionHandler. The Non-UI extension should subclass this extension handler and implement its abstract methods. Reference the following code to set up the principal class for the Non-UI extension:


// WNonUIExtHandler.swift

import PassKit

class WNonUIExtHandler: PKIssuerProvisioningExtensionHandler {

}
    

Method: Status

The status(completion:) method is a completion handler that indicates whether a payment pass is available to add to Apple Pay and whether adding the pass requires authentication. The issuer app icon displays in Apple Wallet under "From Apps on Your iPhone" if the user has logged into the iOS issuer app at least once and there is at least one payment pass available to add to Apple Pay.

Within the status(completion:) method, create a PKIssuerProvisioningExtensionStatus object. Below is a list of the object’s instance properties, which should be set to true oder false boolean values based on the status of the extension.

  • passEntriesAvailable: The issuer app declares whether a payment pass is available to add to an iPhone.
  • remotePassEntriesAvailable: The issuer app declares whether a payment pass is available to add to an Apple Watch. To determine if an iPhone has a paired Apple Watch, use the WatchConnectivity framework.
  • requiresAuthentication: The issuer app declares whether adding a payment pass requires the user to authenticate in the authorization UI extension that the issuer app provides.

Use appGroupSharedDefaults variable created above, which is an instance of UserDefaults, to access cached values from the user’s defaults database. Use the defaults database to persist and manage key-value pairs that support the issuer app’s provisioning process. When a user initially launches the issuer app, create a key in the defaults database—for example, PaymentPassCredentials. Use this key to store the pass identifier and credentials data about all of users’ issued cards within the issuer app, for later retrieval by the Non-UI extension.

In the Non-UI extension code, retrieve the identifiers of all payment passes within the issuer app from the user’s defaults database using the above key. Compare these identifiers of issuer cards against the identifiers of passes that have already been provisioned in Wallet. The primaryAccountIdentifier identifiers should be used to filter passes that have already been provisioned and added to the user's pass library in Apple Wallet.

To access passes within the user’s pass library, instantiate a PKPassLibrary object as an instance property of the WNonUIExtHandler class. Then, use the object’s passes(of:) method to access passes of the secureElement PKPassType from the user’s pass library. The count of passes available to add to Apple Wallet can be used to determine the passEntriesAvailable and remotePassEntriesAvailable statuses of the extension (depending on pass eligibility).

The use of PKPassLibrary requires the Wallet capability. Follow the steps in Add a capability to add the Wallet capability to your app’s Non-UI Extension target in Xcode.

Wallet Capability

After determining whether the user has passes available to add to Apple Pay for an iPhone or Apple Watch device, update the properties of the PKIssuerProvisioningExtensionStatus, then pass the status to the completion handler. Reference the following code to create the Status method:


// WNonUIExtHandler.swift

import PassKit

class WNonUIExtHandler: PKIssuerProvisioningExtensionHandler {
    let passLibrary = PKPassLibrary()
    let watchSession = WatchConnectivitySession()

    /**
     Sets the status of the extension to indicate whether a payment pass is available to add and whether
     adding it requires authentication.
     */
    override func status(completion: @escaping (PKIssuerProvisioningExtensionStatus) -> Void) {
        
        // This status will be passed to the completion handler.
        let status = PKIssuerProvisioningExtensionStatus()
        var paymentPassLibrary: [PKPass] = []
        var passIdentifiers: Set<String> = []
        var remotePassIdentifiers: Set<String> = []
        var availablePassesForIphone: Int = 0
        var availableRemotePassesForAppleWatch: Int = 0
        
        // Get the identifiers of payment passes that are already added
        // to Apple Pay.
        paymentPassLibrary = self.passLibrary.passes(of: .secureElement)
        
        for pass in paymentPassLibrary {
            if let identifier = pass.secureElementPass?.primaryAccountIdentifier {
                if pass.isRemotePass && pass.deviceName.localizedCaseInsensitiveContains("Apple Watch") {
                    remotePassIdentifiers.insert(identifier)
                } else if !pass.isRemotePass {
                    passIdentifiers.insert(identifier)
                }
            }
        }
        
        // Get cached credentials data of all of the user's issued cards,
        // within the issuer app, from the user's defaults database.
        if let cachedCredentialsData = appGroupSharedDefaults.data(forKey: "PaymentPassCredentials") {
            
            // JSON decode the cached credential data of all of the user's
            // issued cards.
            //
            // Note: ProvisioningCredential is not a member of PassKit.
            // You should modify this logic based on how the issuer app
            // structures persisted data of an issued card.
            if let decoded = try? JSONDecoder().decode([String: ProvisioningCredential].self, from: cachedCredentialsData) {
                for identifier in decoded.keys {
                    
                    // Count number of passes available to add to iPhone
                    if !passIdentifiers.contains(identifier) {
                        availablePassesForIphone += 1
                    }
                    
                    // Count number of passes available to add to Apple Watch
                    if !remotePassIdentifiers.contains(identifier) {
                        availableRemotePassesForAppleWatch += 1
                    }
                }
            } else {
                log.error("Error occurred while JSON decoding cachedCredentialsData")
            }
        } else {
            log.warning("Unable to find credentials of passes available to add to Apple Pay.")
        }
        
        // Set the status of the extension.
        status.passEntriesAvailable = availablePassesForIphone > 0
        status.remotePassEntriesAvailable = watchSession.isPaired && availableRemotePassesForAppleWatch > 0
        
        // You can also set requiresAuthentication to "true" or "false"
        // directly, if not wanting to rely on a cached value.
        status.requiresAuthentication = appGroupSharedDefaults.bool(forKey: "ShouldRequireAuthenticationForAppleWallet")
        
        // Invoke the completion handler.
        completion(status)
    }
}
    


Method: Pass Entries (for iPhone)

The passEntries(completion:) method is a completion handler that returns an array of payment passes that are available to add to the user’s iPhone. This list of eligible payment passes displays to the user when the pass selection view is shown during the provisioning experience.

Within the passEntries(completion:) method, retrieve the unique identifiers of payment passes that are available to add to Apple Pay. Use this set of primaryAccountIdentifier identifiers to filter available payment passes that do not exist in a user’s pass library. Then, create payment pass entries based on the credentials of the filtered available passes. The appGroupSharedDefaults can be used to retrieve pass related identifier and credentials data, persisted by the issuer app, from the user’s defaults database.

The pass entries should be instances of PKIssuerProvisioningExtensionPaymentPassEntry. After creating a payment pass entry for an available pass, add it to an array of entries of the type [PKIssuerProvisioningExtensionPassEntry]. Then, pass this array of entries to the completion handler after the array includes all payment passes that are available to add to the user’s iPhone.

Reference the following code to create the Pass Entries (for iPhone) method:


// WNonUIExtHandler.swift

import PassKit

class WNonUIExtHandler: PKIssuerProvisioningExtensionHandler {
    let passLibrary = PKPassLibrary()

    . . .

    override func passEntries(completion: @escaping ([PKIssuerProvisioningExtensionPassEntry]) -> Void) {
        
        // This list will be passed to the completion handler.
        var passEntries: [PKIssuerProvisioningExtensionPassEntry] = []
        var paymentPassLibrary: [PKPass] = []
        var passLibraryIdentifiers: Set<String> = []
    
        // Get the identifiers of payment passes that are already added
        // to Apple Pay.
        paymentPassLibrary = self.passLibrary.passes(of: .secureElement)
        
        for pass in paymentPassLibrary {
            if !pass.isRemotePass, let identifier = pass.secureElementPass?.primaryAccountIdentifier {
                passLibraryIdentifiers.insert(identifier)
            }
        }
        
        // Get cached credentials data of all of the user's issued cards,
        // within the issuer app, from the user's defaults database.
        if let cachedCredentialsData = appGroupSharedDefaults.data(forKey: "PaymentPassCredentials") {
            
            // JSON decode the cached credential data of all of the user's
            // issued cards.
            //
            // Note: ProvisioningCredential is not a member of PassKit.
            // You should modify this logic based on how the issuer app
            // structures persisted data of an issued card.
            if let decoded = try? JSONDecoder().decode([String: ProvisioningCredential].self, from: cachedCredentialsData) {
                
                // Create a payment pass entry only for cards that are available
                // to add to Apple Pay, and add the entry to the passEntries list.
                for (identifier, paymentPassCredential) in decoded {
                    if !passLibraryIdentifiers.contains(identifier) {
                        let entry = getPaymentPassEntry(provisioningCredential: paymentPassCredential)
                        passEntries.append(entry)
                    }
                }
            } else {
                log.error("Error occurred while JSON decoding cachedCredentialsData")
            }
        } else {
            log.warning("Unable to find credentials of passes available to add to Apple Pay on iPhone.")
        }
        
        // Invoke the completion handler.
        completion(passEntries)
    }
}
    

You can implement private helper methods to handle creating a payment pass entry with PKIssuerProvisioningExtensionPaymentPassEntry. A payment pass entry is initialized with the following properties and configuration object:

  • identifier: An internal value the issuer uses to identify the card. This identifier needs to be unique. Use the primaryAccountIdentifier to identify passes. If the issuer app does not have access to the primary account identifier / FPANID (only created after the first provision), use either passes(of:) method or the remoteSecureElementPasses property of the PKPassLibrary to filter passes.
  • art: The image representing the card that displays to the user. The image needs to have square corners and can’t include personally identifiable information such as user’s name or account number. See the card art requirements available in the Functional Requirements for Apple Pay and Direct NFC Access. Please contact your Apple project contact, PNO, or service provider relationship manager for documentation on Functional Requirements for Apple Pay and Direct NFC Access.
  • title: The name of the payment pass that Wallet displays to the user.
  • PKAddPaymentPassRequestConfiguration: The configuration data for setting up and displaying a view controller that lets the user add a payment pass.

Reference the following code to create private helper methods:


// WNonUIExtHandler.swift

import PassKit

class WNonUIExtHandler: PKIssuerProvisioningExtensionHandler {

    . . .

    // MARK: - Private Methods

    /**
     Returns a payment pass entry.
     */
    private func getPaymentPassEntry(provisioningCredential: ProvisioningCredential) -> PKIssuerProvisioningExtensionPaymentPassEntry {
    
        // If using PNO Payment Data Configuration 1 (FPAN) or 3 (eFPAN), set
        // the identifier as the primaryAccountNumber. If using PNO Payment Data
        // Configuration 2 (FPANID), set the identifier as the
        // primaryAccountIdentifier.
        let identifier = provisioningCredential.primaryAccountIdentifier
        let label = provisioningCredential.label
        
        // Create a request configuration for adding a payment pass, which will
        // be included in the payment pass entry.
        let requestConfig = PKAddPaymentPassRequestConfiguration(encryptionScheme: .ECC_V2)!
        requestConfig.primaryAccountIdentifier = identifier
        requestConfig.paymentNetwork = .masterCard
        requestConfig.cardholderName = provisioningCredential.cardholderName
        requestConfig.localizedDescription = provisioningCredential.localizedDescription
        requestConfig.primaryAccountSuffix = provisioningCredential.primaryAccountSuffix
        requestConfig.style = .payment
        
        // Append additional card details. The value of the expiration label 
        // should be a string in the following format: "11/18".
        requestConfig.cardDetails.append(PKLabeledValue(label: "expiration", value: provisioningCredential.expiration))
        
        // Instantiate and return a payment pass entry.
        if let uiImage = UIImage(named: provisioningCredential.assetName) {
            return PKIssuerProvisioningExtensionPaymentPassEntry(identifier: identifier,
                                                                 title: label,
                                                                 art: getEntryArt(image: uiImage),
                                                                 addRequestConfiguration: requestConfig)!
        } else {
            return PKIssuerProvisioningExtensionPaymentPassEntry(identifier: identifier,
                                                                 title: label,
                                                                 art: getEntryArt(image: #imageLiteral(resourceName: "generic")),
                                                                 addRequestConfiguration: requestConfig)!
        }
    }
    
    /**
     Converts a UIImage to a CGImage.
     */
    private func getEntryArt(image: UIImage) -> CGImage {
        let ciImage = CIImage(image: image)
        let ciContext = CIContext(options: nil)
        return ciContext.createCGImage(ciImage!, from: ciImage!.extent)!
    }
}
    


Method: Remote Pass Entries (for Apple Watch)

The remotePassEntries(completion:) method is a completion handler that returns an array of payment passes that are available to add to the user’s Apple Watch. This list of eligible payment passes displays to the user when the pass selection view is shown during the provisioning experience.

The logic for this method should be similar to the logic of the passEntries(completion:) method, from which code can be copied and reused to create the Remote Pass Entries (for Apple Watch) method. For details of the reusable logic, see Method: Pass Entries (for iPhone).

When building the array of payment pass entries, ensure to only append payment passes that have not been provisioned for Apple Watch. The appGroupSharedDefaults can be used to retrieve cached identifier and credentials data of passes from the user’s defaults database.

The below sample code highlights changes to be made for the Remote Pass Entries (for Apple Watch) method when repurposing code from the Pass Entries (for iPhone) method. You will need to modify the logic of the paymentPassLibrary iteration to filter identifiers of remote payment passes that are NOT available to add to an Apple Watch, due to their existence in the user’s pass library within Apple Wallet. Reference the following code for the logic change:


// WNonUIExtHandler.swift

import PassKit

class WNonUIExtHandler: PKIssuerProvisioningExtensionHandler {

    . . .

    override func remotePassEntries(completion: @escaping ([PKIssuerProvisioningExtensionPassEntry]) -> Void) {

        . . .

        // Get the identifiers of payment passes that are already added
        // to Apple Pay.

        . . .

        for pass in paymentPassLibrary {
            if pass.isRemotePass, pass.deviceName.localizedCaseInsensitiveContains("Apple Watch"),
               let identifier = pass.secureElementPass?.primaryAccountIdentifier  {
                passLibraryIdentifiers.insert(identifier)
            }
        }

        . . .

        // Invoke the completion handler
        completion(passEntries)
    }
}
    


Method: Generate Add Payment Pass Request

The generateAddPaymentPassRequestForPassEntryWithIdentifier() method is a completion handler that invokes a PKAddPaymentPassRequest. The system uses this request to add a payment pass to Apple Pay that is selected by the user. In addition to the completion handler, this method takes in the following parameters:

  • identifier: The value that you use to identify the card, commonly the FPANID, the FPAN or the eFPAN.
  • configuration: The configuration the system uses to add a secure pass. The configuration is an instance of PKAddPaymentPassRequestConfiguration.
  • certificates: An array of data objects. Each object contains a DER encoded X.509 certificate, with the leaf first and root last. You must include the root CA to validate the entire chain.
  • nonce: A one-time nonce value generated by Apple’s servers. You must include this signature nonce in the add-payment request’s encrypted data.
  • nonceSignature: The device-specific signature for the nonce. This signature must be included in the add payment request’s encrypted data.

A PKAddPaymentPassRequest includes Data that the system needs to add a card to Apple Pay. The contents of the data can be hosted on the issuer’s servers and, later, accessed by making a request to the servers from the issuer’s iOS app.

The activationData property contains the data provided to the payment network as a cryptographic one-time password (OTP), per the Payment Network API specification. The cryptographic OTP is not interpreted by Apple or iOS. The OTP should be verified by the issuer or payment network upon receipt of the provisioning request to ensure the request’s authenticity. For more information about the activation data’s content, contact your payment network.

The ephemeralPublicKey property is used by elliptic curve cryptography (ECC). When using an ECC scheme, this property contains the bytes of your ephemeral public key, which is required as a part of PKEncryptionSchemeECC_V2. The conversion of ECPoint to byte sequence must be performed without point compression. Any byte sequence leading with the octet 0x02 or 0x03 to indicate point compression will fail.

The wrappedKey is a randomly generated AES-256 bit key that is encrypted with the decryptor’s public key, which is required as a part of PKEncryptionSchemeRSA_V2.

The encryptedPassData is an encrypted JSON file containing the sensitive information needed to add a card to Apple Pay. The JSON file must contain the appropriate configuration keys (see below example keys). Wallet Extensions supports three configurations for the JSON file that the issuer host generates. See section 5.5 PNO Payment Data Configurations within the In-App Provisioning documentation for a list of configurations and examples. Because the supported configurations vary by PNO or service provider, the issuer needs to contact their PNO or service provider relationship manager to confirm which of the following configurations to support for Apple Pay. Of the keys below, the nonce and nonceSignature are accessible within the Generate Add Payment Pass Request method as parameters.


Key Typ Description
primaryAccountNumber String The full primary account number (PAN). Digits only.
expiration String The expiration date as a string. For example, "11/18".
name String The name of the cardholder.
nonce String The hex string for the nonce value, provided in the delegate callback.
nonceSignature String The hex string for the nonce signature, provided in the delegate callback.


Once you have updated the data properties of the PKAddPaymentPassRequest, pass the request to the completion handler. Reference the following code to create the Generate Add Payment Pass Request method:


// WNonUIExtHandler.swift

import PassKit

class WNonUIExtHandler: PKIssuerProvisioningExtensionHandler {

    . . .

    override func generateAddPaymentPassRequestForPassEntryWithIdentifier(_ identifier: String, configuration: PKAddPaymentPassRequestConfiguration,
                                                                          certificateChain certificates: [Data], nonce: Data, nonceSignature: Data,
                                                                          completionHandler completion: @escaping (PKAddPaymentPassRequest?) ->
                                                                          Void) {
        
        // This request object will be passed to the completion handler.
        let request = PKAddPaymentPassRequest()
            
        // Generate the encrypted pass data.
        //
        // EncryptedPassDataResponse and PassResource are not members of
        // PassKit. You should modify this logic based on how the issuer app
        // retrieves the required encrypted pass data from the issuer server.
        //
        // You can use the array.first(where:) method to retrieve a
        // specific PKLabeledValue card detail from a configuration.
        // configuration.cardDetails.first(where: { $0.label == "expiration" })!
        let passData: EncryptedPassDataResponse = PassResource.requestPaymentPassData(configuration, certificateChain: certificates,
                                                                                      nonce: nonce, nonceSignature: nonceSignature)
        
        // Insert the encrypted pass data into the PKAddPaymentPassRequest.
        request.activationData = passData.activationData
        request.encryptedPassData = passData.encryptedPassData
        request.ephemeralPublicKey = passData.ephemeralPublicKey
        
        // Invoke the completion handler.
        completion(request)
    }
}
    

UI Extension

The issuer app needs a UI extension to perform authentication of the user if the non-UI extension reports that authentication is required. The UI extension isn’t a redirect to the issuer app, but a separate login view screen. We generally recommend that the extension use the same authentication logic as the main application.

UI Extension Login View

The principal class of the UI extension needs to be a subclass of UIViewController that adopts the protocol PKIssuerProvisioningExtensionAuthorizationProviding. Import the UIKit framework, which contains UIViewController and its subclasses, into the WUIExtHandler.swift file. Also import the PassKit framework, which contains PKIssuerProvisioningExtensionAuthorizationProviding.

If you already have a shared framework responsible for the user authentication, you can call it directly from the UI Extension. If not, you can copy and reuse the containing issuer app’s login logic into the extension to perform the user authentication. If a subclass of UIViewController is used for the containing issuer app’s login view, reuse the containing app’s login code by copying the code logic over into the UI extension’s principal class (WUIExtHandler). Then, refactor the code to include and invoke the completionHandler based on the status of the user’s authorization.

The protocol PKIssuerProvisioningExtensionAuthorizationProviding includes this completion handler as an instance property. The completion handler is a function that takes an enum parameter of .authorized oder .canceled, which represents the authorization status of the user’s login attempt during the Wallet Extensions process. Use the .authorized enum value to declare that a user is authorized to provision a payment pass. The .canceled enum should be used to declare that the user canceled authorization or doesn’t have authorization to add a payment pass to Apple Pay.

When implementing the UI extension, some issuers may want to use the SwiftUI framework instead of UIKit. It is possible to implement the UI extension using a SwiftUI view, but you will need to interface SwiftUI with UIKit, as the UI extension’s principal class must be a subclass of UIViewController. The guide in this section is catered to issuers who are interested in implementing the UI extension with SwiftUI. To get started, also import SwiftUI into the WUIExtHandler.swift file.

Reference the following code to set up the UI extension’s principal class:


// WUIExtHandler.swift

import UIKit
import SwiftUI
import PassKit

class WUIExtHandler: UIViewController, PKIssuerProvisioningExtensionAuthorizationProviding {

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

Method: View Did Load

To interface SwiftUI with UIKit, override the viewDidLoad() method of the UIViewController parent class. This method is called after the view controller has loaded its view hierarchy into memory. Within the method, create an instance of UIHostingController, which is a UIKit view controller that manages a SwiftUI view hierarchy. When instantiating a UIHostingController, the hosting controller’s init(rootView:) initializer takes an instance of a SwiftUI view that conforms to the View protocol. This conforming view should render the UI extension’s login view for authorization.

Normally, you create views in your storyboards by dragging them from the library to your canvas. You can also create views programmatically. Add the UIHostingController subview to the destination UIViewController by calling the view controller’s addChild(_:) method and calling the view.addSubview(_:) method on the superview.

Next, create and activate the auto layout constrains of the extension’s UIHostingController view using the NSLayoutConstraint.activate(_:) method. This convenience method provides an easy way to activate a set of layout constraints with a single method call. Once you have activated the constraints, notify the child view controller (UIHostingController) that its move to the parent view controller (UIViewController) is complete. You can notify the child view controller of the move using the controller’s didMove(toParent:) method.

Reference the following code to implement the viewDidLoad() method override:


// WUIExtHandler.swift

import UIKit
import SwiftUI
import PassKit

class WUIExtHandler: UIViewController, PKIssuerProvisioningExtensionAuthorizationProviding {

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

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Create an instance of the SwiftUI view.
        // The completion handler should be passed to the SwiftUI view.
        let swiftUIView = WUIExtView(completionHandler: completionHandler)
        
        // Create a UIHostingController with the extension's SwiftUI view as
        // its root view.
        let controller = UIHostingController(rootView: swiftUIView)
        
        // Add the UIHostingController's view to the destination
        // view controller.
        addChild(controller)
        controller.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(controller.view)
        
        // Set and activate the constraints for the extension's SwiftUI view.
        NSLayoutConstraint.activate([
            controller.view.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1),
            controller.view.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 1),
            controller.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            controller.view.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
        
        // Notify the child view controller that the move is complete.
        controller.didMove(toParent: self)
    }
}
    

Create a SwiftUI View

Add a new SwiftUI view file, naming it WUIExtView.swift, to the UI extension’s module. To add a new file to the module in Xcode, follow the steps in Add new files to a project. The newly created file will include a Swift structure named WUIExtView that conforms to the View protocol. An instance of this structure will be passed to the UIHostingController init(rootView:) initializer within the WUIExtHandler class.

Next, import the PassKit framework into the view file. This framework includes the enum PKIssuerProvisioningExtensionAuthorizationResult, which is passed to the completionHandler of the UI extension. Include the completionHandler as an instance variable of the WUIExtView structure. The inclusion of this instance variable will allow the completion handler to be passed from the parent view (UIViewController WUIExtHandler) to the child view (View WUIExtView), via the UIHostingController.

Reference the following code to set up the SwiftUI view file:


// WUIExtView.swift

import SwiftUI
import PassKit

struct WUIExtView: View {
    var completionHandler: ((PKIssuerProvisioningExtensionAuthorizationResult) -> Void)?

    var body: some View {
        // Add SwiftUI code for the login view. You can copy the
        // code logic from the containing issuer app's login view,
        // then place the code logic within this UI extension view.

    }
}
    

Once you have created the SwiftUI view structure, you can add methods that handle the logic of authorizing a user during the Wallet Extensions process. Many issuer apps rely on biometric authentication like Face ID and Touch ID to provide the most seamless experience for the user. For more information, see Logging a User into Your App with Face ID or Touch ID. After carrying out the UI extension’s authorization logic, call the completionHandler with the appropriate .authorized oder .canceled enum based on the user’s authorization status.

Remember, you can copy and reuse the login code logic and authorization mechanisms from the containing issuer app to create the authorization methods within the UI extension. Then, refactor the code logic to call the completionHandler and pass the appropriate .authorized oder .canceled enum based on the user’s authorization status.

Reference the following code for examples of calling the completionHandler for authorization:


// WUIExtView.swift

import SwiftUI
import PassKit

struct WUIExtView: View {
    var completionHandler: ((PKIssuerProvisioningExtensionAuthorizationResult) -> Void)?

    func handleLogin() -> Void {
        // Create username/password login logic.
        // You can copy and reuse the username/password
        // login logic from the containing issuer app.
        print("Log In button tapped")
        let randomNum = Int.random(in: 1..<10)
        let authorized = randomNum > 5 ? true : false

        // Call the completion handler
        if authorized {
            completionHandler!(.authorized)
        } else {
            completionHandler!(.canceled)
        }
    }

    func handleBiometricLogin() -> Void {
        // Create biometric login logic.
        // You can copy and reuse the biometric
        // login logic from the containing issuer app.
        print("Face ID button tapped")
        let randomNum = Int.random(in: 1..<10)
        let authorized = randomNum > 5 ? true : false

        // Call the completion handler
        if authorized {
            completionHandler!(.authorized)
        } else {
            completionHandler!(.canceled)
        }
    }

    var body: some View {
        // Add SwiftUI code for the login view. You can copy the
        // code logic from the containing issuer app's login view,
        // then place the code logic within this UI extension view.

    }
}
    

After creating a method to handle the authorization of a user, you can set the method as an action of a Button within the view using the init(action:label:) initializer. The action is either a method or closure property that does something when a user clicks or taps the button. The label of a button can be any kind of view, such as a Text view for text-only labels or a Label view for buttons with both a title and an icon. For icons, you can use icons that are available in the SF Symbols library. Simply, enter the name of a library icon as a string in the Label init(_:image:) initializer.

Reference the following code to create SwiftUI buttons for authorization:


// WUIExtView.swift

import SwiftUI
import PassKit

struct WUIExtView: View {
    var completionHandler: ((PKIssuerProvisioningExtensionAuthorizationResult) -> Void)?

    . . .

    var body: some View {
        // Add SwiftUI code for the login view. You can copy the
        // code logic from the containing issuer app's login view,
        // then place the code logic within this UI extension view.

        VStack {
            let smallConfig = UIImage.SymbolConfiguration(pointSize: 50, weight: .bold, scale: .small)
            if let banknoteLogo = UIImage(systemName: "banknote.fill", withConfiguration: smallConfig) {
                Image(uiImage: banknoteLogo.withRenderingMode(.alwaysTemplate))
                    .foregroundColor(.white)
                    .padding([.bottom], 10)
            }
            Text("Issuer App")
                .font(.title)
                .bold()
                .padding([.bottom], 20)
                .foregroundColor(.white)

            List {
                . . .

                HStack(spacing: 18) {
                    Spacer()
                    Button(
                        action: handleBiometricLogin,
                        label: {
                            HStack {
                                Image(systemName: "faceid")
                                Text("Face ID")
                                    .bold()
                                    .font(.system(size: 16.0))
                            }
                            .padding(6)
                        }
                    )
                    .buttonStyle(.bordered)
                    .background(Color.blue)
                    .cornerRadius(26)
                    .foregroundColor(.white)
                    Button(
                        action: handleLogin,
                        label: {
                        Text("Log In")
                            .bold()
                            .font(.system(size: 16.0))
                            .padding(6)
                            .frame(width: 70)
                    })
                    .buttonStyle(.bordered)
                    .background(Color.orange)
                    .cornerRadius(26)
                    .foregroundColor(.white)
                }
                .listRowBackground(Color.clear)
            }

            . . .
        }
        .background(Color.blue)
    }
}
    

Configure Storyboard

You will need to configure the Storyboard interface builder to set the UI extension’s principal class as the initial view controller for the extension.

  1. First, select the UI extension’s MainInterface file within the Project navigator. This file will display the UI extension’s Storyboard interface builder.

  2. Select the Library button (+) in Xcode’s toolbar and add a View Controller object to the Storyboard. You can double-click the object to add it to the Storyboard.

    Object Library Dialog
  3. Select the Identity inspector for the View Controller. Within the Custom Class pane of the inspector, set the class to WUIExtHandler principal class.

    Xcode Identity Inspector
  4. Select the Attributes inspector for the View Controller. Within the View Controller pane of the inspector, select the "Is Initial View Controller" checkbox.

    Xcode Attributes Inspector

Tools for Testing

You can test Wallet Extensions by writing unit tests in Xcode and releasing beta builds of an iOS issuer app to TestFlight.

Unit Testing in Xcode

The principal class of the Non-UI extension can be unit tested using the XCTest framework. For more information, see Adding unit tests to your existing project. To add unit tests to an Xcode project, you first need to create a test target using the Unit Testing Bundle template.

  1. To add a new target to your Xcode app project, choose File > New > Target.

  2. In the Multiplatform bar at the top of the new target dialog, choose iOS. In the center pane of the dialog, Xcode displays the templates you can choose. Filter and select the Unit Testing Bundle template, then click Next.

    New Target Template Dialog
  3. In the next dialog, enter a Product Name of your choice, then click Finish.

    New Target Options Dialog

Once you have created the Unit Testing Bundle, its target will be listed under the Targets pane within the Xcode project editor.

Xcode Project Editor

Usually, when writing tests for a class, you would create a Swift file in your test target, then import the class using the @testable declaration to make the class accessible to the test target. However, app extension targets cannot be set as the Target to be Tested option when creating a Unit Testing Bundle. Therefore, classes within the Wallet Extensions targets cannot be imported into a test target with the @testable declaration.

To unit test the Non-UI extension, you must add a duplicate of the Non-UI extension’s principal class to the Unit Testing Bundle. Add a new Swift file WNonUIExtHandler.swift to the Unit Testing Bundle by choosing File > New > File, then copy the code of the Non-UI extension’s principal class into the WNonUIExtHandler.swift file in the test target. The copied principal class will be used to create unit tests.

Once you have copied the principal class of the Non-UI extension to the Unit Testing Bundle, you will need to modify the class to support dependency injection. This edit will allow you to create instances of the principal class with mock objects for PKPassLibrary, UserDefaults, and WatchConnectivitySession to support unit tests. Update the principal class with the following instance variables and initializer:


// WNonUIExtHandler.swift (Unit Testing Bundle)

class WNonUIExtHandler: PKIssuerProvisioningExtensionHandler {
    let passLibrary: PKPassLibrary
    let appGroupSharedDefaults: UserDefaults
    let watchSession: WatchConnectivitySession
    
    init(passLibrary: PKPassLibrary, sharedDefaults: UserDefaults, watchSession: WatchConnectivitySession) {
        self.passLibrary = passLibrary
        self.appGroupSharedDefaults = sharedDefaults
        self.watchSession = watchSession
    }

    . . .
}
    

Also, modify the return value of the getPaymentPassEntry(provisioningCredential:) private helper method. Return a PKIssuerProvisioningExtensionPaymentPassEntry that has a card art created from a system image for testing purposes.


// WNonUIExtHandler.swift (Unit Testing Bundle)

class WNonUIExtHandler: PKIssuerProvisioningExtensionHandler {
    . . .

    private func getPaymentPassEntry(provisioningCredential: ProvisioningCredential) -> PKIssuerProvisioningExtensionPaymentPassEntry {

        . . .

        // Instantiate and return a payment pass entry
        return PKIssuerProvisioningExtensionPaymentPassEntry(identifier: identifier,
                                                             title: label,
                                                             art: getEntryArt(image: UIImage(systemName: "creditcard.and.123")!),
                                                             addRequestConfiguration: requestConfig)!
}
    

Next, you will need to copy over any model structures or classes from your Non-UI extension target that the principal class depends on. Add the dependent models to the Unit Testing Bundle. For example, the following models were created to support the sample code for the Non-UI extension’s implementation:


// TestModels.swift

import os
import PassKit

let log = Logger()

/**
 This structure mocks the data that can be stored in the user's defaults
 database to support the code in the non-UI extension. You will need to
 refactor this structure to support the issuer app's persisted payment
 card data.
 */
struct ProvisioningCredential: Equatable, Codable, Hashable {
    var primaryAccountIdentifier: String
    var label: String
    var assetName: String
    var cardholderName: String
    var localizedDescription: String
    var primaryAccountSuffix: String
    var expiration: String
}

/**
 This structure mocks retrieving required data for a payment pass to support the
 sample code in the non-UI extension's principal class. You will need to refactor
 this structure to support retrieving the necessary pass data from the issuer's
 server.
 */
struct PassResource {

    static public func requestPaymentPassData(_ configuration: PKAddPaymentPassRequestConfiguration, certificateChain certificates: [Data],
                                              nonce: Data, nonceSignature: Data) -> EncryptedPassDataResponse {

        var response = EncryptedPassDataResponse()
        response.activationData = Data()
        response.encryptedPassData = Data()

        if configuration.encryptionScheme == .ECC_V2 {
            response.ephemeralPublicKey = Data()
        } else if configuration.encryptionScheme == .RSA_V2 {
            response.wrappedKey = Data()
        }

        return response
    }
}

/**
 This structure mocks a response object for encrypted pass data to support the
 sample code in the non-UI extension.
 */
struct EncryptedPassDataResponse {
    var activationData: Data?
    var encryptedPassData: Data?
    var ephemeralPublicKey: Data?
    var wrappedKey: Data?
}
    

Create Mock Classes

To support testing the WNonUIExtHandler principal class, implement mock classes of PKPassLibrary and UserDefaults. When writing unit tests, the mock classes will be used to create mock objects. The mock objects can be passed to the initializer of WNonUIExtHandler via dependency injection. Mocking an object allows you to control the state and behavior of the object during testing to support different test cases.

Mock UserDefaults

Mocking UserDefaults will allow your test runs to mock the retrieval of payment passes that have been added to the user’s defaults database. You can mock the retrieval of cached data representing a user’s entire set of payment passes within the issuer app.

Reference the following code to create a mock UserDefaults:


// TestModels.swift

class MockUserDefaults: UserDefaults {
    private var passCredentialsData: [String: ProvisioningCredential] = [:]

    override func data(forKey defaultName: String) -> Data? {
        if defaultName == "PaymentPassCredentials" && !passCredentialsData.isEmpty {
            if let encoded = try? JSONEncoder().encode(passCredentialsData) {
                return encoded
            }
        }
        return nil
    }

    override func bool(forKey defaultName: String) -> Bool {
        if defaultName == "ShouldRequireAuthenticationForAppleWallet" {
            return true
        }
        return false
    }

    func addPassCredentialJson(_ primaryAccountIdentifier: String, cardholderName: String,
                               primaryAccountSuffix: String, expiration: String) {

        let credential = ProvisioningCredential(primaryAccountIdentifier: primaryAccountIdentifier,
                                                label: "",
                                                assetName: "",
                                                cardholderName: cardholderName,
                                                localizedDescription: "",
                                                primaryAccountSuffix: primaryAccountSuffix,
                                                expiration: expiration)

        passCredentialsData[primaryAccountIdentifier] = credential
    }
}
    

Mock PKPassLibrary

Mocking PKPassLibrary will allow your test runs to mock the retrieval of payment passes that have already been added to Apple Pay on an iPhone or Apple Watch. You can mock the retrieval of such passes, which would exist in a user’s pass library.

Reference the following code to create classes that support mocking a user’s pass library:


// TestModels.swift

/**
 This class mocks PKPassLibrary to support testing the non-UI extension.
 */
class MockPKPassLibrary: PKPassLibrary {
    var mockPasses: [PKPass] = []

    override func passes(of passType: PKPassType) -> [PKPass] {
        if passType == .secureElement {
            return mockPasses
        }
        return []
    }
}

/**
 This class mocks PKPass to support testing the non-UI extension.
 */
class MockPKPass: PKPass {
    var primaryAccountIdentifier: String = ""
    var isRemote: Bool = false

    init(primaryAccountIdentifier: String, isRemote: Bool) {
        super.init()
        self.primaryAccountIdentifier = primaryAccountIdentifier
        self.isRemote = isRemote
    }

    override var passType: PKPassType { .secureElement }

    override var deviceName: String {
        if isRemote {
            return "Apple Watch"
        } else {
            return "iPhone"
        }
    }

    override var isRemotePass: Bool { isRemote }

    override var secureElementPass: PKSecureElementPass? {
        let pass = MockPKSecureElementPass()
        pass.primaryAccountIdentifierOverride = primaryAccountIdentifier
        return pass
    }
}

/**
 This class mocks PKSecureElementPass to support testing the non-UI extension.
 */
class MockPKSecureElementPass: PKSecureElementPass {
    var primaryAccountIdentifierOverride = ""

    override var primaryAccountIdentifier: String { primaryAccountIdentifierOverride }
}
    

Mock WatchConnectivitySession

Mocking WatchConnectivitySession, which subclasses WCSessionDelegate, will allow your test runs to mock determining if an iPhone device is paired with an Apple Watch. You can mock the isPaired computed property of a WCSession.

Reference the following code to create a class that mocks WatchConnectivitySession:


// TestModels.swift
        
class MockWatchConnectivitySession: WatchConnectivitySession {
    var paired: Bool

    init(paired: Bool = true) {
        self.paired = paired
    }

    override var isPaired: Bool { paired }
}
    

Extend PKIssuerProvisioningExtensionStatus

Extend PKIssuerProvisioningExtensionStatus to override its isEqual(_:) method. This modification creates an equal comparator for the class to support unit testing the Non-UI extension’s status(completion:) method.

Reference the following code to extend PKIssuerProvisioningExtensionStatus:


// TestModels.swift

extension PKIssuerProvisioningExtensionStatus {

    override open func isEqual(_ object: Any?) -> Bool {
        guard let rhs = object as? PKIssuerProvisioningExtensionStatus else {
            return false
        }

        let lhs = self
        return lhs.passEntriesAvailable == rhs.passEntriesAvailable &&
            lhs.remotePassEntriesAvailable == rhs.remotePassEntriesAvailable &&
            lhs.requiresAuthentication == rhs.requiresAuthentication
    }
}
    

Create a Unit Test Case Class

Create a Unit Test Case Class by choosing File > New > File, then selecting the Unit Test Case Class template within the new file dialog. Name the file WNonUIExtHandlerTests.

New Target Template Dialog

Once the Unit Test Case Class is created to test the Non-UI extension’s principal class, add unit tests by creating class methods. You must start a method name with test in order for Xcode to recognize the method as a unit test, such as in the below example:


// WNonUIExtHandlerTests.swift

import XCTest

final class WNonUIExtHandlerTests: XCTestCase {

    func testExample() {
            // Add testing logic
    }
}
    

Test Status Method

Import PassKit into the Unit Test Case Class WNonUIExtHandlerTests. Reference the following code for a sample unit test of the status(completion:) method:


// WNonUIExtHandlerTests.swift

import XCTest
import PassKit

final class WNonUIExtHandlerTests: XCTestCase {

    func testStatus() {
        
        // Create mock objects.
        let mockUserDefaults = MockUserDefaults()
        let mockPassLibrary = MockPKPassLibrary()
        let mockWatchSession = MockWatchConnectivitySession()
        
        // Initialize WNonUIExtHandler with mock objects.
        let nonUIExt = WNonUIExtHandler(passLibrary: mockPassLibrary, sharedDefaults: mockUserDefaults, watchSession: mockWatchSession)
        
        // Create pass 1 (available for Apple Watch).
        let pass1 = MockPKPass(primaryAccountIdentifier: "123", isRemote: false)
        mockUserDefaults.addPassCredentialJson("123", cardholderName: "Johnny Appleseed",
                                               primaryAccountSuffix: "1234", expiration: "10/28")
        
        // Create pass 2 (available for Apple Watch).
        let pass2 = MockPKPass(primaryAccountIdentifier: "456", isRemote: false)
        mockUserDefaults.addPassCredentialJson("456", cardholderName: "Johnny Appleseed",
                                               primaryAccountSuffix: "4567", expiration: "01/28")
        
        // Create pass 3 (available for iPhone and Apple Watch).
        mockUserDefaults.addPassCredentialJson("789", cardholderName: "Johnny Appleseed",
                                               primaryAccountSuffix: "7891", expiration: "03/28")
        
        // Add passes to the mock pass library.
        mockPassLibrary.mockPasses = [pass1, pass2]
        
        // Create a status object with the expected values for its
        // instance properties.
        let expectedStatus = PKIssuerProvisioningExtensionStatus()
        expectedStatus.passEntriesAvailable = true
        expectedStatus.remotePassEntriesAvailable = true
        expectedStatus.requiresAuthentication = true
        
        // Create a status object to store the actual result of calling the
        // Non-UI extension's status method.
        var actualStatus: PKIssuerProvisioningExtensionStatus?
        
        // Create a stub completion handler.
        func statusCompletion(_ status: PKIssuerProvisioningExtensionStatus) {
            actualStatus = status
        }
        
        // Call the status method with the completion handler.
        nonUIExt.status(completion: statusCompletion)

        // Compare the expected status with the actual status.
        XCTAssertEqual(expectedStatus, actualStatus)
    }
}
    

Test Pass Entries (for iPhone) Method

Reference the following code for a sample unit test of the passEntries(completion:) method:


// WNonUIExtHandlerTests.swift

import XCTest
import PassKit

final class WNonUIExtHandlerTests: XCTestCase {

     func testPassEntriesForIphone() {
        
        // Create mock objects.
        let mockUserDefaults = MockUserDefaults()
        let mockPassLibrary = MockPKPassLibrary()
        let mockWatchSession = MockWatchConnectivitySession()
        
        // Initialize WNonUIExtHandler with mock objects.
        let nonUIExt = WNonUIExtHandler(passLibrary: mockPassLibrary, sharedDefaults: mockUserDefaults, watchSession: mockWatchSession)
        
        // Create pass 1.
        let pass1A = MockPKPass(primaryAccountIdentifier: "123", isRemote: false)
        let pass1B = MockPKPass(primaryAccountIdentifier: "123", isRemote: true)
        mockUserDefaults.addPassCredentialJson("123", cardholderName: "Johnny Appleseed",
                                               primaryAccountSuffix: "1234", expiration: "10/28")
        
        // Create pass 2 (available for Apple Watch).
        let pass2 = MockPKPass(primaryAccountIdentifier: "456", isRemote: false)
        mockUserDefaults.addPassCredentialJson("456", cardholderName: "Johnny Appleseed",
                                               primaryAccountSuffix: "4567", expiration: "01/28")
        
        // Create pass 3 (available for iPhone and Apple Watch).
        mockUserDefaults.addPassCredentialJson("789", cardholderName: "Johnny Appleseed",
                                               primaryAccountSuffix: "7891", expiration: "03/28")
        
        // Add passes to mock pass library.
        mockPassLibrary.mockPasses = [pass1A, pass1B, pass2]
        
        // Create an array to store the actual list of pass entries that are
        // created when calling the Non-UI extension's pass entries method.
        var entries: [PKIssuerProvisioningExtensionPassEntry] = []
        
        // Create a stub completion handler.
        func passEntriesCompletion(_ passEntries: [PKIssuerProvisioningExtensionPassEntry]) {
            entries = passEntries
        }
        
        // Call the pass entries method with the completion handler.
        nonUIExt.passEntries(completion: passEntriesCompletion)
        
        // Get the first pass entry from the list. The test run should result
        // in the list containing only one pass entry.
        var entry: PKIssuerProvisioningExtensionPaymentPassEntry?
        if !entries.isEmpty {
            entry = entries[0] as? PKIssuerProvisioningExtensionPaymentPassEntry
        }
        
        // Create the expected expiration.
        let expectedExpirationLabel = PKLabeledValue(label: "expiration", value: "03/28")
        
        // Extract the actual expiration.
        let actualExpirationLabel = entry?.addRequestConfiguration.cardDetails.first(where: { $0.label == "expiration" })
        
        // Compare the expected data against the actual data of the pass entry.
        XCTAssertNotNil(entry)
        XCTAssertEqual(1, entries.count)
        XCTAssertEqual("789", entry?.identifier)
        XCTAssertEqual("789", entry?.addRequestConfiguration.primaryAccountIdentifier)
        XCTAssertEqual("7891", entry?.addRequestConfiguration.primaryAccountSuffix)
        XCTAssertEqual("Johnny Appleseed", entry?.addRequestConfiguration.cardholderName)
        XCTAssertEqual(expectedExpirationLabel, actualExpirationLabel)
    }
}
    

Test Remote Pass Entries (for Apple Watch) Method

Reference the following code for a sample unit test of the remotePassEntries(completion:) method:


// WNonUIExtHandlerTests.swift

import XCTest
import PassKit

final class WNonUIExtHandlerTests: XCTestCase {

     func testRemotePassEntriesForAppleWatch() {
        
        // Create mock objects.
        let mockUserDefaults = MockUserDefaults()
        let mockPassLibrary = MockPKPassLibrary()
        let mockWatchSession = MockWatchConnectivitySession()
        
        // Initialize WNonUIExtHandler with mock objects.
        let nonUIExt = WNonUIExtHandler(passLibrary: mockPassLibrary, sharedDefaults: mockUserDefaults, watchSession: mockWatchSession)
        
        // Create pass 1.
        let pass1A = MockPKPass(primaryAccountIdentifier: "123", isRemote: false)
        let pass1B = MockPKPass(primaryAccountIdentifier: "123", isRemote: true)
        mockUserDefaults.addPassCredentialJson("123", cardholderName: "Johnny Appleseed",
                                               primaryAccountSuffix: "1234", expiration: "10/28")
        
        // Create pass 2 (available for Apple Watch).
        let pass2 = MockPKPass(primaryAccountIdentifier: "456", isRemote: false)
        mockUserDefaults.addPassCredentialJson("456", cardholderName: "Johnny Appleseed",
                                               primaryAccountSuffix: "4567", expiration: "01/28")
        
        // Add passes to mock pass library.
        mockPassLibrary.mockPasses = [pass1A, pass1B, pass2]
        
        // Create an array to store the actual list of pass entries that are
        // created when calling the Non-UI extension's pass entries method.
        var entries: [PKIssuerProvisioningExtensionPassEntry] = []
        
        // Create a stub completion handler.
        func passEntriesCompletion(_ passEntries: [PKIssuerProvisioningExtensionPassEntry]) {
            entries = passEntries
        }
        
        // Call the remote pass entries method with the completion handler.
        nonUIExt.remotePassEntries(completion: passEntriesCompletion)
        
        // Get the first pass entry from the list. The test run should result
        // in the list containing only one pass entry.
        var entry: PKIssuerProvisioningExtensionPaymentPassEntry?
        if !entries.isEmpty {
            entry = entries[0] as? PKIssuerProvisioningExtensionPaymentPassEntry
        }
        
        // Create the expected expiration.
        let expectedExpirationLabel = PKLabeledValue(label: "expiration", value: "01/28")
        
        // Extract the actual expiration.
        let actualExpirationLabel = entry?.addRequestConfiguration.cardDetails.first(where: { $0.label == "expiration" })
        
        // Compare the expected data against the actual data of the pass entry.
        XCTAssertNotNil(entry)
        XCTAssertEqual(1, entries.count)
        XCTAssertEqual("456", entry?.identifier)
        XCTAssertEqual("456", entry?.addRequestConfiguration.primaryAccountIdentifier)
        XCTAssertEqual("4567", entry?.addRequestConfiguration.primaryAccountSuffix)
        XCTAssertEqual("Johnny Appleseed", entry?.addRequestConfiguration.cardholderName)
        XCTAssertEqual(expectedExpirationLabel, actualExpirationLabel)
    }
}
    

Test Generate Add Payment Pass Request Method

Reference the following code for a sample unit test of the generateAddPaymentPassRequestForPassEntryWithIdentifier() method:


// WNonUIExtHandlerTests.swift

import XCTest
import PassKit

final class WNonUIExtHandlerTests: XCTestCase {

    func testGenerateAddPaymentPassRequest() {
        
        // Create mock objects.
        let mockUserDefaults = MockUserDefaults()
        let mockPassLibrary = MockPKPassLibrary()
        let mockWatchSession = MockWatchConnectivitySession()
        
        // Initialize WNonUIExtHandler with mock objects.
        let nonUIExt = WNonUIExtHandler(passLibrary: mockPassLibrary, sharedDefaults: mockUserDefaults, watchSession: mockWatchSession)
        
        // Create the objects to be passed as parameters of the generate
        // add payment pass request method.
        let identifier = "123"
        let config = PKAddPaymentPassRequestConfiguration(encryptionScheme: .ECC_V2)!
        let certificateChain: [Data] = []
        let nonce = Data()
        let nonceSignature = Data()
        
        // Create a request object to store the actual result of calling the
        // Non-UI extension's generate add payment pass request method.
        var actualRequest: PKAddPaymentPassRequest?
        
        // Create a stub completion handler.
        func generateRequestCompletion(_ request: PKAddPaymentPassRequest?) {
            actualRequest = request
        }
        
        // Call the generate method with the required parameters,
        // including the completion handler.
        nonUIExt.generateAddPaymentPassRequestForPassEntryWithIdentifier(identifier,
                                                                         configuration: config,
                                                                         certificateChain: certificateChain,
                                                                         nonce: nonce,
                                                                         nonceSignature: nonceSignature,
                                                                         completionHandler: generateRequestCompletion)
        
        // Assert that the actual request object's data is not nil.
        XCTAssertNotNil(actualRequest)
        XCTAssertNotNil(actualRequest?.activationData)
        XCTAssertNotNil(actualRequest?.encryptedPassData)
        XCTAssertNotNil(actualRequest?.ephemeralPublicKey)
    }
}
    

Beta Testing with TestFlight

TestFlight is a beta testing tool that can be used to test the issuer app on real user devices. TestFlight lets you distribute beta builds of your app, manage beta testers, and collect feedback. Designate up to 100 members of your team who hold the Account Holder, Admin, App Manager, Developer, or Marketing role in App Store Connect as beta testers. You can create multiple groups and add different builds to each one, depending on which features you want each group to focus on. For more information, see Add internal testers.

Additionally, you can apply tester metrics filters to better evaluate tester engagement and manage participation. For more information, see View and manage tester information. While you iterate on your app, each member can quickly test beta builds on up to 30 devices and access all of your beta builds that are available for testing.

With the TestFlight app for iOS, testers can send feedback directly from an iOS issuer app simply by taking a screenshot. They can also provide additional context about an app crash immediately after it occurs. You can view this feedback by going to your app’s TestFlight page in App Store Connect, and clicking Crashes or Screenshots in the Feedback section. For more information, see View tester feedback.

Up to 100 apps can be tested at a time with TestFlight, internally or externally, and multiple builds can be tested simultaneously. To discover how you can use TestFlight for beta testing an iOS issuer app, see the video Get started with TestFlight.

We also recommend testing in production environments. Testing occurs in the production environments using production devices through TestFlight after the necessary approvals. Once the app is approved from App Review, the issuer can distribute the app using TestFlight. Side-loading from Xcode does not work in the production environments, so you must use TestFlight to carry out end-to-end (E2E) testing of the Wallet Extensions feature.

For TestFlight and the App Store, be sure to budget additional time for the App Review process, as the approval time may vary. Please contact your Apple project contact for additional support with production E2E testing of Wallet Extensions.

FAQ

  • Is it necessary to update passEntriesAvailable and remotePassEntriesAvailable properties after each login?

    The process is similar to In-App Provisioning. Use the PKPassLibrary APIs to retrieve passes in iPhone and Apple Watch, and update these property values based on the passes’ presence in Apple Wallet.

  • How can code be shared between the main app and the extensions?

    You can use a shared framework, or you can copy reusable code logic and Swift files from the main app into the extensions. In this demo we are taking the latter approach.

  • Is the authentication page a redirection to the issuer app or does the issuer app authentication present as a view controller within the Apple Wallet app?

    The authentication page is not a redirection to the issuer app. When a user starts provisioning from Wallet using the Wallet Extensions, Apple Wallet invokes the UI extension, which is simply a login view. Since the UI extension is a login page that handles authorizing the user to add a pass, the extension can reuse the app’s existing authentication mechanism (username/password or biometric authentication) and login code for the view.

  • How can I reuse the application authentication code?

    You will need to copy the issuer app’s login/authentication code into the UI extension, then refactor the code to include and call the authorization completionHandler. A second approach is to create a shared framework to host the reusable authentication code. In this demo we are taking the first approach.

  • Does the extension need to call the status completion handler three times (once each for setting passEntriesAvailable, remotePassEntriesAvailable, and requiresAuthentication properties)? Can the system pass all three properties at the same time?

    The Non-UI extension only needs to call the status completion handler once. The status completion handler indicates whether a payment pass is available to add to Apple Wallet and whether adding a pass requires authentication. Its status is an object, PKIssuerProvisioningExtensionStatus, within which the three instance properties are present and can be set.

  • What is the difference in the behavior of the extension when calling passEntriesAvailable=true, remotePassEntriesAvailable=true, and requiresAuthentication=true versus calling passEntriesAvailable=true, remotePassEntriesAvailable=false, and requiresAuthentication=true?

    The extension provides information required for both iPhone and Apple Watch, in which iOS shows the extension for the correct device. For example, if Apple Wallet launches on an iPhone, iOS uses the passEntriesAvailable status. When the bridge app (Wallet from Watch app) launches to add cards to Apple Watch, iOS uses the remotePassEntriesAvailable status.

  • The issuer app needs to provide information to Apple Wallet if any cards are available for provisioning before the user logs in. How do issuers do this?

    The user needs to log in to the app at least once to update the extension status before trying to use the Wallet extensions. So, the issuer app needs to populate these values during a regular login. The issuer can use an App Group container to share data between the issuer app and the extensions. The issuer can store the required card data into the container during a regular login, then access the data during the Wallet Extensions process. Use an instance of UserDefaults and its methods to interact with a user’s defaults database within the container. You can instantiate the object by calling UserDefaults(suiteName: AppGroupID)!.

  • Is it possible to invoke UIApplication.shared from the extensions?

    Extensions cannot access a UIApplication.shared object, or use any of its methods, because it operates in a restricted environment with limited access to the main, containing app’s resources. Instead, developers should consider alternative methods, such as using shared containers through App Groups for data sharing or extension-friendly APIs. An app extension cannot use any API marked in header files with the NS_EXTENSION_UNAVAILABLE macro, or similar unavailability macro, or any API in an unavailable framework.

  • If a login fails, where should the issuer app display the error to the user?

    We recommend to show the error on the login screen. The UI extension should follow the same policy as a regular app login.

  • Can the app use any value as the card identifier as long as it’s unique per card?

    Yes, the app can use any value unique to each card. However, it is best practice to use the FPAN/FPANID to uniquely identify each card.

  • Does the provisioning of multiple cards happen at the same time?

    Apple Wallet cycles the provisioning card by card.

  • What are the size and resolution requirements for the card images?

    The card image should follow the same requirements as in the Functional Requirements for Apple Pay and Direct NFC Access. (1536 x 969 resolution, <4 MB, squared corners, no chip contacts, and so forth). Please contact your Apple project contact, PNO, or service provider relationship manager for more information on Functional Requirements for Apple Pay and Direct NFC Access.

  • Do the extensions require a new bundle identifier?

    Yes, the extensions require new bundle IDs that are different from the bundle ID of the issuer app. The extensions’ bundle IDs need to be added to the associatedApplicationIdentifier at the PNO. iOS now supports wildcard bundle identifiers. Issuers also need to update all the existing passes in Apple Wallet, using the PNO APIs.

    It is best practice to not change the bundle ID of the main issuer app when implementing Wallet Extensions, as changing the issuer app’s bundle ID may adversely affect In-App Provisioning configurations. Only the extensions require new bundle IDs (which are automatically created when adding extension targets to an Xcode project).

  • Which extension point do issuers use for the UI extension?

    The UI extension uses com.apple.PassKit.issuer-provisioning.authorization as its extension point. It is different from the non-UI extension.

  • Which extension point do issuers use for the Non-UI extension?

    The Non-UI extension uses com.apple.PassKit.issuer-provisioning as its extension point.

  • What could be wrong if the issuer app icon doesn’t show?

    Ensure the new issuer app is installed and opened at least once to update the extensions. Also check the PNO metadata and the identifiers of the extensions.

  • Do issuers need to implement In-App Provisioning for Wallet extensions to work?

    Yes, issuers need to implement In-App Provisioning before implementing Wallet extensions.

  • Can the Intent Extension template be used to implement Wallet Extensions?

    Yes, the Intent Extension template can be used to implement Wallet Extensions, as there is not a specific extension template in Xcode for Wallet Extensions. Extension templates primarily serve to generate skeleton code in Xcode.

    The documentation will direct you to choose an Action Extension template to get started, then instruct you on the steps of making the necessary changes to conform this extension template to Wallet Extensions. However, you can choose the Intent Extension template as an alternative option. When using an Intent Extension, you should carry out the same steps in the Wallet Extensions documentation for configuring the entitlement, updating the plists, implementing the code logic, etc.

  • Which iOS versions support Wallet extensions?

    iOS 14 and later.

  • Are Wallet extensions public extensions?

    Wallet extensions are custom extensions.

  • Do Wallet extensions work for all apps with the same Team ID?

    No, it is per Adam ID, which is a unique ID assigned to an app.

Additional Resources

Ready to add Wallet Extensions to your iOS issuer app? Here are a few links you may find useful:

Questions or Feedback

Check out our developer forums or reach out to us.