While building my client's art portfolio project, I was tasked with processing payments using Firebase. Processing payments is not generally considered a front-end process unless you might be using Paypal. In my case, I was using Square API. Square depends on a backend to process payments. In my case, I concluded that I need to create a Firebase Cloud Function.
The process happens like this: the front end passes the data to the cloud function which acts like a backend. After the backend receives the data, the Square SDK to processes the payment and the order data is stored into my database after the payment is complete. To understand this whole process about creating payments, see the Square API payments docs.
In this post, I will go over how to use the Google Firebase Cloud Functions written in NodeJS to process a payment.
The first step is to setup Firebase Functions. Here is a quick setup video:
In my example, I will be using Typescript as they recommended in the video. Typescript in many ways is like Javascript; however, I noticed it can be very picky about how to properly code, so I would For beginners (like me), I had to learn a little bit about how Typescript uses tslint to ensure correct usage. Here is a video walkthrough for Typescript:
After setting up the Firebase functions, it's time to get into how to setup NodeJS. Start by importing and initializing firebase:
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin'
admin.initializeApp()
This will do the import for the functions that will later be used. When creating Firebase Cloud Functions using NodeJS, it is important to understand the slight differences in just using a NodeJS app. When creating the function, that function can be directly called or called via HTTP requests. The Firebase docs have several examples below.
Instead of creating a listen to a port and starting nodemon, you have to export the function. Also, you can perform an emulate and inspect your function to perform continuous development updates as I discussed in a previous post here
So in this post, we are going to create an express app that becomes an exported cloud function. More on that in a little while.
const cors = require('cors')
const express = require('express');
const bodyParser = require('body-parser');
const { Client, Environment, ApiError } = require('square');
import { v4 as uuidv4 } from 'uuid'
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
I have installed cors to ensure that cross-origin is enabled. I create the express app, so all of this should be familiar with regard to creating an express application. I also initialize the square SDK and use the Client, Environment, and ApiError for processing errors. I also install body-parser to process HTTP POST requests. More info about body parser can be found here. On a side note, I also create an instance of uuidv4 to be used later to create an order ID.
Also, if you don't already have a square account, you should create one here: Square developer
This will give you a sandbox square API access token to use in your application.
In this post, most of the NodeJS code used is from the payment form walkthrough below.
I will not be going through the front-end portion of this tutorial. I will post a front end tutorial in a future post.
So the next step is to create the square SDK client as shown below.
const client = new Client({
environment: Environment.Sandbox,
accessToken: 'your-square-access-token',
});
So before getting into the details of how to process the POST payment request in NodeJS, let's delve in a little bit about the contents of the POST request that is sent to the backend. I used a React front end to create a POST fetch to the back end. Here is the code for the POST fetch:
fetch('https://project.cloudfunctions.net/payments/', {
method: 'POST',
headers: {
'Square-Version': "2020-12-16",
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + process.env.REACT_APP_SQUAREPAY_SANDBOX_ACCESS_TOKEN,
},
body: JSON.stringify({
idempotency_key: idempotency_key,
location_id: LOCATION_ID,
nonce: nonce,
amount: cart.reduce((acc, item) => {
return acc + parseInt(item.price)
}, 0) * 100,
uid: localStorage.getItem('JWT'), //uid of the cart
emailAddress: emailRef.current.value,
orderLocaleDate: (new Date()).toLocaleDateString('en', { year: 'numeric', month: 'long', day: 'numeric' }) + " " + (new Date()).toLocaleTimeString('en-US') + " " + (new Date()).toString().match(/([A-Z]+[\+-][0-9]+.*)/)[1],
billing: {
firstName: billingFirstNameRef.current.value,
lastName: billingLastNameRef.current.value,
address1: billingAddress1Ref.current.value,
address2: billingAddress2Ref.current.value,
city: billingCityRef.current.value,
state: billingStateRef.current.value,
zip: billingZipRef.current.value,
phone: billingPhoneRef.current.value
},
shipping: {
firstName: shippingFirstNameRef.current.value,
lastName: shippingLastNameRef.current.value,
address1: shippingAddress1Ref.current.value,
address2: shippingAddress2Ref.current.value,
city: shippingCityRef.current.value,
state: shippingStateRef.current.value,
zip: shippingZipRef.current.value,
phone: shippingPhoneRef.current.value
},
buyerVerificationToken: buyerVerificationToken
})
})
.catch(err => {
alert('Network error: ' + err);
})
.then(response => {
console.log(response)
if (!response.ok) {
return response.json().then(
errorInfo => Promise.reject(errorInfo));
}
return response.json();
})
.then(data => {
console.log(data);
alert('Payment complete successfully!');
})
.catch(err => {
console.log(err);
alert('Payment failed to complete!');
});
So the most important points of this code to keep in mind are to obtain the 'nonce' or the single access token that is generated from the Square Form. This needs to be passed into the body. Also, the idempotency key to required to ensure that no duplicate charges are made. Here is Square's explanation idempotency.
Also required fields are the location ID that is created with the square developer sandbox account. And the last required field is the amount to be charged. In my application, I ensured that the billing and shipping information is also passed in to create the order details. Another field that might be required is the buyer verification token. Here is an explanation about the workings of that: buyer verification token
After the fetch is sent, now we want to discuss how to set up the post payment in NodeJS. We will walk through this step by step.
app.post('/', async (req: any, res: any) => {
const requestParams = req.body;
const orderId = uuidv4()
let lineItems :any = [];
const paymentsApi = client.paymentsApi;
const requestBody = {
sourceId: requestParams.nonce,
amountMoney: {
amount: requestParams.amount,
currency: 'USD',
},
order_id: orderId,
locationId: requestParams.location_id,
idempotencyKey: requestParams.idempotency_key,
buyer_email_address: requestParams.emailAddress,
billing_address: {
first_name: requestParams.billing.firstName,
last_name: requestParams.billing.lastName,
address_1: requestParams.billing.address1,
address_2: requestParams.billing.address2,
locality: requestParams.billing.city,
postal_code: requestParams.billing.zip,
},
shipping_address: {
first_name: requestParams.shipping.firstName,
last_name: requestParams.shipping.lastName,
address_1: requestParams.shipping.address1,
address_2: requestParams.shipping.address2,
locality: requestParams.shipping.city,
postal_code: requestParams.shipping.zip,
},
statement_description_identifier: orderId,
verification_token: requestParams.buyerVerificationToken,
};
try {
const response = await paymentsApi.createPayment(requestBody);
res.status(200).json({
'title': 'Payment Successful',
'result': response.result,
});
jwt.verify(requestParams.uid, functions.config().jwt.secret, async (err :any , data :any) => {
if(err){
res.sendStatus(403)
}
else if(data.uid){
req.uid = data.uid
const cartsRef = admin.database().ref('carts/' + data.uid)
cartsRef.once('value').then(async snap => {
const cartData = snap.val()
let updatedAt;
for (const [key, item] of Object.entries(cartData)) {
const itemValue:any = item
if (key === 'updatedAt') {
updatedAt = itemValue
} else {
lineItems.push({
quantity: "1",
name: itemValue.item.title,
image: itemValue.item.imageUrl,
description: itemValue.item.description,
price: itemValue.item.price,
basePriceMoney: {
amount: itemValue.item.price,
currency: 'USD',
},
})
}
}
client.ordersApi.createOrder({
order: {
locationId: requestParams.location_id,
referenceId: response.result.payment.orderId,
lineItems: lineItems,
idempotencyKey: requestParams.idempotency_key,
},
})
const orderRef = admin.database().ref('orders/' + orderId)
await orderRef.set({
squareOrderId: response.result.payment.orderId,
orderId: orderId,
lineItems: lineItems,
squareUpdatedAt: response.result.payment.updatedAt,
updatedAt: updatedAt,
billing: requestParams.billing,
orderLocaleDate: requestParams.orderLocaleDate,
totalPrice: requestParams.amount,
shipping: requestParams.shipping,
emailAddress: requestParams.emailAddress,
squarePaymentId: response.result.payment.id,
receiptNumber: response.result.payment.receiptNumber,
receiptUrl: response.result.payment.receiptUrl,
})
}).catch(errorData => {
res.json({error: errorData})
})
}
})
} catch(error) {
let errorResult = null;
if (error instanceof ApiError) {
errorResult = error.errors;
} else {
errorResult = error;
}
res.status(500).json({
'title': 'Payment Failure',
'result': errorResult,
});
}
});
Let's go through a few lines to setup. We want to store the request body in a variable. We also want to create a unique order number. Also, for our order, we need to retrieve the line items from the cart and process them. And lastly, we want to create an instance of the square payments API using the Square SDK.
const requestParams = req.body;
const orderId = uuidv4()
let lineItems :any = [];
const paymentsApi = client.paymentsApi;
After we have all of this initial code, we want to create the body for the payment because we are using the cloud function to create another post to Square:
const requestBody = {
sourceId: requestParams.nonce,
amountMoney: {
amount: requestParams.amount,
currency: 'USD',
},
order_id: orderId,
locationId: requestParams.location_id,
idempotencyKey: requestParams.idempotency_key,
buyer_email_address: requestParams.emailAddress,
billing_address: {
first_name: requestParams.billing.firstName,
last_name: requestParams.billing.lastName,
address_1: requestParams.billing.address1,
address_2: requestParams.billing.address2,
locality: requestParams.billing.city,
postal_code: requestParams.billing.zip,
},
shipping_address: {
first_name: requestParams.shipping.firstName,
last_name: requestParams.shipping.lastName,
address_1: requestParams.shipping.address1,
address_2: requestParams.shipping.address2,
locality: requestParams.shipping.city,
postal_code: requestParams.shipping.zip,
},
statement_description_identifier: orderId,
verification_token: requestParams.buyerVerificationToken,
};
All of these key value pairs are sent to the payments API. When Square processes the payment, it will keep hold of this as part of the record. In my case, I want to also send information to my database to keep record of the order that was processed. We will cover that later. So now I create a try block to process the payment.
try {
const response = await paymentsApi.createPayment(requestBody);
res.status(200).json({
'title': 'Payment Successful',
'result': response.result,
});
And for the last step, I retrieve the contents of the shopping cart to create an order in my database:
jwt.verify(requestParams.uid, 'jwt_secret', async (err :any , data :any) => {
if(err){
res.sendStatus(403)
}
else if(data.uid){
req.uid = data.uid
const cartsRef = admin.database().ref('carts/' + data.uid)
cartsRef.once('value').then(async snap => {
const cartData = snap.val()
let updatedAt;
for (const [key, item] of Object.entries(cartData)) {
const itemValue:any = item
if (key === 'updatedAt') {
updatedAt = itemValue
} else {
lineItems.push({
quantity: "1",
name: itemValue.item.title,
image: itemValue.item.imageUrl,
description: itemValue.item.description,
price: itemValue.item.price,
basePriceMoney: {
amount: itemValue.item.price,
currency: 'USD',
},
})
}
}
const orderRef = admin.database().ref('orders/' + orderId)
await orderRef.set({
squareOrderId: response.result.payment.orderId,
orderId: orderId,
lineItems: lineItems,
squareUpdatedAt: response.result.payment.updatedAt,
updatedAt: updatedAt,
billing: requestParams.billing,
orderLocaleDate: requestParams.orderLocaleDate,
totalPrice: requestParams.amount,
shipping: requestParams.shipping,
emailAddress: requestParams.emailAddress,
squarePaymentId: response.result.payment.id,
receiptNumber: response.result.payment.receiptNumber,
receiptUrl: response.result.payment.receiptUrl,
})
}).catch(errorData => {
res.json({error: errorData})
})
}
})
}
After this, I do the catch block if the order was not successful:
catch(error) {
let errorResult = null;
if (error instanceof ApiError) {
errorResult = error.errors;
} else {
errorResult = error;
}
res.status(500).json({
'title': 'Payment Failure',
'result': errorResult,
});
}
In a future post, I will try to tackle how to send the order confirmation. I hope this was informative. Happy coding!
Top comments (0)