Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ struct CartView: View {
.onFail { error in
print("[AcceleratedCheckout] Failed: \(error)")
}
.onCancel {
print("[AcceleratedCheckout] Cancelled")
.onDismiss {
print("[AcceleratedCheckout] Dismissed")
}
.connect(client)
.environment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ struct ProductView: View {
.onFail { error in
print("[AcceleratedCheckout] Failed: \(error)")
}
.onCancel {
print("[AcceleratedCheckout] Cancelled")
.onDismiss {
print("[AcceleratedCheckout] Dismissed")
}
.environment(
\.shopifyAcceleratedCheckoutsConfiguration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ struct ButtonSet: View {
.onFail { error in
print("❌ Checkout failed: \(error)")
}
.onCancel {
print("🚫 Checkout cancelled")
.onDismiss {
print("🚫 Checkout dismissed")
}
.onRenderStateChange {
cartRenderState = $0
Expand All @@ -50,8 +50,8 @@ struct ButtonSet: View {
.onFail { error in
print("❌ Variant checkout failed: \(error)")
}
.onCancel {
print("🚫 Variant checkout cancelled")
.onDismiss {
print("🚫 Variant checkout dismissed")
}
.onRenderStateChange {
variantRenderState = $0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,23 +182,23 @@ extension AcceleratedCheckoutButtons {
return newView
}

/// Adds an action to perform when the checkout is cancelled by the user.
/// Adds an action to perform when the buyer dismisses the checkout experience.
///
/// Use this modifier to handle checkout cancellation:
/// Use this modifier to handle checkout dismissal:
///
/// ```swift
/// AcceleratedCheckoutButtons(cartID: cartId)
/// .onCancel {
/// // Reset checkout state
/// .onDismiss {
/// // Reset checkout presentation state
/// resetCheckoutState()
/// }
/// ```
///
/// - Parameter action: The action to perform when checkout is cancelled
/// - Returns: A view with the checkout cancel handler set
public func onCancel(_ action: @escaping () -> Void) -> AcceleratedCheckoutButtons {
/// - Parameter action: The action to perform when the buyer dismisses checkout
/// - Returns: A view with the checkout dismissal handler set
public func onDismiss(_ action: @escaping () -> Void) -> AcceleratedCheckoutButtons {
var newView = self
newView.eventHandlers.checkoutDidCancel = action
newView.eventHandlers.checkoutDidDismiss = action
return newView
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ struct Internal_ApplePayButton: View {
self.buttonStyle = buttonStyle
self.cornerRadius = cornerRadius
controller.onCheckoutFail = eventHandlers.checkoutDidFail
controller.onCheckoutCancel = eventHandlers.checkoutDidCancel
controller.onCheckoutDismiss = eventHandlers.checkoutDidDismiss
}

var body: some View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,18 @@ class ApplePayViewController: WalletController, PayController {
@MainActor
public var onCheckoutFail: ((CheckoutError) -> Void)?

/// Callback invoked when the checkout process is cancelled by the user.
/// Callback invoked when the buyer dismisses the checkout experience.
/// This closure is called on the main thread when the user dismisses the checkout.
///
/// Example usage:
/// ```swift
/// applePayViewController.onCheckoutCancel = { [weak self] in
/// applePayViewController.onCheckoutDismiss = { [weak self] in
/// self?.resetCheckoutState()
/// self?.logAnalyticsEvent(.checkoutCancelled)
/// self?.logAnalyticsEvent(.checkoutDismissed)
/// }
/// ```
@MainActor
public var onCheckoutCancel: (() -> Void)?
public var onCheckoutDismiss: (() -> Void)?

/// Initialization workaround for passing self to ApplePayAuthorizationDelegate
private var __authorizationDelegate: ApplePayAuthorizationDelegate!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ public enum Wallet: String {
/// Event handlers for wallet buttons
public struct EventHandlers {
public var checkoutDidFail: ((CheckoutError) -> Void)?
public var checkoutDidCancel: (() -> Void)?
public var checkoutDidDismiss: (() -> Void)?
public var renderStateDidChange: ((RenderState) -> Void)?

public init(
checkoutDidFail: ((CheckoutError) -> Void)? = nil,
checkoutDidCancel: (() -> Void)? = nil,
checkoutDidDismiss: (() -> Void)? = nil,
renderStateDidChange: ((RenderState) -> Void)? = nil
) {
self.checkoutDidFail = checkoutDidFail
self.checkoutDidCancel = checkoutDidCancel
self.checkoutDidDismiss = checkoutDidDismiss
self.renderStateDidChange = renderStateDidChange
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ final class ApplePayCallbackTests: XCTestCase {
var mockConfiguration: ApplePayConfigurationWrapper!
var mockIdentifier: CheckoutIdentifier!
var errorExpectation: XCTestExpectation!
var cancelExpectation: XCTestExpectation!
var dismissExpectation: XCTestExpectation!

// MARK: - Setup

Expand Down Expand Up @@ -58,7 +58,7 @@ final class ApplePayCallbackTests: XCTestCase {
mockConfiguration = nil
mockIdentifier = nil
errorExpectation = nil
cancelExpectation = nil
dismissExpectation = nil
try await super.tearDown()
}

Expand Down Expand Up @@ -97,34 +97,34 @@ final class ApplePayCallbackTests: XCTestCase {
XCTAssertTrue(true, "Should not crash when callback is nil")
}

// MARK: - Cancel Callback Tests
// MARK: - Dismiss Callback Tests

func testCancelCallbackInvoked() async {
cancelExpectation = expectation(description: "Cancel callback should be invoked")
let callbackInvokedExpectation = expectation(description: "Cancel callback invoked")
func testDismissCallbackInvoked() async {
dismissExpectation = expectation(description: "Dismiss callback should be invoked")
let callbackInvokedExpectation = expectation(description: "Dismiss callback invoked")

await MainActor.run {
viewController.onCheckoutCancel = { [weak self] in
viewController.onCheckoutDismiss = { [weak self] in
callbackInvokedExpectation.fulfill()
self?.cancelExpectation.fulfill()
self?.dismissExpectation.fulfill()
}
}

await MainActor.run {
viewController.onCheckoutCancel?()
viewController.onCheckoutDismiss?()
}

await fulfillment(of: [cancelExpectation, callbackInvokedExpectation], timeout: 1.0)
await fulfillment(of: [dismissExpectation, callbackInvokedExpectation], timeout: 1.0)
}

func testCancelCallbackNotInvokedWhenNil() async {
func testDismissCallbackNotInvokedWhenNil() async {
let isNil = await MainActor.run {
viewController.onCheckoutCancel == nil
viewController.onCheckoutDismiss == nil
}
XCTAssertTrue(isNil, "onCancel should be nil")
XCTAssertTrue(isNil, "onDismiss should be nil")

await MainActor.run {
viewController.onCheckoutCancel?() // Should not crash
viewController.onCheckoutDismiss?() // Should not crash
}

try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
Expand All @@ -134,45 +134,45 @@ final class ApplePayCallbackTests: XCTestCase {
// MARK: - No Callback Tests

@MainActor
func testNoCallbackWhenCheckoutCancelled() async {
func testNoFailCallbackWhenCheckoutIsDismissed() async {
var errorInvoked = false
var cancelInvoked = false
var dismissInvoked = false

viewController.onCheckoutFail = { _ in
errorInvoked = true
}
viewController.onCheckoutCancel = {
cancelInvoked = true
viewController.onCheckoutDismiss = {
dismissInvoked = true
}

viewController.onCheckoutCancel?()
viewController.onCheckoutDismiss?()

try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
XCTAssertFalse(errorInvoked, "Error callback should not be invoked")
XCTAssertTrue(cancelInvoked, "Cancel callback should be invoked")
XCTAssertTrue(dismissInvoked, "Dismiss callback should be invoked")
}

// MARK: - Thread Safety Tests

@MainActor
func testCallbackThreadSafety() async {
let iterations = 10 // Even distribution between error and cancel
let iterations = 10 // Even distribution between error and dismissal
let errorExpectations = (0 ..< iterations / 2).map { _ in expectation(description: "Error") }
let cancelExpectations = (0 ..< iterations / 2).map { _ in expectation(description: "Cancel") }
let dismissExpectations = (0 ..< iterations / 2).map { _ in expectation(description: "Dismiss") }

var errorIndex = 0
var cancelIndex = 0
var dismissIndex = 0

viewController.onCheckoutFail = { _ in
if errorIndex < errorExpectations.count {
errorExpectations[errorIndex].fulfill()
errorIndex += 1
}
}
viewController.onCheckoutCancel = {
if cancelIndex < cancelExpectations.count {
cancelExpectations[cancelIndex].fulfill()
cancelIndex += 1
viewController.onCheckoutDismiss = {
if dismissIndex < dismissExpectations.count {
dismissExpectations[dismissIndex].fulfill()
dismissIndex += 1
}
}

Expand All @@ -181,38 +181,38 @@ final class ApplePayCallbackTests: XCTestCase {
let mockError = CheckoutError.sdkError(underlying: NSError(domain: "TestError", code: 0, userInfo: nil))
viewController.onCheckoutFail?(mockError)
} else {
viewController.onCheckoutCancel?()
viewController.onCheckoutDismiss?()
}

// Give time for callback to execute
try? await Task.sleep(nanoseconds: 50_000_000) // 0.05 seconds
}

// Wait for all expectations
await fulfillment(of: errorExpectations + cancelExpectations, timeout: 2.0)
await fulfillment(of: errorExpectations + dismissExpectations, timeout: 2.0)
}

// MARK: - Edge Case Tests

func testMultipleCancelCallbackAssignments() async {
let firstCallbackExpectation = expectation(description: "First cancel callback")
func testMultipleDismissCallbackAssignments() async {
let firstCallbackExpectation = expectation(description: "First dismiss callback")
firstCallbackExpectation.isInverted = true
let secondCallbackExpectation = expectation(description: "Second cancel callback")
let secondCallbackExpectation = expectation(description: "Second dismiss callback")

await MainActor.run {
// First assignment
viewController.onCheckoutCancel = {
viewController.onCheckoutDismiss = {
firstCallbackExpectation.fulfill()
}

// Second assignment (should replace first)
viewController.onCheckoutCancel = {
viewController.onCheckoutDismiss = {
secondCallbackExpectation.fulfill()
}
}

await MainActor.run {
viewController.onCheckoutCancel?()
viewController.onCheckoutDismiss?()
}

await fulfillment(of: [secondCallbackExpectation], timeout: 1.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,20 @@ final class ApplePayIntegrationTests: XCTestCase {
}
}

func testViewModifierWithButtonIntegrationIncludingCancel() async {
func testViewModifierWithButtonIntegrationIncludingDismiss() async {
let failExpectation = expectation(description: "Fail callback")
failExpectation.isInverted = true
let cancelExpectation = expectation(description: "Cancel callback")
cancelExpectation.isInverted = true
let dismissExpectation = expectation(description: "Dismiss callback")
dismissExpectation.isInverted = true

await MainActor.run {
let view = AcceleratedCheckoutButtons(cartID: "gid://Shopify/Cart/test-cart")
.wallets([.applePay])
.onFail { _ in
failExpectation.fulfill()
}
.onCancel {
cancelExpectation.fulfill()
.onDismiss {
dismissExpectation.fulfill()
}
.environment(\.shopifyAcceleratedCheckoutsConfiguration, mockCommonConfiguration)
.environment(\.shopifyApplePayConfiguration, mockApplePayConfiguration)
Expand All @@ -102,7 +102,7 @@ final class ApplePayIntegrationTests: XCTestCase {
XCTAssertNotNil(hostingController.rootView, "Root view should exist")
}

await fulfillment(of: [failExpectation, cancelExpectation], timeout: 0.2)
await fulfillment(of: [failExpectation, dismissExpectation], timeout: 0.2)
}

// MARK: - Edge Case Tests
Expand Down Expand Up @@ -148,22 +148,22 @@ final class ApplePayIntegrationTests: XCTestCase {
// MARK: - Delegate Tests

@MainActor
func testCheckoutDelegateCancelCallback() async {
var cancelCallbackInvoked = false
func testCheckoutDelegateDismissCallback() async {
var dismissCallbackInvoked = false

let viewController = ApplePayViewController(
identifier: .cart(cartID: "gid://Shopify/Cart/test-cart"),
configuration: mockConfiguration
)

viewController.onCheckoutCancel = {
cancelCallbackInvoked = true
viewController.onCheckoutDismiss = {
dismissCallbackInvoked = true
}

viewController.onCheckoutCancel?()
viewController.onCheckoutDismiss?()

try? await Task.sleep(nanoseconds: 100_000_000)

XCTAssertTrue(cancelCallbackInvoked, "Cancel callback should be invoked when onCheckoutCancel is called")
XCTAssertTrue(dismissCallbackInvoked, "Dismiss callback should be invoked when onCheckoutDismiss is called")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,13 @@ class ApplePayViewControllerTests: XCTestCase {
// MARK: - Delegate

@MainActor
func test_checkoutDidCancel_whenInvoked_invokesOnCancelCallback() async {
let cancelCallbackExpectation = XCTestExpectation(description: "Cancel callback should be invoked")
viewController.onCheckoutCancel = { cancelCallbackExpectation.fulfill() }
func test_checkoutDidDismiss_whenInvoked_invokesOnDismissCallback() async {
let dismissCallbackExpectation = XCTestExpectation(description: "Dismiss callback should be invoked")
viewController.onCheckoutDismiss = { dismissCallbackExpectation.fulfill() }

viewController.onCheckoutCancel?()
viewController.onCheckoutDismiss?()

await fulfillment(of: [cancelCallbackExpectation], timeout: 1.0)
await fulfillment(of: [dismissCallbackExpectation], timeout: 1.0)
}

// MARK: - WalletController Inheritance
Expand Down
Loading
Loading