Apple Pay

I got Apple Pay working on the DNA Lounge store. Hooray.

There's one weirdness that I'm not sure how to solve, though. Clicking "buy" used to work like this: do some AJAX to turn the CC into a token, when that comes back, actually submit the form; on the back end, do some database junk; do some payment junk; and finally emit HTML saying "thank you" or "error".

With Apple Pay it's basically the same but with different JS junk -- however, said JS junk seems to want me to invoke its "completePayment" method after I've done my backend payment junk and determined whether it succeeded or failed. And calling that method on is, you know, tough to do after I've submitted a form and loaded a new page and the ApplePaySession object no longer exists.

If I don't call that method, it seems to work anyway -- the order goes through, the card is charged -- but the Apple Pay popup on the phone times out and says the order didn't complete, which is less than ideal.

So, short of rewriting everything to work completely differently... any brilliant suggestions?

Anyway, take a look at checkout.js and let me know if you see anything stupid in there.


Tags: , , , ,

9 Responses:

  1. phred says:

    Unless Apple recently changed this you _have_ to do the Apple Pay completePayment on the Payment before you navigate away. Your
    JavaScript is running inside some kind of GUI event loop for Apple Pay payments and it’s calling your JS to know what to do next in the native UI.

    There’s no way around the “must stay on same page” requirement, this is a JavaScript driven thing through and through & Safari won’t let you persist the PaymentResult object you need to call the method on to the next page.

    Your choices involve using JavaScript to post the form with AJAX, and then interpret that response. completePayment() takes an enum that is either STATUS_SUCCESS / STATUS_FAILURE and your JS is supposed to look at your payment gateway response and decide which. My approach would be to use jquery (or equivalent) to do the POST. Maybe also make it so that the server sends JSON back to your frontend code.

    tl;dr put the webshit with the webshit to call the webshit API

    • jwz says:

      So it turns out that it works to just always call completePayment with "success" before submitting the form to my backend. This causes the Apple Pay dialog to say "Ok" and dismiss itself, and then the result of the form submission shows the user whether the transaction succeeded or failed.

      This seems sketchy, but I guess it's fine? So long as the API doesn't change to cause 'completePayment' to do something more necessary than simply dismissing the dialog.

      • phred says:

        Ah yep, always calling completePayment() before form submission is fine. completePayment() has no effect whatsoever on your ability to charge the Apple Pay token, it’s purely a GUI thing.

        The idea in the API is that you can handle payment failures without ever leaving the Apple Pay sheet, by means of Javascript cleverness. You trade tight coupling with Apple Pay for nicer UX. It’s not at all a requirement to do it that way, though, so your approach is sound; just ask the customer to provide Apple Pay again (or use your regular checkout form) if the payment declines.

        FWIW I don’t think this flow or completePayment() have changed measurably since the original release of Apple Pay on the Web back ca. 2016. Apple did a smart job of making sure that you don’t have to upgrade to a new version of their JS API to keep getting paid: you have to specify an API version when you set up the Payment Request, and as far as I’ve seen there’s no pressure to upgrade.

        • jwz says:

          Yeah, I read the changelogs of the various API versions and it seemed like I needed to go with version 6 (macOS 10.14.4, iOS 12.2) because that introduced requiredShippingContactFields which is the only way to tell it "I absolutely require an email address with this order".

  2. M says:

    If you don't want to change much

    - add a hidden-ish iframe with a name attribute
    - set the form target attribute to that name
    - the backend does payment junk and then outputs just a script tag, that postMessages either "thank you" or "error" to parent window
    - parent window listens for message event and completes the Apple Pay junk
    - if you want to avoid further webshit, parent window redirects to "thank you" or "error" page

    At least then the only JS is window.parent.postMessage('foo') and window.addEventListener('message', dealWithIt). And you don't have to rewrite everything.

    • jwz says:

      This sounds nearly indistinguishable from "changing everything".

      Maybe I could read the output of the POST and then replace the entire 'document' with it. But I vaguely recall having tried to do that with some other app in the past and it not working out very well.

  3. jwz says:

    It's working with the credit card that I have in Apple Pay, but it's not working when I try to use "Apple Pay Cash". I'm getting "155 This processor does not support this method of submitting payment data". The "cash" transaction in ApplePaySession.onpaymentauthorized is showing up as a Discover debit card (I don't have one of those, so this must be an Apple implementation detail.) As far as I can tell, my account is configured to accept Discover. Any ideas?