- ⚛️ Card In-App Provisioning seamless integration
- ♻️ Universal solution (Apple Wallet and Google Wallet)
- 💳 Easy and secure addition of payment cards from your app directly
- 💰 Supports Visa, MasterCard, Amex and Discover payment cards
- 🔐 UAPP (Unified Android Push Provisioning) support for enhanced security
- 🎉 Expo support (expo-plugin)
Using the react-native-wallet Push Provisioning features requires proper configuration and access from both Google and Apple.
Please follow the instructions below before installing the library:
To be able to interact with the Google Wallet on the Android please make sure to complete all of the steps below following the Google Push Provisioning documentation:
-
Visit the Google Pay Android Push Provisioning API documentation and request access to it.
-
Once getting an approval from the Google team
- Download the TapAndPay SDK. The library uses the UAPP APIs (
PaymentCredentialsGenerator,PushTokenizeResult), so older SDK versions without them will not compile. Tested with 18.10.1. - Unzip it and extract the SDK into the
/android/libsfolder in your React Native project (if there is nolibsfolder, create one). - Add
/android/libsto.gitignore.
- Download the TapAndPay SDK. The library uses the UAPP APIs (
-
Then connect the SDK to your project in
build.gradle, for example like in example/android/build.gradle:
allprojects {
repositories {
google()
maven { url "file://${rootDir}/libs" }
}
}To use the Google SDK in your app, you will need to whitelist your app details. Without it, calling some functions will result in a Not verified error.
To resolve it, please follow the instructions from the official Google documentation.
For your builds, you will need to prepare your app's package name and fingerprint following these steps. The values should, for example, look like this:
Package name: com.app.package.name
Fingerprint: SHA256: 36:38:63:59:6E:...:00:82:16:4E:FF
Tip
You can display your SHA-256 certificate checksum using apksigner or keytool.
With all the required data, submit the form and wait for Google's response. After successfully whitelisting your app you will be able to use Google SDK within our library.
To enable In-App Provisioning in your app, you must request activation of the appropriate Apple Pay entitlements for your developer Team ID. Enterprise Team IDs are not supported. This entitlement is not available by default in our panel; you will need to request it.
Submit your request through the In-App Provisioning Submission Form. Only Account Holders can access this form — if you are unable to access it, coordinate with the Account Holder of your team. Alternatively, you can email iap_entitlements@apple.com with the following format:
Subject: Apple Pay Entitlements and Allow List Request - [Issuer Name] - [Country Code]
Body:
- Issuer Name
- Country [Country Code]
- Team ID
- Adam ID
- App Name
Note
Only a production Team ID and Adam ID can receive the entitlement. Test Team IDs don't qualify.
Once Apple verifies your identity, you should get documentation named Getting Started with Apple Pay In-App Provisioning, Verification & Security. In this confidential document you will find all the relevant information about:
- Prerequisites for your project
- Best practices to follow
- How In-App Provisioning works
- UX requirements
- Testing process
- and more...
Make sure to familiarize yourself with that document before deploying your app.
After getting a positive response from Apple, open the developer portal panel and search Certificates, Identifiers & Profiles -> Profiles -> Our distribution profile -> Edit and add the ApplePay In-App Provisioning Distribution entitlement.
It’s available only for the production environment so your QA must work with physical devices and cards.
Add com.apple.developer.payment-pass-provisioning entitlement to your project. Find or create .entitlements file in your project and add the entitlement like below (similarly to WalletExample.entitlements):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
...
<!-- In-App Provisioning special entitlement-->
<key>com.apple.developer.payment-pass-provisioning</key>
<true/>
</dict>
</plist>After completing the setup from the section above, install the react-native-wallet package from npm:
npm install @expensify/react-native-walletor
yarn add @expensify/react-native-walletTo use @expensify/react-native-wallet with expo, you will need to use the expo-plugin.
In your app.json file, add the following configuration:
{
"expo": {
"plugins": [
[
"@expensify/react-native-wallet",
{
enableApplePayProvisioning: true,
googleTapAndPaySdkPath: "./libs/tapandpay-v18.10.1.zip", // path to the Google Tap & Pay SDK zip file
},
],
]
}
}You will need to rebuild your app after that since the lib has native files.
npx expo prebuild --clean
npx expo run:ios
npx expo run:androidHere you can find data elements used in the library, essential to work with Google Wallet and Apple Wallet APIs.
- Opaque Payment Card (OPC) - a binary blob of information Google Pay receives from the issuer app that could be presented to TSP to receive a token.
- Google Opaque Payment Card (Google OPC) - an encrypted version of the OPC required for UAPP (Unified Android Push Provisioning) flow. This is generated by your backend using Google's encryption keys during the provisioning process. Learn more about UAPP →
- Token Service Provider (TSP) - a service that enhances payment security by replacing a credit card number during transactions with a unique digital identifier - token. The TSP specifies the tokenization service used to create a given token e.g. Visa, MasterCard, American Express.
- Token Reference ID - a unique identifying number to refer to a DPAN (Dynamic Personal Account Number). Token Providers will assign each DPAN an issuer token ID at the time of tokenization.
- Nonce - a one-time nonce value generated by Apple’s servers. This nonce must be included in the add payment request’s encrypted data.
- Nonce Signature - the device-specific signature for the nonce. This signature must be included in the add payment request’s encrypted data.
- Certificates - an array of NSData objects. Each object contains a DER encoded X.509 certificate, with the leaf first and root last. You must download the root CA to validate the entire chain.
- Encrypted Pass Data- an encrypted JSON file containing the sensitive information needed to add a card to Apple Pay (Base 64 encoded). More details here.
- Ephemeral Public Key - a key used by elliptic curve cryptography (ECC) (Base 64 encoded).
- Primary Account Identifier - an opaque issuer id (also called the FPAN ID) that PassKit uses to recognize the same card across reissues and devices. Pass it in
IOSCardDataand tocanAddCardToAppleWallet.getCardStatusByIdentifiermatches this value exactly and does not fall back.addCardToAppleWallet, the inferreddeviceType, andcanAddCardToAppleWalletwhen you also passlastDigitsshare one lookup: an exact id match wins, and otherwise the library uses the id of the only pass that is notdeactivatedand has the same last digits. A different card that is still active and ends in the same digits can be picked. Pass the id PassKit stored on the pass (theprimaryAccountIdentifierfromlistAppleWalletPasses) to skip that lookup. - Device type - the device the card is being provisioned to:
phoneorwatch. PassKit does not report the device the user selected, so the library infers it from where the card is already provisioned. When it cannot tell, the value isunknown.
The library offers functions for seamless integration and use of the Apple Wallet and Google Wallet APIs. Additionally, it includes listeners and handlers for managing provisioning events. Below, these functions are described along with the data types involved.
| Function | Description | Parameters | Returns / Type | iOS | Android |
|---|---|---|---|---|---|
| getSecureWalletInfo | Returns necessary platform-specific wallet information for secure transactions. | None | WalletData |
❌ | ✅ |
| checkWalletAvailability | Checks if the wallet is ready and initializes it if possible. | None | boolean |
✅ | ✅ |
| getCardStatusBySuffix | Retrieves the current status of a card in the wallet. | lastDigits: string(The last few digits of the card number) |
CardStatus |
✅ | ✅ |
| getCardStatusByIdentifier | Returns the state of a card based on a platform-specific identifier. On Android, it's Token Reference ID and on iOS, it's Primary Account Identifier. |
identifier: string,tsp: string |
CardStatus |
✅ | ✅ |
| hasEligibleTokenizationTarget | Checks whether the card can still be added to a Google Wallet target (the phone or a paired wearable). Google recommends it for the "Add to Google Wallet" button when wearables are supported, because a card already on the phone can still be added to a watch. | last4Digits: string,network: string,issuerName: string |
boolean |
❌ | ✅ |
| addCardToGoogleWallet | Initiates native Push Provisioning flow for adding a card to the Google Wallet. Requires a handler that returns the payment credentials for the UAPP flow. Learn more → | data: AndroidCardData,handlePaymentCredentials: (data: onPaymentCredentialsRequestPayload) => Promise<AndroidPaymentCredentialsResponse> |
TokenizationStatus |
❌ | ✅ |
| resumeAddCardToGoogleWallet | Resumes the Push Provisioning flow for adding a card to the Google Wallet using existing token reference ID. | data: AndroidResumeCardData |
TokenizationStatus |
❌ | ✅ |
| listTokens | Lists all tokens currently stored in the Google Wallet. | None | TokenInfo[] |
❌ | ✅ |
| listAppleWalletPasses | Lists Apple Wallet Secure Element passes available to the app, including activation state and whether the pass is remote (for example on Apple Watch). Filter the result for activationState === 'requireActivation' or activationState === 'pending' to build a pending-cards UI. |
None | IOSWalletPass[] |
✅ | ❌ |
| presentAppleWalletPass | Opens the installed iPhone pass for these last digits, preferring one that still needs verification. Returns false when lastDigits is empty, when no pass matches, when more than one card ends in those digits, and on Android. |
lastDigits: string |
boolean |
✅ | ✅ |
| addCardToAppleWallet | Initiates native Push Provisioning flow for adding a card to the Apple Wallet. When IOSCardData.primaryAccountIdentifier is set, the device picker only lists devices that do not already have the card. If PassKit cannot present the flow because the card is already on every device, the call fails with a clear error. If the issuer callback throws, the Add Payment Pass sheet is closed and the promise rejects with that error. Otherwise it resolves IOSAddCardResult. A successful add includes activationState when PassKit reports a known state. A decline includes errorDomain and errorCode and does not reject, because Wallet has already shown its own message. See Breaking change. |
data: IOSCardData,issuerEncrypt-PayloadCallback: IOSIssuerCallback |
IOSAddCardResult |
✅ | ❌ |
| canAddCardToAppleWallet | Checks whether the card can still be added to the iPhone or a paired Apple Watch. Pass lastDigits to use the same id lookup as addCardToAppleWallet. Returns false on Android. |
primaryAccountIdentifier: string,lastDigits?: string |
boolean |
✅ | ✅ |
| activateAppleWalletPass | Activates an existing Secure Element pass in Apple Wallet using activation data from your issuer backend. Resolves true when the pass is already active. For requireActivation, returns Apple's result, which can be false. Rejects for any other state (pending, suspended), a missing pass, unavailable activation, or invalid or non-base64 activation data. |
data: IOSPassActivationData |
boolean |
✅ | ❌ |
addCardToAppleWallet resolves IOSAddCardResult instead of TokenizationStatus. Read result.status.
A Wallet decline resolves { status: 'error', errorDomain, errorCode } instead of rejecting. A cancel after the issuer step resolves { status: 'canceled' } instead of rejecting. An error thrown by the issuer callback still rejects, and the Add Payment Pass sheet is closed.
| Type | Description | Fields |
|---|---|---|
| AndroidWalletData | Specific information for Android devices required for wallet transactions. | deviceID: string,walletAccountID: string |
| AndroidCardData | Data related to a card that is to be added on Android platform wallets. | network: string,opaquePaymentCard: string,cardHolderName: string,lastDigits: string,userAddress: UserAddress,isVirtualCard?: boolean,isBounceProvisioned?: boolean |
| AndroidResumeCardData | Simplified data structure for resuming card addition to Google Wallet using existing token reference ID. | network: string,tokenReferenceID: string,cardHolderName?: string,lastDigits?: string |
| AndroidPaymentCredentialsResponse | Response object containing encrypted payment credentials for UAPP flow. Learn more → | opaquePaymentCard: string,googleOpaquePaymentCard: string |
| onPaymentCredentialsRequestPayload | Payload received when Google Wallet requests payment credentials during UAPP provisioning. Learn more → | requestId: string,serverSessionId: string,walletId: string,tokenRequestorId: string | null,opaquePaymentCard: string |
| UserAddress | Structured address used for cardholder verification. | name: string,addressOne: string,addressTwo: string,city: string,administrativeArea: string,countryCode: string,postalCode: string,phoneNumber: string |
| IOSCardData | Data related to a card that is to be added on iOS. Set primaryAccountIdentifier after the first provisioning so the next attempt targets devices that do not already have the card. |
network: string,activationData: string,encryptedPassData: string,ephemeralPublicKey: string,cardHolderTitle: string,cardHolderName: string,lastDigits: string,cardDescription: string,cardDescriptionComment: string,primaryAccountIdentifier?: string |
| onCardActivatedPayload | Data used by listener to notice when a card's status changes. | tokenId?: string | null,status: 'activated' | 'canceled' |
| IOSIssuerCallback | This callback is invoked with a nonce, its signature, a certificate array obtained from Apple, and the device the card is being added to. Forward these details to your server or the card issuer's API to securely encrypt the payload required for adding cards to the Apple Wallet. deviceType is a best-effort value; see IOSDeviceType. |
(nonce: string, nonceSignature: string, certificate: string[], deviceType: IOSDeviceType) => IOSEncryptPayload |
| IOSDeviceType | Best-effort device the card is being provisioned to. PassKit does not report the selected device, so this is inferred from existing passes when primaryAccountIdentifier is set, using the same id lookup as addCardToAppleWallet. The value is computed when Apple asks for the encrypted payload, including when the user tries again. Passes on a paired Apple Watch are never counted as iPhone passes. phone means the card is already on the paired Apple Watch only. watch means it is already on the iPhone only. unknown means the card is on neither device, or no identifier was provided, so the user may still choose either device. Passes in the deactivated state count as not present. |
'phone' | 'watch' | 'unknown' |
| IOSAddPaymentPassData | Data returned when the user continues the provisioning flow. Apple supplies the nonce, its signature, and the certificates. The library adds deviceType; Apple does not return it. See IOSDeviceType. |
status: number,nonce: string,nonceSignature: string,certificates: string[],deviceType?: string |
| IOSEncryptPayload | An object containing the necessary elements to complete the addition of a card to Apple Wallet. | encryptedPassData: string,activationData: string,ephemeralPublicKey: string |
| IOSPassActivationData | Identifies an existing Apple Wallet Secure Element pass and provides the activation payload returned by your issuer backend. activationData must be Base 64 encoded. Set isRemote to true for a paired Apple Watch pass or false for the iPhone pass when the same card is on both. Omit it to look up the iPhone pass first and then the Watch. |
passTypeIdentifier: string,serialNumber: string,activationData: string,isRemote?: boolean |
| IOSWalletPass | Information about an Apple Wallet Secure Element pass currently available on the device or a paired remote device. | passTypeIdentifier: string,serialNumber: string,primaryAccountIdentifier?: string,lastDigits?: string,activationState: CardStatus,isRemote: boolean |
| IOSAddCardResult | Result of addCardToAppleWallet. activationState is set on success when PassKit reports a known card state. errorDomain and errorCode are set when Wallet declines the card after showing its own message. |
status: TokenizationStatus,activationState?: CardStatus,errorDomain?: string,errorCode?: number |
| TokenInfo | Information about a token stored in Google Wallet. | identifier: string,lastDigits: string,tokenState: number |
The AndroidCardData type includes two optional boolean fields that provide additional information about the card being provisioned:
Indicates whether the card being provisioned is a virtual card. Virtual cards are digitally-generated card numbers that are linked to a physical card account. They provide enhanced security by:
- Allowing users to make online purchases without exposing their physical card number
- Enabling easy cancellation and regeneration without affecting the physical card
- Providing transaction-specific or merchant-specific card numbers
When set to true, this flag informs Google Wallet that the card being provisioned is a virtual card, which may affect how the card is displayed and managed in the wallet interface.
Indicates whether the card has been provisioned through "bounce provisioning". Bounce provisioning is a two-step provisioning flow where:
- The user initially provisions a card directly through Google Wallet (outside your app)
- Your app later "bounces" or redirects the user back to complete additional verification or setup steps
This flag helps Google Wallet track cards that were initially added through the Google Wallet app but require additional app-specific configuration or verification. When set to true, it signals that this card was previously provisioned through the Google Wallet interface and is now being completed or updated through your application.
| Type | Possible Values |
|---|---|
| CardStatus | not found, active, requireActivation, pending, suspended, deactivated |
| Listener | Event Description | Register Function | Unregister Function |
|---|---|---|---|
| cardActivation | Notifies when a card's status changes to activated. | addListener(event: string, callback: (data: onCardActivatedPayload) => void): EmitterSubscription |
removeListener(subscription: EmitterSubscription): void |
A ready-to-use component that simplifies the addition of payment cards to Google Wallet and Apple Wallet. The button automatically adapts its appearance according to the platform and language specified. It uses official assets provided by Google and Apple in their Wallet-related branding guidelines.
Important
Please bear in mind the brand rules provided by Google and Apple when adding this component to your application.
Adhering to these guidelines is crucial not only to comply with legal requirements but also to reassure users of the authenticity and security of your application.
Note
Localization is automatically handled by the system settings, ensuring the button language matches the device's preferred language.
| Property | Type | Required | Description | Platform Specific |
|---|---|---|---|---|
| onPress | (e: GestureResponderEvent) => void |
Yes | A callback that is triggered when the button is pressed. | No |
| style | ViewStyle |
No | Customizes the overall style of the component. | No |
| buttonStyle | 'black' | 'blackOutline' |
No | Customizes the button style. | iOS Only |
| buttonType | 'basic' | 'badge' |
No | Defines the button's type (e.g., primary, secondary). | No |
| borderRadius | number |
No | The border radius of the button, applicable to iOS buttons only. | iOS Only |
<AddToWalletButton
onPress={() => console.log('Button pressed!')}
style={styles.longButton}
buttonStyle="blackOutline"
buttonType="badge"
borderRadius={4}
/>| Android | iOS |
|---|---|
This library use base64 encoding to encode the data. Yours might use hex encoding.
If that is the case, you will need to convert the data in the proper format.
This is an example of how to convert data to hex format:
import { Buffer } from "buffer"
const base64ToHex = (base64: string): string => Buffer.from(base64, "base64").toString("hex")
[...]
const issuerEncryptPayloadCallback = async (
nonce: string,
nonceSignature: string,
certificate: string[],
deviceType: IOSDeviceType,
): Promise<IOSEncryptPayload> => {
try {
const payload = {
encryptionDetails: {
nonce: base64ToHex(nonce),
nonceSignature: base64ToHex(nonceSignature),
wspCertificates: certificate,
},
deviceType,
serialNumber,
wspId: "APPLE" as const,
}
[...]This will depend on the implementation of your backend/issuer so make sure to check with them otherwise IOS might fail to provision the card.
For Android applications that require enhanced security for card provisioning, this library supports UAPP (Unified Android Push Provisioning) flow with Google Wallet.
📖 Read the complete UAPP documentation →
The UAPP guide covers:
- What UAPP is and how it works
- Step-by-step setup instructions
- Backend implementation requirements
- Error handling and troubleshooting
- Complete code examples
To successfully publish your app, you will need to navigate through a series of mandatory test cases on both platforms.
Warning
Testing is mandatory before production release. Your access to the Google Wallet production environment may be revoked if you do not provide the required test evidence for launch approval. Complete all testing requirements before deploying your app.
Before deploying your app to the Google Play Store, make sure you have taken care of the security when implementing Push Provisioning and properly tested your code. To accomplish it, please familiarize yourself with the Google's App Push Provisioning best practices.
The latest information about deploying apps with Google TapAndPay SDK can be found in the pre-launch process and beta tests sections in Google documentation. Make sure to complete all of the steps specified by Google connected to the Google's branding, API safety, and app stability.
The app will need to be reviewed by Google. During this process, it will need to pass 4 mandatory test cases that are specified in the official Google testing documentation. They verify how your app handles card state tracking in different scenarios.
Key testing criteria include:
- Proper handling of card states (active, pending, suspended, etc.)
- Correct UI behavior when cards are already provisioned
- Secure handling of sensitive payment data
- Compliance with Google's branding guidelines
- Proper error handling and user feedback
Please refer to the complete testing criteria documentation before submitting your app for review.
Note
Please make sure to hide the Add to Google Wallet buttons when cards are already added to the wallet.
When implementing the In-App Push Provisioning feature in your App make sure that your app follows Apple's branding guidelines connected to Apple Wallet. Remember that you must not create your own buttons or your app could be rejected at revision. You can use the AddWalletButton component instead!
Keep the Add to Apple Wallet button visible while canAddCardToAppleWallet(primaryAccountIdentifier, lastDigits) returns true, including after the card is added to the iPhone but not yet to a paired Apple Watch. Show text like Added to Apple Wallet only once that call returns false. Pass primaryAccountIdentifier in IOSCardData so the next attempt targets the remaining device, and pass the same lastDigits to both calls so they agree. This matches Apple's in-app provisioning guidance.
Next to branding guidelines, please follow the instructions and best practices from the In-App provisioning documentation provided by Apple.
The In-App Provisioning feature must be reviewed and verified by Apple's certification team before submitting your app to the App Store. Below are some key scenarios that may be tested during the review process:
- Enrolling a card into Apple Wallet
- Enrolling a card into the Apple Watch
- Attempting to enroll a card that has already been added via the Wallet app
- Manually adding a card using its IBAN
- Adding multiple cards from the same issuer
- Adding and removing the same card
- Handling an incoming call during the provisioning process
- ...
Apple may request a demonstration of certain test scenarios, such as verifying the ability to add up to twenty different cards for a single user. Be prepared to provide validation for such cases.
Additionally, when submitting your app to the App Store, you must include:
- A demo account for testing.
- A demo video showcasing the In-App Provisioning experience.
@expensify/react-native-wallet is compatible with the five latest minor releases of React Native (≥0.76.0) and works exclusively with the new architecture.
Contributions to this library are done under https://github.com/Expensify/App. Please refer to that repo and all its guidelines for contributing.
MIT
This library is co-developed by Expensify, the fastest way to do expenses, travel, and corporate cards, in collaboration with Software Mansion, React Native core contributors and experts in building high-performing mobile solutions.
