Skip to content

Commit 16bedd1

Browse files
[AC]: Pad postal codes for UK/CA postalCodes (#424)
* feat: pad postal codes for GB / CA * refactor: ensure we only use prepare for completion for cart results * refactor: remove logic for picking different delivery methods * fix: ensure we guard empty postalcodes * fix: remove cart check on cartSelectedDeliveryOptionsUpdate * fix: ensure we don't pad full postal codes
1 parent 0dd9c81 commit 16bedd1

File tree

9 files changed

+544
-49
lines changed

9 files changed

+544
-49
lines changed

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ extension StorefrontAPI {
264264
id: GraphQLScalars.ID,
265265
deliveryGroupId: GraphQLScalars.ID,
266266
deliveryOptionHandle: String
267-
) async throws -> Cart {
267+
) async throws -> Cart? {
268268
let variables: [String: Any] = [
269269
"cartId": id.rawValue,
270270
"selectedDeliveryOptions": [
@@ -283,11 +283,14 @@ extension StorefrontAPI {
283283
throw GraphQLError.invalidResponse
284284
}
285285

286-
let cart = try validateCart(payload.cart, requestName: "cartSelectedDeliveryOptionsUpdate")
286+
// Temporarily skipping this check due to cart bug where cartSelectedDeliveryOptionsUpdate
287+
// is wrongly returning PendingTerms causing cart:nil despite successfully setting deliveryOption
288+
// See: https://github.com/shop/issues-fulfillment/issues/2594
289+
// let cart = try validateCart(payload.cart, requestName: "cartSelectedDeliveryOptionsUpdate")
287290

288-
try validateUserErrors(payload.userErrors, checkoutURL: cart.checkoutUrl.url)
291+
try validateUserErrors(payload.userErrors, checkoutURL: payload.cart?.checkoutUrl.url)
289292

290-
return cart
293+
return payload.cart
291294
}
292295

293296
/// Update cart payment

Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ protocol StorefrontAPIProtocol {
6464

6565
// MARK: - Mutation Methods
6666

67-
func cartCreate(
67+
@discardableResult func cartCreate(
6868
with items: [GraphQLScalars.ID], customer: ShopifyAcceleratedCheckouts.Customer?
6969
) async throws -> StorefrontAPI.Cart
7070

@@ -73,29 +73,29 @@ protocol StorefrontAPIProtocol {
7373
input buyerIdentity: StorefrontAPI.CartBuyerIdentityUpdateInput
7474
) async throws -> StorefrontAPI.Cart
7575

76-
func cartDeliveryAddressesAdd(
76+
@discardableResult func cartDeliveryAddressesAdd(
7777
id: GraphQLScalars.ID,
7878
address: StorefrontAPI.Address,
7979
validate: Bool
8080
) async throws -> StorefrontAPI.Cart
8181

82-
func cartDeliveryAddressesUpdate(
82+
@discardableResult func cartDeliveryAddressesUpdate(
8383
id: GraphQLScalars.ID,
8484
addressId: GraphQLScalars.ID,
8585
address: StorefrontAPI.Address,
8686
validate: Bool
8787
) async throws -> StorefrontAPI.Cart
8888

89-
func cartDeliveryAddressesRemove(
89+
@discardableResult func cartDeliveryAddressesRemove(
9090
id: GraphQLScalars.ID,
9191
addressId: GraphQLScalars.ID
9292
) async throws -> StorefrontAPI.Cart
9393

94-
func cartSelectedDeliveryOptionsUpdate(
94+
@discardableResult func cartSelectedDeliveryOptionsUpdate(
9595
id: GraphQLScalars.ID,
9696
deliveryGroupId: GraphQLScalars.ID,
9797
deliveryOptionHandle: String
98-
) async throws -> StorefrontAPI.Cart
98+
) async throws -> StorefrontAPI.Cart?
9999

100100
@discardableResult func cartPaymentUpdate(
101101
id: GraphQLScalars.ID,

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

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,19 @@ extension ApplePayAuthorizationDelegate: PKPaymentAuthorizationControllerDelegat
4848
// Store current cart state before attempting address update
4949
let previousCart = controller.cart
5050

51-
let cart = try await upsertShippingAddress(to: shippingAddress)
51+
try await upsertShippingAddress(to: shippingAddress)
52+
53+
let result = try await controller.storefront.cartPrepareForCompletion(id: cartID)
54+
try setCart(to: result.cart)
5255

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

60+
ShopifyAcceleratedCheckouts.logger.error("ApplePay: didSelectShippingContact deliveryGroups were unexpectedly empty")
5761
return pkDecoder.paymentRequestShippingContactUpdate(errors: [ValidationErrors.addressUnserviceableError])
5862
}
5963

60-
try setCart(to: cart)
61-
62-
let result = try await controller.storefront.cartPrepareForCompletion(id: cartID)
63-
64-
try setCart(to: result.cart)
65-
6664
return pkDecoder.paymentRequestShippingContactUpdate()
6765
} catch {
6866
ShopifyAcceleratedCheckouts.logger.error("ApplePay: didSelectShippingContact error: \(error)")
@@ -105,8 +103,10 @@ extension ApplePayAuthorizationDelegate: PKPaymentAuthorizationControllerDelegat
105103
)
106104
)
107105

108-
try await controller.storefront
109-
.cartBillingAddressUpdate(id: cartID, billingAddress: billingPostalAddress)
106+
try await controller.storefront.cartBillingAddressUpdate(
107+
id: cartID,
108+
billingAddress: billingPostalAddress
109+
)
110110

111111
let result = try await controller.storefront.cartPrepareForCompletion(id: cartID)
112112
try setCart(to: result.cart)
@@ -125,26 +125,30 @@ extension ApplePayAuthorizationDelegate: PKPaymentAuthorizationControllerDelegat
125125
_: PKPaymentAuthorizationController,
126126
didSelectShippingMethod shippingMethod: PKShippingMethod
127127
) async -> PKPaymentRequestShippingMethodUpdate {
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)
128+
let isValidShippingMethod = pkDecoder.shippingMethods.contains { $0.identifier == shippingMethod.identifier }
129+
if !isValidShippingMethod {
130+
return pkDecoder
131+
.paymentRequestShippingMethodUpdate(
132+
errors: [ShopifyAcceleratedCheckouts.Error.invariant(
133+
expected: "isValidShippingMethod true"
134+
)]
135+
)
136+
}
132137

133-
pkEncoder.selectedShippingMethod = methodToUse
134-
pkDecoder.selectedShippingMethod = methodToUse
138+
pkEncoder.selectedShippingMethod = shippingMethod
139+
pkDecoder.selectedShippingMethod = shippingMethod
135140

136141
do {
137142
let cartID = try pkEncoder.cartID.get()
138143
let selectedDeliveryOptionHandle = try pkEncoder.selectedDeliveryOptionHandle.get()
139144
let deliveryGroupID = try pkEncoder.deliveryGroupID.get()
140145

141-
let cart = try await controller.storefront
146+
try await controller.storefront
142147
.cartSelectedDeliveryOptionsUpdate(
143148
id: cartID,
144149
deliveryGroupId: deliveryGroupID,
145150
deliveryOptionHandle: selectedDeliveryOptionHandle.rawValue
146151
)
147-
try setCart(to: cart)
148152

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

@@ -188,7 +192,7 @@ extension ApplePayAuthorizationDelegate: PKPaymentAuthorizationControllerDelegat
188192

189193
if try pkDecoder.isShippingRequired() {
190194
let shippingAddress = try pkEncoder.shippingAddress.get()
191-
_ = try await upsertShippingAddress(to: shippingAddress, validate: true)
195+
try await upsertShippingAddress(to: shippingAddress, validate: true)
192196

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ class ApplePayAuthorizationDelegate: NSObject, ObservableObject {
241241
}
242242
}
243243

244-
func upsertShippingAddress(to address: StorefrontAPI.Types.Address, validate: Bool = false)
244+
@discardableResult func upsertShippingAddress(to address: StorefrontAPI.Types.Address, validate: Bool = false)
245245
async throws -> StorefrontAPI.Types.Cart
246246
{
247247
let cartID = try pkEncoder.cartID.get()

Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PKEncoder.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,21 +204,40 @@ class PKEncoder {
204204
return countryCode
205205
}
206206

207+
/// Apple trims half of the zip exclusively for GB/Canada
208+
/// https://developer.apple.com/documentation/applepayontheweb/applepaysession/onshippingcontactselected
209+
/// checkout-web pads the postal code to ensure that deliveryGroups are returned when no flat rates
210+
/// https://github.com/shop/world/blob/01066aec0ab38cc4c14ece1a00eceef6cfa162ef/areas/clients/checkout-web/app/utilities/wallets/helpers.ts#L175-L188
211+
func addPaddingToPostalCode(for postalCode: String?, in country: String) -> String? {
212+
guard let postalCode, !postalCode.isEmpty else { return nil }
213+
return switch country {
214+
case "GB": "\(postalCode)0ZZ"
215+
case "CA": "\(postalCode)0Z0"
216+
default: postalCode
217+
}
218+
}
219+
207220
func pkContactToAddress(contact: PKContact?)
208221
-> Result<StorefrontAPI.Types.Address, ShopifyAcceleratedCheckouts.Error>
209222
{
210223
guard let postalAddress = contact?.postalAddress else {
211224
return .failure(.invariant(expected: "postalAddress"))
212225
}
226+
let isFullAddress = !postalAddress.street.isEmpty
213227
let country = mapToCountryCode(code: postalAddress.isoCountryCode)
228+
let paddedZipCode = isFullAddress ? postalAddress.postalCode : addPaddingToPostalCode(
229+
for: postalAddress.postalCode,
230+
in: country
231+
)
232+
214233
// HK does not have postal codes. Apple Pay puts Region in postalCode
215234
// See: https://github.com/Shopify/portable-wallets/blob/main/src/components/ApplePayButton/helpers/map-to-address.ts#L17
216235
var (zip, province): (String?, String?) =
217236
switch country {
218-
case "HK": (nil, postalAddress.postalCode)
237+
case "HK": (nil, paddedZipCode)
219238
default:
220239
(
221-
postalAddress.postalCode,
240+
paddedZipCode,
222241
!postalAddress.state.isEmpty
223242
? postalAddress.state
224243
: !postalAddress.subLocality.isEmpty ? postalAddress.subLocality : nil

Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIMutationsTests.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -885,8 +885,9 @@ final class StorefrontAPIMutationsTests: XCTestCase {
885885
deliveryOptionHandle: "express"
886886
)
887887

888-
XCTAssertEqual(cart.deliveryGroups.nodes.first?.selectedDeliveryOption?.handle, "express")
889-
XCTAssertEqual(cart.cost.totalAmount.amount, Decimal(string: "34.99")!)
888+
XCTAssertNotNil(cart)
889+
XCTAssertEqual(cart?.deliveryGroups.nodes.first?.selectedDeliveryOption?.handle, "express")
890+
XCTAssertEqual(cart?.cost.totalAmount.amount, Decimal(string: "34.99")!)
890891
}
891892

892893
// MARK: - Cart Payment Update Tests

Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ class MockStorefrontAPI: StorefrontAPIProtocol {
291291
func cartSelectedDeliveryOptionsUpdate(
292292
id _: GraphQLScalars.ID, deliveryGroupId _: GraphQLScalars.ID,
293293
deliveryOptionHandle _: String
294-
) async throws -> StorefrontAPI.Cart {
294+
) async throws -> StorefrontAPI.Cart? {
295295
fatalError(
296296
"cartSelectedDeliveryOptionsUpdate(id:deliveryGroupId:deliveryOptionHandle:) not implemented in test. Override this method in your test class."
297297
)

Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateControllerTests.swift

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,16 @@ final class ApplePayAuthorizationDelegateControllerTests: XCTestCase {
9191
}
9292

9393
func test_didSelectShippingMethod_withCartIDError_shouldReturnFailureStatus() async throws {
94-
try delegate.setCart(to: nil)
95-
94+
// Add the shipping method to available methods so it passes validation
9695
let shippingMethod = PKShippingMethod()
9796
shippingMethod.identifier = "standard-shipping"
9897
shippingMethod.label = "Standard Shipping"
9998

99+
delegate.pkDecoder = makeStubDecoder(methods: [shippingMethod])
100+
101+
// Now set cart to nil to cause cartID.get() to fail
102+
try delegate.setCart(to: nil)
103+
100104
let result = await delegate.paymentAuthorizationController(
101105
PKPaymentAuthorizationController(paymentRequest: .testPaymentRequest),
102106
didSelectShippingMethod: shippingMethod
@@ -223,7 +227,7 @@ final class ApplePayAuthorizationDelegateControllerTests: XCTestCase {
223227
XCTAssertFalse(delegate.pkDecoder.paymentSummaryItems.isEmpty)
224228
}
225229

226-
func test_didSelectShippingMethod_whenMethodIsInvalid_shouldFallbackToFirstAvailable() async throws {
230+
func test_didSelectShippingMethod_whenMethodIsInvalid_shouldNotSetShippingMethod() async throws {
227231
let firstAvailable = PKShippingMethod()
228232
firstAvailable.identifier = "first-available"
229233

@@ -233,27 +237,29 @@ final class ApplePayAuthorizationDelegateControllerTests: XCTestCase {
233237
delegate.pkDecoder = makeStubDecoder(methods: [firstAvailable])
234238
MockURLProtocol.lastOperation = nil
235239

236-
_ = await delegate.paymentAuthorizationController(
240+
let result = await delegate.paymentAuthorizationController(
237241
PKPaymentAuthorizationController(paymentRequest: .testPaymentRequest),
238242
didSelectShippingMethod: selected
239243
)
240244

241-
XCTAssertEqual(delegate.pkEncoder.selectedShippingMethod?.identifier, "first-available")
245+
XCTAssertNil(delegate.pkEncoder.selectedShippingMethod, "Should not set invalid shipping method")
246+
XCTAssertEqual(result.status, .failure, "Should return failure status for invalid method")
242247
}
243248

244-
func test_didSelectShippingMethod_whenNoMethodsAvailable_shouldKeepOriginal() async throws {
249+
func test_didSelectShippingMethod_whenNoMethodsAvailable_shouldNotSetShippingMethod() async throws {
245250
let selected = PKShippingMethod()
246251
selected.identifier = "only-method"
247252

248253
delegate.pkDecoder = makeStubDecoder(methods: [])
249254
MockURLProtocol.lastOperation = nil
250255

251-
_ = await delegate.paymentAuthorizationController(
256+
let result = await delegate.paymentAuthorizationController(
252257
PKPaymentAuthorizationController(paymentRequest: .testPaymentRequest),
253258
didSelectShippingMethod: selected
254259
)
255260

256-
XCTAssertEqual(delegate.pkEncoder.selectedShippingMethod?.identifier, "only-method")
261+
XCTAssertNil(delegate.pkEncoder.selectedShippingMethod, "Should not set shipping method when no methods available")
262+
XCTAssertEqual(result.status, .failure, "Should return failure status when no methods available")
257263
}
258264

259265
func test_didSelectShippingMethod_withSelectedDeliveryOptionHandleError_shouldReturnFailureStatus() async throws {

0 commit comments

Comments
 (0)