Wallet Extensions Guide
Wallet Extensions (In-App Provisioning or Issuer Extensions) introduced in iOS 14 makes it easier for users to know that they can add a payment pass to Apple Pay by improving discoverability from within Apple Wallet. In-App provisioning process via Wallet Extensions starts and finishes within Apple Wallet.
- Refer to Apple Pay Wallet Extensions for native implementation details.
- Official React Native App Extensions documentation.
Adding Wallet Extensions
MPP React Native library allows to add Wallet Extensions to an existing React Native project.
1. Creating App Extension targets
Open your react native project in Xcode and select Intents Extension as the base. Create two app extensions IssuerNonUIExtension
and IssuerUIExtension
following the steps below.
Add an app extension to Xcode app project, choose
File > New > Target, select iOS > Application Extension > Intents Extension
.Set options for the new target. Create a unique bundle ID for the extension and add to
associatedApplicationIdentifiers
in PNO metadata, e.g.:com.meawallet.app
com.meawallet.app.IssuerNonUIExtension
com.meawallet.app.IssuerUIExtensionActivate the created scheme, if asked.
Add
PassKit.framework
andMeaPushProvisioning.xcframework
toFrameworks and Libraries
of theIssuerNonUIExtension
target, and removeIntents.framework
which is not needed.Remove
Intents.framework
of theIssuerUIExtension
target.Make sure that in
Signing & Capabilities > Deployment info
iOS version is set to at least14.0
. Also, if you test device's iOS version is older than version set in Extension's TargetDeployment info
, your Issuer Extension won't be loaded by the System and it won't be visible in the Apple Wallet.It is safe to remove
.swift
sources created byIntents Extension
template. However, at least one empty "dummy" native source file is needed forIssuerNonUIExtension
andIssuerUIExtension
target for build system to work.Make sure that
mea_config
configuration file is added to your Extension's Target and included in Extension App Bundle:
2. Updating Info.plist
Remove
NSExtensionAttributes
entry fromNSExtension
dictionary in extension'sInfo.plist
file.Specify
NSExtensionPointIdentifier
andNSExtensionPrincipalClass
in theNSExtension
dictionary:Issuer Non-UI App Extension
Key Type Value NSExtensionPointIdentifier
String
com.apple.PassKit.issuer-provisioning
NSExtensionPrincipalClass
String
meawallet_react_native_mpp.MppIssuerExtensionHandler
Issuer UI App Extension
Key Type Value NSExtensionPointIdentifier
String
com.apple.PassKit.issuer-provisioning.authorization
NSExtensionPrincipalClass
String
meawallet_react_native_mpp.MppIssuerAuthorizationExtensionHandler
3. Updating Podfile specification
Define IssuerNonUIExtension
and IssuerUIExtension
nested targets`:
target 'MainApp' do
# ...
target 'IssuerNonUIExtension' do
end
target 'IssuerUIExtension' do
end
# ...
end
Run pod install
to update the Xcode project.
4. Setting Code Signing Entitlements
Issuer Extensions use the same entitlement file used for issuer app In-App Provisioning functionality.
5. Registering App Extension Components
Component for Issuer Non-UI App Extension
index.IssuerNonUIExtension.jsimport { AppRegistry } from 'react-native';
import IssuerNonUIExtension from './src/extensions/App.IssuerNonUIExtension';
AppRegistry.registerComponent("IssuerNonUIExtension", () => IssuerNonUIExtension)Component for Issuer UI App Extension
index.IssuerUIExtension.jsimport { AppRegistry } from 'react-native';
import IssuerUIExtension from './src/extensions/App.IssuerUIExtension';
AppRegistry.registerComponent("IssuerUIExtension", () => IssuerUIExtension)
6. Adding React Native Bundle Build Phase
Add a new
Bundle React Native code and images
Build Phase Run Script to both extension targets.Define a custom
.js
entry file for each Extension by settingENTRY_FILE
environment variable used byreact-native-xcode.sh
script.Issuer Non-UI App Extension
export NODE_BINARY=node
export ENTRY_FILE=index.IssuerNonUIExtension.js
../node_modules/react-native/scripts/react-native-xcode.shIssuer UI App Extension
export NODE_BINARY=node
export ENTRY_FILE=index.IssuerUIExtension.js
../node_modules/react-native/scripts/react-native-xcode.sh
7. Sharing Data between App and App Extensions
App and App Extensions can access shared App Group data. Use react-native-default-preference
React Native package or other library to access the shared App Group data.
Run npm i react-native-default-preference
to install the package.
Sample:
import GroupPreference from 'react-native-default-preference';
// Set your App Group name
GroupPreference.setName('group.com.issuer.app')
// Save data in one App / App Extension
GroupPreference.set('data', JSON.stringify(data))
// Read data in another App / App Extension
dataJSON = await GroupPreference.get('session')
References
8. Implementing Issuer Authorization Provider Extension
Apple Wallet uses Issuer UI App Extension to determine the user's authorization status.
The issuer app performs authentication of the user as a part of existing security framework, such as Face ID, Touch ID, or another authentication method subject to issuer requirements. Many issuer apps rely on a biometric authentication like Face ID and Touch ID to provide the most seamless experience for the user.
It is a good practice to use an existing segment of login from the main issuer app for the UI extension. The screen can be identical to the issuer app login and use the same login credentials.
import React from "react";
import { TextInput, Button, SafeAreaView } from "react-native";
import MeaPushProvisioning from "@meawallet/react-native-mpp";
import GroupPreference from 'react-native-default-preference';
// Set your App Group name
GroupPreference.setName('group.com.issuer.app')
async function login(email: string, password: string) {
try {
// Authenticate user and fetch session
const session = await authenticate({ email: email, password:password })
if (session) {
// Store your custom session data for future use by Non-UI App Extension
GroupPreference.set('session', JSON.stringify(session))
// The issuer declares to the Apple Wallet
// that the user successfully authorized adding a payment pass.
MeaPushProvisioning.ApplePay.IssuerUIExtension.completeAuthentication(true)
} else {
throw new Error("Authentication error")
}
} catch(error) {
console.log(`Login error: ${error}`)
// The issuer declares that the user canceled authorization or
// doesn’t have authorization to add the payment pass.
MeaPushProvisioning.ApplePay.IssuerUIExtension.completeAuthentication(false)
}
}
const IssuerUIExtension = () => {
const [email, onChangeEmail] = React.useState('')
const [password, onChangePassword] = React.useState('')
return (
<SafeAreaView style={{flex:1,justifyContent: "center"}}>
<TextInput
style={{borderWidth:1, margin:16, padding:12}}
onChangeText={onChangeEmail}
value={email}
/>
<TextInput
style={{borderWidth:1, margin:16, padding:12}}
onChangeText={onChangePassword}
value={password}
secureTextEntry={true}
/>
<Button
title="Log In"
onPress={ () => login(email, password) }
/>
</SafeAreaView>
)
}
export default IssuerUIExtension
9. Implementing IssuerExtensionHandler
After authorization, Apple Wallet calls Non-UI App Extension to interrogate the issuer app to determine the list of payment passes available to be added to iPhone Wallet and Apple Watch.
Implement following methods of an abstract class IssuerExtensionHandler
:
status(): Promise<IssuerExtensionStatus>
method reports the status of your Wallet Extension.passEntries(): Promise<IssuerExtensionPaymentPassEntry[]>
method reports the list of passes available for iPhone Wallet.remotePassEntries(): Promise<IssuerExtensionPaymentPassEntry[]>
method reports the list of passes available for Apple Watch.
- System needs to invoke
status()
handler within100 ms
, or the extension doesn’t display to the user in Apple Wallet. - System needs to retrieve passes within
20 s
, or it treats the call as a failure and the attempt stops. - Don't return payment passes that are already present in the user’s pass library.
List of eligible payment passes displayed to the user, each pass entry contains the following data:
identifier
: tokenization receipt value received fromMeaPushProvisioning.ApplePay.initializeOemTokenization(cardParams)
call.title
: A name for the pass that the system displays to the user when they add or select the card.art
: An image to that the system displays to the user when they add or select the card. The card image should follow the same requirements as in the Functional Requirements for Apple Pay and Direct NFC Access:- 1536 x 969 resolution.
- Size no larger than
4 MB
. - Have squared (not rounded) corners.
- Exclude elements that are relevant only for physical cards, which include the card number, embossed characters, hologram, chip contacts.
- Must be in landscape orientation; if the physical card has a portrait orientation, it must be modified for presentation in landscape orientation.
addRequestConfiguration
: The configuration that the system uses to add a payment pass.
import MeaPushProvisioning, { IssuerExtensionHandler, IssuerExtensionPaymentPassEntry, MppCardDataParameters } from '@meawallet/react-native-mpp';
import GroupPreference from 'react-native-default-preference';
// Set your App Group name.
GroupPreference.setName('group.com.issuer.app')
const NonUIExtension = () => {
new class extends IssuerExtensionHandler {
async status() {
return {
requiresAuthentication: true, // Adding a card requires an Authorization UI Extension.
passEntriesAvailable: true, // Payment card is available to add to an iPhone.
remotePassEntriesAvailable: true // Payment card is available to add to an Apple Watch.
}
}
async passEntries() {
return this.getPassEntries()
}
async remotePassEntries() {
return this.getPassEntries(true)
}
async getPassEntries(isRemote = false): Promise<IssuerExtensionPaymentPassEntry[]> {
// Read common shared App Group session data
const session = await GroupPreference.get('session').then(sessionJSON => sessionJSON ? JSON.parse(sessionJSON) : null)
// Use session to retrieve your Issuer's implementation specific card metadata
const cards = await getUserCards(session)
// Check if card with given 4 digit suffix already added to the Wallet
const canAdd = async (card: Card) => {
const cardExists = await ( isRemote ?
MeaPushProvisioning.ApplePay.remoteSecureElementPassExistsWithPrimaryAccountNumberSuffix(card.suffix) :
MeaPushProvisioning.ApplePay.secureElementPassExistsWithPrimaryAccountNumberSuffix(card.suffix)
)
return ! cardExists
}
// Filter eligible cards with `reduce()` instead of `filter()` due to async predicate
const eligibleCards = await cards.reduce(
async (result: Card[], card: Card) => (await canAdd(card) ? [...await result, card] : result),
[]
)
// Retrieve tokenization data for eligible cards and construct pass entry list
return Promise.all(
eligibleCards.map(async (instrument: any) => {
const cardParams = MppCardDataParameters.withCardSecret(card.id, card.generateSecret())
const tokenizationData = await MeaPushProvisioning.ApplePay.initializeOemTokenization(cardParams)
const addRequestConfiguration: MppAddPaymentPassRequestConfiguration = {
style: 'payment',
cardholderName: tokenizationData.cardholderName,
primaryAccountSuffix: tokenizationData.primaryAccountSuffix,
cardDetails: [],
primaryAccountIdentifier: tokenizationData.primaryAccountIdentifier,
paymentNetwork: tokenizationData.networkName,
productIdentifiers: [],
requiresFelicaSecureElement: false
}
const passEntry: IssuerExtensionPaymentPassEntry = {
identifier: tokenizationData.tokenizationReceipt,
title: tokenizationData.cardholderName,
art: cardImage,
addRequestConfiguration: addRequestConfiguration
}
return passEntry
})
)
}
}()
return null
}
export default NonUIExtension
Debugging and Testing
Wallet Extension development requires a physical iOS device.
TestFlight is required for complete push provisioning workflow testing. However, it is possible to test most of the Wallet Extension features locally using Ad-Hoc provisioning profiles.
System Logs
Use Console App to view System Logs.
App Extensions Performance
React Native loading time and JavaScript performance is sufficient for Wallet Extensions, when built in Release
mode.
React Native console logs are disabled in Release
(production) mode.
In Debug
mode it takes time for the Metro Bundler to load JavaScript code, initial App Extensions loading time may exceed 100 ms
threshold, and the App is not visible in Apple Wallet.
Quit and Reopen Apple Wallet App when Metro completes loading the App Extension.
App Extensions Memory Limits
- Issuer Non-UI App Extension:
55 MB
- Issuer UI App Extension:
60 MB
JavaScript Environment
Hermes
App Extensions with Hermes in
Debug
mode allocates~50 MB
of memory during start-up. Even few imports could lead to excessive memory allocation, resulting in process's immediate termination by the System:kernel EXC_RESOURCE -> IssuerNonUIExtension[1234] exceeded mem limit: ActiveHard 55 MB (fatal)
kernel EXC_RESOURCE -> IssuerUIExtension[5678] exceeded mem limit: ActiveHard 60 MB (fatal)When App Extension is terminated by the System, no logs are available in React Native console, so review System Logs.
Memory consumption is substantially better in
Release
mode,~16 MB
aftert React Native initialization. Build and run App and bundled Extensions inRelease
mode using the following command:npx react-native run-ios --mode Release
JavaScriptCore
JSC offers much smaller initial memory fooprint of
~25 MB
inDebug
mode. If you would like to use Fast Refresh feature and React Native logging, you could temporarily switch to JSC JavaScript engine:ios/Podfileuse_react_native!(
:path => config[:reactNativePath],
# Hermes is now enabled by default. Disable by setting this flag to false.
:hermes_enabled => false,
)