diff --git a/StripeCryptoOnramp/StripeCryptoOnramp/Source/Components/CryptoOnrampCoordinator.swift b/StripeCryptoOnramp/StripeCryptoOnramp/Source/Components/CryptoOnrampCoordinator.swift index 88b8ed94c45f..9f5c955681c9 100644 --- a/StripeCryptoOnramp/StripeCryptoOnramp/Source/Components/CryptoOnrampCoordinator.swift +++ b/StripeCryptoOnramp/StripeCryptoOnramp/Source/Components/CryptoOnrampCoordinator.swift @@ -174,6 +174,14 @@ public final class CryptoOnrampCoordinator: NSObject, CryptoOnrampCoordinatorPro private let appearance: LinkAppearance private let analyticsClient: CryptoOnrampAnalyticsClient private var applePayCompletionContinuation: CheckedContinuation? + + /// Apple Pay payment source created by `didCreatePaymentMethod` but not yet committed. + /// + /// Apple Pay can create the payment method before the sheet reports final success. Keep it here + /// until `didCompleteWith(.success)`, then promote it to `selectedPaymentSource`. Cancellation + /// or failure leaves the existing selection untouched; this value is cleared when the Apple Pay + /// attempt starts, completes, or the user logs out. + private var pendingApplePayPaymentSource: SelectedPaymentSource? private var selectedPaymentSource: SelectedPaymentSource? private let cryptoCustomerState: CryptoCustomerState @@ -478,7 +486,6 @@ public final class CryptoOnrampCoordinator: NSObject, CryptoOnrampCoordinatorPro supportedPaymentMethodTypes: supportedPaymentMethodTypes, collectName: type.requiresNameCollection ) else { - selectedPaymentSource = nil return .canceled } @@ -492,7 +499,8 @@ public final class CryptoOnrampCoordinator: NSObject, CryptoOnrampCoordinatorPro analyticsClient.log(.collectPaymentMethodCompleted(paymentMethodType: type.analyticsValue)) return .completed(displayData: preview, kycInfo: nil) case .applePay(let paymentRequest): - // This presents Apple Pay and fills the selected payment source in the delegate. + // This presents Apple Pay and promotes the pending payment source on success. + pendingApplePayPaymentSource = nil do { let status = try await presentApplePay(using: paymentRequest, from: viewController) switch status { @@ -524,10 +532,10 @@ public final class CryptoOnrampCoordinator: NSObject, CryptoOnrampCoordinatorPro return .completed(displayData: paymentMethodPreview, kycInfo: kycInfo) case .canceled: - selectedPaymentSource = nil return .canceled } } catch { + pendingApplePayPaymentSource = nil analyticsClient.log(.errorOccurred(during: .collectPaymentMethod, errorMessage: error.localizedDescription)) throw error } @@ -629,6 +637,8 @@ public final class CryptoOnrampCoordinator: NSObject, CryptoOnrampCoordinatorPro public func logOut() async throws { do { + pendingApplePayPaymentSource = nil + selectedPaymentSource = nil try await linkController.logOut() analyticsClient.log(.userLoggedOut) } catch { @@ -647,7 +657,7 @@ extension CryptoOnrampCoordinator: ApplePayContextDelegate { didCreatePaymentMethod paymentMethod: StripeAPI.PaymentMethod, paymentInformation: PKPayment ) async throws -> String { - selectedPaymentSource = .applePay(paymentMethod, KycInfo(payment: paymentInformation)) + pendingApplePayPaymentSource = .applePay(paymentMethod, KycInfo(payment: paymentInformation)) return STPApplePayContext.COMPLETE_WITHOUT_CONFIRMING_INTENT } @@ -655,18 +665,21 @@ extension CryptoOnrampCoordinator: ApplePayContextDelegate { public func applePayContext(_ context: STPApplePayContext, didCompleteWith status: STPApplePayContext.PaymentStatus, error: Swift.Error?) { switch status { case .success: - applePayCompletionContinuation?.resume(returning: .success) + if let pendingApplePayPaymentSource { + selectedPaymentSource = pendingApplePayPaymentSource + applePayCompletionContinuation?.resume(returning: .success) + } else { + applePayCompletionContinuation?.resume(throwing: ApplePayPaymentStatus.Error.applePayFallbackError) + } case .userCancellation: - selectedPaymentSource = nil applePayCompletionContinuation?.resume(returning: .canceled) case .error: - selectedPaymentSource = nil applePayCompletionContinuation?.resume(throwing: error ?? ApplePayPaymentStatus.Error.applePayFallbackError) @unknown default: - selectedPaymentSource = nil applePayCompletionContinuation?.resume(throwing: error ?? ApplePayPaymentStatus.Error.applePayFallbackError) } + pendingApplePayPaymentSource = nil applePayCompletionContinuation = nil } } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/LinkController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/LinkController.swift index 243096478084..69d82c51fd67 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/LinkController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/LinkController.swift @@ -368,7 +368,7 @@ import UIKit with email: String?, supportedPaymentMethodTypes: [LinkPaymentMethodType] = LinkPaymentMethodType.allCases, collectName: Bool = false, - completion: @escaping () -> Void + completion: @escaping (_ didSelectPaymentMethod: Bool) -> Void ) { var configuration = self.configuration configuration.defaultBillingDetails.email = email @@ -395,12 +395,12 @@ import UIKit if shouldClearSelection { self?.internalPaymentOption = nil } - completion() + completion(false) return } self?.internalPaymentOption = .link(option: confirmOption) - completion() + completion(true) } } @@ -1037,9 +1037,9 @@ extension LinkController: LinkFullConsentViewControllerDelegate { with: email, supportedPaymentMethodTypes: supportedPaymentMethodTypes, collectName: collectName - ) { [weak self] in + ) { [weak self] didSelectPaymentMethod in guard let self else { return } - continuation.resume(returning: self.paymentMethodPreview) + continuation.resume(returning: didSelectPaymentMethod ? self.paymentMethodPreview : nil) } } }