diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index 32cff1623..2155a56ab 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -114,6 +114,10 @@ public class PaywallViewController: UIViewController, LoadingDelegate { /// Tracks whether explicit stripe_checkout_abandon was already received for this checkout flow. private var didReceiveStripeCheckoutAbandonMessage = false + /// The product ID from the most recent stripe_checkout_start message, used as a fallback + /// when the user swipe-dismisses the checkout sheet before stripe_checkout_abandon fires. + private var lastStripeCheckoutProductId: String? + /// Ensures Stripe checkout callbacks are forwarded to WebEntitlementRedeemer in order. private var previousStripeCheckoutTask: Task? @@ -1126,12 +1130,13 @@ extension PaywallViewController: UIAdaptivePresentationControllerDelegate { // MARK: - PaywallMessageHandlerDelegate extension PaywallViewController: PaywallMessageHandlerDelegate { - func openPaymentSheet(_ url: URL) { + func openPaymentSheet(_ url: URL, productId: String?) { #if !os(visionOS) // Reset flags when opening checkout didRedeemSucceedDuringCheckout = false isCheckoutDismissedProgrammatically = false didReceiveStripeCheckoutAbandonMessage = false + lastStripeCheckoutProductId = productId transactionAbandonWorkItem?.cancel() transactionAbandonWorkItem = nil @@ -1155,11 +1160,12 @@ extension PaywallViewController: PaywallMessageHandlerDelegate { if !self.didRedeemSucceedDuringCheckout, !self.isCheckoutDismissedProgrammatically, !self.didReceiveStripeCheckoutAbandonMessage { + let abandonProductId = self.lastStripeCheckoutProductId ?? "" let workItem = DispatchWorkItem { [weak self] in guard let self = self else { return } Task { let event = InternalSuperwallEvent.Transaction( - state: .abandon(StoreProduct.blank()), + state: .abandon(StoreProduct.blank(productIdentifier: abandonProductId)), paywallInfo: self.info, product: nil, transaction: nil, @@ -1178,6 +1184,7 @@ extension PaywallViewController: PaywallMessageHandlerDelegate { self.didRedeemSucceedDuringCheckout = false self.isCheckoutDismissedProgrammatically = false self.didReceiveStripeCheckoutAbandonMessage = false + self.lastStripeCheckoutProductId = nil } checkoutVC.modalPresentationStyle = .pageSheet diff --git a/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessage.swift b/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessage.swift index cb1c3f5f5..d729ced7a 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessage.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessage.swift @@ -50,7 +50,7 @@ enum PaywallMessage: Decodable, Equatable { case restore case openUrl(_ url: URL) case openUrlInSafari(_ url: URL) - case openPaymentSheet(_ url: URL) + case openPaymentSheet(_ url: URL, productId: String?) case openDeepLink(url: URL) case purchase(productId: String, shouldDismiss: Bool) case custom(data: String) @@ -184,7 +184,12 @@ enum PaywallMessage: Decodable, Equatable { // On visionOS, always use openUrl instead of payment sheet self = .openUrl(url) #else - self = browserType == "payment_sheet" ? .openPaymentSheet(url) : .openUrl(url) + if browserType == "payment_sheet" { + let productId = try? values.decode(String.self, forKey: .productId) + self = .openPaymentSheet(url, productId: productId) + } else { + self = .openUrl(url) + } #endif return } diff --git a/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift b/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift index 657bdff35..11bc4dd17 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift @@ -22,7 +22,7 @@ protocol PaywallMessageHandlerDelegate: AnyObject { func presentSafariInApp(_ url: URL) func presentSafariExternal(_ url: URL) func requestReview(type: ReviewType) - func openPaymentSheet(_ url: URL) + func openPaymentSheet(_ url: URL, productId: String?) func handleStripeCheckoutSubmit(checkoutContextId: String, productId: String) func handleStripeCheckoutComplete( checkoutContextId: String, @@ -176,8 +176,8 @@ final class PaywallMessageHandler: WebEventDelegate { openUrl(url) case .openUrlInSafari(let url): openUrlInSafari(url) - case .openPaymentSheet(let url): - openPaymentSheet(url) + case let .openPaymentSheet(url, productId): + openPaymentSheet(url, productId: productId) case .openDeepLink(let url): openDeepLink(url) case .restore: @@ -474,13 +474,13 @@ final class PaywallMessageHandler: WebEventDelegate { delegate?.presentSafariExternal(url) } - private func openPaymentSheet(_ url: URL) { + private func openPaymentSheet(_ url: URL, productId: String?) { detectHiddenPaywallEvent( "openPaymentSheet", userInfo: ["url": url] ) hapticFeedback() - delegate?.openPaymentSheet(url) + delegate?.openPaymentSheet(url, productId: productId) } private func openDeepLink(_ url: URL) { diff --git a/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift b/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift index 22de441dd..6dd9f49cb 100644 --- a/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift +++ b/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift @@ -89,8 +89,11 @@ final class PaywallMessageHandlerDelegateMock: PaywallMessageHandlerDelegate { didRequestReview = true } - func openPaymentSheet(_ url: URL) { + var openPaymentSheetProductId: String? + + func openPaymentSheet(_ url: URL, productId: String?) { didOpenPaymentSheet = true + openPaymentSheetProductId = productId } func handleStripeCheckoutSubmit(checkoutContextId: String, productId: String) {