mobile-buy-sdk-ios: Checkout failed: Shipping rate can't be blank
None of the above? Create an issue. Be sure to include all the necessary information for us to understand and reproduce the problem:
I’ve seen a few older posts about this but no solutions were helpful. I’ve followed the readme and also copied the functions and code almost verbatim (minus the view models) from the example app in this repo, yet when I try to checkout with Apple Pay, I still get the error
Optional([<CheckoutUserError: [“message”: Shipping rate can’t be blank, “field”: <null>]>, <CheckoutUserError: [“message”: Shipping line can’t be blank, “field”: <null>]>]) Checkout failed to complete.
SDK Version 3.6.0
In my CartViewController
:
extension CartViewController: PaySessionDelegate {
private func convert(checkout: Storefront.Checkout) -> PayCheckout {
let lineItems: [PayLineItem] = checkout.lineItems.edges.map { item in
let price = item.node.variant!.priceV2.amount
let quantity = item.node.quantity
return PayLineItem(price: price, quantity: Int(quantity))
}
let shippingAddress = PayAddress(
addressLine1: checkout.shippingAddress?.address1,
addressLine2: checkout.shippingAddress?.address2,
city: checkout.shippingAddress?.city,
country: checkout.shippingAddress?.country,
province: checkout.shippingAddress?.province,
zip: checkout.shippingAddress?.zip,
firstName: checkout.shippingAddress?.firstName,
lastName: checkout.shippingAddress?.lastName,
phone: checkout.shippingAddress?.phone,
email: nil
)
let (subtotal, tax, shipping, total) = PaymentManager.paymentParts(subtotal: self.subtotal)
let shippingRate: PayShippingRate
if let shippingLine = checkout.shippingLine {
// this hits automatically as Apple Pay seems to select the cheapest shipping rate. It also hits when the user explicitly selects a shipping rate
shippingRate = PayShippingRate(handle: shippingLine.handle, title: shippingLine.handle, price: shippingLine.priceV2.amount)
} else {
shippingRate = PayShippingRate(handle: "Default Shipping", title: "Default Shipping", price: shipping)
}
return PayCheckout(
id: checkout.id.rawValue,
lineItems: lineItems,
giftCards: nil,
discount: nil,
shippingDiscount: nil,
shippingAddress: shippingAddress,
shippingRate: shippingRate,
currencyCode: "USD",
subtotalPrice: subtotal,
needsShipping: true,
totalTax: tax,
paymentDue: total
)
}
private func convertShippingRates(_ rates: [Storefront.ShippingRate]) -> [PayShippingRate] {
return rates.map { PayShippingRate(handle: $0.handle, title: $0.title, price: $0.priceV2.amount) }
}
func paySession(_ paySession: PaySession, didRequestShippingRatesFor address: PayPostalAddress, checkout: PayCheckout, provide: @escaping (PayCheckout?, [PayShippingRate]) -> Void) {
print("didRequestShippingRatesFor")
print("Updating checkout with address...")
ShopifyClient.shared.updateCheckout(checkout.id, updatingPartialShippingAddress: address) { checkout in
guard let checkout = checkout else {
print("Update for checkout failed.")
provide(nil, [])
return
}
print("Getting shipping rates...")
ShopifyClient.shared.fetchShippingRatesForCheckout(checkout.id.rawValue) { result in
if let result = result {
print("Fetched shipping rates.")
let payCheckout = self.convert(checkout: result.checkout)
let rates = self.convertShippingRates(result.rates)
provide(payCheckout, rates)
} else {
provide(nil, [])
}
}
}
}
func paySession(_ paySession: PaySession, didUpdateShippingAddress address: PayPostalAddress, checkout: PayCheckout, provide: @escaping (PayCheckout?) -> Void) {
print("Updating checkout with shipping address for tax estimate...")
ShopifyClient.shared.updateCheckout(checkout.id, updatingPartialShippingAddress: address) { checkout in
if let checkout = checkout {
let payCheckout = self.convert(checkout: checkout)
provide(payCheckout)
} else {
print("Update for checkout failed.")
provide(nil)
}
}
}
func paySession(_ paySession: PaySession, didSelectShippingRate shippingRate: PayShippingRate, checkout: PayCheckout, provide: @escaping (PayCheckout?) -> Void) {
print("Selecting shipping rate...")
ShopifyClient.shared.updateCheckout(checkout.id, updatingShippingRate: shippingRate) { updatedCheckout in
print("Selected shipping rate.")
let payCheckout = self.convert(checkout: updatedCheckout!)
provide(payCheckout)
}
}
func paySession(_ paySession: PaySession, didAuthorizePayment authorization: PayAuthorization, checkout: PayCheckout, completeTransaction: @escaping (PaySession.TransactionStatus) -> Void) {
// I have an `authorization.shippingRate` here... and a good `authorization.shippingAddress`
guard let email = authorization.shippingAddress.email else {
print("Unable to update checkout email. Aborting transaction.")
return completeTransaction(.failure)
}
ShopifyClient.shared.updateCheckout(checkout.id, updatingCompleteShippingAddress: authorization.shippingAddress) { updatedCheckout in
guard let _ = updatedCheckout else {
print("unable to update shipping address")
return completeTransaction(.failure)
}
print("Updating checkout email...")
ShopifyClient.shared.updateCheckout(checkout.id, updatingEmail: email) { updatedCheckout in
guard let _ = updatedCheckout else {
print("unable to update checkout email, aborting txn.")
return completeTransaction(.failure)
}
print("Checkout email updated: \(email)")
print("Completing checkout...")
ShopifyClient.shared.completeCheckout(checkout, billingAddress: authorization.billingAddress, applePayToken: authorization.token, idempotencyToken: paySession.identifier) { payment in
if let payment = payment, checkout.paymentDue == payment.amountV2.amount {
print("Checkout completed successfully.")
completeTransaction(.success)
} else {
print("Checkout failed to complete.")
completeTransaction(.failure)
}
}
}
}
}
func paySessionDidFinish(_ paySession: PaySession) {
// Do something after the Pay modal is dismissed
print("didFinish")
}
}
In my ShopifyClient
func completeCheckout(_ checkout: PayCheckout, billingAddress: PayAddress, applePayToken token: String, idempotencyToken: String, completion: @escaping (Storefront.Payment?) -> Void) {
let mutation = ClientQuery.mutationForCompleteCheckoutUsingApplePay(checkout, billingAddress: billingAddress, token: token, idempotencyToken: idempotencyToken)
let task = self.client.mutateGraphWith(mutation) { response, error in
error.debugPrint()
print(response?.checkoutCompleteWithTokenizedPaymentV2?.checkoutUserErrors) // errors appear here
if let payment = response?.checkoutCompleteWithTokenizedPaymentV2?.payment {
print("Payment created, fetching status...")
self.fetchCompletedPayment(payment.id.rawValue) { paymentViewModel in
completion(paymentViewModel)
}
} else {
completion(nil)
}
}
task.resume()
}
Expected: Apple Pay checkout works
Actual: I keep getting the error above, even when I go into the Apple Pay prompt and explicitly select the shipping option.
As I mentioned before, I just copied identically what happens in the example app except for the view models. I return the actual Storefront
. objects instead.
I’d love to know what I’m doing wrong here. Let me know if you need to see any additional code
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Comments: 17 (17 by maintainers)
One thing I forgot to mention is
allowPartialAddresses
. Please make sure this is set totrue
on the checkout. It’s a requirement for Apple Pay. I’m verifying now whether it impact theshippingLine
preservation between updates.I took it out and it worked! Thanks @dbart01! Really appreciate your help here. I’d recommend removing this function and callback from the example app so others don’t get confused.
So it looks like it’s sending the handle correctly:
"usps-FirstPackage-3.09"
. Could you do the same for the response? Let’s see if the updated checkout comes back with that shipping handle attached.