Skip to content

Allow Apple Wallet retries after canceling Download Wallet - #95

Merged
JakubKorytko merged 2 commits into
mainfrom
francois-ios-wallet-cancel-retry
Oct 9, 2026
Merged

JakubKorytko merged 2 commits into
mainfrom
francois-ios-wallet-cancel-retry

Conversation

@francoisl

@francoisl francoisl commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Details

Allow users to retry adding a card after canceling the Download Wallet prompt on iOS.

When iOS has already dismissed the payment-pass controller, hideModal() leaves addPassViewController set. The next attempt treats that stale controller as an active presentation and blocks the retry. In the App's pinned wallet version, that request never settles and the button keeps loading.

This change:

  • Clears the stored controller when it is no longer presented.
  • Allows a new presentation when the stored controller is stale.
  • Preserves the existing error for an active duplicate request without replacing the original completion handler.
  • Returns an error when no presenting view controller is available, before saving the request's state.
  • Guards dismissal cleanup so an older controller cannot clear a newer controller.

Related Issues

For Expensify/App#102403

Based on Melvin's approved proposal.

Manual Tests

QA confirmed that the original issue is no longer reproducible in the
App ad-hoc test build.
QA confirmation and video.

  1. Install the App ad-hoc build on a physical iPhone.
  2. Sign in to an account with an assigned virtual card.
  3. Remove the Wallet app from the iPhone.
  4. Open Account > Wallet and select the virtual card.
  5. Tap Add to Apple Wallet.
  6. Cancel the Download Wallet prompt.
  7. Confirm the button stops loading.
  8. Tap Add to Apple Wallet again.
  9. Confirm the Download Wallet prompt reappears.
  10. Cancel the prompt again.
  11. Confirm the button stops loading.
  12. Reinstall Wallet.
  13. Add the virtual card to Apple Wallet.
  14. Confirm the normal card-provisioning flow completes successfully.

Local verification completed:

  • Swift type checking passes against the iOS simulator SDK using the real UIKit and PassKit frameworks and a minimal React declaration shim.
  • Six scenarios pass in a temporary Swift harness with mocked UIKit and PassKit: cancellation after system dismissal, active duplicate requests, duplicates during presentation, delayed dismissal cleanup, a missing presenter, and successful provisioning callbacks.
  • The relevant regression scenarios fail against the unchanged base code.
  • git diff --check passes.

No repository tests are added. The repo has no native test target; the temporary harness checks are local verification, not committed regression coverage. The simulator cannot verify the real Download Wallet or payment-card provisioning flow.

Linked PRs

App test PR: pending. It will temporarily pin @expensify/react-native-wallet to this PR's commit so we can build an ad-hoc IPA before merging the wallet fix. The App currently uses 0.1.22; this branch is based on 0.1.38, so the test build will also include the intervening library changes.

After this fix is published, the App dependency-update PR will use the released version.

Clear dismissed payment-pass controllers so canceling the Download Wallet
prompt doesn't block later enrollment attempts. Preserve active requests,
reject missing presenters, and guard delayed dismissal cleanup.

Refs: Expensify/App#102403
@github-actions

This comment has been minimized.

@francoisl francoisl changed the title fix: allow Apple Wallet retries after canceling Download Wallet Allow Apple Wallet retries after canceling Download Wallet Oct 7, 2026
@francoisl

This comment has been minimized.

@JakubKorytko

Copy link
Copy Markdown
Member

@mountiny I see this is tied to an E/App issue. Is it going to be reviewed internally / by C+, or do you want me to handle it? If mine is needed, @francoisl just ping me once it's out of draft.

Two things I noticed at first glance:

  • When a stale controller gets replaced, presentAddPaymentPassCompletionHandler is overwritten without settling the old one. If the earlier present silently did nothing, that JS promise never resolves.
  • didFinishAdding only checks addPassViewController == nil, not controller === addPassViewController, so a late callback from the old controller could cancel the new request.

@francoisl

Copy link
Copy Markdown
Contributor Author

Hi @JakubKorytko, yes I was going definitely going to tag you for a review. For now I was waiting for a retest via Expensify/App#103360, because I don't have a physical iPhone to test the entire flow of adding a card to a wallet.

I can update the PR with your feedback, though if you feel like taking over, let me know - that's fine with me too.

@JakubKorytko

Copy link
Copy Markdown
Member

Oh I didn't mean I want to take over, just review, test, merge. You have much more context than I do about this change, so I think it's better for you to keep the PR. LMK if you feel different though

Reject pending completions before replacing a stale enrollment controller.
Ignore delegate callbacks and queued cleanup from superseded controllers,
and clear consumed handlers before invoking them to preserve retries.

Address review feedback for #95.

@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.

Tested on a physical iPhone 11 (iOS 18.7.8), staging, virtual Expensify Card, Wallet app removed:

  • Without the fix (ad-hoc from Expensify/App#102774, wallet 0.1.22): Add to Apple Wallet -> "Download Wallet?" -> Cancel, then a second tap shows no prompt and the button spins forever.
  • With the fix (ad-hoc from Expensify/App#103360, wallet at 12e1a68): the same steps three times in a row. The prompt comes back every time and the button never gets stuck.
  • Wallet reinstalled (fix build): the Apple Pay sheet opens, Cancel works, and Next reaches the issuer step. Apple rejects the staging card as expected; retry inside the sheet works, and closing it shows the "Failed to add card" alert instead of hanging.

fda345f isn't in that build, but the code looks good to me. LGTM.

@JakubKorytko
JakubKorytko merged commit e91c005 into main Oct 9, 2026
7 of 8 checks passed
@os-botify

os-botify Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

🚀 Published to npm in 0.1.41 🎉

@francoisl
francoisl deleted the francois-ios-wallet-cancel-retry branch October 9, 2026 16:18
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