Skip to content

Conversation

dbracamonte
Copy link

@dbracamonte dbracamonte commented Aug 29, 2025

This PR introduces new features to integrate the Google Pay yellow path flow, allowing users to resume adding cards to Google Wallet and managing existing tokens. This is essential to comply with Google Pay requirements.

Summary

  • Add resumeAddCardToGoogleWallet() method for resuming card provisioning using existing token reference ID
  • Add listTokens() method to retrieve all tokens stored in Google Wallet
  • Add new TypeScript types: AndroidResumeCardData and TokenInfo
  • Update README.md with comprehensive documentation for the new methods

Changes Made

Android Native (Kotlin)

  • WalletModule.kt: Added resumeAddCardToGoogleWallet and listTokens methods with proper error handling
  • NativeWalletSpec.java: Added method signatures for the new functionality

TypeScript/React Native

  • NativeWallet.ts: Added new types and method signatures to the TurboModule spec
  • index.tsx: Implemented JavaScript wrapper functions with proper platform checks and error handling

Documentation

  • README.md: Updated API reference table, data types section, and function count

Features

resumeAddCardToGoogleWallet(cardData: AndroidResumeCardData)

  • Resumes card provisioning flow using an existing tokenReferenceId
  • Simplified data structure compared to full card provisioning
  • Supports optional cardholder name and last digits for display purposes
  • Returns TokenizationStatus to track the operation result

listTokens()

  • Retrieves all tokens currently stored in Google Wallet
  • Returns array of TokenInfo objects containing:
    • tokenReferenceId: Unique token identifier
    • fpanLastFour: Last four digits of the tokenized card
    • tokenState: Current state of the token (numeric value)
  • Handles empty results gracefully

- add resumeAddCardToGoogleWallet() method for resuming card provisioning using existing token reference ID
- add listTokens() method to retrieve all tokens stored in Google Wallet
- add AndroidResumeCardData and TokenInfo types for new functionality
- update README.md with documentation for new methods

These methods provide better token lifecycle management and support for existing card tokens in Google Wallet integration.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Copy link

github-actions bot commented Aug 29, 2025

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@dbracamonte
Copy link
Author

I have read the CLA Document and I hereby sign the CLA

CLABotify added a commit to Expensify/CLA that referenced this pull request Aug 29, 2025
@dbracamonte dbracamonte changed the title feat: add Google Wallet token management methods feat: add Google Pay yellow path Aug 29, 2025
@Skalakid Skalakid requested review from Skalakid and zfurtak September 1, 2025 06:25
Copy link
Collaborator

@Skalakid Skalakid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, it looks good. I've left some comments. Additionally, could you please add a video to the PR description that demonstrates how your solution works in our example app?


if (!existingToken) {
throw new Error(
`No se encontró el token para la tarjeta terminada en ${CONST.AndroidDummyResumeCardData.lastDigits}`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change this error message to english

Suggested change
`No se encontró el token para la tarjeta terminada en ${CONST.AndroidDummyResumeCardData.lastDigits}`,
`Token not found for card ending with ${CONST.AndroidDummyResumeCardData.lastDigits}`,

@@ -29,6 +29,13 @@ type AndroidCardData = {
userAddress: UserAddress;
};

type AndroidResumeCardData = {
network: string;
tokenReferenceId: string;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tokenReferenceId: string;
tokenReferenceID: string;

src/index.tsx Outdated
Comment on lines 113 to 116
async function listTokens(): Promise<TokenInfo[]> {
if (Platform.OS === 'ios') {
throw new Error('listTokens is not available on iOS');
}
Copy link
Collaborator

@Skalakid Skalakid Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to add the listTokens() function also for iOS. Since it's Android-specific PR, how about adding an iOS stub here, and renaming TokenInfo return type keys to be more cross-platform reusable. For example:

type TokenInfo = {
  identifier: string;
  lastDigits: string;
  tokenState: number;
};

Thanks to that iOS implementation can be added in a follow-up PR

Comment on lines 66 to 70
type TokenInfo = {
tokenReferenceId: string;
fpanLastFour: string;
tokenState: number;
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As said before, how about renaming keys to something like this:

Suggested change
type TokenInfo = {
tokenReferenceId: string;
fpanLastFour: string;
tokenState: number;
};
type TokenInfo = {
identifier: string;
lastDigits: string;
tokenState: number;
};

README.md Outdated
| **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> `actionStatus: '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. | `tokenReferenceId: string`,<br>`fpanLastFour: string`,<br>`tokenState: number` |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update type value names here too

@ReactMethod
override fun resumeAddCardToGoogleWallet(data: ReadableMap, promise: Promise) {
try {
val tokenReferenceId = data.getString("tokenReferenceId")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val tokenReferenceId = data.getString("tokenReferenceId")
val tokenReferenceID = data.getString("tokenReferenceID")

@dbracamonte
Copy link
Author

Overall, it looks good. I've left some comments. Additionally, could you please add a video to the PR description that demonstrates how your solution works in our example app?

Thanks for the review!

Regarding the demo video, I need to clarify the testing constraints:

To demonstrate the Google Pay tokenization flow, I would need:

  • Production certificates from Google Pay (which I don't currently have access to)
  • Google-authorized test cards for the sandbox environment

The testing I performed was done using my work environment, but I cannot share that footage publicly as it contains
proprietary information from an unreleased application.

@dbracamonte
Copy link
Author

Greetings @Skalakid, I'm looking forward to continuing the approval process. If there's any way I can proceed, please let me know.

@Skalakid
Copy link
Collaborator

Skalakid commented Sep 5, 2025

Hello, had to finish some other tasks. We need to check if it actually works before merging. Since you've tested it in your unreleased app and can't attach a video here, I think we can try testing it in the Expensify app. I will try to add this flow there and will come back to you with more information

Copy link
Collaborator

@Skalakid Skalakid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dbracamonte Finally got some time to test this PR. Everything works fine. Left some last comments, and I think we can merge it ;)

yellow-path.mp4


val cardNetwork = getCardNetwork(network)
val tokenServiceProvider = getTokenServiceProvider(network)
val displayName = getDisplayName(data, network)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we use just cardHolderName as a display name, like in the addCardToGoogleWallet function? Is the getDisplayName function necessary?

}

return "${network.uppercase(Locale.getDefault())} Card"
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the above comment, why do we need this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants