Contactless payments with iOS Shortcuts, Stripe, and Serverless Functions

May 19, 2021

What do you do if you need to take contactless card payments in person, but don’t have a point of sale?

You build one.

tldw: use iOS Shortcuts to prompt for what's being sold; make an API call to stripe; turn the response into a QR code; customer scans QR code; Apple Pay!

My best friend and I are in the business of delivering choripanes from our grill to your door—but for one weekend, we wanted hand them out directly to the people of London.

We checked the forecast and spotted, due in 5 days, on a Saturday, the first predicted sunny day in a while; and, best of all, it fell on the first weekend Londoners could have social gatherings outside since the third Covid-19 lockdown. Perfect!

Our kitchen is online only so we’ve never had a reason to own a physical PoS. We thought about ordering one for the weekend, but it looked unlikely that we’d receive any in time.

A distinguishing advantage of ours is that we’re vertically integrated. Taking inspiration from Crave Cookie, we built our own checkout, delivery tracking, and operations software to keep costs low and margins high. That means we have a bunch of software on hand to play with.

So we decided to piece together a PoS using the tools already in our toolbox: Stripe, Cloudflare, and our iPhones.

Tool one: Stripe

Our checkout is optimised for our online order flow, it wasn’t going to be useful to us in the physical world. Instead of hacking changes onto what existed, we opted to use Stripe Checkout to relieve the burden of creating a throwaway experience. For those not in the know:

Checkout creates a secure, Stripe-hosted payment page that lets you collect payments quickly. It works across devices and can help increase your conversion

https://stripe.com/docs/payments/checkout

Integrating Checkout is a little easier if used alongside Stripe’s Prices API.

The only thing to do in this step is open Stripe’s dashboard, create a new product and set a price. The price ID will be used later when creating the checkout session.

Stripe Dashboard

Tool two: Serverless Functions

Checkout requires the customer be redirected via a client-side javascript call to redirectToCheckout. That means we can’t send the customer to the checkout in a single step; we have to use an intermediary webpage that will perform the redirect.

Rather than add another route to our application’s router, hook up a redirect page, cut a release, and deploy to our servers, we decided to use our first serverless function.

We already host our DNS with Cloudflare—using their serverless function service, workers, made sense for us.

Creating the worker project

Download wrangler, Cloudflare’s CLI application for managing workers.

Create a workers project

wrangler init && cd worker

Configuration

The worker will need to know your:

  • Cloudflare account ID
  • Cloudflare zone ID
  • Stripe API key

Configure both production and test environments so that you can take your integration for a spin before going out into the real world.

# wrangler.toml
name = "checkout-session-dev"
type = "javascript"
account_id = "your_cloudflare_account_id"
workers_dev = true
route = ""
zone_id = "your_cloudflare_zone_id"
usage_model = ""
vars = { STRIPE_API_KEY = "pk_test_abcxyz..." }

[env.prod]
name = "checkout-session"
vars = { STRIPE_API_KEY = "pk_live_abcxyz..." }

The Worker itself

The worker is a simple HTML page with an embedded script that pulls your Stripe API key from the workers environment then redirects the customer to the checkout session found in the session_id query parameter.

// index.js
function html(session_id) {
  return `<!DOCTYPE html>
  <body>
    <h1>Redirecting</h1>
    <p>This shouldn't take long...</p>
    <script src="https://js.stripe.com/v3/"></script>
    <script>
        var stripe = Stripe("${STRIPE_API_KEY}");
        stripe.redirectToCheckout({ sessionId: "${session_id}" });
    </script>
  </body>`
}

/**
 * worker API
 */
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  const { searchParams } = new URL(request.url)
  const session_id = searchParams.get('session_id')

  return new Response(html(session_id), {
    headers: {
      "content-type": "text/html;charset=UTF-8",
    },
  })
}

Deploying the worker

Publish both versions of your worker at once

wrangler publish && wrangler publish --env=prod

Tool three: iOS Shortcuts

We have our product, and we have the mechanism to load the checkout—let’s use iOS Shortcuts to glue them together.

As far as shortcuts go, this one is simple:

  • A dictionary for some configuration values;
  • Ask for quantity of your product;
  • Create a Checkout session;
  • Construct a URL: webhook + session_id;
  • Turn the result into a QR code.

Get the shortcut. Or, in case of bit rot, here’s what the shortcut should look like.

# Dictionary
api_key: sk_test_abc
price_id: price_abc
worker_url: https://worker-url.dev
# Ask for Input
Input type: number
Prompt: "How many?"
# Get Contents of URL
URL: "https://api.stripe.com/v1/checkout/session"
Method: POST
Headers:
	Authorization: Bearer $api_key
Request Body (Form):
	success_url: "https://your.website"
	cancel_url: "https://indiehackers.com"
	payment_method: "card"
	mode: "payment"
	line_items[0][price]: $price_id
	line_items[0][quantity]: $ProvidedInput
# Get Dictionary Value
Get: Value
For: "id"
In: ${Contents of URL}
# Generate QR Code
Generate From: ${worker_url}?session_id=${Dictionary Value}
# Show Notification
Value: ${QR Code}

Once you’re set up, run your action to see it play out.

Swipe down on the notification and grab a friend to point their camera at the QR code.

If you’re using test API keys you can complete the payment without spending a penny, and then when you’ve confirmed everything works, update the dictionary at the start of the shortcut to your production keys and you’ll be ready to go!

* I recommend a 404 or page you don’t own for the cancel_url so you know the payment failed. If your customer can show you your own landing page after payment, you know it was successful.

Wrap Up

The Team

In the end we had a successful day. The weather turned out perfect, and the parks were packed as we had hoped. We sold choripan, made friends, and brought more attention to our brand. Our PoS solution worked wonderfully, even turning a few heads when people expected us to be a cash-only operation.

One last thing

We actually went a step further and set up Stripe webhooks with Zapier and Slack for real-time notifications, but in the end they weren’t useful for us. The landing page was enough confirmation to hand over the goods.


Expect posts on engineering, bootstrapping, and ramblings

or subscribe with rss