Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
24858c0
Add base PaymentCredentialsGenerator
Skalakid Feb 12, 2026
c82af87
Update Push Tokenization results handling
Skalakid Feb 12, 2026
e1af64e
Add UAPP new props
Skalakid Feb 13, 2026
54cd3fe
Add ability to fetch GOPC in RN during push provisioning
Skalakid Feb 13, 2026
fbcd164
Add new function to paper spec
Skalakid Feb 13, 2026
4d2c2aa
Merge branch 'main' into feat/upgrade-to-Google-UAPP
Skalakid Feb 16, 2026
3412195
Update docs
Skalakid Feb 16, 2026
759ea6f
Merge remote-tracking branch 'origin/main' into feat/upgrade-to-Googl…
Skalakid Sep 29, 2026
2dc9b0a
chore: merge main
JakubKorytko Oct 9, 2026
31c1613
fix: clear pending tokenize promise on async pushTokenize failures
JakubKorytko Oct 8, 2026
8283655
fix: settle pending payment credentials requests on error, timeout an…
JakubKorytko Oct 8, 2026
22c5f7e
fix: report activation only for successful tokenization outcomes
JakubKorytko Oct 8, 2026
138c622
docs: require payment credentials handler and note tested TapAndPay SDK
JakubKorytko Oct 8, 2026
68447d6
chore: rename Android dummy credentials and drop unused card field
JakubKorytko Oct 8, 2026
81f9973
fix: keep legacy result handling for resume tokenize flow
JakubKorytko Oct 8, 2026
6c40f88
fix: type onCardActivated tokenId as nullable
JakubKorytko Oct 8, 2026
156aa2e
fix: settle UAPP tokenize result without PushTokenizeResult like the …
JakubKorytko Oct 9, 2026
472ee27
docs: fix UAPP acronym expansion
JakubKorytko Oct 9, 2026
f8e160c
chore: bump version to 0.2.0-0
JakubKorytko Oct 9, 2026
efa4810
refactor: split onActivityResult into per-request handlers
JakubKorytko Oct 9, 2026
467d386
fix: mark onCardActivated tokenId as optional
JakubKorytko Oct 9, 2026
ac9c584
fix: report payment credentials failures with GeneratePaymentCredenti…
JakubKorytko Oct 12, 2026
b219673
feat: pass tokenRequestorId to the payment credentials handler
JakubKorytko Oct 12, 2026
b4c059d
fix: read separate push tokenize result extras when PushTokenizeResul…
JakubKorytko Oct 12, 2026
b6561ad
chore: migrate deprecated TapAndPay calls to request variants
JakubKorytko Oct 12, 2026
63b3cfa
feat: add hasEligibleTokenizationTarget
JakubKorytko Oct 12, 2026
5c8e613
docs: align payload comments in UAPP.md
JakubKorytko Oct 12, 2026
0da090c
refactor: use withTimeoutOrNull for payment credentials timeout
JakubKorytko Oct 13, 2026
d3208ec
fix: start payment credentials coroutine undispatched
JakubKorytko Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 60 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- ♻️ 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)

## Getting started
Expand All @@ -21,7 +22,7 @@ To be able to interact with the Google Wallet on the Android please make sure to
1. Visit [the Google Pay Android Push Provisioning API documentation](https://developers.google.com/pay/issuers/apis/push-provisioning/android/) and request access to it.

2. Once getting an approval from the Google team
1. Download the [TapAndPay SDK](https://developers.google.com/pay/issuers/apis/push-provisioning/android/releases).
1. Download the [TapAndPay SDK](https://developers.google.com/pay/issuers/apis/push-provisioning/android/releases). The library uses the UAPP APIs (`PaymentCredentialsGenerator`, `PushTokenizeResult`), so older SDK versions without them will not compile. Tested with 18.10.1.
2. Unzip it and extract the SDK into the `/android/libs` folder in your React Native project (if there is no `libs` folder, create one).
3. Add `/android/libs` to `.gitignore`.

Expand Down Expand Up @@ -134,7 +135,7 @@ In your `app.json` file, add the following configuration:
"@expensify/react-native-wallet",
{
enableApplePayProvisioning: true,
googleTapAndPaySdkPath: "./libs/tapandpay-v18.7.0.zip", // path to the Google Tap & Pay SDK zip file
googleTapAndPaySdkPath: "./libs/tapandpay-v18.10.1.zip", // path to the Google Tap & Pay SDK zip file
},
],
]
Expand All @@ -157,6 +158,7 @@ Here you can find data elements used in the library, essential to work with Goog

### Android
- **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 →](./docs/UAPP.md)
- **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.

