Skip to content

Commit 2cc8c5b

Browse files
committed
Create AcceleratedCheckoutError interface
1 parent 22cce08 commit 2cc8c5b

File tree

9 files changed

+272
-70
lines changed

9 files changed

+272
-70
lines changed

Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Mutations.swift

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ extension StorefrontAPI {
6868
throw GraphQLError.invalidResponse
6969
}
7070

71-
let cart = try validateCart(payload.cart, requestName: "cartCreate")
72-
7371
try validateUserErrors(payload.userErrors)
7472

73+
let cart = try validateCart(payload.cart, requestName: "cartCreate")
74+
7575
return cart
7676
}
7777

@@ -132,10 +132,10 @@ extension StorefrontAPI {
132132
throw GraphQLError.invalidResponse
133133
}
134134

135-
let cart = try validateCart(payload.cart, requestName: "cartBuyerIdentityUpdate")
136-
137135
try validateUserErrors(payload.userErrors)
138136

137+
let cart = try validateCart(payload.cart, requestName: "cartBuyerIdentityUpdate")
138+
139139
return cart
140140
}
141141

@@ -171,10 +171,10 @@ extension StorefrontAPI {
171171
throw GraphQLError.invalidResponse
172172
}
173173

174-
let cart = try validateCart(payload.cart, requestName: "cartDeliveryAddressesAdd")
175-
176174
try validateUserErrors(payload.userErrors)
177175

176+
let cart = try validateCart(payload.cart, requestName: "cartDeliveryAddressesAdd")
177+
178178
return cart
179179
}
180180

@@ -213,10 +213,10 @@ extension StorefrontAPI {
213213
throw GraphQLError.invalidResponse
214214
}
215215

216-
let cart = try validateCart(payload.cart, requestName: "cartDeliveryAddressesUpdate")
217-
218216
try validateUserErrors(payload.userErrors)
219217

218+
let cart = try validateCart(payload.cart, requestName: "cartDeliveryAddressesUpdate")
219+
220220
return cart
221221
}
222222

@@ -242,10 +242,10 @@ extension StorefrontAPI {
242242
throw GraphQLError.invalidResponse
243243
}
244244

245-
let cart = try validateCart(payload.cart, requestName: "cartDeliveryAddressesRemove")
246-
247245
try validateUserErrors(payload.userErrors)
248246

247+
let cart = try validateCart(payload.cart, requestName: "cartDeliveryAddressesRemove")
248+
249249
return cart
250250
}
251251

@@ -278,10 +278,10 @@ extension StorefrontAPI {
278278
throw GraphQLError.invalidResponse
279279
}
280280

281-
let cart = try validateCart(payload.cart, requestName: "cartSelectedDeliveryOptionsUpdate")
282-
283281
try validateUserErrors(payload.userErrors)
284282

283+
let cart = try validateCart(payload.cart, requestName: "cartSelectedDeliveryOptionsUpdate")
284+
285285
return cart
286286
}
287287

@@ -349,10 +349,10 @@ extension StorefrontAPI {
349349
throw GraphQLError.invalidResponse
350350
}
351351

352-
let cart = try validateCart(payload.cart, requestName: "cartPaymentUpdate")
353-
354352
try validateUserErrors(payload.userErrors)
355353

354+
let cart = try validateCart(payload.cart, requestName: "cartPaymentUpdate")
355+
356356
return cart
357357
}
358358

@@ -380,10 +380,10 @@ extension StorefrontAPI {
380380
throw GraphQLError.invalidResponse
381381
}
382382

383-
let cart = try validateCart(payload.cart, requestName: "cartBillingAddressUpdate")
384-
385383
try validateUserErrors(payload.userErrors)
386384

385+
let cart = try validateCart(payload.cart, requestName: "cartBillingAddressUpdate")
386+
387387
return cart
388388
}
389389

@@ -403,9 +403,9 @@ extension StorefrontAPI {
403403
throw GraphQLError.invalidResponse
404404
}
405405

406-
let cart = try validateCart(payload.cart, requestName: "cartRemovePersonalData")
407-
408406
try validateUserErrors(payload.userErrors)
407+
408+
let cart = try validateCart(payload.cart, requestName: "cartRemovePersonalData")
409409
}
410410

