Inspiration

We do kiosks. Kiosks are great, but they mainly take payment from credit card swipes. What if you're in a bathing suit, or at a festival? Or in another country? Or hate carrying a wallet?

With Square's new APIs we could add pay-on-phone to our kiosks. So cool!

What it does

Our previous user experience:

  1. Start checkout
  2. Choose cash/card/gift card
  3. [Choose tip amount]
  4. Swipe/dip/tap to pay

old payment screen the old payment screen

Now we added two new options:

  1. QR Code
  2. Cash App

old payment screen

Presto! Customers can now pay for the order from their phone.

How we built it

We built this using two new APIs:

  1. Cash App Pay
  2. Checkout API

Cash App Pay

Since Cash App Pay is a web API we had to get a little tricky to get it into our native iOS app. Fortunately, native apps can run little mini web views ("micro browsers") and we have a ton of control of these little views. We can even send messages back and forth between our Swift code and the Javascript running inside the page.

To embed Cash App on a webpage it creates a button, and when a user taps the button the API--not our code--takes over and presents a QR code for the user to scan with their phone. We couldn't really touch this process (ahem, yes, we tried) so we had to give users direct access to this web view.

So, the game plan became:

  1. Load up the native buttons (Card, Cash, Gift Card, QR code, Cash App)
  2. Start loading a Cash App enabled web view off screen
  3. Once the html has loaded, resize & place it over the entire "Cash App" button
  4. Make it mostly transparent (if we make it all the way transparent it loses its user interactivity. We could have worked harder to avoid this but for now it will do)

secret html button if you look closely you can see the actual Cash App Button from the mostly transparent overlain web view

  1. Also, we had to make the button the full size of the web view, so we had to find it in the DOM and resize it. We also took away its black background and added our own event handler, cashAppClicked so we can react when it gets tapped.

    function waitForButton() {
    const button = document.querySelector('#cash_app_pay_v1_element')
    if(button) {
        const btn = button.firstChild.shadowRoot.firstChild
        btn.style.width = '100%'
        btn.style.height = '100%'
        btn.style.background = 'rgba(0,0,0,0)'
        clearInterval( findButtonInterval )
        btn.addEventListener('click', cashAppClicked)
        window.webkit?.messageHandlers.flashOrder.postMessage( {ready: true} )
    }
    }
    

    (Thank you, Cash App team, for not making the shadow root "closed" or we wouldn't have been able to do this!) (Also, note the call to window.webkit?.messageHandlers.flashOrder.postMessage(), which is how Javascript tells Swift that this web view is ready to get placed over the button)

  2. Once the cashAppClicked method is called (the user has tapped the button) we hide the other buttons and grow it to a bigger size.

    async function cashAppClicked() {
    window.webkit?.messageHandlers.flashOrder.postMessage( {event: 'clicked'} )
    ...
    }
    

    Javascript telling Swift it's time to go big

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if let body = message.body as? [String:Any],
           let keyValue = body.first {
            switch keyValue.key {
            case "ready":
                putWebAppInPosition()
    
            case "event":
                switchToCashAppScanMode()
            ...
            }
        }
    }
    
    fileprivate func switchToCashAppScanMode() {
        UIView.animate(withDuration: 0.5) {
            self.wkView?.frame = self.view.frame.zeroed
            self.wkView?.alpha = 1.0
            self.collectionView.alpha = 0.0
        }
    }
    

    Swift code reacting to user tapping the Cash App button

reaction to cash app tappped

  1. The devil is in the details, so we also had to listen for the user canceling the payment:
async function cashAppClicked() {
    let cancelButton = await waitFor( () => {
        const sr = document.querySelector('#cash_app_pay_v1_element')?.firstChild.shadowRoot
        if(sr && sr.querySelectorAll('button').length > 1) {
            return sr.querySelectorAll('button')[1]
        }
        return null
    })
    thing.addEventListener('click', () => {
        window.webkit?.messageHandlers.flashOrder.postMessage( {cancel: 'clicked'} )
    })
}

Javascript adding listener to Cash App's cancel button. Admittedly pretty hacky!

    ...
    case  "cancel":
        cancelledCashApp()
    ...

fileprivate func cancelledCashApp() {
    resetUI()
}

Swift handling cancel

  1. Once the user authorizes the payment, Cash App fires an ontokenization event that includes the token that can be used to make the payment. Here we use the pretty standard way of receiving that event, but then once again use our window.webkit?.messageHandlers to send the token on to the native app for the next steps.

    async function setupCashApp() {
    try {
        const cashAppPay = await initializeCashApp(payments);
    
        cashAppPay.addEventListener('ontokenization', function (event) {
            const { tokenResult, error } = event.detail;
            if (error) {
                window.webkit?.messageHandlers.flashOrder.postMessage( {error: error} )
            }
            else if (tokenResult.status === 'OK') {
                window.webkit?.messageHandlers.flashOrder.postMessage( {token: tokenResult.token} )
            }
        });
    
        findButtonInterval = setInterval(waitForButton, 100)
    
    } catch (e) {
        console.error('Initializing Cash App Pay failed', e);
    }
    }
    
  2. A this point we have a payment token so we send it to our backend which handles the rest, including loyalty, sending receipts, logging, etc.

Checkout API