Expand All @@ -168,7 +170,7 @@ Here you can find data elements used in the library, essential to work with Goog
- **Ephemeral Public Key** - a key used by elliptic curve cryptography (ECC) (Base 64 encoded).

# API Reference
The library offers seven functions for seamless integration and use of the Apple Wallet and Google Wallet APIs. Additionally, it includes one listener that informs when the added card has been activated. Below, these functions are described along with the data types involved.
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.

## Functions

Expand All @@ -178,26 +180,49 @@ The library offers seven functions for seamless integration and use of the Apple
| **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`<br>(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`,<br>`tsp: string` | `CardStatus` | ✅ | ✅ |
| **addCardToGoogleWallet** | Initiates native Push Provisioning flow for adding a card to the Google Wallet. | `data`: `AndroidCardData` | `TokenizationStatus` | ❌ | ✅ |
| **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`,<br>`network: string`,<br>`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 →](./docs/UAPP.md) | `data`: `AndroidCardData`,<br>`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[]` | ❌ | ✅ |
| **addCardToAppleWallet** | Initiates native Push Provisioning flow for adding a card to the Apple Wallet. | `data`: `IOSCardData`,<br>`issuerEncrypt-`<br>`PayloadCallback: IOSIssuerCallback` | `void` | ✅ | ❌ |


## Data Types

| Type | Description | Fields |
|------|-------------|--------|
| **AndroidWalletData** | Specific information for Android devices required for wallet transactions. | `deviceID: string`,<br>`walletAccountID: string` |
| **AndroidCardData** | Data related to a card that is to be added on Android platform wallets. | `network: string`,<br>`opaquePaymentCard: string`,<br>`cardHolderName: string`,<br>`lastDigits: string`,<br>`userAddress: UserAddress` |
| **AndroidCardData** | Data related to a card that is to be added on Android platform wallets. | `network: string`,<br>`opaquePaymentCard: string`,<br>`cardHolderName: string`,<br>`lastDigits: string`,<br>`userAddress: UserAddress`,<br>`isVirtualCard?: boolean`,<br>`isBounceProvisioned?: boolean` |
| **AndroidResumeCardData** | Simplified data structure for resuming card addition to Google Wallet using existing token reference ID. | `network: string`,<br>`tokenReferenceID: string`,<br>`cardHolderName?: string`,<br>`lastDigits?: string` |
| **AndroidPaymentCredentialsResponse** | Response object containing encrypted payment credentials for UAPP flow. [Learn more →](./docs/UAPP.md) | `opaquePaymentCard: string`,<br>`googleOpaquePaymentCard: string` |
| **onPaymentCredentialsRequestPayload** | Payload received when Google Wallet requests payment credentials during UAPP provisioning. [Learn more →](./docs/UAPP.md) | `requestId: string`,<br>`serverSessionId: string`,<br>`walletId: string`,<br>`tokenRequestorId: string \| null`,<br>`opaquePaymentCard: string` |
| **UserAddress** | Structured address used for cardholder verification. | `name: string`,<br>`addressOne: string`,<br>`addressTwo: string`,<br>`city: string`,<br>`administrativeArea: string`,<br>`countryCode: string`,<br>`postalCode: string`,<br>`phoneNumber: string` |
| **IOSCardData** | Data related to a card that is to be added on iOS platform. | `network: string`,<br>`activationData: string`,<br>`encryptedPassData: string`,<br>`ephemeralPublicKey: string`,<br>`cardHolderTitle: string`,<br>`cardHolderName: string`,<br>`lastDigits: string`,<br>`cardDescription: string`,<br>`cardDescriptionComment: string` |
| **onCardActivatedPayload** | Data used by listener to notice when a card's status changes. | `tokenId: string`,<br> `status: 'activated' \| 'canceled'`<br> |
| **onCardActivatedPayload** | Data used by listener to notice when a card's status changes. | `tokenId?: string \| null`,<br> `status: 'activated' \| 'canceled'`<br> |
| **IOSIssuerCallback** | This callback is invoked with a nonce, its signature, and a certificate array obtained from Apple. It is expected that you will 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. | `(nonce: string, nonceSignature: string, certificate: string[]) => IOSEncryptPayload` |
| **IOSEncryptPayload** | An object containing the necessary elements to complete the addition of a card to Apple Wallet. | `encryptedPassData: string`,<br>`activationData: string`,<br>`ephemeralPublicKey: string` |
| **TokenInfo** | Information about a token stored in Google Wallet. | `identifier: string`,<br>`lastDigits: string`,<br>`tokenState: number` |

