From 12e1a6827ac0cbcd1efaa06b6e69368f95132e78 Mon Sep 17 00:00:00 2001 From: Francois Laithier Date: Wed, 7 Oct 2026 09:15:20 -0700 Subject: [PATCH 1/2] fix: allow Apple Wallet retries after cancellation 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: https://github.com/Expensify/App/issues/102403 --- ios/WalletManager.swift | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/ios/WalletManager.swift b/ios/WalletManager.swift index 53c31e8d..bc96825c 100644 --- a/ios/WalletManager.swift +++ b/ios/WalletManager.swift @@ -120,16 +120,26 @@ open class WalletManager: UIViewController { } DispatchQueue.main.async { - if self.addPassViewController == nil { - self.presentAddPaymentPassCompletionHandler = completion - self.addPassViewController = enrollViewController - RCTPresentedViewController()?.present(enrollViewController, animated: true, completion: nil) - } else { + if let enrollVC = self.addPassViewController, enrollVC.isBeingPresented || enrollVC.presentingViewController != nil { self.logInfo(message: "EnrollViewController is already presented.") completion(.error, [ "errorMessage": "EnrollViewController is already presented." ]) + return + } + + self.addPassViewController = nil + + guard let presentingViewController = RCTPresentedViewController() else { + completion(.error, [ + "errorMessage": "Presenting view controller is unavailable." + ]) + return } + + self.presentAddPaymentPassCompletionHandler = completion + self.addPassViewController = enrollViewController + presentingViewController.present(enrollViewController, animated: true, completion: nil) } } @@ -200,9 +210,12 @@ open class WalletManager: UIViewController { DispatchQueue.main.async { if let enrollVC = self.addPassViewController, enrollVC.isBeingPresented || enrollVC.presentingViewController != nil { enrollVC.dismiss(animated: true, completion: { - self.addPassViewController = nil + if self.addPassViewController === enrollVC { + self.addPassViewController = nil + } }) } else { + self.addPassViewController = nil self.logInfo(message: "EnrollViewController is not presented currently.") } } From fda345fd8bca7f332041fcb44d3fbb8c7421e5cc Mon Sep 17 00:00:00 2001 From: Francois Laithier Date: Thu, 8 Oct 2026 13:54:20 -0700 Subject: [PATCH 2/2] fix: isolate wallet callbacks by enrollment controller 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 https://github.com/Expensify/react-native-wallet/pull/95. --- ios/WalletManager.swift | 49 ++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/ios/WalletManager.swift b/ios/WalletManager.swift index bc96825c..6b3e4c35 100644 --- a/ios/WalletManager.swift +++ b/ios/WalletManager.swift @@ -128,7 +128,18 @@ open class WalletManager: UIViewController { return } + let presentPassHandler = self.presentAddPaymentPassCompletionHandler + let addPaymentPassHandler = self.addPaymentPassCompletionHandler self.addPassViewController = nil + self.presentAddPaymentPassCompletionHandler = nil + self.addPaymentPassCompletionHandler = nil + self.addPassHandler = nil + + let errorResponse: NSDictionary = [ + "errorMessage": "EnrollViewController is no longer presented." + ] + presentPassHandler?(.error, errorResponse) + addPaymentPassHandler?(.error, errorResponse) guard let presentingViewController = RCTPresentedViewController() else { completion(.error, [ @@ -207,8 +218,16 @@ open class WalletManager: UIViewController { } private func hideModal() { + guard let enrollVC = addPassViewController else { + return + } + DispatchQueue.main.async { - if let enrollVC = self.addPassViewController, enrollVC.isBeingPresented || enrollVC.presentingViewController != nil { + guard self.addPassViewController === enrollVC else { + return + } + + if enrollVC.isBeingPresented || enrollVC.presentingViewController != nil { enrollVC.dismiss(animated: true, completion: { if self.addPassViewController === enrollVC { self.addPassViewController = nil @@ -233,6 +252,10 @@ extension WalletManager: PKAddPaymentPassViewControllerDelegate { generateRequestWithCertificateChain certificates: [Data], nonce: Data, nonceSignature: Data, completionHandler handler: @escaping (PKAddPaymentPassRequest) -> Void) { + guard controller === addPassViewController else { + return + } + let stringNonce = nonce.base64EncodedString() as NSString let stringNonceSignature = nonceSignature.base64EncodedString() as NSString let stringCertificates = certificates.map { @@ -243,15 +266,16 @@ extension WalletManager: PKAddPaymentPassViewControllerDelegate { // Retry the JS issuer callback if the user tries again to add a payment pass if let addPaymentPassHandler = addPaymentPassCompletionHandler { - addPaymentPassHandler(.retry, reqestCardData.toNSDictionary()) + // The callback can synchronously register the next completion. addPaymentPassCompletionHandler = nil + addPaymentPassHandler(.retry, reqestCardData.toNSDictionary()) return } // Finish IOSPresentAddPaymentPassView function if let presentPassHandler = presentAddPaymentPassCompletionHandler { - presentPassHandler(.completed, reqestCardData.toNSDictionary()) presentAddPaymentPassCompletionHandler = nil + presentPassHandler(.completed, reqestCardData.toNSDictionary()) } } @@ -260,10 +284,16 @@ extension WalletManager: PKAddPaymentPassViewControllerDelegate { _ controller: PKAddPaymentPassViewController, didFinishAdding pass: PKPaymentPass?, error: Error?) { - if addPassViewController == nil { + guard controller === addPassViewController else { return } - + + let presentPassHandler = presentAddPaymentPassCompletionHandler + let addPaymentPassHandler = addPaymentPassCompletionHandler + presentAddPaymentPassCompletionHandler = nil + addPaymentPassCompletionHandler = nil + addPassHandler = nil + let errorMessage = error?.localizedDescription ?? "" if error != nil { @@ -274,13 +304,13 @@ extension WalletManager: PKAddPaymentPassViewControllerDelegate { } // Cancel the IOSPresentAddPaymentPassView function when the user cancelled the modal - if let handler = presentAddPaymentPassCompletionHandler { + if let handler = presentPassHandler { let response = AddPassResponse(status: .canceled, nonce: nil, nonceSignature: nil, certificates: nil) handler(.canceled, response.toNSDictionary()) } // If the pass is returned complete the IOSHandleAddPaymentPassResponse function - if let addPaymentPassHandler = addPaymentPassCompletionHandler { + if let addPaymentPassHandler = addPaymentPassHandler { if pass != nil { addPaymentPassHandler(.completed, nil) } else { @@ -289,11 +319,8 @@ extension WalletManager: PKAddPaymentPassViewControllerDelegate { ]) } } - - addPassHandler = nil + hideModal() - addPaymentPassCompletionHandler = nil - presentAddPaymentPassCompletionHandler = nil } }