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 @@ -3,7 +3,7 @@
set -e

CONFIG_FILE="Storefront.xcconfig"
STOREFRONT_DOMAIN=$(grep '^STOREFRONT_DOMAIN' "$CONFIG_FILE" | cut -d '=' -f2 | tr -d ' ')
STOREFRONT_DOMAIN=$(grep '^[[:space:]]*STOREFRONT_DOMAIN' "$CONFIG_FILE" | cut -d '=' -f2 | tr -d ' ')

TEMPLATE_FILE="MobileBuyIntegration/MobileBuyIntegration.entitlements.template"
OUTPUT_FILE="MobileBuyIntegration/MobileBuyIntegration.entitlements"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,6 @@ extension StorefrontAPI {

/// Remove personal data from cart
/// - Parameter id: Cart ID
/// Available since 2025-07 - must be called with a custom StorefrontAPI
func cartRemovePersonalData(id: GraphQLScalars.ID) async throws {
let variables: [String: Any] = [
"cartId": id.rawValue
Expand Down Expand Up @@ -446,11 +445,13 @@ extension StorefrontAPI {
throw GraphQLError.networkError(
"Cart preparation throttled. Poll after: \(throttled.pollAfter.date)"
)
case let .notReady(notReady):
let errorMessages = notReady.errors.map { "\($0.code): \($0.message)" }.joined(
separator: ", "
case .notReady:
let errorMessages = payload.result.map { "\($0)" } ?? "unknown"
throw Errors.response(
requestName: "cartPrepareForCompletion",
message: "Cart not ready: \(errorMessages)",
payload: .cartPrepareForCompletion(payload)
)
throw GraphQLError.networkError("Cart not ready: \(errorMessages)")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -594,86 +594,7 @@ extension StorefrontAPI {
/// Cart status not ready
struct CartStatusNotReady: Codable {
let cart: Cart?
let errors: [CompletionError]
}

/// Completion error
struct CompletionError: Codable {
let code: CompletionErrorCode
let message: String
}

/// Completion error codes
enum CompletionErrorCode: String, Codable {
case error = "ERROR"
case inventoryLocationNotFound = "INVENTORY_LOCATION_NOT_FOUND"
case paymentAmountTooSmall = "PAYMENT_AMOUNT_TOO_SMALL"
case paymentCallIssuer = "PAYMENT_CALL_ISSUER"
case paymentCardDeclined = "PAYMENT_CARD_DECLINED"
case paymentError = "PAYMENT_ERROR"
case paymentGatewayNotEnabledForShop = "PAYMENT_GATEWAY_NOT_ENABLED_FOR_SHOP"
case paymentInsufficientFunds = "PAYMENT_INSUFFICIENT_FUNDS"
case paymentInvalidAmount = "PAYMENT_INVALID_AMOUNT"
case paymentInvalidBillingAddress = "PAYMENT_INVALID_BILLING_ADDRESS"
case paymentInvalidCreditCard = "PAYMENT_INVALID_CREDIT_CARD"
case paymentInvalidCurrency = "PAYMENT_INVALID_CURRENCY"
case paymentInvalidPaymentMethod = "PAYMENT_INVALID_PAYMENT_METHOD"
case paymentTransientError = "PAYMENT_TRANSIENT_ERROR"
case billingAddressInvalid = "BILLING_ADDRESS_INVALID"
case checkoutCompletionTargetInvalid = "CHECKOUT_COMPLETION_TARGET_INVALID"
case colorInvalid = "COLOR_INVALID"
case deliveryAddress1Invalid = "DELIVERY_ADDRESS1_INVALID"
case deliveryAddress1Missing = "DELIVERY_ADDRESS1_MISSING"
case deliveryAddress1TooLong = "DELIVERY_ADDRESS1_TOO_LONG"
case deliveryAddress2Invalid = "DELIVERY_ADDRESS2_INVALID"
case deliveryAddress2Required = "DELIVERY_ADDRESS2_REQUIRED"
case deliveryAddress2TooLong = "DELIVERY_ADDRESS2_TOO_LONG"
case deliveryAddressInvalid = "DELIVERY_ADDRESS_INVALID"
case deliveryAddressMissing = "DELIVERY_ADDRESS_MISSING"
case deliveryAddressRequired = "DELIVERY_ADDRESS_REQUIRED"
case deliveryOptionInvalid = "DELIVERY_OPTION_INVALID"
case deliveryOptionsMissing = "DELIVERY_OPTIONS_MISSING"
case deliveryPostalCodeInvalid = "DELIVERY_POSTAL_CODE_INVALID"
case deliveryPostalCodeRequired = "DELIVERY_POSTAL_CODE_REQUIRED"
case deliveryZoneNotFound = "DELIVERY_ZONE_NOT_FOUND"
case deliveryZoneRequiredForCountry = "DELIVERY_ZONE_REQUIRED_FOR_COUNTRY"
case deliveryCityInvalid = "DELIVERY_CITY_INVALID"
case deliveryCityRequired = "DELIVERY_CITY_REQUIRED"
case deliveryCityTooLong = "DELIVERY_CITY_TOO_LONG"
case deliveryCompanyInvalid = "DELIVERY_COMPANY_INVALID"
case deliveryCompanyRequired = "DELIVERY_COMPANY_REQUIRED"
case deliveryCompanyTooLong = "DELIVERY_COMPANY_TOO_LONG"
case deliveryCountryRequired = "DELIVERY_COUNTRY_REQUIRED"
case deliveryFirstNameInvalid = "DELIVERY_FIRST_NAME_INVALID"
case deliveryFirstNameRequired = "DELIVERY_FIRST_NAME_REQUIRED"
case deliveryFirstNameTooLong = "DELIVERY_FIRST_NAME_TOO_LONG"
case deliveryInvalidPostalCodeForCountry = "DELIVERY_INVALID_POSTAL_CODE_FOR_COUNTRY"
case deliveryInvalidPostalCodeForZone = "DELIVERY_INVALID_POSTAL_CODE_FOR_ZONE"
case deliveryLastNameInvalid = "DELIVERY_LAST_NAME_INVALID"
case deliveryLastNameTooLong = "DELIVERY_LAST_NAME_TOO_LONG"
case deliveryNoDeliveryAvailable = "DELIVERY_NO_DELIVERY_AVAILABLE"
case deliveryNoDeliveryAvailableForMerchandiseLine = "DELIVERY_NO_DELIVERY_AVAILABLE_FOR_MERCHANDISE_LINE"
case deliveryOptionsPhoneNumberInvalid = "DELIVERY_OPTIONS_PHONE_NUMBER_INVALID"
case deliveryOptionsPhoneNumberRequired = "DELIVERY_OPTIONS_PHONE_NUMBER_REQUIRED"
case deliveryPhoneNumberInvalid = "DELIVERY_PHONE_NUMBER_INVALID"
case deliveryPhoneNumberRequired = "DELIVERY_PHONE_NUMBER_REQUIRED"
case emailInvalid = "EMAIL_INVALID"
case firstNameInvalid = "FIRST_NAME_INVALID"
case firstNameRequired = "FIRST_NAME_REQUIRED"
case firstNameTooLong = "FIRST_NAME_TOO_LONG"
case functionInvalid = "FUNCTION_INVALID"
case invalidAddress = "INVALID_ADDRESS"
case lastNameInvalid = "LAST_NAME_INVALID"
case lastNameRequired = "LAST_NAME_REQUIRED"
case lastNameTooLong = "LAST_NAME_TOO_LONG"
case deliveryLastNameRequired = "DELIVERY_LAST_NAME_REQUIRED"
case noDeliveryGroupSelected = "NO_DELIVERY_GROUP_SELECTED"
case paymentBillingAddressInvalid = "PAYMENT_BILLING_ADDRESS_INVALID"
case paymentInvalid = "PAYMENT_INVALID"
case paymentMethodInvalid = "PAYMENT_METHOD_INVALID"
case paymentMethodRequired = "PAYMENT_METHOD_REQUIRED"
case phoneInvalid = "PHONE_INVALID"
case unknown = "UNKNOWN"
let errors: [CartCompletionError]
}

/// Cart submit for completion payload
Expand Down Expand Up @@ -742,7 +663,7 @@ extension StorefrontAPI {
/// Submit failed
struct SubmitFailed: Codable {
let checkoutUrl: GraphQLScalars.URL?
let errors: [SubmissionError]
let errors: [CartCompletionError]
}

/// Submit already accepted
Expand All @@ -755,14 +676,32 @@ extension StorefrontAPI {
let pollAfter: GraphQLScalars.DateTime
}

/// Submission error
struct SubmissionError: Codable {
let code: SubmissionErrorCode
/// Cart completion error (used by both cartPrepareForCompletion and cartSubmitForCompletion)
struct CartCompletionError: Codable {
let code: CartCompletionErrorCode
let rawCode: String
let message: String

init(code: CartCompletionErrorCode, message: String) {
self.code = code
rawCode = code.rawValue
self.message = message
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
rawCode = try container.decode(String.self, forKey: .code)
code = CartCompletionErrorCode(rawValue: rawCode) ?? .unknownValue
message = try container.decode(String.self, forKey: .message)
}

private enum CodingKeys: String, CodingKey {
case code, message
}
}

/// Submission error codes for cart submit
enum SubmissionErrorCode: String, Codable {
/// Error codes for cart completion (shared by prepare and submit stages)
enum CartCompletionErrorCode: String, Codable {
// Buyer identity errors
case buyerIdentityEmailIsInvalid = "BUYER_IDENTITY_EMAIL_IS_INVALID"
case buyerIdentityEmailRequired = "BUYER_IDENTITY_EMAIL_REQUIRED"
Expand Down Expand Up @@ -883,7 +822,7 @@ extension StorefrontAPI {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let value = try container.decode(String.self)
self = SubmissionErrorCode(rawValue: value) ?? .unknownValue
self = CartCompletionErrorCode(rawValue: value) ?? .unknownValue
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ import ShopifyCheckoutSheetKit

public enum ShopifyAcceleratedCheckouts {
/// Storefront API version used for cart operations
/// Note: We also use `2025-07` for `cartRemovePersonalData` mutations. We are working towards migrating all requests to `2025-07`.
internal static let apiVersion = "2025-04"
internal static let apiVersion = "2025-07"

internal static let name = "ShopifyAcceleratedCheckouts"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,10 @@

try await upsertShippingAddress(to: shippingAddress)

let result = try await controller.storefront.cartPrepareForCompletion(id: cartID)
try setCart(to: result.cart)
try await prepareCartForCompletion(id: cartID)

// If address update cleared delivery groups, revert to previous cart and show error
if result.cart?.deliveryGroups.nodes.isEmpty == true, previousCart?.deliveryGroups.nodes.isEmpty == false {
if controller.cart?.deliveryGroups.nodes.isEmpty == true, previousCart?.deliveryGroups.nodes.isEmpty == false {
try setCart(to: previousCart)

ShopifyAcceleratedCheckouts.logger.error("ApplePay: didSelectShippingContact deliveryGroups were unexpectedly empty")
Expand Down Expand Up @@ -120,8 +119,7 @@
billingAddress: billingPostalAddress
)

let result = try await controller.storefront.cartPrepareForCompletion(id: cartID)
try setCart(to: result.cart)
try await prepareCartForCompletion(id: cartID)

return pkDecoder.paymentRequestPaymentMethodUpdate()
} catch {
Expand Down Expand Up @@ -162,9 +160,7 @@
deliveryOptionHandle: selectedDeliveryOptionHandle.rawValue
)

let result = try await controller.storefront.cartPrepareForCompletion(id: cartID)

try setCart(to: result.cart)
try await prepareCartForCompletion(id: cartID)

return pkDecoder.paymentRequestShippingMethodUpdate()
} catch {
Expand Down Expand Up @@ -198,8 +194,7 @@
customerAccessToken: configuration.common.customer?.customerAccessToken
)
)
let result = try await controller.storefront.cartPrepareForCompletion(id: cartID)
try setCart(to: result.cart)
try await prepareCartForCompletion(id: cartID)
}

if try pkDecoder.isShippingRequired() {
Expand All @@ -214,8 +209,7 @@
try await reapplySelectedShippingMethod()
}

let result = try await controller.storefront.cartPrepareForCompletion(id: cartID)
try setCart(to: result.cart)
try await prepareCartForCompletion(id: cartID)
} else {
// If the cart is entirely digital updating with a complete billingAddress
// allows us to resolve pending terms on taxes prior to cartPaymentUpdate
Expand All @@ -230,8 +224,7 @@
billingAddress: billingPostalAddress
)

let result = try await controller.storefront.cartPrepareForCompletion(id: cartID)
try setCart(to: result.cart)
try await prepareCartForCompletion(id: cartID)
}

let totalAmount = try pkEncoder.totalAmount.get()
Expand All @@ -240,7 +233,7 @@
// Taxes may become pending again fail to resolve despite updating within the didUpdatePaymentMethod
// So we retry one time to see if the error clears on retry
_ = try await Task.retrying(priority: nil, maxRetryCount: 1) {
try await self.controller.storefront.cartPaymentUpdate(

Check warning on line 236 in Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegate+Controller.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

capture of 'self' with non-Sendable type 'ApplePayAuthorizationDelegate' in a '@sendable' closure

Check warning on line 236 in Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegate+Controller.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

capture of 'self' with non-Sendable type 'ApplePayAuthorizationDelegate' in a '@sendable' closure
id: cartID,
totalAmount: totalAmount,
applePayPayment: applePayPayment
Expand Down Expand Up @@ -301,14 +294,42 @@
}

let cartID = try pkEncoder.cartID.get()
try await controller.storefront.cartPrepareForCompletion(id: cartID)
_ = try await prepareCartForCompletion(id: cartID)
try await controller.storefront.cartSelectedDeliveryOptionsUpdate(
id: cartID,
deliveryGroupId: deliveryGroupID,
deliveryOptionHandle: selectedDeliveryOptionHandle.rawValue
)
}

// `prepareCartForCompletion` filters 'non-terminal' violations
// See: `ErrorHandler.filterApplePayResolvableViolations`
//
// Before `didAuthorizePayment`, Apple Pay redacts PII, these are expected to be resolved by submit
// After `didAuthorizePayment`, PII is available, but we continue filtering in `prepareForCompletion` so
// `cartSubmitForCompletion` can return all violations at once
// This gives the user the best chance to fix everything in a single pass.
//
@discardableResult
func prepareCartForCompletion(id: GraphQLScalars.ID) async throws -> StorefrontAPI.Cart? {
do {
let result = try await controller.storefront.cartPrepareForCompletion(id: id)
try setCart(to: result.cart)
return result.cart
} catch let error as StorefrontAPI.Errors {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to catch non-response errors? i.e network timeouts, decode failures etc

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any other error will follow the existing error handling which will be caught and handled in the callsite (those indicate an unexpected error and will abort to checkout kit)

if case let .response(_, _, .cartPrepareForCompletion(payload)) = error,
case let .notReady(notReady) = payload.result
{
let actionableErrors = ErrorHandler.filterApplePayResolvableViolations(errors: notReady.errors)
if actionableErrors.isEmpty {
try setCart(to: notReady.cart)
return notReady.cart
}
}
throw error
}
}

func handleError<T>(
error: Error,
cart _: StorefrontAPI.Cart?,
Expand All @@ -329,6 +350,8 @@
self.checkoutURL = checkoutURL
}
return completion([abortError])
case .continueFlow:
return completion([])
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
func dismiss(completion: (() -> Void)?)
}

extension PKPaymentAuthorizationController: PaymentAuthorizationController {}

Check warning on line 38 in Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegate.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

sendability of function types in instance method 'dismiss(completion:)' does not match requirement in protocol 'PaymentAuthorizationController'; this is an error in the Swift 6 language mode

Check warning on line 38 in Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegate.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

sendability of function types in instance method 'dismiss(completion:)' does not match requirement in protocol 'PaymentAuthorizationController'; this is an error in the Swift 6 language mode

Check warning on line 38 in Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegate.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

sendability of function types in instance method 'dismiss(completion:)' does not match requirement in protocol 'PaymentAuthorizationController'; this is an error in the Swift 6 language mode

Check warning on line 38 in Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegate.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

sendability of function types in instance method 'dismiss(completion:)' does not match requirement in protocol 'PaymentAuthorizationController'; this is an error in the Swift 6 language mode

Check warning on line 38 in Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegate.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

sendability of function types in instance method 'dismiss(completion:)' does not match requirement in protocol 'PaymentAuthorizationController'; this is an error in the Swift 6 language mode

@available(iOS 16.0, *)
typealias PKAuthorizationControllerFactory = (PKPaymentRequest) -> PaymentAuthorizationController
Expand Down Expand Up @@ -174,7 +174,7 @@
default:
let cartID = try pkEncoder.cartID.get()
try? await _Concurrency.Task.retrying(clock: clock) {
try await self.controller.storefrontJulyRelease.cartRemovePersonalData(id: cartID)
try await self.controller.storefront.cartRemovePersonalData(id: cartID)
}.value

ShopifyAcceleratedCheckouts.logger.debug("Cleared PII from cart")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,13 @@
protocol PayController: AnyObject {
var cart: StorefrontAPI.Types.Cart? { get set }
var storefront: StorefrontAPIProtocol { get set }
/// Temporary workaround due to July release changing the validation strategy
var storefrontJulyRelease: StorefrontAPIProtocol { get set }

/// Opens ShopifyCheckoutSheetKit
func present(url: URL) async throws
}

@available(iOS 16.0, *)
class ApplePayViewController: WalletController, PayController {
@Published var storefrontJulyRelease: StorefrontAPIProtocol
@Published var paymentController: PKPaymentAuthorizationController?

var cart: StorefrontAPI.Types.Cart?
Expand Down Expand Up @@ -134,11 +131,6 @@
identifier: CheckoutIdentifier,
configuration: ApplePayConfigurationWrapper
) {
storefrontJulyRelease = StorefrontAPI(
storefrontDomain: configuration.common.storefrontDomain,
storefrontAccessToken: configuration.common.storefrontAccessToken,
apiVersion: "2025-07"
)
super.init(
identifier: identifier,
storefront: StorefrontAPI(
Expand Down Expand Up @@ -226,7 +218,7 @@
}

@available(iOS 16.0, *)
extension ApplePayViewController: CheckoutDelegate {

Check warning on line 221 in Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayViewController.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

conformance of 'ApplePayViewController' to protocol 'CheckoutDelegate' crosses into main actor-isolated code and can cause data races; this is an error in the Swift 6 language mode

Check warning on line 221 in Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayViewController.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

conformance of 'ApplePayViewController' to protocol 'CheckoutDelegate' crosses into main actor-isolated code and can cause data races; this is an error in the Swift 6 language mode

Check warning on line 221 in Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayViewController.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

conformance of 'ApplePayViewController' to protocol 'CheckoutDelegate' crosses into main actor-isolated code and can cause data races; this is an error in the Swift 6 language mode

Check warning on line 221 in Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayViewController.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

conformance of 'ApplePayViewController' to protocol 'CheckoutDelegate' crosses into main actor-isolated code and can cause data races; this is an error in the Swift 6 language mode

Check warning on line 221 in Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayViewController.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

conformance of 'ApplePayViewController' to protocol 'CheckoutDelegate' crosses into main actor-isolated code and can cause data races; this is an error in the Swift 6 language mode
func checkoutDidComplete(event: CheckoutCompletedEvent) {
Task { @MainActor in
self.onCheckoutComplete?(event)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class ErrorHandler {
enum PaymentSheetAction {
case showError(errors: [Error])
case interrupt(reason: InterruptReason, checkoutURL: URL? = nil)
case continueFlow
}

static func useEmirate(shippingCountry: String?) -> Bool {
Expand Down Expand Up @@ -99,9 +100,9 @@ class ErrorHandler {
case let .response(_, _, payload):
switch payload {
case let .cartSubmitForCompletion(submitPayload):
return ErrorHandler.map(payload: submitPayload, shippingCountry: shippingCountry, requiredContactFields: requiredContactFields)
return ErrorHandler.map(stage: .submit(submitPayload), shippingCountry: shippingCountry, requiredContactFields: requiredContactFields)
case let .cartPrepareForCompletion(preparePayload):
return ErrorHandler.map(payload: preparePayload)
return ErrorHandler.map(stage: .prepare(preparePayload), shippingCountry: shippingCountry, requiredContactFields: requiredContactFields)
}
case let .userError(userErrors, cart):
return ErrorHandler.map(errors: userErrors, shippingCountry: shippingCountry, cart: cart, requiredContactFields: requiredContactFields)
Expand Down Expand Up @@ -135,6 +136,8 @@ class ErrorHandler {
return 1
case .showError:
return 2
case .continueFlow:
return 4
}
}

Expand Down
Loading
Loading