Integration of the Checkout API included:

  1. Provide the payment option (pictured earlier. We labeled it "QR code" for now)
  2. When tapped, hit our backend, which hits Square for the checkout link
  3. Show link to customer
  4. Wait for the payment.created webhook and notify the kiosk via websocket

QR code from checkout APIThis required us to up our webhook game a little.

Challenges we ran into

We ran into these challenges:

  1. Timing of asking for the tip
  2. Our new enemy: the dreaded "GENERIC DECLINE"
  3. Using a web API in a native app
  4. Checkout API links didn't work initially
  5. Styling the Cash App button
  6. Order of the Order API Sequence

Tip timing

In almost all of our other payment methods we ask for the tip after the payment method is selected. We do this primarily because we just skip the question when "Cash" is selected.

Also with Checkout API, we don't even pass in a tip as they decide that within the Square hosted checkout page, so whatever choice they make within the kiosk would be irrelevant.

However, asking for the tip after the payment method screen caused a problem when using Cash App, because the amount we send to Cash App needs to match including the tip.

This wasn't a huge deal--we just switched up the order and now ask for tip first.

GENERIC_DECLINE

This one is still an open issue. We got our Cash App payments to go through once we got everything right--using the correct OAuth token, matching the amount to the order amount + tip, etc. However, we found that on some accounts we just couldn't get the payment to go through, and Square's servers would respond with a GENERIC_DECLINE error reason. We'll have to work with Square's team to figure this one out.

Web API instead of Native

Of course we have dealt with this before and it wasn't a huge deal. We explain a lot of the details above but it was worth mentioning here since some of the things we had to do really put the "hack" in "hackathon"!

Broken Checkout API

We integrated the Checkout API very early on, and in the very early stages the checkout page had some issues. We could not actually get through a payment so we tabled it for awhile and it magically started working when we came back to it.

Styling the Cash App Button

A lot of this is also mentioned in the previous section, but in addition to embedding the Cash App button transparently, we wanted it to be a bit bigger on our own web app (which we will discuss below). We basically used the same approach of looking for a known HTML element, which was the parent of the shadow root, which was an ancestor of the button. We couldn't modify the style sheet of the shadow element (for good reason--we get it) so we just modified some properties with Javascript. Not too bad in the end, just took a little experimentation.

    const cap = document.querySelector('#cash_app_pay_v1_element')
    const btn = cap.firstChild.shadowRoot.firstChild
    btn.style.width = '100%'


Order of the Orders API Sequence

The main challenge of working with the Cash App integration was our lack of control. We could not initiate the payment collection process programmatically--the button had to be pressed by the user. We even tried fabricating and dispatching our own 'click' event on their button, but the API knew it by checking the isTrusted property.

This would be less of a problem if we weren't extremely picky about keeping the number of taps from the customer to a minimum. Because of this we wanted the action of selecting Cash App as a payment method to be the same as pressing the web button. Anything else would require an unnecessary tap.

So as a result, we had to do all the tricky stuff mentioned earlier. Another thing not mentioned previously is that we had to actually call Orders API before the user selects the payment method. We do this to get the final total, which has to be passed onto the Cash App API before it does its thing. Since this takes a slight delay, along with subsequently loading the HTML we made the button asynchronous and added a waiting indicator. It only takes a moment and looks like this: waiting for final total Once the order is created, the Cash App button is set up, the button's disabled out state goes away and it is ready to be tapped.

But wait... there's more!

Also, drum roll.... we built this into our web app! So we actually have 3 integrations: 2 for the kiosk and 1 for the web.

Here is a look at the web app checkout: Web checkout

Accomplishments that we're proud of

We are super happy that we were able to overcome the challenges and build this.

Also, it's real. It is live right now and for our web customers, and you can try it out!. Here are some examples: Our fake coffee shop Bumblebee's BBQ Funnel Cake House

Notes: Use your phone to see the magic. Our formatting for desktops is forthcoming. If you actually check out be advised you won't actually get anything

Our kiosk customers can actually roll this out if they want in our next build cycle.

What's next for Flash Order - Readerless Checkout

The coolest thing happened the other day... someone called us and asked... "do you support paying with Cash App? What about QR codes?

Yes, and yes!

This vendor will be selling at festivals and said the young people only want to pay with their phones. She is going live in two weeks after the due date for this hackathon.

In short, we are rolling this baby out. We are all about seamless ordering so these new methods will enable some great new experiences.

Feedback for Square

Some of this is woven through the drama above, but in summary:

  1. We would love to be able to initiate the Cash App payment process programmatically, as opposed to just in response to the user clicking the button. We can see why you would prevent this, especially since the experience in the Cash App didn't ask for authorization or amount confirmation. Maybe the Cash app could make a distinction from a payment triggered programmatically and then treat it as less trusted? I.e. ask the user to confirm?
  2. We would love to have the ability to pass in a tip amount to the Checkout API. In our case the user has already chosen this before choosing the payment method (although mostly because of a Cash App workaround, mentioned earlier)
  3. Would it be possible to have a little more control of the Square hosted Checkout API page? Specifically, not requiring a name and address? Perhaps another flag in CreatePaymentLinkRequest? Or would it be bad for us to pre-populate those fields with "N/A"?
  4. We are really excited about the Checkout API opening up new international markets for us. It will also enable different form factors and situations. Keep up the good work!

Built With

Share this project:

Updates