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)

Most upvoted comments

One thing I forgot to mention is allowPartialAddresses. Please make sure this is set to true on the checkout. It’s a requirement for Apple Pay. I’m verifying now whether it impact the shippingLine 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.