Serverless Instant Checkout Links
Richard Moot
Posted on June 27, 2018
Servers beware! Watch out containers! Our serverless overlords are here! There is a lot of hype around serverless and talk about it being the end of containers, but we’re still not seeing a whole lot of major adoption of the technology (at least not seeing many companies vocal about it). My hope is to spark a little interest on what is possible in this space by showing different ways to use serverless beyond image processing, ETL, or general async tasks (but its really great for those things!). We’re still sticking with eCommerce though.
In a previous post, I talked a bit about how to create a Super Simple Serverless eCommerce site that used AWS Lambda and S3 to do serverless checkout payments. This got me thinking about creating something a little more advanced—something almost any Square seller could use to allow customers to immediately purchase products using just a link. We’ll go over a simple version of the application I built using only four Google Cloud functions via Firebase. The idea behind switching from AWS Lambda to Firebase here is to demonstrate different platform offerings in the serverless space, but also because there were a lot more tools available on Firebase to support this kind of service. It’s also worth having some examples out there that aren’t needing to use the Serverless Framework.
The application consists of:
Cloud Functions (duh)
Shortlinks via Firebase Dynamic Links
Warning! Clicking on this image will not actually connect your Square account.
The front-end of the application originally was just static pages, but has since been refactored into a fairly simple React application. The main focus here will be on using serverless functions for doing our processing though. The first step in getting our instant checkout links created requires setting up OAuth in order to get our user’s access token. For this, we will create our first two functions, named “authorize” and “code”. Google does a great job of explaining how to use Firebase CLI on their site so we’ll skip that.
If you’re not familiar with OAuth or want to know more about OAuth with Square, checkout (pun intended) my previous blog post “OAuth, wherefor art thou?”.
Now let’s dig into some sweet, sweet code examples.
const functions = require('firebase-functions');
const crypto = require('crypto');
exports.authorize = functions.https.onRequest((request, response) => {
const squareAuthURL = "https://connect.squareup.com/oauth2/authorize?";
const state = (crypto.randomBytes(32)).toString('hex');
response.set('Set-Cookie', `__session=${state}; Secure`);
response.send(
squareAuthURL +
`client_id=${functions.config().square.prod.app_id}&` +
`response_type=code&` +
`scope=MERCHANT_PROFILE_READ PAYMENTS_WRITE ORDERS_WRITE ITEMS_READ&` +
`session=false&` +
`locale=en-US&` +
`state=${state}`
);
});
Here we are just creating a string to send to the client-side for redirecting our user. The main purpose here is more about generating our state, creating the cookie, and passing it along for us to verify later when we get the callback from Square redirecting our user back to us.
exports.code = functions.https.onRequest((request, response) => {
const tokenURL = "https://connect.squareup.com/oauth2/token";
const redirectURI = "https://checkout-now.firebaseapp.com/callback";
const cookieState = getSessionCookie(request.get('cookie'))
const { code, state } = request.body;
if (cookieState === state) {
const { app_id, secret } = functions.config().square.prod;
let access_token, id, email, name;
return axios.post(tokenURL, {
client_id: app_id,
client_secret: secret,
code: decodeURIComponent(code),
redirect_uri: redirectURI
})
.then(token => {
({ access_token } = token.data);
return axios.get("https://connect.squareup.com/v1/me", {
"headers": {
"Authorization": `Bearer ${access_token}`
}
}).then(user => {
({ id, email, name } = user.data);
return createFirebaseAccount(id, email, name, access_token)
}).then(firebaseToken => {
return response.json({
"email": email,
"token": firebaseToken
});
}).catch(error => {
console.log(error);
return response.send(error);
});
});
} else {
console.log(`INVALID STATE: ${cookieState} === ${state}`);
return response.send(`INVALID STATE: ${cookieState} === ${state}`);
}
});
We’re definitely doing a bit more here than just getting our access token from the callback. We are looking up the user’s info using their access token and then creating an account in Firebase for that user’s email. The intention here was to disallow a user to create an account using any email they wanted, and strictly having it tied to the email that is set for their Square account. We’ve omitted the createFirebaseAccount()
function definition, since it’s just another detail peculiar to Firebase for creating accounts.
We’re in a pretty good place! We have authorized our user, received an access token, set the user up in Firebase, stored their token, and now we can start using Square’s API’s for generating our instant checkout links.
After our user gets their sign-in link in their email and is redirected back to our app to sign in, we need to generate our links to send to the user. This is where we’ll need the catalog
function.
exports.catalog = functions.https.onRequest((request, response) => {
let uid;
admin.auth().verifyIdToken(request.body.idToken)
.then(decodedToken => {
({
uid
} = decodedToken);
return admin.database()
.ref(`/squareAccessToken/${uid}`)
.once('value')
}).then(userToken => {
(SquareConnect.ApiClient.instance).authentications["oauth2"]
.accessToken = userToken.val();
const catalogApi = new SquareConnect.CatalogApi();
return catalogApi.listCatalog()
}).then(catalog => {
let formattedResponse = catalog.objects.map((elem) => {
return axios.post(`https://firebasedynamiclinks.googleapis.com/v1/shortLinks?` +
`key=${functions.config().web.key}`, {
"longDynamicLink": `https://checkout.page.link/?` +
`link=https://checkout-now.firebaseapp.com/checkout/` +
`${(uid.split(":"))[1]}/${elem.item_data.variations[0].id}`,
"suffix": {
"option": "SHORT"
}
}).then(resp => {
return {
name: elem.item_data.name,
catalogId: elem.id,
varId: elem.item_data.variations[0].id,
price: elem.item_data.variations[0].item_variation_data.price_money,
checkoutUrl: resp.data.shortLink
}
}).catch(err => console.log(err))
});
return Promise.all(formattedResponse)
}).then(values => {
return response.json(values);
}).catch(error => {
console.log(error);
response.send(error);
});
});
This function is a little bit dense. We’re taking the token send to us, decoding the Firebase token, looking up our user, getting their Square access token, querying the catalog API, iterating over the catalog items, creating a list of item objects, and creating short url links for checking out with.
Take a breath, there was a lot in that list.
This is a not so pretty example of our checkout link.
Okay, break’s over. Back to work.
In the catalog links, we’re constructing a URL the follows the format/merchantId/catalogVariantId/
which allows us to look up our Square merchant and then what item is being purchased. This might be a little confusing, since we haven’t looked at the final function yet. We have one more function to go over: the checkout
function. Our checkout
function will handle the process of looking up a merchant, looking up the catalog item, creating a Square checkout URL, and then redirecting the user to that checkout url. Let’s take a look at what this function looks like.
const app = express();
app.get('/checkout/:userId/:variantId', (request, response) => {
admin.database()
.ref(`/squareAccessToken/square:${request.params.userId}`)
.once('value')
.then( userToken => {
(SquareConnect.ApiClient.instance).authentications["oauth2"].accessToken = userToken.val();
const catalogApi = new SquareConnect.CatalogApi();
return catalogApi.retrieveCatalogObject(request.params.variantId)
}).then( item => {
const checkoutApi = new SquareConnect.CheckoutApi();
const idempotencyKey = crypto.randomBytes(48).toString('base64');
const locationId = item.object.present_at_location_ids[0];
const checkoutReq = {
idempotency_key: idempotencyKey,
order: {
idempotency_key: idempotencyKey,
reference_id: Date.now().toString(),
line_items: [
{
catalog_object_id: request.params.variantId,
quantity: "1"
}
],
}
}
return checkoutApi.createCheckout(locationId, checkoutReq)
}).then( returnedCheckout => {
return response.redirect(returnedCheckout.checkout.checkout_page_url);
}).catch( error => response.send(error));
});
exports.checkout = functions.https.onRequest(app);
We switched things up here by wrapping our function up as an Express application. This allows an easier time in parsing route variables so we could look up our merchant and catalog IDs. Since our prices are already set in Square, all we need to do is place the item as a line item in our order, generate our checkout link, and then redirect!
Candidly: this example is missing a lot. It is meant as a proof of concept (which is why I omitted the actual URLs for the application) since there are a few scenarios we’re not really handling here that actual merchants would likely want. For starters, the catalog
function for generating links entirely disregards other item variants (i.e. large, medium, small, for t-shirt sizes). The function just grabs the first variant. Clearly more could be done here to accommodate that, but the focus was to show how easily we could get instant checkout links.
To add to this, we’re best supporting a digital good or an item that had flat-rate shipping (we’d need to capture a shipping address and add a line item for the shipping charges, but the checkout API supports that). That’s all easy enough, but if we wanted dynamic shipping rates, we might need to capture their address earlier in the process to create a line item that included the shipping rate.
In my previous post on Super Simple Serverless eCommerce, I touched on the fact that cold starts can really harm something like a checkout experience since the functions not having run for a while could mean a slow startup. We would have to monitor the impact on that checkout
function, since a single social media post could spike traffic drastically and cause a lot of instances of our function to be spun up to handle it all. The hope is that so many people would use this particular function that it could reasonably handle all of the traffic. The more concerning issue might be the cost of having so many calls against this function. You’d have to weigh the cost of having a lot of invocations of your function against just having your own server handling these requests. All things to consider before trying to go fully serverless!
We hope this helped spark some ideas on what you can do with serverless, but even more so, we hope it makes you want to try out Square’s APIs! If you have a project you’re working on that you want to talk about, pains with payments, or just want to chat with some devs, check out the links below to get in touch with us!
Want more? Sign up for our monthly developer newsletter or come say hi in the Square dev Slack channel!
Posted on June 27, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
January 20, 2023