Skip to content

Upgrade Android to Unified Push Provisioning - #75

Open
Skalakid wants to merge 27 commits into
mainfrom
feat/upgrade-to-Google-UAPP
Open

Skalakid wants to merge 27 commits into
mainfrom
feat/upgrade-to-Google-UAPP

Conversation

@Skalakid

@Skalakid Skalakid commented Feb 13, 2026 •

Copy link
Copy Markdown
Contributor

Important

Per Google docs, all apps using TSP-only integration must upgrade by the end of 2026.

Details

Upgrades the Android integration to Google's Unified Android Push Provisioning (UAPP), following the official guide. Google requires apps with a TSP-only integration to upgrade by the end of 2026.

  • addCardToGoogleWallet uses the Task-based pushTokenize(request) with a PaymentCredentialsGenerator. Google only calls the generator when it needs credentials. If Google doesn't request a Google OPC, the TSP OPC from cardData is returned immediately. Otherwise the library emits onPaymentCredentialsRequest, and the JS handler fetches the credentials from the issuer backend.
  • The handler gets requestId, serverSessionId, walletId, tokenRequestorId and the TSP OPC, and must return both the TSP OPC and the Google OPC. tokenRequestorId identifies the tokenization target; for Visa on Fitbit the TSP OPC has to include it.
  • When the handler throws, is missing, or returns no Google OPC, the request is rejected through AndroidRejectPaymentCredentials. A 60s native timeout fails requests that never get an answer, and invalidate() fails anything still pending, so Google is never left waiting. Every failure reaches Google as GeneratePaymentCredentialsException(UNKNOWN_ERROR), the only documented way to report it. Google doesn't guarantee a TSP-only fallback, so a failed request means the card is not added.
  • The result is read from PushTokenizeResult. A card counts as added when cardResult is true or any TokenizationOutcome.tokenResult is true. The token ID comes from the successful outcome, otherwise null. Google's guide also documents the result as separate extras (EXTRA_TOKEN_RESULT, EXTRA_CARD_RESULT, EXTRA_ISSUER_TOKEN_ID), so those are read when the object is missing. When there is neither, the outcome matches Fix push tokenization promise never settling in onActivityResult #70: RESULT_OK resolves success, any other code resolves error.
  • resumeAddCardToGoogleWallet uses tokenize(activity, TokenizeRequest, code) (the documented way to resume a yellow path token) with its own request code, so its legacy result format (EXTRA_ISSUER_TOKEN_ID) is handled separately.
  • listTokens and getTokenStatus move to their request variants (ListTokensRequest, GetTokenStatusRequest). The only deprecated calls left are environment and stableHardwareId, which have no documented replacement.
  • New hasEligibleTokenizationTarget(last4Digits, network, issuerName) (Android only). 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.
  • AndroidCardData gets optional isVirtualCard and isBounceProvisioned.
  • iOS gets no-op stubs for the new spec methods, for both new and old arch.

Breaking changes (should ship as a minor release, see the note below):

  • Requires a TapAndPay SDK with the UAPP APIs (PaymentCredentialsGenerator, PushTokenizeResult). Tested with 18.10.1. Older SDKs such as 17.1.0 will not compile.
  • addCardToGoogleWallet(cardData, handlePaymentCredentials) now requires the handler.
  • onCardActivated tokenId is typed tokenId?: string | null. It can be null when Google saves the card without a token ID, and it is missing from the iOS canceled event.

Release note: the publish workflow always runs npm version patch, so this PR sets the version to 0.2.0-0, which publishes as 0.2.0 instead of 0.1.x.

Related Issues

#58

Manual Tests

Tested on a physical Android device against the real Google Wallet flow, plus a harness covering every result and credentials path.

Physical Android (moto g84 5G, Android 15, Google Wallet, GMS 26.37.37, debug build, TapAndPay SDK 18.10.1):

  1. Call addCardToGoogleWallet with the example's dummy card data. Verify that Google Wallet opens "Add payment card".
  2. Tap Cancel. Verify that the promise resolves canceled and onCardActivated fires with status: "canceled".
  3. Call addCardToGoogleWallet again. Verify that Google Wallet opens again (no "A tokenization request is already in progress").
  4. Tap "Add to Wallet" and go through the Google Wallet flow:
    • With a real card from an allowlisted app: verify that the card is added, the promise resolves success, and onCardActivated fires with status: "activated" and the issuer token ID. If Google asks for a Google OPC, verify that the payment credentials handler is called and its result is used.
    • With the example app's dummy card data: Google stops at the eligibility check. Verify that the promise resolves canceled and nothing hangs.

Simulated on the same device (temporary harness calling onActivityResult and the generator directly):

  • Push results:
    • all outcomes failed: rejects
    • one outcome OK: resolves success with that token ID
    • card saved with no outcomes: resolves success, tokenId: null
    • RESULT_OK with no data or no result: resolves success
    • unknown result code: resolves error
    • cancel: resolves canceled
  • Resume results: the legacy EXTRA_ISSUER_TOKEN_ID resolves success with that token ID. No data resolves success, an unknown code resolves error.
  • Generator with the JS handler:
    • the handler's credentials reach the generator response
    • a throwing handler fails the request
    • a handler that never resolves times out
    • no Google OPC requested: the TSP OPC is returned without calling the handler
    • no handler set: fails
    • no pending requests are left after any of these

After the device run: the Google exception reporting, the required Google OPC, the separate result extras, the request-variant migration and hasEligibleTokenizationTarget were verified with typecheck, lint and a Kotlin compile against SDK 18.10.1. These paths can only be exercised end to end from an allowlisted app with a real Google OPC.

iOS: the example app builds on both new and old arch with the stubs for the payment credentials methods. The hasEligibleTokenizationTarget stub follows the same pattern but hasn't been rebuilt.

Linked PRs

The Expensify/App bump (TapAndPay SDK 18.10.1 + payment credentials handler) will follow in a separate PR.

@Skalakid
Skalakid requested a review from zfurtak February 16, 2026 10:19
@JaG31

JaG31 commented Aug 20, 2026

Copy link
Copy Markdown

Any update on when this could potentially be implemented? Thanks!

@JaG31

JaG31 commented Aug 20, 2026

Copy link
Copy Markdown

@Skalakid @zfurtak

@Skalakid
Skalakid marked this pull request as ready for review August 21, 2026 06:47
@DTupalov

Copy link
Copy Markdown

+1
Also waiting on this UAPP upgrade.
What is the plan for this PR?

@JaG31

JaG31 commented Sep 17, 2026

Copy link
Copy Markdown

Any updates would be appreciated! We are nearing the deadline for this. @Skalakid @zfurtak

@Skalakid

Copy link
Copy Markdown
Contributor Author

@JaG31 @DTupalov Hey, sorry I had to prioritize other projects. Will merge it as soon as possible

…e-UAPP

# Conflicts:
#	android/src/main/java/com/expensify/wallet/WalletModule.kt
@JakubKorytko JakubKorytko self-assigned this Oct 9, 2026
@mountiny
mountiny self-requested a review October 12, 2026 12:45
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.

4 participants