Skip to content

Commit a6d7bb5

Browse files
authored
Fix for cart addresses update (#372)
* Fix for cart addresses update * Remove debug logs * PR feedback * Add tests * Update tests * Remove unused test helper
1 parent 803ec74 commit a6d7bb5

File tree

9 files changed

+574
-15
lines changed

9 files changed

+574
-15
lines changed

Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLDocument/GraphQLDocument+Mutations.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,19 @@ extension GraphQLDocument {
7575
}
7676
"""
7777

78+
case cartDeliveryAddressesRemove = """
79+
mutation CartDeliveryAddressesRemove($cartId: ID!, $addressIds: [ID!]!) {
80+
cartDeliveryAddressesRemove(cartId: $cartId, addressIds: $addressIds) {
81+
cart {
82+
...CartFragment
83+
}
84+
userErrors {
85+
...CartUserErrorFragment
86+
}
87+
}
88+
}
89+
"""
90+
7891
case cartSelectedDeliveryOptionsUpdate = """
7992
mutation CartSelectedDeliveryOptionsUpdate($cartId: ID!, $selectedDeliveryOptions: [CartSelectedDeliveryOptionInput!]!) {
8093
cartSelectedDeliveryOptionsUpdate(cartId: $cartId, selectedDeliveryOptions: $selectedDeliveryOptions) {

Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLRequest/GraphQLRequest+Operations.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ enum Operations {
6363
)
6464
}
6565

66+
static func cartDeliveryAddressesRemove(
67+
variables: [String: Any] = [:]
68+
) -> GraphQLRequest<StorefrontAPI.CartDeliveryAddressesRemoveResponse> {
69+
return GraphQLRequest(
70+
operation: .cartDeliveryAddressesRemove,
71+
responseType: StorefrontAPI.CartDeliveryAddressesRemoveResponse.self,
72+
variables: variables
73+
)
74+
}
75+
6676
static func cartSelectedDeliveryOptionsUpdate(
6777
variables: [String: Any] = [:]
6878
) -> GraphQLRequest<StorefrontAPI.CartSelectedDeliveryOptionsUpdateResponse> {

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,35 @@ extension StorefrontAPI {
220220
return cart
221221
}
222222

223+
/// Remove delivery addresses from cart
224+
/// - Parameters:
225+
/// - id: Cart ID
226+
/// - addressId: ID of the address to remove
227+
/// - Returns: Updated cart
228+
func cartDeliveryAddressesRemove(
229+
id: GraphQLScalars.ID,
230+
addressId: GraphQLScalars.ID
231+
) async throws -> Cart {
232+
let variables: [String: Any] = [
233+
"cartId": id.rawValue,
234+
"addressIds": [addressId.rawValue]
235+
]
236+
237+
let response = try await client.mutate(
238+
Operations.cartDeliveryAddressesRemove(variables: variables)
239+
)
240+
241+
guard let payload = response.data?.cartDeliveryAddressesRemove else {
242+
throw GraphQLError.invalidResponse
243+
}
244+
245+
let cart = try validateCart(payload.cart, requestName: "cartDeliveryAddressesRemove")
246+
247+
try validateUserErrors(payload.userErrors, checkoutURL: cart.checkoutUrl.url)
248+
249+
return cart
250+
}
251+
223252
/// Update selected delivery options
224253
/// - Parameters:
225254
/// - id: Cart ID
@@ -493,6 +522,10 @@ extension StorefrontAPI {
493522
let cartDeliveryAddressesUpdate: CartDeliveryAddressesUpdatePayload
494523
}
495524

525+
struct CartDeliveryAddressesRemoveResponse: Codable {
526+
let cartDeliveryAddressesRemove: CartDeliveryAddressesRemovePayload
527+
}
528+
496529
struct CartSelectedDeliveryOptionsUpdateResponse: Codable {
497530
let cartSelectedDeliveryOptionsUpdate: CartSelectedDeliveryOptionsUpdatePayload
498531
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,9 @@ extension StorefrontAPI {
507507
/// Cart delivery addresses update payload
508508
typealias CartDeliveryAddressesUpdatePayload = CartPayload
509509

510+
/// Cart delivery addresses remove payload
511+
typealias CartDeliveryAddressesRemovePayload = CartPayload
512+
510513
/// Cart selected delivery options update payload
511514
typealias CartSelectedDeliveryOptionsUpdatePayload = CartPayload
512515

Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegate+Controller.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,27 @@ extension ApplePayAuthorizationDelegate: PKPaymentAuthorizationControllerDelegat
3636
) async -> PKPaymentRequestShippingContactUpdate {
3737
pkEncoder.shippingContact = .success(contact)
3838

39+
// Clear selected shipping method to prevent stale identifier errors
40+
pkEncoder.selectedShippingMethod = nil
41+
pkDecoder.selectedShippingMethod = nil
42+
3943
do {
4044
let cartID = try pkEncoder.cartID.get()
4145

4246
let shippingAddress = try pkEncoder.shippingAddress.get()
47+
48+
// Store current cart state before attempting address update
49+
let previousCart = controller.cart
50+
4351
let cart = try await upsertShippingAddress(to: shippingAddress)
52+
53+
// If address update cleared delivery groups, revert to previous cart and show error
54+
if cart.deliveryGroups.nodes.isEmpty, previousCart?.deliveryGroups.nodes.isEmpty == false {
55+
try setCart(to: previousCart)
56+
57+
return pkDecoder.paymentRequestShippingContactUpdate(errors: [ValidationErrors.addressUnserviceableError])
58+
}
59+
4460
try setCart(to: cart)
4561

4662
let result = try await controller.storefront.cartPrepareForCompletion(id: cartID)
@@ -109,8 +125,13 @@ extension ApplePayAuthorizationDelegate: PKPaymentAuthorizationControllerDelegat
109125
_: PKPaymentAuthorizationController,
110126
didSelectShippingMethod shippingMethod: PKShippingMethod
111127
) async -> PKPaymentRequestShippingMethodUpdate {
112-
pkEncoder.selectedShippingMethod = shippingMethod
113-
pkDecoder.selectedShippingMethod = shippingMethod
128+
// Check if this shipping method identifier is still valid
129+
let availableShippingMethods = pkDecoder.shippingMethods
130+
let isValidMethod = availableShippingMethods.contains { $0.identifier == shippingMethod.identifier }
131+
let methodToUse: PKShippingMethod = isValidMethod ? shippingMethod : (availableShippingMethods.first ?? shippingMethod)
132+
133+
pkEncoder.selectedShippingMethod = methodToUse
134+
pkDecoder.selectedShippingMethod = methodToUse
114135

115136
do {
116137
let cartID = try pkEncoder.cartID.get()

Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegate.swift

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -241,23 +241,38 @@ class ApplePayAuthorizationDelegate: NSObject, ObservableObject {
241241
let cartID = try pkEncoder.cartID.get()
242242

243243
if let addressID = selectedShippingAddressID {
244-
return try await controller.storefront.cartDeliveryAddressesUpdate(
244+
do {
245+
// First, remove the existing delivery address to clear any tax policy contamination
246+
_ = try await controller.storefront.cartDeliveryAddressesRemove(
247+
id: cartID,
248+
addressId: addressID
249+
)
250+
251+
// Clear the selected address ID since we removed it
252+
selectedShippingAddressID = nil
253+
} catch {
254+
if let responseError = error as? StorefrontAPI.Errors {
255+
print("upsertShippingAddress - Storefront API Error: \(responseError)")
256+
}
257+
}
258+
}
259+
260+
do {
261+
let cart = try await controller.storefront.cartDeliveryAddressesAdd(
245262
id: cartID,
246-
addressId: addressID,
247263
address: address,
248264
validate: validate
249265
)
250-
}
251-
252-
let cart = try await controller.storefront.cartDeliveryAddressesAdd(
253-
id: cartID,
254-
address: address,
255-
validate: validate
256-
)
257266

258-
selectedShippingAddressID = cart.delivery?.addresses.first { $0.selected }?.id
267+
selectedShippingAddressID = cart.delivery?.addresses.first { $0.selected }?.id
259268

260-
return cart
269+
return cart
270+
} catch {
271+
if let responseError = error as? StorefrontAPI.Errors {
272+
print("upsertShippingAddress - Storefront API Error: \(responseError)")
273+
}
274+
throw error
275+
}
261276
}
262277
}
263278

Tests/ShopifyAcceleratedCheckoutsTests/Internal/GraphQLClient/GraphQLClientTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ final class GraphQLClientTests: XCTestCase {
3232
storefrontDomain: String = "test.myshopify.com",
3333
storefrontAccessToken: String? = "test-token",
3434
apiVersion: String = "2025-07",
35-
context: InContextDirective = InContextDirective()
35+
context: InContextDirective = InContextDirective(countryCode: CountryCode.US)
3636
) -> GraphQLClient {
3737
let url = URL(string: "https://\(storefrontDomain)/api/\(apiVersion)/graphql.json")!
3838
var headers: [String: String] = [:]
@@ -108,7 +108,7 @@ final class GraphQLClientTests: XCTestCase {
108108

109109
func testContextInitialization() {
110110
// Test default initialization
111-
let defaultContext = InContextDirective()
111+
let defaultContext = InContextDirective(countryCode: CountryCode.US)
112112
XCTAssertEqual(defaultContext.countryCode, CountryCode.US)
113113
XCTAssertEqual(defaultContext.languageCode, LanguageCode.EN)
114114

Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIMutationsTests.swift

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,129 @@ final class StorefrontAPIMutationsTests: XCTestCase {
524524
XCTAssertEqual(buyerIdentity?["customerAccessToken"] as? String, "customer-access-token-456")
525525
}
526526

527+
// MARK: - Delivery Address Remove Tests
528+
529+
func testCartDeliveryAddressesRemoveSuccess() async throws {
530+
let json = """
531+
{
532+
"data": {
533+
"cartDeliveryAddressesRemove": {
534+
"cart": {
535+
"id": "gid://shopify/Cart/123",
536+
"checkoutUrl": "https://test.myshopify.com/checkout/123",
537+
"totalQuantity": 1,
538+
"buyerIdentity": null,
539+
"deliveryGroups": {
540+
"nodes": [{
541+
"id": "gid://shopify/CartDeliveryGroup/1",
542+
"groupType": "ONE_TIME_PURCHASE",
543+
"deliveryOptions": [{
544+
"handle": "standard",
545+
"title": "Standard Shipping",
546+
"code": "STANDARD",
547+
"deliveryMethodType": "SHIPPING",
548+
"description": "5-7 business days",
549+
"estimatedCost": {"amount": "5.00", "currencyCode": "USD"}
550+
}],
551+
"selectedDeliveryOption": null
552+
}]
553+
},
554+
"delivery": {
555+
"addresses": []
556+
},
557+
"lines": {"nodes": []},
558+
"cost": {
559+
"totalAmount": {"amount": "19.99", "currencyCode": "USD"},
560+
"subtotalAmount": {"amount": "19.99", "currencyCode": "USD"},
561+
"totalTaxAmount": null
562+
},
563+
"discountCodes": [],
564+
"discountAllocations": []
565+
},
566+
"userErrors": []
567+
}
568+
}
569+
}
570+
"""
571+
mockJSONResponse(json)
572+
573+
let cart = try await storefrontAPI.cartDeliveryAddressesRemove(
574+
id: GraphQLScalars.ID("gid://shopify/Cart/123"),
575+
addressId: GraphQLScalars.ID("gid://shopify/CartSelectableAddress/1")
576+
)
577+
578+
XCTAssertEqual(cart.id.rawValue, "gid://shopify/Cart/123")
579+
XCTAssertEqual(cart.delivery?.addresses.count, 0)
580+
XCTAssertEqual(cart.deliveryGroups.nodes.first?.deliveryOptions.count, 1)
581+
}
582+
583+
func testCartDeliveryAddressesRemoveWithInvalidAddress() async {
584+
let json = """
585+
{
586+
"data": {
587+
"cartDeliveryAddressesRemove": {
588+
"cart": null,
589+
"userErrors": [{
590+
"field": ["addressIds"],
591+
"message": "Address not found",
592+
"code": "NOT_FOUND"
593+
}]
594+
}
595+
}
596+
}
597+
"""
598+
mockJSONResponse(json)
599+
600+
await XCTAssertThrowsGraphQLError(
601+
try await storefrontAPI.cartDeliveryAddressesRemove(
602+
id: GraphQLScalars.ID("gid://shopify/Cart/123"),
603+
addressId: GraphQLScalars.ID("gid://shopify/CartSelectableAddress/999")
604+
),
605+
{ if case .invalidResponse = $0 { return true } else { return false } },
606+
"Expected GraphQLError.invalidResponse to be thrown"
607+
)
608+
}
609+
610+
func testCartDeliveryAddressesRemoveRequestValidation() async throws {
611+
let json = """
612+
{"data": {"cartDeliveryAddressesRemove": {"cart": null, "userErrors": []}}}
613+
"""
614+
mockJSONResponse(json)
615+
616+
do {
617+
_ = try await storefrontAPI.cartDeliveryAddressesRemove(
618+
id: GraphQLScalars.ID("gid://shopify/Cart/123"),
619+
addressId: GraphQLScalars.ID("gid://shopify/CartSelectableAddress/456")
620+
)
621+
XCTFail("Expected error to be thrown")
622+
} catch {
623+
XCTAssertTrue(
624+
error is GraphQLError,
625+
"Unexpected error type: \(type(of: error))"
626+
)
627+
628+
guard case .invalidResponse = error as? GraphQLError else {
629+
XCTFail("Expected GraphQLError.invalidResponse but got: \(error)")
630+
return
631+
}
632+
}
633+
634+
XCTAssertNotNil(MockURLProtocol.capturedRequestBody)
635+
636+
guard let body = MockURLProtocol.capturedRequestBody else {
637+
XCTFail("Expected request body to be captured")
638+
return
639+
}
640+
641+
let jsonBody = try JSONSerialization.jsonObject(with: body) as? [String: Any]
642+
let variables = jsonBody?["variables"] as? [String: Any]
643+
let addressIds = variables?["addressIds"] as? [String]
644+
645+
XCTAssertEqual(variables?["cartId"] as? String, "gid://shopify/Cart/123")
646+
XCTAssertEqual(addressIds?.count, 1)
647+
XCTAssertEqual(addressIds?.first, "gid://shopify/CartSelectableAddress/456")
648+
}
649+
527650
// MARK: - Delivery Address Add Tests
528651

529652
func testCartDeliveryAddressesAddSuccess() async throws {

0 commit comments

Comments
 (0)