Skip to content

List Apple Wallet passes and activate them in-app - #96

Open
Brunonascdev wants to merge 3 commits into
Expensify:mainfrom
Brunonascdev:feat/ios-wallet-pass-activation
Open

Brunonascdev wants to merge 3 commits into
Expensify:mainfrom
Brunonascdev:feat/ios-wallet-pass-activation

Conversation

@Brunonascdev

@Brunonascdev Brunonascdev commented Oct 12, 2026 •

Copy link
Copy Markdown

Details

Adds two iOS functions so an app can show a card's verification state and finish activation without sending the user to Wallet:

  • listAppleWalletPasses() returns every Secure Element pass on the iPhone and on a paired Apple Watch, with activationState (requireActivation, pending, active, and the other CardStatus values) and isRemote.
  • activateAppleWalletPass({ passTypeIdentifier, serialNumber, activationData, isRemote }) activates a pass that is waiting for activation. isRemote selects the Watch pass when the same card is on both devices. It is optional. Omit it to look up the iPhone pass first and then a paired Apple Watch. The new architecture only forwards the flag when it is present, and the old architecture only when the value is a boolean, so a missing flag stays nil and the fallback runs.

Ported from ShpetimA/react-native-wallet (d122f802, ca6c4044), adapted to current main: the old architecture reads struct fields with the existing per-parameter #ifdef, and Android gets no-op stubs so the shared TurboModule spec still builds.

getCardStatusBySuffix and getCardStatusByIdentifier are unchanged and still report only the iPhone pass.

Merged current main.

Related Issues

#84
#45

Manual Tests

This repo has no automated tests. Typecheck and lint pass. The example app builds for the iOS simulator with the new architecture. Android was not compiled here: the example app does not include the TapAndPay SDK. The Kotlin changes are stubs.

  • On an iPhone with a card in Wallet, call listAppleWalletPasses() and confirm the pass is returned with isRemote: false and an activationState.
  • Pair an Apple Watch that has the same card (or only the Watch has it). Confirm a second entry with isRemote: true, and that a Watch-only card is no longer reported as not found.
  • For a pass in requireActivation, call activateAppleWalletPass with issuer activation data and the matching isRemote flag. Confirm it resolves true and a following list shows active.
  • Call activateAppleWalletPass without isRemote for a Watch-only pass. Confirm it activates that pass instead of failing with "Secure Element pass not found".
  • Confirm activateAppleWalletPass rejects when activation data is not base64, the pass is missing, or the pass is not eligible.
  • Confirm both functions are safe to import on Android: listing resolves [], and activation throws.

Linked PRs

#91, #132, #133, and #134 also edit the spec, the README, or WalletManager.swift / RNWallet.mm. Whichever lands second will need a small merge. This PR does not depend on #91.

Apps can read each pass's activation state on the iPhone and a paired Watch, then finish verification without leaving the app.

@JakubKorytko JakubKorytko left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One thing on the isRemote lookup

Comment thread ios/WalletManager.swift
return pass.secureElementPass
}

if isRemote == false {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The iPhone-then-Watch fallback only runs when isRemote is nil, but it never is: TS requires it, the new arch always passes a bool, and the old arch maps missing to false. So a caller passing false for a Watch-only card gets "Secure Element pass not found". Could we make isRemote optional end to end (isRemote?: boolean in TS, nil from RNWallet.mm when missing) so the fallback actually runs?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

isRemote is optional end to end. The new architecture only sets it when the value is present, and the old architecture only when it is a boolean. A missing value stays nil, so the iPhone-then-Watch lookup runs. The README describes that.

@JakubKorytko

Copy link
Copy Markdown
Member

@Brunonascdev gentle bump & please merge main 🙏

A missing flag now reaches the iPhone-then-Watch lookup instead of being treated as the iPhone.
@Brunonascdev

Copy link
Copy Markdown
Author

Merged current main. isRemote is now optional, so a missing flag reaches the iPhone-then-Watch lookup.

@Brunonascdev

Copy link
Copy Markdown
Author

@JakubKorytko this is ready for another look. Requesting a review from the fork returns a permissions error, so this comment is the ping.

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