Overview
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 from Apple.
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.
Note
Implementing In-App Provisioning is a prerequisite for the configuration of Wallet Extensions. In-App Provisioning requires a minimum of iOS 10.3 or the version communicated during your country In-App Provisioning launch (whichever version is greater).
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.
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.
Note
After Wallet Extensions are added to an issuer app, users will need to install the latest updated build version of the app that includes the new extensions in order to use the Wallet Extensions functionality.
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.
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.
- 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.
-
Apple Wallet to the issuer app: The principal class of the non-UI extension
needs to be a subclass of
PKIssuerProvisioningExtensionHandler
. Apple Wallet usesPKIssuerProvisioningExtensionStatus
, 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. -
The issuer app to Apple Wallet: The
status(completion:)
method withinPKIssuerProvisioningExtensionHandler
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.
-
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. -
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.
Important
The system needs to invoke the handler within 100 ms, or the extension does not display to the user in Apple Wallet.
-
- 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.
-
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 aUIViewController
from the issuer app to perform authentication. The UI extension needs to be a subclass ofPKIssuerProvisioningExtensionAuthorizationProviding
.Important
It’s a good practice for the issuer to reuse the existing login/authentication code from the main app for the UI extension. The screen can be identical to the issuer app login UI and expect the user to login with same credentials.
- 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.
-
The issuer app to Apple Wallet: The result of the
completionHandler
, an instance property ofPKIssuerProvisioningExtensionAuthorizationProviding
, indicates whether the user has authorization to add a payment pass.-
authorized
: The issuer declares that the user successfully authorized adding a payment pass. -
canceled
: The issuer declares that the user canceled authorization or doesn’t have authorization to add the payment pass.
-
-
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.Important
With successful authorization, the non-UI extension is capable of fetching all data necessary to create and provision payment passes for a reasonable period of time.
For Apple Wallet to correctly show the issuer app icon, the app needs to exclude any passes the user has already added to their device from the list of available passes. Passes need to include the extension’s bundle identifier in
associatedApplicationIdentifiers
to ensure accurate status of existing passes in Apple Wallet. To learn more about this identifier key, see PNO Pass Metadata Configuration. -
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.-
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. -
title
: The name of the payment pass that displays to the user. -
identifier
: An internal value the issuer uses to identify the card. This identifier needs to be unique. -
PKAddPaymentPassRequestConfiguration
: The configuration data for setting up and displaying a view controller that lets the user add a payment pass.
Important
Don’t return payment passes that are already present in the user’s pass library. The system needs to invoke the handler within 20 seconds, or it treats the call as a failure and the attempt stops.
-
- The user selects the passes to add: The user selects one or more cards to provision.
-
Apple Wallet to the issuer app: Apple Wallet uses the completion handler
generateAddPaymentPassRequestForPassEntryWithIdentifier
to interrogate the issuer app for thePKAddPaymentPassRequest
and supplies the identifier, configuration data, certificates, nonce, and nonce signatures for the selected payment passes.Important
The system needs to invoke the handler within 20 seconds, or it treats the call as a failure and the attempt stops.
-
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 applepayentitlements@apple.com.
Subject |
Apple Pay Entitlement and Allow List Request - Issuer Name - [Country Code]
|
---|---|
Body |
|
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.
Note
Only a production Team ID and Adam ID can receive the entitlement and be on the allow list. The Team ID needs to be that of the developer publishing the app on the App Store. If the issuer app has been previously published to the App Store, it is best practice to use the same Team ID to publish new builds of the app that contains the Wallet Extensions process. Apple does not grant private entitlements to test Team IDs, and does not add test apps to allow lists.
Note
If an issuer app that has successfully implemented In-App Provisioning makes changes to its Team ID, Adam ID, or bundle ID during the implementation of Wallet Extensions, the issuer may need to re-request the required private entitlement.
Create Extension Targets
To configure entitlements, you first need to create new targets for the Non-UI and UI extensions.
-
To add a new target to your Xcode app project, choose File > New > Target.
-
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.
-
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.
-
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.
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.
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.
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:
-
Click the add button (+) below the App Groups list.
-
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. -
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.
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.
Note
Issuers should contact their PNO or service provider relationship manager to learn which PNO portal to use for Apple.
PNO Pass Metadata |
---|
The issuer name. |
The name of the iOS app to use for In-App Verification. |
The issuer’s App ID. This key allows the respective apps to see, access, and activate the issuer’s payment passes. |
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. |
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 |
Note
Wallet will not allow the issuer app and its extensions to access the list of its provisioned payment passes if the associatedApplicationIdentifiers
key isn’t set up correctly at the PNO or the service provider. Issuers should ensure to update this field
to include the extensions’ App IDs. Without a correctly configured key, the issuer application will have no way to determine whether a given pass has already been added to the user’s device.
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 |
Note
The following wildcard options for App IDs are also supported in iOS 14:
1234ABCD.com.example.IssuerApp.*
or
1234ABCD.com.example.*
.
If the issuer uses an explicit App ID, its prefix may not match their developer account Team ID. See Developer Account Help for more information.
Important
All these values are case-sensitive, so they need to be exact when entering them in the PNO portal.
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.
Note
The In-App Provisioning capability will be listed as an option in Xcode’s capabilities dialog after Apple has granted the issuer app permission to use the entitlement.
Follow the steps in Add a capability to add the In-App Provisioning capability to your app’s target in Xcode.
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.
Important
Remember to add the In-App Provisioning capability to both the Non-UI extension target and UI extension target.
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.
Note
Depending on the version of Xcode, the Info.plist
file may display as
Info
within the Project navigator. The following figure shows an example of an Info
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.
Tip
To display the Info.plist
source code as a property list again, control-click
the property list file in the Project navigator, then choose Open As > Property List.
Note
The principal class names for the Non-UI extension and UI extension in the figures below
have been refactored to WNonUIExtHandler
and WUIExtHandler
,
respectively.
Notice the name change of the Swift principal class files in the Project navigator. The NSExtensionPrincipalClass
property values in the tables
below are based on this refactoring.
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>
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>
Important
When editing an Info.plist
file, ensure to remove any additional whitespace,
including vertical whitespace from line skips. Since Xcode will interpret spaces as additional characters
within a plist
, any additional whitespace in a plist
can break an extension’s functionality.
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 { }
Note
The following structures are NOT members of the PassKit
framework. They were created for the purpose of
the sample code below. You may have to adjust your implementation of the sample code based on how payment pass
related data is structured within the issuer app.
ProvisioningCredential
EncryptedPassDataResponse
PassResource
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
or
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 theWatchConnectivity
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.
Important
The system needs to invoke the status completion handler within 100 ms, or the extension doesn’t display to the user in Apple Wallet.
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.
Important
Don’t return payment passes that are already present in the user’s pass library. The system needs to invoke the handler within 20 seconds, or it treats the call as a failure and the attempt stops.
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 theprimaryAccountIdentifier
to identify passes. If the issuer app does not have access to the primary account identifier / FPANID (only created after the first provision), use eitherpasses(of:)
method or theremoteSecureElementPasses
property of thePKPassLibrary
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.
Important
Don’t return remote payment passes that are already present in the user’s pass library. The system needs to invoke the handler within 20 seconds, or it treats the call as a failure and the attempt stops.
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 ofPKAddPaymentPassRequestConfiguration
. -
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 | Type | 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. |
Note
Instances of PKAddPaymentPassRequestConfiguration
are created during calls to the Pass Entries (for iPhone) and Remote Pass Entries (for Apple Watch)
methods. These configuration instances are passed to the Generate Add Payment Pass Request method calls during the provisioning process.
Important
The system needs to invoke the handler within 20 seconds, or it treats the call as a failure and the attempt stops.
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.
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
or .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
or .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
or .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.
Note
Some of the SwiftUI
view elements in the sample code below require a minimum of iOS 16. You will need to consider other view elements in your implementation
of the UI extension if planning to support versions less than iOS 16 for the issuer app’s deployment of Wallet Extensions.
Wallet Extensions can be deployed to iOS 14 or later.
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.
-
First, select the UI extension’s
MainInterface
file within the Project navigator. This file will display the UI extension’s Storyboard interface builder. -
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.
-
Select the Identity inspector for the View Controller. Within the Custom Class pane of the inspector, set the class to
WUIExtHandler
principal class. -
Select the Attributes inspector for the View Controller. Within the View Controller pane of the inspector, select the "Is Initial View Controller" checkbox.
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.
-
To add a new target to your Xcode app project, choose File > New > Target.
-
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.
-
In the next dialog, enter a Product Name of your choice, then click Finish.
Note
When creating a Unit Testing Bundle, app extension targets cannot be set as the Target to be Tested option within the target dialog. You should choose the target of the containing issuer app for this option.
Once you have created the Unit Testing Bundle, its target will be listed under the Targets pane within the 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.
Important
Any below reference to the principal class of the Non-UI extension is referring to the copied version of
WNonUIExtHandler.swift
file added to the Unit Testing Bundle target. Only make
the following suggested changes to this file to support dependency injection for testing purposes. DO NOT modify
the original file located in the Non-UI extension target.
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:
Note
The models below are for demo purposes only. You may need to adjust the models based on how the issuer app
requests
Data
required for a PKAddPaymentPassRequest
from the issuer’s servers.
// 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
.
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.
Note
Each beta build in TestFlight is available to test for up to 90 days, starting from the day the developer uploads their build. For more information, see Testing Apps with TestFlight.
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.
Important
It’s the responsibility of the issuer to thoroughly test the Wallet Extensions functionality for the app across all supported iOS versions and devices prior to releasing to the general public.
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.
Important
When testing any new build of the issuer app that has the Wallet Extensions feature, you must log in to the issuer app at least once for Apple Wallet to detect the extensions.
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
andremotePassEntriesAvailable
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
, andrequiresAuthentication
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
, andrequiresAuthentication=true
versus callingpassEntriesAvailable=true
,remotePassEntriesAvailable=false
, andrequiresAuthentication=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 theremotePassEntriesAvailable
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 callingUserDefaults(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 theNS_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.