411411
/// Prepare cart for completion
@@ -430,8 +430,8 @@ extension StorefrontAPI {
430430

431431
switch result {
432432
case let .ready(ready):
433-
let cart = try validateCart(ready.cart, requestName: "cartPrepareForCompletion")
434433
try validateUserErrors(payload.userErrors)
434+
let cart = try validateCart(ready.cart, requestName: "cartPrepareForCompletion")
435435
return ready
436436
case let .throttled(throttled):
437437
throw GraphQLError.networkError(
@@ -489,8 +489,8 @@ extension StorefrontAPI {
489489
extension StorefrontAPI {
490490
private func validateUserErrors(_ userErrors: [CartUserError]) throws {
491491
guard userErrors.isEmpty else {
492-
// Always throw the actual CartUserError so the error handler can properly map it
493-
throw userErrors.first!
492+
// Throw a validation error that contains all user errors for comprehensive debugging
493+
throw CartValidationError(userErrors: userErrors)
494494
}
495495
}
496496

Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,23 @@ extension StorefrontAPI {
418418
let field: [String]?
419419
}
420420

421+
/// Cart validation error that contains all user errors from a GraphQL response
422+
struct CartValidationError: Error, CustomStringConvertible {
423+
let userErrors: [CartUserError]
424+
425+
var description: String {
426+
if userErrors.count == 1 {
427+
return userErrors[0].message
428+
} else {
429+
let errorMessages = userErrors.map { error in
430+
let fieldInfo = error.field?.isEmpty == false ? " (field: \(error.field!.joined(separator: ".")))" : ""
431+
return error.message + fieldInfo
432+
}
433+
return "\(userErrors.count) validation errors: " + errorMessages.joined(separator: "; ")
434+
}
435+
}
436+
}
437+
421438
/// Cart error codes
422439
enum CartErrorCode: String, Codable {
423440
case addressFieldContainsEmojis = "ADDRESS_FIELD_CONTAINS_EMOJIS"

Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,23 @@ extension AcceleratedCheckoutButtons {
199199
/// }
200200
/// ```
201201
///
202-
/// - Parameter action: The action to perform when checkout fails
203-
/// - Returns: A view with the checkout error handler set
204-
public func onFail(_ action: @escaping (CheckoutError) -> Void) -> AcceleratedCheckoutButtons {
202+
/// - Parameter action: The action to perform when accelerated checkout fails
203+
/// - Returns: A view with the accelerated checkout error handler set
204+
public func onFail(_ action: @escaping (AcceleratedCheckoutError) -> Void) -> AcceleratedCheckoutButtons {
205205
var newView = self
206-
newView.eventHandlers.checkoutDidFail = action
206+
207+
// Handle validation errors directly
208+
newView.eventHandlers.validationDidFail = { validationError in
209+
let acceleratedError = AcceleratedCheckoutError.validation(validationError)
210+
action(acceleratedError)
211+
}
212+
213+
// Handle checkout errors directly
214+
newView.eventHandlers.checkoutDidFail = { checkoutError in
215+
let acceleratedError = AcceleratedCheckoutError.checkout(checkoutError)
216+
action(acceleratedError)
217+
}
218+
207219
return newView
208220
}
209221

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
MIT License
3+
4+
Copyright 2023 - Present, Shopify Inc.
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22+
*/
23+
24+
import ShopifyCheckoutSheetKit
25+
26+
/// Comprehensive error type for Accelerated Checkout operations
27+
public enum AcceleratedCheckoutError: Error {
28+
/// Cart validation failed - API correctly rejected input data
29+
case validation(StorefrontAPI.CartValidationError)
30+
31+
/// Checkout process failed - issues during the actual checkout flow
32+
case checkout(CheckoutError)
33+
34+
// MARK: - Convenience accessors
35+
36+
/// Get validation error if this is a validation error
37+
public var validationError: StorefrontAPI.CartValidationError? {
38+
if case let .validation(error) = self {
39+
return error
40+
}
41+
return nil
42+
}
43+
44+
/// Get checkout error if this is a checkout error
45+
public var checkoutError: CheckoutError? {
46+
if case let .checkout(error) = self {
47+
return error
48+
}
49+
return nil
50+
}
51+
52+
// MARK: - Utility methods
53+
54+
/// All validation error messages
55+
public var validationMessages: [String] {
56+
validationError?.userErrors.map(\.message) ?? []
57+
}
58+
59+
/// Check if this is a specific type of validation error
60+
public func hasValidationError(code: StorefrontAPI.CartErrorCode) -> Bool {
61+
return validationError?.userErrors.contains { $0.code == code } ?? false
62+
}
63+
64+
/// Check if this represents validation issues
65+
public var isValidationError: Bool {
66+
if case .validation = self { return true }
67+
return false
68+
}
69+
70+
/// Check if this represents checkout flow issues
71+
public var isCheckoutError: Bool {
72+
if case .checkout = self { return true }
73+
return false
74+
}
75+
}

Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayButton.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ struct Internal_ApplePayButton: View {
123123
Task { @MainActor [controller] in
124124
controller.onCheckoutComplete = eventHandlers.checkoutDidComplete
125125
controller.onCheckoutFail = eventHandlers.checkoutDidFail
126+
controller.onValidationFail = eventHandlers.validationDidFail
126127
controller.onCheckoutCancel = eventHandlers.checkoutDidCancel
127128
controller.onShouldRecoverFromError = eventHandlers.shouldRecoverFromError
128129
controller.onCheckoutClickLink = eventHandlers.checkoutDidClickLink

Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayViewController.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@ class ApplePayViewController: PayController, ObservableObject {
7575
@MainActor
7676
public var onCheckoutFail: ((CheckoutError) -> Void)?
7777

78+
/// Callback invoked when cart validation fails.
79+
/// This closure is called on the main thread when the input data is rejected by the API.
80+
///
81+
/// Example usage:
82+
/// ```swift
83+
/// applePayViewController.onValidationFail = { [weak self] validationError in
84+
/// for userError in validationError.userErrors {
85+
/// self?.showFieldError(userError.message, field: userError.field)
86+
/// }
87+
/// }
88+
/// ```
89+
@MainActor
90+
public var onValidationFail: ((StorefrontAPI.CartValidationError) -> Void)?
91+
7892
/// Callback invoked when the checkout process is cancelled by the user.
7993
/// This closure is called on the main thread when the user dismisses the checkout.
8094
///
@@ -187,6 +201,11 @@ class ApplePayViewController: PayController, ObservableObject {
187201
}
188202
} catch let error as StorefrontAPI.Errors {
189203
return try await handleStorefrontError(error)
204+
} catch let validationError as StorefrontAPI.CartValidationError {
205+
// Direct path for validation errors - never becomes CheckoutError.sdkError
206+
await onValidationFail?(validationError)
207+
try? await authorizationDelegate.transition(to: .terminalError(error: validationError))
208+
throw validationError
190209
} catch {
191210
if let checkoutError = error as? CheckoutError {
192211
await onCheckoutFail?(checkoutError)

Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ErrorHandler/ErrorHandler.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ class ErrorHandler {
7171

7272
switch action {
7373
case .showError:
74-
// We want to surface messages for all errors, not just the first one
7574
let allErrors = combinedErrors(actions: sortedActions)
7675
return .showError(errors: allErrors)
7776
default:
@@ -89,9 +88,9 @@ class ErrorHandler {
8988
static func map(error: Error, cart: StorefrontAPI.Cart?) -> PaymentSheetAction? {
9089
let shippingCountry = getShippingCountry(cart: cart)
9190
switch error {
91+
case let cartValidationError as StorefrontAPI.CartValidationError:
92+
return ErrorHandler.map(errors: cartValidationError.userErrors, shippingCountry: shippingCountry, cart: nil)
9293
case let cartUserError as StorefrontAPI.CartUserError:
93-
// Handle StorefrontAPI errors directly - we don't have the cart here but the checkout URL
94-
// is already captured in the delegate
9594
return ErrorHandler.map(errors: [cartUserError], shippingCountry: shippingCountry, cart: nil)
9695
case let apiError as StorefrontAPI.Errors:
9796
switch apiError {

Sources/ShopifyAcceleratedCheckouts/Wallets/Wallet.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public enum Wallet {
3434
public struct EventHandlers {
3535
public var checkoutDidComplete: ((CheckoutCompletedEvent) -> Void)?
3636
public var checkoutDidFail: ((CheckoutError) -> Void)?
37+
public var validationDidFail: ((StorefrontAPI.CartValidationError) -> Void)?
3738
public var checkoutDidCancel: (() -> Void)?
3839
public var shouldRecoverFromError: ((CheckoutError) -> Bool)?
3940
public var checkoutDidClickLink: ((URL) -> Void)?
@@ -43,6 +44,7 @@ public struct EventHandlers {
4344
public init(
4445
checkoutDidComplete: ((CheckoutCompletedEvent) -> Void)? = nil,
4546
checkoutDidFail: ((CheckoutError) -> Void)? = nil,
47+
validationDidFail: ((StorefrontAPI.CartValidationError) -> Void)? = nil,
4648
checkoutDidCancel: (() -> Void)? = nil,
4749
shouldRecoverFromError: ((CheckoutError) -> Bool)? = nil,
4850
checkoutDidClickLink: ((URL) -> Void)? = nil,
@@ -51,6 +53,7 @@ public struct EventHandlers {
5153
) {
5254
self.checkoutDidComplete = checkoutDidComplete
5355
self.checkoutDidFail = checkoutDidFail
56+
self.validationDidFail = validationDidFail
5457
self.checkoutDidCancel = checkoutDidCancel
5558
self.shouldRecoverFromError = shouldRecoverFromError
5659
self.checkoutDidClickLink = checkoutDidClickLink

0 commit comments

Comments
 (0)