### AndroidCardData Optional Fields

The `AndroidCardData` type includes two optional boolean fields that provide additional information about the card being provisioned:

#### `isVirtualCard?: boolean`

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.

#### `isBounceProvisioned?: boolean`

Indicates whether the card has been provisioned through "bounce provisioning." Bounce provisioning is a two-step provisioning flow where:
1. The user initially provisions a card directly through Google Wallet (outside your app)
2. 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.

## Card Status

| Type | Possible Values |
Expand Down Expand Up @@ -283,17 +308,44 @@ const base64ToHex = (base64: string): string => Buffer.from(base64, "base64").to

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.

# UAPP (Unified Android Push Provisioning)

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 →](./docs/UAPP.md)**

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

# Publishing your app

To successfully publish your app, you will need to navigate through a series of mandatory test cases on both platforms.

### Android

> [!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](https://developers.google.com/pay/issuers/apis/push-provisioning/android/best-practices).

The latest information about deploying apps with Google TapAndPay SDK can be found in the [pre-launch process](https://developers.google.com/pay/issuers/apis/push-provisioning/android/launch-process#step_3_issuer_app_product_review) and [beta tests](https://developers.google.com/pay/issuers/apis/push-provisioning/android/beta-testing) 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 [here](https://developers.google.com/pay/issuers/apis/push-provisioning/android/test-cases). They verify how your app handles card state tracking in different scenarios.
#### Testing Requirements

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](https://developers.google.com/pay/issuers/apis/push-provisioning/android/test-cases). 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](https://developers.google.com/pay/issuers/apis/push-provisioning/android/test-cases) 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.
Expand Down
7 changes: 6 additions & 1 deletion android/src/main/java/com/expensify/wallet/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ object Utils {
.setPhoneNumber(addressMap.getString("phoneNumber") ?: "")
.build()

val isVirtual = if (hasKey("isVirtualCard")) this.getBoolean("isVirtualCard") else false
val isBounce = if (hasKey("isBounceProvisioned")) this.getBoolean("isBounceProvisioned") else false

return CardData(
network = this.getString("network") ?: "",
opaquePaymentCard = this.getString("opaquePaymentCard") ?: "",
cardHolderName = this.getString("cardHolderName") ?: "",
lastDigits = this.getString("lastDigits") ?: "",
userAddress = userAddress
userAddress = userAddress,
isVirtualCard = isVirtual,
isBounceProvisioned = isBounce,
)
}

Expand Down
Loading
Loading