diff --git a/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/Views/Components/ButtonSet.swift b/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/Views/Components/ButtonSet.swift index 08ada99b..a61e6d70 100644 --- a/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/Views/Components/ButtonSet.swift +++ b/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/Views/Components/ButtonSet.swift @@ -23,6 +23,7 @@ import Apollo import ShopifyAcceleratedCheckouts +import ShopifyCheckoutSheetKit import SwiftUI struct ButtonSet: View { @@ -35,11 +36,12 @@ struct ButtonSet: View { CheckoutSection(title: "AcceleratedCheckoutButtons(cartID:)") { // Cart-based checkout example with event handlers AcceleratedCheckoutButtons(cartID: cartID) - .onComplete { + .onComplete { event in print("✅ Checkout completed successfully") + print(" Order ID: \(event.orderDetails.id)") } - .onFail { - print("❌ Checkout failed") + .onFail { error in + print("❌ Checkout failed: \(error)") } .onCancel { print("🚫 Checkout cancelled") @@ -52,8 +54,16 @@ struct ButtonSet: View { .onClickLink { url in print("🔗 Link clicked: \(url)") } - .onWebPixelEvent { _ in - print("📊 Web pixel event received") + .onWebPixelEvent { event in + let eventName: String = { + switch event { + case let .customEvent(customEvent): + return customEvent.name ?? "Unknown custom event" + case let .standardEvent(standardEvent): + return standardEvent.name ?? "Unknown standard event" + } + }() + print("📊 Web pixel event: \(eventName)") } } } @@ -69,11 +79,12 @@ struct ButtonSet: View { ) .cornerRadius(24) .withWallets([.applepay, .shoppay]) - .onComplete { + .onComplete { event in print("✅ Variant checkout completed") + print(" Order ID: \(event.orderDetails.id)") } - .onFail { - print("❌ Variant checkout failed") + .onFail { error in + print("❌ Variant checkout failed: \(error)") } .onCancel { print("🚫 Variant checkout cancelled") @@ -85,8 +96,16 @@ struct ButtonSet: View { .onClickLink { url in print("🔗 Variant - Link clicked: \(url)") } - .onWebPixelEvent { _ in - print("📊 Variant - Web pixel event received") + .onWebPixelEvent { event in + let eventName: String = { + switch event { + case let .customEvent(customEvent): + return customEvent.name ?? "Unknown custom event" + case let .standardEvent(standardEvent): + return standardEvent.name ?? "Unknown standard event" + } + }() + print("📊 Variant - Web pixel event: \(eventName)") } } } diff --git a/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift b/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift index 12e4d4df..6274d7bb 100644 --- a/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift +++ b/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift @@ -136,15 +136,15 @@ extension AcceleratedCheckoutButtons { /// /// ```swift /// AcceleratedCheckoutButtons(cartID: cartId) - /// .onComplete { - /// // Navigate to success screen - /// showSuccessView = true + /// .onComplete { event in + /// // Navigate to success screen with order ID + /// showSuccessView(orderId: event.orderId) /// } /// ``` /// /// - Parameter action: The action to perform when checkout succeeds /// - Returns: A view with the checkout success handler set - public func onComplete(_ action: @escaping () -> Void) -> AcceleratedCheckoutButtons { + public func onComplete(_ action: @escaping (CheckoutCompletedEvent) -> Void) -> AcceleratedCheckoutButtons { var newView = self newView.eventHandlers.checkoutDidComplete = action return newView @@ -156,15 +156,15 @@ extension AcceleratedCheckoutButtons { /// /// ```swift /// AcceleratedCheckoutButtons(cartID: cartId) - /// .onFail { - /// // Show error alert - /// showErrorAlert = true + /// .onFail { error in + /// // Show error alert with details + /// showErrorAlert(error: error) /// } /// ``` /// /// - Parameter action: The action to perform when checkout fails /// - Returns: A view with the checkout error handler set - public func onFail(_ action: @escaping () -> Void) -> AcceleratedCheckoutButtons { + public func onFail(_ action: @escaping (CheckoutError) -> Void) -> AcceleratedCheckoutButtons { var newView = self newView.eventHandlers.checkoutDidFail = action return newView @@ -205,7 +205,7 @@ extension AcceleratedCheckoutButtons { /// - Parameter action: The action to determine if recovery should be attempted /// - Returns: A view with the error recovery handler set public func onShouldRecoverFromError( - _ action: @escaping (ShopifyCheckoutSheetKit.CheckoutError) -> Bool + _ action: @escaping (CheckoutError) -> Bool ) -> AcceleratedCheckoutButtons { var newView = self newView.eventHandlers.shouldRecoverFromError = action @@ -246,7 +246,7 @@ extension AcceleratedCheckoutButtons { /// /// - Parameter action: The action to perform when a pixel event is emitted /// - Returns: A view with the web pixel event handler set - public func onWebPixelEvent(_ action: @escaping (ShopifyCheckoutSheetKit.PixelEvent) -> Void) + public func onWebPixelEvent(_ action: @escaping (PixelEvent) -> Void) -> AcceleratedCheckoutButtons { var newView = self diff --git a/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayButton.swift b/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayButton.swift index e4115cb7..ac10032f 100644 --- a/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayButton.swift +++ b/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayButton.swift @@ -121,12 +121,12 @@ struct Internal_ApplePayButton: View { self.label = label self.cornerRadius = cornerRadius MainActor.assumeIsolated { - controller.onComplete = eventHandlers.checkoutDidComplete - controller.onFail = eventHandlers.checkoutDidFail - controller.onCancel = eventHandlers.checkoutDidCancel + controller.onCheckoutComplete = eventHandlers.checkoutDidComplete + controller.onCheckoutFail = eventHandlers.checkoutDidFail + controller.onCheckoutCancel = eventHandlers.checkoutDidCancel controller.onShouldRecoverFromError = eventHandlers.shouldRecoverFromError - controller.onClickLink = eventHandlers.checkoutDidClickLink - controller.onWebPixelEvent = eventHandlers.checkoutDidEmitWebPixelEvent + controller.onCheckoutClickLink = eventHandlers.checkoutDidClickLink + controller.onCheckoutWebPixelEvent = eventHandlers.checkoutDidEmitWebPixelEvent } } diff --git a/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayViewController.swift b/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayViewController.swift index 525201f0..9415bed9 100644 --- a/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayViewController.swift +++ b/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayViewController.swift @@ -54,39 +54,39 @@ protocol PayController: AnyObject { /// /// Example usage: /// ```swift - /// applePayViewController.onComplete = { [weak self] in + /// applePayViewController.onCheckoutComplete = { [weak self] event in /// self?.presentSuccessScreen() - /// self?.logAnalyticsEvent(.checkoutCompleted) + /// self?.logAnalyticsEvent(.checkoutCompleted, orderId: event.orderId) /// } /// ``` @MainActor - public var onComplete: (() -> Void)? + public var onCheckoutComplete: ((CheckoutCompletedEvent) -> Void)? /// Callback invoked when an error occurs during the checkout process. - /// This closure is called on the main thread when the payment fails or is cancelled. + /// This closure is called on the main thread when the payment fails. /// /// Example usage: /// ```swift - /// applePayViewController.onFail = { [weak self] in - /// self?.showErrorAlert() - /// self?.logAnalyticsEvent(.checkoutFailed) + /// applePayViewController.onCheckoutFail = { [weak self] error in + /// self?.showErrorAlert(for: error) + /// self?.logAnalyticsEvent(.checkoutFailed, error: error) /// } /// ``` @MainActor - public var onFail: (() -> Void)? + public var onCheckoutFail: ((CheckoutError) -> Void)? /// Callback invoked when the checkout process is cancelled by the user. /// This closure is called on the main thread when the user dismisses the checkout. /// /// Example usage: /// ```swift - /// applePayViewController.onCancel = { [weak self] in + /// applePayViewController.onCheckoutCancel = { [weak self] in /// self?.resetCheckoutState() /// self?.logAnalyticsEvent(.checkoutCancelled) /// } /// ``` @MainActor - public var onCancel: (() -> Void)? + public var onCheckoutCancel: (() -> Void)? /// Callback invoked to determine if checkout should recover from an error. /// This closure is called on the main thread when an error occurs. @@ -100,33 +100,33 @@ protocol PayController: AnyObject { /// } /// ``` @MainActor - public var onShouldRecoverFromError: ((ShopifyCheckoutSheetKit.CheckoutError) -> Bool)? + public var onShouldRecoverFromError: ((CheckoutError) -> Bool)? /// Callback invoked when the user clicks a link during checkout. /// This closure is called on the main thread when a link is clicked. /// /// Example usage: /// ```swift - /// applePayViewController.onClickLink = { [weak self] url in + /// applePayViewController.onCheckoutClickLink = { [weak self] url in /// self?.handleExternalLink(url) /// self?.logAnalyticsEvent(.linkClicked, url: url) /// } /// ``` @MainActor - public var onClickLink: ((URL) -> Void)? + public var onCheckoutClickLink: ((URL) -> Void)? /// Callback invoked when a web pixel event is emitted during checkout. /// This closure is called on the main thread when pixel events occur. /// /// Example usage: /// ```swift - /// applePayViewController.onWebPixelEvent = { [weak self] event in + /// applePayViewController.onCheckoutWebPixelEvent = { [weak self] event in /// self?.trackPixelEvent(event) /// self?.logAnalyticsEvent(.pixelFired, event: event) /// } /// ``` @MainActor - public var onWebPixelEvent: ((ShopifyCheckoutSheetKit.PixelEvent) -> Void)? + public var onCheckoutWebPixelEvent: ((PixelEvent) -> Void)? /// Initialization workaround for passing self to ApplePayAuthorizationDelegate private var __authorizationDelegate: ApplePayAuthorizationDelegate! @@ -186,7 +186,9 @@ protocol PayController: AnyObject { return try await handleStorefrontError(error) } catch { await authorizationDelegate.transition(to: .terminalError(error: error)) - await onFail?() + if let checkoutError = error as? CheckoutError { + await onCheckoutFail?(checkoutError) + } throw error } } @@ -241,30 +243,41 @@ protocol PayController: AnyObject { @available(iOS 17.0, *) extension ApplePayViewController: CheckoutDelegate { - @MainActor func checkoutDidComplete(event _: ShopifyCheckoutSheetKit.CheckoutCompletedEvent) { - onComplete?() + func checkoutDidComplete(event: CheckoutCompletedEvent) { + Task { @MainActor in + self.onCheckoutComplete?(event) + } } - @MainActor func checkoutDidFail(error _: ShopifyCheckoutSheetKit.CheckoutError) { - onFail?() + func checkoutDidFail(error: CheckoutError) { + Task { @MainActor in + self.onCheckoutFail?(error) + } } - @MainActor func checkoutDidCancel() { - /// x right button on CSK doesn't dismiss automatically - checkoutViewController?.dismiss(animated: true) - - onCancel?() + func checkoutDidCancel() { + Task { @MainActor in + /// x right button on CSK doesn't dismiss automatically + checkoutViewController?.dismiss(animated: true) + self.onCheckoutCancel?() + } } - @MainActor func shouldRecoverFromError(error: ShopifyCheckoutSheetKit.CheckoutError) -> Bool { - return onShouldRecoverFromError?(error) ?? false + func shouldRecoverFromError(error: CheckoutError) -> Bool { + return MainActor.assumeIsolated { + self.onShouldRecoverFromError?(error) ?? false + } } - @MainActor func checkoutDidClickLink(url: URL) { - onClickLink?(url) + func checkoutDidClickLink(url: URL) { + Task { @MainActor in + self.onCheckoutClickLink?(url) + } } - @MainActor func checkoutDidEmitWebPixelEvent(event: ShopifyCheckoutSheetKit.PixelEvent) { - onWebPixelEvent?(event) + func checkoutDidEmitWebPixelEvent(event: PixelEvent) { + Task { @MainActor in + self.onCheckoutWebPixelEvent?(event) + } } } diff --git a/Sources/ShopifyAcceleratedCheckouts/Wallets/ShopPay/ShopPayViewController.swift b/Sources/ShopifyAcceleratedCheckouts/Wallets/ShopPay/ShopPayViewController.swift index 34ccd7cd..4e7336a0 100644 --- a/Sources/ShopifyAcceleratedCheckouts/Wallets/ShopPay/ShopPayViewController.swift +++ b/Sources/ShopifyAcceleratedCheckouts/Wallets/ShopPay/ShopPayViewController.swift @@ -107,13 +107,13 @@ import SwiftUI @available(iOS 17.0, *) extension ShopPayViewController: CheckoutDelegate { - func checkoutDidComplete(event _: ShopifyCheckoutSheetKit.CheckoutCompletedEvent) { - eventHandlers.checkoutDidComplete?() + func checkoutDidComplete(event: CheckoutCompletedEvent) { + eventHandlers.checkoutDidComplete?(event) } - func checkoutDidFail(error _: ShopifyCheckoutSheetKit.CheckoutError) { + func checkoutDidFail(error: CheckoutError) { checkoutViewController?.dismiss(animated: true) - eventHandlers.checkoutDidFail?() + eventHandlers.checkoutDidFail?(error) } func checkoutDidCancel() { @@ -122,7 +122,7 @@ extension ShopPayViewController: CheckoutDelegate { eventHandlers.checkoutDidCancel?() } - func checkoutShouldRecoverFromError(error: ShopifyCheckoutSheetKit.CheckoutError) -> Bool { + func shouldRecoverFromError(error: CheckoutError) -> Bool { return eventHandlers.shouldRecoverFromError?(error) ?? false } @@ -130,7 +130,7 @@ extension ShopPayViewController: CheckoutDelegate { eventHandlers.checkoutDidClickLink?(url) } - func checkoutDidEmitWebPixelEvent(event: ShopifyCheckoutSheetKit.PixelEvent) { + func checkoutDidEmitWebPixelEvent(event: PixelEvent) { eventHandlers.checkoutDidEmitWebPixelEvent?(event) } } diff --git a/Sources/ShopifyAcceleratedCheckouts/Wallets/Wallet.swift b/Sources/ShopifyAcceleratedCheckouts/Wallets/Wallet.swift index cd5db026..8be9cba0 100644 --- a/Sources/ShopifyAcceleratedCheckouts/Wallets/Wallet.swift +++ b/Sources/ShopifyAcceleratedCheckouts/Wallets/Wallet.swift @@ -30,32 +30,6 @@ public enum Wallet { case shoppay } -/// Event handlers for wallet buttons -public struct EventHandlers { - public var checkoutDidComplete: (() -> Void)? - public var checkoutDidFail: (() -> Void)? - public var checkoutDidCancel: (() -> Void)? - public var shouldRecoverFromError: ((ShopifyCheckoutSheetKit.CheckoutError) -> Bool)? - public var checkoutDidClickLink: ((URL) -> Void)? - public var checkoutDidEmitWebPixelEvent: ((ShopifyCheckoutSheetKit.PixelEvent) -> Void)? - - public init( - checkoutDidComplete: (() -> Void)? = nil, - checkoutDidFail: (() -> Void)? = nil, - checkoutDidCancel: (() -> Void)? = nil, - shouldRecoverFromError: ((ShopifyCheckoutSheetKit.CheckoutError) -> Bool)? = nil, - checkoutDidClickLink: ((URL) -> Void)? = nil, - checkoutDidEmitWebPixelEvent: ((ShopifyCheckoutSheetKit.PixelEvent) -> Void)? = nil - ) { - self.checkoutDidComplete = checkoutDidComplete - self.checkoutDidFail = checkoutDidFail - self.checkoutDidCancel = checkoutDidCancel - self.shouldRecoverFromError = shouldRecoverFromError - self.checkoutDidClickLink = checkoutDidClickLink - self.checkoutDidEmitWebPixelEvent = checkoutDidEmitWebPixelEvent - } -} - extension View { func walletButtonStyle(bg: Color = Color.black, cornerRadius: CGFloat? = nil) -> some View { let defaultCornerRadius: CGFloat = 8 diff --git a/Sources/ShopifyCheckoutSheetKit/CheckoutViewController.swift b/Sources/ShopifyCheckoutSheetKit/CheckoutViewController.swift index e44e503f..aa6e7220 100644 --- a/Sources/ShopifyCheckoutSheetKit/CheckoutViewController.swift +++ b/Sources/ShopifyCheckoutSheetKit/CheckoutViewController.swift @@ -24,6 +24,32 @@ import SwiftUI import UIKit +/// Event handlers for checkout lifecycle events +public struct EventHandlers { + public var checkoutDidComplete: ((CheckoutCompletedEvent) -> Void)? + public var checkoutDidFail: ((CheckoutError) -> Void)? + public var checkoutDidCancel: (() -> Void)? + public var shouldRecoverFromError: ((CheckoutError) -> Bool)? + public var checkoutDidClickLink: ((URL) -> Void)? + public var checkoutDidEmitWebPixelEvent: ((PixelEvent) -> Void)? + + public init( + checkoutDidComplete: ((CheckoutCompletedEvent) -> Void)? = nil, + checkoutDidFail: ((CheckoutError) -> Void)? = nil, + checkoutDidCancel: (() -> Void)? = nil, + shouldRecoverFromError: ((CheckoutError) -> Bool)? = nil, + checkoutDidClickLink: ((URL) -> Void)? = nil, + checkoutDidEmitWebPixelEvent: ((PixelEvent) -> Void)? = nil + ) { + self.checkoutDidComplete = checkoutDidComplete + self.checkoutDidFail = checkoutDidFail + self.checkoutDidCancel = checkoutDidCancel + self.shouldRecoverFromError = shouldRecoverFromError + self.checkoutDidClickLink = checkoutDidClickLink + self.checkoutDidEmitWebPixelEvent = checkoutDidEmitWebPixelEvent + } +} + public class CheckoutViewController: UINavigationController { public init(checkout url: URL, delegate: CheckoutDelegate? = nil) { let rootViewController = CheckoutWebViewController(checkoutURL: url, delegate: delegate) @@ -32,6 +58,13 @@ public class CheckoutViewController: UINavigationController { presentationController?.delegate = rootViewController } + init(checkout url: URL, eventHandlers: EventHandlers) { + let rootViewController = CheckoutWebViewController(checkoutURL: url, eventHandlers: eventHandlers) + rootViewController.notifyPresented() + super.init(rootViewController: rootViewController) + presentationController?.delegate = rootViewController + } + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -63,7 +96,7 @@ public struct CheckoutSheet: UIViewControllerRepresentable, CheckoutConfigurable public typealias UIViewControllerType = CheckoutViewController var checkoutURL: URL - var delegate = CheckoutDelegateWrapper() + var eventHandlers = EventHandlers() public init(checkout url: URL) { checkoutURL = url @@ -74,7 +107,7 @@ public struct CheckoutSheet: UIViewControllerRepresentable, CheckoutConfigurable } public func makeUIViewController(context _: Self.Context) -> CheckoutViewController { - return CheckoutViewController(checkout: checkoutURL, delegate: delegate) + return CheckoutViewController(checkout: checkoutURL, eventHandlers: eventHandlers) } public func updateUIViewController(_ uiViewController: CheckoutViewController, context _: Self.Context) { @@ -84,68 +117,79 @@ public struct CheckoutSheet: UIViewControllerRepresentable, CheckoutConfigurable .compactMap({ $0 as? CheckoutWebViewController }) .first else { + OSLogger.shared.debug( + "[CheckoutViewController#updateUIViewController]: No ViewControllers matching CheckoutWebViewController \(uiViewController.viewControllers.map { String(describing: $0.self) }.joined(separator: ""))" + ) return } - OSLogger.shared.debug( - "[CheckoutViewController#updateUIViewController]: No ViewControllers matching CheckoutWebViewController \(uiViewController.viewControllers.map { String(describing: $0.self) }.joined(separator: ""))" - ) - webViewController.delegate = delegate + webViewController.updateEventHandlers(eventHandlers) } /// Lifecycle methods @discardableResult public func onCancel(_ action: @escaping () -> Void) -> Self { - delegate.onCancel = action - return self + var newView = self + newView.eventHandlers.checkoutDidCancel = action + return newView } @discardableResult public func onComplete(_ action: @escaping (CheckoutCompletedEvent) -> Void) -> Self { - delegate.onComplete = action - return self + var newView = self + newView.eventHandlers.checkoutDidComplete = action + return newView } @discardableResult public func onFail(_ action: @escaping (CheckoutError) -> Void) -> Self { - delegate.onFail = action - return self + var newView = self + newView.eventHandlers.checkoutDidFail = action + return newView } @discardableResult public func onPixelEvent(_ action: @escaping (PixelEvent) -> Void) -> Self { - delegate.onPixelEvent = action - return self + var newView = self + newView.eventHandlers.checkoutDidEmitWebPixelEvent = action + return newView } @discardableResult public func onLinkClick(_ action: @escaping (URL) -> Void) -> Self { - delegate.onLinkClick = action - return self + var newView = self + newView.eventHandlers.checkoutDidClickLink = action + return newView + } + + @discardableResult public func onShouldRecoverFromError(_ action: @escaping (CheckoutError) -> Bool) -> Self { + var newView = self + newView.eventHandlers.shouldRecoverFromError = action + return newView } } public class CheckoutDelegateWrapper: CheckoutDelegate { - var onComplete: ((CheckoutCompletedEvent) -> Void)? - var onCancel: (() -> Void)? - var onFail: ((CheckoutError) -> Void)? - var onPixelEvent: ((PixelEvent) -> Void)? - var onLinkClick: ((URL) -> Void)? + var eventHandlers: EventHandlers + + init(eventHandlers: EventHandlers = EventHandlers()) { + self.eventHandlers = eventHandlers + } public func checkoutDidFail(error: CheckoutError) { - onFail?(error) + eventHandlers.checkoutDidFail?(error) } public func checkoutDidEmitWebPixelEvent(event: PixelEvent) { - onPixelEvent?(event) + eventHandlers.checkoutDidEmitWebPixelEvent?(event) } public func checkoutDidComplete(event: CheckoutCompletedEvent) { - onComplete?(event) + eventHandlers.checkoutDidComplete?(event) } public func checkoutDidCancel() { - onCancel?() + eventHandlers.checkoutDidCancel?() } public func checkoutDidClickLink(url: URL) { - if let onLinkClick { + if let onLinkClick = eventHandlers.checkoutDidClickLink { onLinkClick(url) return } @@ -155,6 +199,10 @@ public class CheckoutDelegateWrapper: CheckoutDelegate { UIApplication.shared.open(url) } } + + public func shouldRecoverFromError(error: CheckoutError) -> Bool { + return eventHandlers.shouldRecoverFromError?(error) ?? error.isRecoverable + } } public protocol CheckoutConfigurable { diff --git a/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift b/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift index 1d5f73de..ddc0eaaa 100644 --- a/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift +++ b/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift @@ -26,6 +26,7 @@ import WebKit class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControllerDelegate { weak var delegate: CheckoutDelegate? + private var delegateWrapper: CheckoutDelegateWrapper? var checkoutView: CheckoutWebView @@ -81,6 +82,29 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl view.backgroundColor = ShopifyCheckoutSheetKit.configuration.backgroundColor } + init(checkoutURL url: URL, eventHandlers: EventHandlers) { + checkoutURL = url + + let wrapper = CheckoutDelegateWrapper(eventHandlers: eventHandlers) + self.delegateWrapper = wrapper + self.delegate = wrapper + + let checkoutView = CheckoutWebView.for(checkout: url) + checkoutView.translatesAutoresizingMaskIntoConstraints = false + checkoutView.scrollView.contentInsetAdjustmentBehavior = .never + self.checkoutView = checkoutView + + super.init(nibName: nil, bundle: nil) + + title = ShopifyCheckoutSheetKit.configuration.title + + navigationItem.rightBarButtonItem = closeBarButtonItem + + checkoutView.viewDelegate = self + + view.backgroundColor = ShopifyCheckoutSheetKit.configuration.backgroundColor + } + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -90,6 +114,17 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl progressObserver?.invalidate() } + func updateEventHandlers(_ eventHandlers: EventHandlers) { + if let wrapper = delegateWrapper { + wrapper.eventHandlers = eventHandlers + } else { + // Create wrapper on-demand for mixed init path + let wrapper = CheckoutDelegateWrapper(eventHandlers: eventHandlers) + self.delegateWrapper = wrapper + self.delegate = wrapper + } + } + // MARK: UIViewController Lifecycle override public func viewWillAppear(_ animated: Bool) { diff --git a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayCallbackTests.swift b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayCallbackTests.swift index 17c57311..44600d64 100644 --- a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayCallbackTests.swift +++ b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayCallbackTests.swift @@ -23,7 +23,7 @@ import PassKit @testable import ShopifyAcceleratedCheckouts -import ShopifyCheckoutSheetKit +@testable import ShopifyCheckoutSheetKit import XCTest @available(iOS 17.0, *) @@ -95,14 +95,15 @@ final class ApplePayCallbackTests: XCTestCase { let callbackInvokedExpectation = expectation(description: "Callback invoked") await MainActor.run { - viewController.onComplete = { [weak self] in + viewController.onCheckoutComplete = { [weak self] _ in callbackInvokedExpectation.fulfill() self?.successExpectation.fulfill() } } await MainActor.run { - viewController.onComplete?() + let mockEvent = createEmptyCheckoutCompletedEvent(id: "test-order-123") + viewController.onCheckoutComplete?(mockEvent) } await fulfillment(of: [successExpectation, callbackInvokedExpectation], timeout: 1.0) @@ -110,11 +111,12 @@ final class ApplePayCallbackTests: XCTestCase { func testSuccessCallbackNotInvokedWhenNil() async { await MainActor.run { - XCTAssertNil(viewController.onComplete) + XCTAssertNil(viewController.onCheckoutComplete) } await MainActor.run { - viewController.onComplete?() // Should not crash + let mockEvent = createEmptyCheckoutCompletedEvent(id: "test-order-123") + viewController.onCheckoutComplete?(mockEvent) // Should not crash } // Wait a moment to ensure no crash occurs @@ -129,14 +131,15 @@ final class ApplePayCallbackTests: XCTestCase { let callbackInvokedExpectation = expectation(description: "Error callback invoked") await MainActor.run { - viewController.onFail = { [weak self] in + viewController.onCheckoutFail = { [weak self] _ in callbackInvokedExpectation.fulfill() self?.errorExpectation.fulfill() } } await MainActor.run { - viewController.onFail?() + let mockError = CheckoutError.sdkError(underlying: NSError(domain: "TestError", code: 0, userInfo: nil), recoverable: false) + viewController.onCheckoutFail?(mockError) } await fulfillment(of: [errorExpectation, callbackInvokedExpectation], timeout: 1.0) @@ -144,11 +147,12 @@ final class ApplePayCallbackTests: XCTestCase { func testErrorCallbackNotInvokedWhenNil() async { await MainActor.run { - XCTAssertNil(viewController.onFail) + XCTAssertNil(viewController.onCheckoutFail) } await MainActor.run { - viewController.onFail?() // Should not crash + let mockError = CheckoutError.sdkError(underlying: NSError(domain: "TestError", code: 0, userInfo: nil), recoverable: false) + viewController.onCheckoutFail?(mockError) // Should not crash } try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds @@ -162,14 +166,14 @@ final class ApplePayCallbackTests: XCTestCase { let callbackInvokedExpectation = expectation(description: "Cancel callback invoked") await MainActor.run { - viewController.onCancel = { [weak self] in + viewController.onCheckoutCancel = { [weak self] in callbackInvokedExpectation.fulfill() self?.cancelExpectation.fulfill() } } await MainActor.run { - viewController.onCancel?() + viewController.onCheckoutCancel?() } await fulfillment(of: [cancelExpectation, callbackInvokedExpectation], timeout: 1.0) @@ -177,12 +181,12 @@ final class ApplePayCallbackTests: XCTestCase { func testCancelCallbackNotInvokedWhenNil() async { let isNil = await MainActor.run { - viewController.onCancel == nil + viewController.onCheckoutCancel == nil } XCTAssertTrue(isNil, "onCancel should be nil") await MainActor.run { - viewController.onCancel?() // Should not crash + viewController.onCheckoutCancel?() // Should not crash } try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds @@ -197,17 +201,17 @@ final class ApplePayCallbackTests: XCTestCase { var errorInvoked = false var cancelInvoked = false - viewController.onComplete = { + viewController.onCheckoutComplete = { _ in successInvoked = true } - viewController.onFail = { + viewController.onCheckoutFail = { _ in errorInvoked = true } - viewController.onCancel = { + viewController.onCheckoutCancel = { cancelInvoked = true } - viewController.onCancel?() + viewController.onCheckoutCancel?() try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds XCTAssertFalse(successInvoked, "Success callback should not be invoked") @@ -228,19 +232,19 @@ final class ApplePayCallbackTests: XCTestCase { var errorIndex = 0 var cancelIndex = 0 - viewController.onComplete = { + viewController.onCheckoutComplete = { _ in if successIndex < successExpectations.count { successExpectations[successIndex].fulfill() successIndex += 1 } } - viewController.onFail = { + viewController.onCheckoutFail = { _ in if errorIndex < errorExpectations.count { errorExpectations[errorIndex].fulfill() errorIndex += 1 } } - viewController.onCancel = { + viewController.onCheckoutCancel = { if cancelIndex < cancelExpectations.count { cancelExpectations[cancelIndex].fulfill() cancelIndex += 1 @@ -249,11 +253,13 @@ final class ApplePayCallbackTests: XCTestCase { for i in 0 ..< iterations { if i % 3 == 0 { - viewController.onComplete?() + let mockEvent = createEmptyCheckoutCompletedEvent(id: "test-order-123") + viewController.onCheckoutComplete?(mockEvent) } else if i % 3 == 1 { - viewController.onFail?() + let mockError = CheckoutError.sdkError(underlying: NSError(domain: "TestError", code: 0, userInfo: nil), recoverable: false) + viewController.onCheckoutFail?(mockError) } else { - viewController.onCancel?() + viewController.onCheckoutCancel?() } // Give time for callback to execute @@ -273,18 +279,19 @@ final class ApplePayCallbackTests: XCTestCase { await MainActor.run { // First assignment - viewController.onComplete = { + viewController.onCheckoutComplete = { _ in firstCallbackExpectation.fulfill() } // Second assignment (should replace first) - viewController.onComplete = { + viewController.onCheckoutComplete = { _ in secondCallbackExpectation.fulfill() } } await MainActor.run { - viewController.onComplete?() + let mockEvent = createEmptyCheckoutCompletedEvent(id: "test-order-123") + viewController.onCheckoutComplete?(mockEvent) } await fulfillment(of: [secondCallbackExpectation], timeout: 1.0) @@ -298,18 +305,18 @@ final class ApplePayCallbackTests: XCTestCase { await MainActor.run { // First assignment - viewController.onCancel = { + viewController.onCheckoutCancel = { firstCallbackExpectation.fulfill() } // Second assignment (should replace first) - viewController.onCancel = { + viewController.onCheckoutCancel = { secondCallbackExpectation.fulfill() } } await MainActor.run { - viewController.onCancel?() + viewController.onCheckoutCancel?() } await fulfillment(of: [secondCallbackExpectation], timeout: 1.0) @@ -365,7 +372,7 @@ final class ApplePayCallbackTests: XCTestCase { let testURL = URL(string: "https://test-shop.myshopify.com/products/test")! var capturedURL: URL? - viewController.onClickLink = { url in + viewController.onCheckoutClickLink = { url in capturedURL = url expectation.fulfill() } @@ -378,7 +385,7 @@ final class ApplePayCallbackTests: XCTestCase { func testCheckoutDidClickLinkCallbackNotInvokedWhenNil() async { await MainActor.run { - XCTAssertNil(viewController.onClickLink) + XCTAssertNil(viewController.onCheckoutClickLink) } let testURL = URL(string: "https://test-shop.myshopify.com")! @@ -406,7 +413,7 @@ final class ApplePayCallbackTests: XCTestCase { var capturedURLs: [URL] = [] var currentIndex = 0 - viewController.onClickLink = { url in + viewController.onCheckoutClickLink = { url in capturedURLs.append(url) if currentIndex < expectations.count { expectations[currentIndex].fulfill() diff --git a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayIntegrationTests.swift b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayIntegrationTests.swift index 842a7fb4..1fc44bfa 100644 --- a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayIntegrationTests.swift +++ b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayIntegrationTests.swift @@ -83,7 +83,7 @@ final class ApplePayIntegrationTests: XCTestCase { // Create a hosting controller to test SwiftUI integration let view = AcceleratedCheckoutButtons(cartID: "gid://Shopify/Cart/test-cart") .withWallets([.applepay]) - .onComplete { + .onComplete { _ in // Callback exists but won't be called during view creation } .environment(mockCommonConfiguration) @@ -113,10 +113,10 @@ final class ApplePayIntegrationTests: XCTestCase { // Create a hosting controller to test SwiftUI integration with all callbacks let view = AcceleratedCheckoutButtons(cartID: "gid://Shopify/Cart/test-cart") .withWallets([.applepay]) - .onComplete { + .onComplete { _ in completeExpectation.fulfill() } - .onFail { + .onFail { _ in failExpectation.fulfill() } .onCancel { @@ -159,7 +159,7 @@ final class ApplePayIntegrationTests: XCTestCase { func testCallbackPersistenceAcrossViewUpdates() async { var successCount = 0 - let successHandler = { + let successHandler = { (_: CheckoutCompletedEvent) in successCount += 1 } @@ -192,12 +192,15 @@ final class ApplePayIntegrationTests: XCTestCase { configuration: mockConfiguration ) - viewController.onCancel = { + viewController.onCheckoutCancel = { cancelCallbackInvoked = true } viewController.checkoutDidCancel() + // Wait for the async callback to complete + try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + XCTAssertTrue(cancelCallbackInvoked, "Cancel callback should be invoked when checkoutDidCancel is called") } @@ -213,7 +216,7 @@ final class ApplePayIntegrationTests: XCTestCase { configuration: mockConfiguration ) - viewController.onClickLink = { url in + viewController.onCheckoutClickLink = { url in callbackInvoked = true receivedURL = url } @@ -221,6 +224,9 @@ final class ApplePayIntegrationTests: XCTestCase { let testURL = URL(string: "https://help.shopify.com/payment-terms")! viewController.checkoutDidClickLink(url: testURL) + // Wait for the async callback to complete + try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + XCTAssertTrue(callbackInvoked, "checkoutDidClickLink callback should be invoked") XCTAssertEqual(receivedURL, testURL, "URL should be passed to callback") } @@ -234,10 +240,10 @@ final class ApplePayIntegrationTests: XCTestCase { var callbackSet = false - viewController.onWebPixelEvent = { _ in + viewController.onCheckoutWebPixelEvent = { _ in callbackSet = true } - XCTAssertNotNil(viewController.onWebPixelEvent, "Web pixel event callback should be set") + XCTAssertNotNil(viewController.onCheckoutWebPixelEvent, "Web pixel event callback should be set") } } diff --git a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewControllerTests.swift b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewControllerTests.swift index 2e8d5780..bb1937b9 100644 --- a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewControllerTests.swift +++ b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewControllerTests.swift @@ -82,19 +82,19 @@ class ApplePayViewControllerTests: XCTestCase { func testOnCheckoutSuccessCallback_defaultsToNil() async { await MainActor.run { - XCTAssertNil(viewController.onComplete) + XCTAssertNil(viewController.onCheckoutComplete) } } func testOnCheckoutErrorCallback_defaultsToNil() async { await MainActor.run { - XCTAssertNil(viewController.onFail) + XCTAssertNil(viewController.onCheckoutFail) } } func testOnCheckoutCancelCallback_defaultsToNil() async { await MainActor.run { - XCTAssertNil(viewController.onCancel) + XCTAssertNil(viewController.onCheckoutCancel) } } @@ -105,7 +105,7 @@ class ApplePayViewControllerTests: XCTestCase { var cancelCallbackInvoked = false let expectation = XCTestExpectation(description: "Cancel callback should be invoked") - viewController.onCancel = { + viewController.onCheckoutCancel = { cancelCallbackInvoked = true expectation.fulfill() } @@ -126,7 +126,7 @@ class ApplePayViewControllerTests: XCTestCase { func testCheckoutDidCancel_worksWithoutOnCancelCallback() async { let isNil = await MainActor.run { - viewController.onCancel == nil + viewController.onCheckoutCancel == nil } XCTAssertTrue(isNil, "onCancel should be nil") diff --git a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewModifierTests.swift b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewModifierTests.swift index c6157659..6eb12730 100644 --- a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewModifierTests.swift +++ b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewModifierTests.swift @@ -22,6 +22,7 @@ */ @testable import ShopifyAcceleratedCheckouts +@testable import ShopifyCheckoutSheetKit import SwiftUI import XCTest @@ -70,7 +71,7 @@ final class ApplePayViewModifierTests: XCTestCase { func testOnSuccessModifier() { var successCallbackInvoked = false - let successAction = { + let successAction = { (_: CheckoutCompletedEvent) in successCallbackInvoked = true } @@ -82,7 +83,7 @@ final class ApplePayViewModifierTests: XCTestCase { XCTAssertNotNil(view, "View should be created successfully with success modifier") - successAction() + successAction(createEmptyCheckoutCompletedEvent()) XCTAssertTrue(successCallbackInvoked, "Success callback should be invoked when called") } @@ -90,10 +91,10 @@ final class ApplePayViewModifierTests: XCTestCase { var firstCallbackInvoked = false var secondCallbackInvoked = false - let firstAction = { + let firstAction = { (_: CheckoutCompletedEvent) in firstCallbackInvoked = true } - let secondAction = { + let secondAction = { (_: CheckoutCompletedEvent) in secondCallbackInvoked = true } @@ -105,7 +106,7 @@ final class ApplePayViewModifierTests: XCTestCase { .environment(mockShopSettings) // The second handler should replace the first - secondAction() + secondAction(createEmptyCheckoutCompletedEvent()) XCTAssertFalse(firstCallbackInvoked, "First callback should not be invoked") XCTAssertTrue(secondCallbackInvoked, "Second callback should be invoked") } @@ -134,22 +135,22 @@ final class ApplePayViewModifierTests: XCTestCase { var firstCallbackInvoked = false var secondCallbackInvoked = false - let firstAction = { + let firstAction = { (_: CheckoutCompletedEvent) in firstCallbackInvoked = true } - let secondAction = { + let secondAction = { (_: CheckoutCompletedEvent) in secondCallbackInvoked = true } let view = AcceleratedCheckoutButtons(cartID: "gid://Shopify/Cart/test-cart-id") - .onCancel(firstAction) - .onCancel(secondAction) // Should replace the first + .onCancel { firstCallbackInvoked = true } + .onCancel { secondCallbackInvoked = true } // Should replace the first .environment(mockConfiguration) .environment(mockApplePayConfiguration) .environment(mockShopSettings) // The second handler should replace the first - secondAction() + secondAction(createEmptyCheckoutCompletedEvent()) XCTAssertFalse(firstCallbackInvoked, "First callback should not be invoked") XCTAssertTrue(secondCallbackInvoked, "Second callback should be invoked") } @@ -158,7 +159,7 @@ final class ApplePayViewModifierTests: XCTestCase { func testOnErrorModifier() { var errorCallbackInvoked = false - let errorAction = { + let errorAction = { (_: CheckoutError) in errorCallbackInvoked = true } @@ -170,7 +171,7 @@ final class ApplePayViewModifierTests: XCTestCase { XCTAssertNotNil(view, "View should be created successfully with error modifier") - errorAction() + errorAction(CheckoutError.sdkError(underlying: NSError(domain: "Test", code: 0))) XCTAssertTrue(errorCallbackInvoked, "Error callback should be invoked when called") } @@ -180,10 +181,10 @@ final class ApplePayViewModifierTests: XCTestCase { var successInvoked = false var errorInvoked = false - let successAction = { + let successAction = { (_: CheckoutCompletedEvent) in successInvoked = true } - let errorAction = { + let errorAction = { (_: CheckoutError) in errorInvoked = true } @@ -196,11 +197,11 @@ final class ApplePayViewModifierTests: XCTestCase { XCTAssertNotNil(view, "View should be created successfully with both modifiers") - successAction() + successAction(createEmptyCheckoutCompletedEvent()) XCTAssertTrue(successInvoked, "Success callback should be invoked") XCTAssertFalse(errorInvoked, "Error callback should not be invoked") - errorAction() + errorAction(CheckoutError.sdkError(underlying: NSError(domain: "Test", code: 0))) XCTAssertTrue(errorInvoked, "Error callback should be invoked") } @@ -212,7 +213,7 @@ final class ApplePayViewModifierTests: XCTestCase { // Create a custom container view struct TestContainer: View { - let childSuccessAction: () -> Void + let childSuccessAction: (CheckoutCompletedEvent) -> Void var body: some View { VStack { @@ -222,7 +223,7 @@ final class ApplePayViewModifierTests: XCTestCase { } } - let containerView = TestContainer(childSuccessAction: { childSuccessInvoked = true }) + let containerView = TestContainer(childSuccessAction: { _ in childSuccessInvoked = true }) XCTAssertNotNil(containerView, "Container view should be created successfully") } @@ -243,8 +244,8 @@ final class ApplePayViewModifierTests: XCTestCase { var errorInvoked = false var cancelInvoked = false - let successAction = { successInvoked = true } - let errorAction = { errorInvoked = true } + let successAction = { (_: CheckoutCompletedEvent) in successInvoked = true } + let errorAction = { (_: CheckoutError) in errorInvoked = true } let cancelAction = { cancelInvoked = true } let view = AcceleratedCheckoutButtons(cartID: "gid://Shopify/Cart/test-cart-id") @@ -257,14 +258,14 @@ final class ApplePayViewModifierTests: XCTestCase { XCTAssertNotNil(view, "View should be created successfully with all modifiers") - successAction() + successAction(createEmptyCheckoutCompletedEvent()) XCTAssertTrue(successInvoked, "Success callback should be invoked") XCTAssertFalse(errorInvoked, "Error callback should not be invoked") XCTAssertFalse(cancelInvoked, "Cancel callback should not be invoked") // Reset successInvoked = false - errorAction() + errorAction(CheckoutError.sdkError(underlying: NSError(domain: "Test", code: 0))) XCTAssertFalse(successInvoked, "Success callback should not be invoked") XCTAssertTrue(errorInvoked, "Error callback should be invoked") XCTAssertFalse(cancelInvoked, "Cancel callback should not be invoked") @@ -286,8 +287,8 @@ final class ApplePayViewModifierTests: XCTestCase { let view = VStack { AcceleratedCheckoutButtons(cartID: "gid://Shopify/Cart/test-cart-id") - .onComplete { successCount += 1 } - .onFail { errorCount += 1 } + .onComplete { _ in successCount += 1 } + .onFail { _ in errorCount += 1 } .onAppear { viewAppeared = true } } .environment(mockConfiguration) diff --git a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ShopPay/ShopPayCallbackTests.swift b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ShopPay/ShopPayCallbackTests.swift index 00009287..43c430cd 100644 --- a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ShopPay/ShopPayCallbackTests.swift +++ b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ShopPay/ShopPayCallbackTests.swift @@ -22,7 +22,7 @@ */ @testable import ShopifyAcceleratedCheckouts -import ShopifyCheckoutSheetKit +@testable import ShopifyCheckoutSheetKit import XCTest @available(iOS 17.0, *) @@ -73,13 +73,14 @@ final class ShopPayCallbackTests: XCTestCase { await MainActor.run { viewController.eventHandlers = EventHandlers( - checkoutDidComplete: { [weak self] in + checkoutDidComplete: { [weak self] _ in callbackInvokedExpectation.fulfill() self?.successExpectation.fulfill() } ) - viewController.eventHandlers.checkoutDidComplete?() + let mockEvent = createEmptyCheckoutCompletedEvent(id: "test-order-123") + viewController.eventHandlers.checkoutDidComplete?(mockEvent) } await fulfillment(of: [successExpectation, callbackInvokedExpectation], timeout: 1.0) @@ -88,7 +89,8 @@ final class ShopPayCallbackTests: XCTestCase { func testSuccessCallbackNotInvokedWhenNil() { XCTAssertNil(viewController.eventHandlers.checkoutDidComplete) - viewController.eventHandlers.checkoutDidComplete?() // Should not crash + let mockEvent = createEmptyCheckoutCompletedEvent(id: "test-order-123") + viewController.eventHandlers.checkoutDidComplete?(mockEvent) // Should not crash XCTAssertTrue(true, "Should not crash when callback is nil") } @@ -101,13 +103,14 @@ final class ShopPayCallbackTests: XCTestCase { let callbackInvokedExpectation = expectation(description: "Error callback invoked") viewController.eventHandlers = EventHandlers( - checkoutDidFail: { [weak self] in + checkoutDidFail: { [weak self] _ in callbackInvokedExpectation.fulfill() self?.errorExpectation.fulfill() } ) - viewController.eventHandlers.checkoutDidFail?() + let mockError = CheckoutError.sdkError(underlying: NSError(domain: "TestError", code: 0, userInfo: nil), recoverable: false) + viewController.eventHandlers.checkoutDidFail?(mockError) await fulfillment(of: [errorExpectation, callbackInvokedExpectation], timeout: 1.0) } @@ -115,7 +118,8 @@ final class ShopPayCallbackTests: XCTestCase { func testErrorCallbackNotInvokedWhenNil() { XCTAssertNil(viewController.eventHandlers.checkoutDidFail) - viewController.eventHandlers.checkoutDidFail?() // Should not crash + let mockError = CheckoutError.sdkError(underlying: NSError(domain: "TestError", code: 0, userInfo: nil), recoverable: false) + viewController.eventHandlers.checkoutDidFail?(mockError) // Should not crash XCTAssertTrue(true, "Should not crash when callback is nil") } @@ -153,10 +157,11 @@ final class ShopPayCallbackTests: XCTestCase { func testCheckoutCompleteCallback() { var completeInvoked = false viewController.eventHandlers = EventHandlers( - checkoutDidComplete: { completeInvoked = true } + checkoutDidComplete: { _ in completeInvoked = true } ) - viewController.eventHandlers.checkoutDidComplete?() + let mockEvent = createEmptyCheckoutCompletedEvent(id: "test-order-123") + viewController.eventHandlers.checkoutDidComplete?(mockEvent) XCTAssertTrue(completeInvoked, "Complete callback should be invoked") } @@ -165,10 +170,11 @@ final class ShopPayCallbackTests: XCTestCase { func testCheckoutFailCallback() { var failInvoked = false viewController.eventHandlers = EventHandlers( - checkoutDidFail: { failInvoked = true } + checkoutDidFail: { _ in failInvoked = true } ) - viewController.eventHandlers.checkoutDidFail?() + let mockError = CheckoutError.sdkError(underlying: NSError(domain: "TestError", code: 0, userInfo: nil), recoverable: false) + viewController.eventHandlers.checkoutDidFail?(mockError) XCTAssertTrue(failInvoked, "Fail callback should be invoked") } diff --git a/Tests/ShopifyCheckoutSheetKitTests/SwiftUITests.swift b/Tests/ShopifyCheckoutSheetKitTests/SwiftUITests.swift index ec914203..f6ef3c0a 100644 --- a/Tests/ShopifyCheckoutSheetKitTests/SwiftUITests.swift +++ b/Tests/ShopifyCheckoutSheetKitTests/SwiftUITests.swift @@ -56,10 +56,10 @@ class CheckoutSheetTests: XCTestCase { func testOnCancel() { var cancelActionCalled = false - checkoutSheet.onCancel { + checkoutSheet = checkoutSheet.onCancel { cancelActionCalled = true } - checkoutSheet.delegate.checkoutDidCancel() + checkoutSheet.eventHandlers.checkoutDidCancel?() XCTAssertTrue(cancelActionCalled) } @@ -68,11 +68,11 @@ class CheckoutSheetTests: XCTestCase { var actionData: CheckoutCompletedEvent? let event = createEmptyCheckoutCompletedEvent() - checkoutSheet.onComplete { event in + checkoutSheet = checkoutSheet.onComplete { event in actionCalled = true actionData = event } - checkoutSheet.delegate.checkoutDidComplete(event: event) + checkoutSheet.eventHandlers.checkoutDidComplete?(event) XCTAssertTrue(actionCalled) XCTAssertNotNil(actionData) } @@ -82,12 +82,12 @@ class CheckoutSheetTests: XCTestCase { var actionData: CheckoutError? let error: CheckoutError = .checkoutUnavailable(message: "error", code: CheckoutUnavailable.httpError(statusCode: 500), recoverable: false) - checkoutSheet.onFail { failure in + checkoutSheet = checkoutSheet.onFail { failure in actionCalled = true actionData = failure } - checkoutSheet.delegate.checkoutDidFail(error: error) + checkoutSheet.eventHandlers.checkoutDidFail?(error) XCTAssertTrue(actionCalled) XCTAssertNotNil(actionData) } @@ -98,11 +98,11 @@ class CheckoutSheetTests: XCTestCase { let standardEvent = StandardEvent(context: nil, id: "testId", name: "checkout_started", timestamp: "2022-01-01T00:00:00Z", data: nil) let pixelEvent = PixelEvent.standardEvent(standardEvent) - checkoutSheet.onPixelEvent { event in + checkoutSheet = checkoutSheet.onPixelEvent { event in actionCalled = true actionData = event } - checkoutSheet.delegate.checkoutDidEmitWebPixelEvent(event: pixelEvent) + checkoutSheet.eventHandlers.checkoutDidEmitWebPixelEvent?(pixelEvent) XCTAssertTrue(actionCalled) XCTAssertNotNil(actionData) } @@ -111,14 +111,54 @@ class CheckoutSheetTests: XCTestCase { var actionCalled = false var actionData: URL? - checkoutSheet.onLinkClick { url in + checkoutSheet = checkoutSheet.onLinkClick { url in actionCalled = true actionData = url } - checkoutSheet.delegate.checkoutDidClickLink(url: URL(string: "https://shopify.com")!) + checkoutSheet.eventHandlers.checkoutDidClickLink?(URL(string: "https://shopify.com")!) XCTAssertTrue(actionCalled) XCTAssertNotNil(actionData) } + + func testOnShouldRecoverFromError() { + var actionCalled = false + let recoverableError: CheckoutError = .checkoutUnavailable(message: "error", code: CheckoutUnavailable.httpError(statusCode: 500), recoverable: true) + + checkoutSheet = checkoutSheet.onShouldRecoverFromError { error in + actionCalled = true + return error.isRecoverable + } + + let shouldRecover = checkoutSheet.eventHandlers.shouldRecoverFromError?(recoverableError) ?? false + XCTAssertTrue(actionCalled) + XCTAssertTrue(shouldRecover) + } + + func testMixedInitializationPath() { + // Create CheckoutWebViewController with delegate API + let webViewController = CheckoutWebViewController(checkoutURL: checkoutURL, delegate: nil) + + // Simulate SwiftUI updating with EventHandlers + var eventHandlerCalled = false + let eventHandlers = EventHandlers( + checkoutDidComplete: { _ in + eventHandlerCalled = true + } + ) + + // Update with new event handlers (simulating SwiftUI update) + webViewController.updateEventHandlers(eventHandlers) + + // Verify the wrapper was created and delegate set + XCTAssertNotNil(webViewController.delegate) + + // Trigger the event through the delegate + let event = createEmptyCheckoutCompletedEvent() + webViewController.delegate?.checkoutDidComplete(event: event) + + // Verify event handler was called + XCTAssertTrue(eventHandlerCalled) + } } class CheckoutConfigurableTests: XCTestCase {