Shopify / mobile-buy-sdk-ios

Shopify’s Mobile Buy SDK makes it simple to sell physical products inside your mobile app. With a few lines of code, you can connect your app with the Shopify platform and let your users buy your products using Apple Pay or their credit card.
MIT License
452 stars 199 forks source link

How to know if completed tokenized payment for Apple Pay was a success or failure after polling for payment.ready == true? #1154

Open xanderbuck opened 3 years ago

xanderbuck commented 3 years ago

I'm asking this question here on GitHub issues rather than Shopify developer forums because neither the documentation on this repo nor the sample app makes this clear.

Question

My question is how do I know if completing a tokenized payment for an Apple Pay payment was a success or failure after polling for payment.ready == true?

What I'm Doing

First I complete the checkout using the mutationcheckoutCompleteWithTokenizedPaymentV3. To confirm this mutation was successful I check the following

  1. Success block is fired
  2. checkoutUserErrors.count == 0
  3. Payment object is created and not nil (which signifies the async operation of payment has begun).

Second, I then poll on the payment object created for payment.ready == true.

From my understanding payment.ready == true doesn't signify a successful payment, it only signifies the async operation of the payment processing has finished.

From here how do I know if the payment was a success or failure once payment.ready == true?

Is it safe to do the following check to confirm if payment was successful or not?:

if payment.errorMessage == nil {
   //payment success
} else {
   /// payment failure
}

The sample app seems to fall short here and marks a success regardless of the error message given. The sample app has:

let retry = Graph.RetryHandler<Storefront.QueryRoot>(endurance: .finite(30)) { response, error -> Bool in
            error.debugPrint()

            if let payment = response?.node as? Storefront.Payment {
                print("Payment not ready yet, retrying...")
                return !payment.ready
            } else {
                return false
            }
        }

        let query = ClientQuery.queryForPayment(id)
        let task  = self.client.queryGraphWith(query, retryHandler: retry) { query, error in

            if let payment = query?.node as? Storefront.Payment {
                print("Payment error: \(payment.errorMessage ?? "none")")
                completion(payment.viewModel)
            } else {
                completion(nil)
            }
        }

But what if there was an error? Why does it completely disregard the errorMessage?

xanderbuck commented 3 years ago

Any help you can provide on this @vixdug?

xanderbuck commented 3 years ago

I want to add to this:

I'm running into a case where a user is using a promo code meant for new users. In this case it looks like the user is using an email that isn't a new user but, because the Apple Pay checkout doesn't let you set the email used for the checkout until the user authorizes payment in Apple Pay - the promo code looks applicable up until the point the user authorizes payment.

For whatever reason, for this case, the payment object looks to be created after calling checkoutCompleteWithTokenizedPaymentV3, I then begin polling for payment.ready. This polling continues until it hits the limit I set (12 times). At the end of polling, I get no errorMessage and payment.ready == false. I'm in the dark here of what really is the result of this payment. Was my limit of 12 times too short and it just needed to poll some more times to confirm a success payment? Did it actually fail and theres a bug where no message is set in payment.errorMessage?

xanderbuck commented 3 years ago

Ultimately I think on Shopify's end, they need to add better error messaging/handling around invalid promo codes being used during Apple Pay checkout. I don't see any errors ever thrown for this on Shopify's end

xanderbuck commented 3 years ago

Update on the new user promo code. It has the following criteria

  1. Must be new user (group based on customers with less than 1 order)
  2. Order must be over $100.

I've noticed two customers so far, both with 1 previous order, that tried using this new user promo code and they both got the following after polling after checkoutCompleteWithTokenizedPaymentV3 (from my firebase logs) :

  1. payment.errorMessage = nil
  2. payment.ready = false

It should be noted that when I check the promo code from the response of checkoutCompleteWithTokenizedPaymentV3, the promo code's applicable property is true - interestingly the checkout object from the checkoutCompleteWithTokenizedPaymentV3 response has checkout.ready = false, which doesn't make sense because I poll for checkout == ready before calling checkoutCompleteWithTokenizedPaymentV3

xanderbuck commented 3 years ago

Update on new user promo code:

Problem

The issue definitely seems to be in us defining a "New Customers" group as customers with less than 1 order.

For users that have exactly 1 order, and then try to use our New Customers promo code, polling for payment.ready == true seems to time out for our 12 times endurance. Since it times out I present to the user an "order could not be processed" error alert. When I check Shopify orders section, theres no order that displays.

Interestingly enough, when 20 minutes go by, the customer seems to get an order confirmation alert and when I look at the Shopify orders section, the order finally displays. It seems like defining the "New Customers" group as customers with less than 1 order, throws off Shopify from being able to process the order in a reasonable amount of time.

Possible solution

I tried redefining our "New Customers" group as customers with 0 orders, and the error checking I have custom built (looking for promoCode.applicable == false) finally seems to work, while it didn't work when we defined new customers as with < 1 orders... the promoCode always had applicable == true.

callosaurus commented 3 years ago

Hey @xanderbuck,

Thanks for all the detailed notes; I can take a look into this. It's quite possible the edge case isn't surfaced/handled appropriately as an error message etc.

Just to rule something out - checking out with Apple Pay works fine for you when not using a promo code at all, correct?

Can you send over the x-request-id, found in the response headers, of an example checkoutCompleteWithTokenizedPaymentV3 (from within the last 10 days)? We can take a look through the logs further.

Can you also try upping the retry limit well past 12 times (e.g. to some interval of time over 5 minutes)? This is obviously an unrealistic UX to expect for the customer but, for testing purposes, I'm curious as to what extent the async nature of a customer being added to the group has an effect here.

xanderbuck commented 3 years ago

Hey @callosaurus, thank you!

Yes - checking out with Apple Pay works just fine when not using a promo code at all. It also works just fine for all promo codes, except this one.

I've got the x-request-id all ready to send you. Do I have to worry security wise sending this over to you in a public forum? Not sure how sensitive this id is. Maybe I could send it over to your Shopify email? (my email alex.buck@outdoorvoices.com)

Let me know and I can send it over right away!

MehboobShahKL commented 1 year ago

@callosaurus could you please help out on how youre generating the token for checking out with the checkoutCompleteWithTokenizedPaymentV3 mutation?