Accept Payments with Square and Svelte

Accept Payments with Square and Svelte

Easily add payments to your Sveltekit app!

If the thought of implementing online payments makes your stomach churn, you’re not alone. Building an online payment flow can be very intimidating. Afterall, an unsecure payment process can have serious repercussions for your business. Luckily, Square is here to help relieve you of all your worries and get you up and running with a safe and secure payments implementation.

In this tutorial, we’re going to take you step-by-step through the process of integrating a simple and secure payment flow into your app. We’re going to use the Square Web Payments SDK and the Square Payments API to get us up and running so you can start accepting payments online. To help us out, we’re going to be using SvelteKit as our framework for this project. SvelteKit is a full stack framework that provides a lot of great features that will help simplify our payments implementation. By the end of this tutorial, you’ll have a fully functional and secure payment flow that accepts credit card payments. Here's what the final result will look like:

Screenshot 2023-09-06 at 9.16.23 AM
Info

This tutorial requires a basic understanding of Svelte and SvelteKit. All of the code for this tutorial can be found in the accept-payments-with-square-and-svelte Github repo.

Before we dive into the details, checkout this demo that highlights each of the steps of implementing an end to end payment flow.

app.html
+page.svelte
+server.js

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
<script type="text/javascript" src="https://sandbox.web.squarecdn.com/v1/square.js"></script>
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body>
<div>%sveltekit.body%</div>
</body>
</html>

Online Payments Overview

It’s important that we understand the steps involved in payment processing. Understanding each stage at a high level will help us make sense of the implementation. There are five steps to creating a payment flow using Square.

Now that we have a general idea of how to process a payment, let’s break down each step and dive into the details that make this work.

Create a Credit Card Payment Form

The first step of building a payment flow is creating a form that accepts credit cards. Fortunately for us, the Square Web Payments SDK takes care of a lot of the heavy lifting in this step. In order to use the Web Payments SDK, we need to add the following script tag to the head of our app.html file.

app.html

<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
<script
type="text/javascript"
src="https://sandbox.web.squarecdn.com/v1/square.js"
></script>
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>

With our script added, we can start to create our payment form. In our main +page.svelte file, let's add the following form.

+page.svelte

<form>
<div id="card-container" />
<button>Pay $1.00</button>
</form>

We can also add some CSS to style our button.

+page.svelte

<style>
button {
color: #ffffff;
background-color: #006aff;
border-radius: 6px;
cursor: pointer;
border-style: none;
font-size: 16px;
line-height: 24px;
padding: 12px 16px;
width: 100%;
}
button:hover {
background-color: #005fe5;
}
button:active {
background-color: #0055cc;
}
button:disabled {
background-color: rgba(0, 0, 0, 0.05);
color: rgba(0, 0, 0, 0.3);
}
</style>

You'll notice that our form includes a div with an id of "card-container". This div is meant to be a placeholder for where our card input field will go. We'll need to use the Square Web Payments SDK to create a secure input field in that slot. Let’s do this in a function called initializePaymentForm.

+page.svelte

<script>
const appId = 'your-app-id';
const locationId = 'your-location-id';
let card;
async function initializePaymentForm() {
if (!Square) {
throw new Error('Square.js failed to load properly');
}
const payments = Square.payments(appId, locationId);
try {
card = await payments.card();
await card.attach('#card-container');
} catch (e) {
console.error('Initializing Card failed', e);
return;
}
}
</script>

There are three things going on in this initializePaymentForm function. Let's break it down:

  1. First, we're checking to make sure the Square object exists. This object comes from that script that we added earlier in the head. This won’t work without that script.

  2. Next, we're initializing the Web Payments SDK by calling Square.payments(appId, locationId). I've defined appId and locationId outside our function, just in case we need to use them anywhere else in our app.

  3. Finally, we initialize the Card payment method by calling payments.card() and storing that in a variable called card. We then attach our card to the placeholder div with an id of "card-container" in order to get our card input to display on the page.

Note

I've declared the card variable outside the initializePaymentForm function because we're going to be using it in other parts of the code later.

Our initializePaymentForm function looks great, but now we need to call it somewhere to get our form set up. This is a perfect opportunity to use Svelte's await blocks.

+page.svelte

<form>
{#await initializePaymentForm()}
<p>Loading...</p>
{:catch error}
<p>{error}</p>
{/await}
<div id="card-container" />
<button>Pay $1.00</button>
</form>

This is how we can execute our initializePaymentForm function. While the function is running, we'll see the text, "Loading...", on the screen. Once it completes, we'll see the card input field. If the function throws an error, we'll display the error message instead.

Tokenize the Payment Method

Ok, so we've got our payment form all set up. Now we need to handle the payment when the form is submitted. To do that, there is another piece to this payment puzzle that we need: tokenization.

Payment tokenization is the process of taking sensitive payment information and replacing it with a nonsensitive generated number called a token. We do this to protect the users’ payment information and add a level of security. If you’re worried about how to implement tokenization, don’t sweat it! Square takes care of this for you by providing a tokenize method that you can call on your payment method. Let's take a look at how we add this to our implementation.

+page.svelte

async function tokenize(paymentMethod) {
const tokenResult = await paymentMethod.tokenize();
if (tokenResult.status === 'OK') {
return tokenResult.token;
} else {
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(tokenResult.errors)}`;
}
throw new Error(errorMessage);
}
}

This function may look a little intimidating, but it's really only doing one thing, getting a token. You can see that this function takes in a payment method, and then calls the tokenize method to get back a token for that payment. The rest of the function is error handling in case the tokenization process fails for some reason. Provided there aren't any errors, this function returns the token that we need to send to our server to process the payment. Speaking of which, let's take a look at how we can do that now.

Send the Token to the Server

Ok, so now we have a way to tokenize our payment method. Let's go ahead and send that token over to our server where it'll be processed. We'll want to do this anytime a user submits the payment form by clicking our "Pay $1.00" button, so let's attach a function called handlePaymentMethodSubmission that we'll run on submit.

+page.svelte

<form on:submit|preventDefault={handlePaymentMethodSubmission}>
{#await initializePaymentForm()}
<p>Loading...</p>
{:catch error}
<p>{error}</p>
{/await}
<div id="card-container" />
<button>Pay $1.00</button>
</form>

And now we can define that function as so:

+page.svelte

async function handlePaymentMethodSubmission() {
try {
const token = await tokenize(card)
const paymentResponse = await fetch('/api/payment', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
locationId,
sourceId: token,
}),
})
} catch (e) {
console.error(e.message)
}
}

The first thing we do in our handlePaymentMethodSubmission function is use the tokenize function that we just wrote to generate a token for our card. Then we send a POST request to our payment endpoint and include our locationId and token in the body. If you're confused about what this payment endpoint is, you should be! We haven't written it yet! That's the next step in our payment pipeline, so let's go create that endpoint now.

Process the Payment with the Payments API

To create an API endpoint in SvelteKit, we first need to create a folder under our routes folder called api. Then, within our api folder, create another folder called payment. Finally, within our payment folder, create a file called +server.js. This folder structure creates an API endpoint with the path /api/payment and it's where we'll handle the POST request that we're sending from the client. Before we do that, though, we need to initialize the Square Payments API.

Square Payments API

Because we’re going to use the Square Payments API, we need to first install the Square Node.js SDK.


npm install square

With the SDK installed, we can import the Client from Square at the top of our +server.js file. Then we can initialize the paymentsApi by providing an access token and environment type to the Client.

+server.js

import { Client } from 'square';
const { paymentsApi } = new Client({
accessToken: import.meta.env.VITE_SQUARE_ACCESS_TOKEN,
environment: 'sandbox',
});

The value of your Square access token can be found in your Square developer dashboard, under your specific application. We need to keep our access token hidden, so we’re storing it in an environment variable. SvelteKit uses Vite under the hood, so we need to create our environment variable in a .env file and prefix it with VITE_.

.env

VITE_SQUARE_ACCESS_TOKEN=yourSquareAccessToken

Handle the Request

Awesome! We’ve got all our setup in place, so let’s go ahead and write our request handler function that will be responsible for processing the payment. This handler is meant to handle a POST request, so we’ll export an async function called POST.

+server.js

import { json } from '@sveltejs/kit';
import { Client } from 'square';
import { randomUUID } from 'crypto';
const { paymentsApi } = new Client({
accessToken: import.meta.env.VITE_SQUARE_ACCESS_TOKEN,
environment: 'sandbox',
});
export async function POST({ request }) {
const { locationId, sourceId } = await request.json();
try {
const { result } = await paymentsApi.createPayment({
locationId,
sourceId,
idempotencyKey: randomUUID(),
amountMoney: {
amount: 100,
currency: 'USD',
},
});
console.log(result);
return json(result);
} catch (error) {
console.log(error);
}
}

For an endpoint like this, Sveltekit provides us access to a request object. We can call the json() method on this object in order to get the contents of the request body. In our case that’s the locationId and sourceId.

Let’s pause for a minute here and take a look at everything we include in the request body to understand what each of them does.

  • idempotencyKey: A unique string that identifies this request. To generate a unique string, we use the randomUUID function from the Node.js crypto module. Don’t forget to import this at the top.
  • sourceId: The ID for the source of funds for the payment. In our case, this will be the payment token that is generated by the Square payment form. We’ll provide this token in the body of the POST request.
  • amountMoney: An object that defines the amount of money to be paid. In our case, we’re specifying the currency to be US dollars and the amount is in the smallest denomination of the currency type. So a value of 100 represents 100 cents or $1.00. We’re hardcoding this value for demonstration’s sake, but you can also send this value in the POST request for dynamic prices.
Note

You can see all the options that you can pass to this method in the API reference.

Finally, after we await the result of creating the payment, we send the result back to the client with SvelteKit's json function. Don’t forget to import that at the top as well.

Common Pitfall

While our endpoint looks complete, if we try to create a payment now, we’ll encounter this error on the server: TypeError: Do not know how to serialize a BigInt. This error occurs because BigInt values aren’t serialized in JSON by default. To fix this, we need to define our own toJSON method to return a string.

+server.js

import { json } from '@sveltejs/kit';
import { Client } from 'square';
import { randomUUID } from 'crypto';
BigInt.prototype.toJSON = function () {
return this.toString();
};
const { paymentsApi } = new Client({
accessToken: import.meta.env.VITE_SQUARE_ACCESS_TOKEN,
environment: 'sandbox'
});
export async function POST({ request }) {
// ...
}

Now our endpoint is complete.

Display the Result

We're almost there, but there's one final piece I want to add in order to provide a more complete user experience. Our API endpoint sends a response back to our client, but our client doesn't do anything with that response yet. Let's add some functionality to report to the user whether or not the payment was processed successfully.

First, we'll create a variable called paymentStatus that will store a string indicating the status of the payment.

+page.svelte

let card;
let paymentStatus = '';

Now we can set the value of paymentStatus based on the response we receive from our POST request.

+page.svelte

async function handlePaymentMethodSubmission() {
try {
paymentStatus = “”;
const token = await tokenize(card);
const paymentResponse = await fetch('/api/payment', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
locationId,
sourceId: token
})
});
if (paymentResponse.ok) {
paymentStatus = 'Payment completed';
} else {
const errorBody = await paymentResponse.text();
throw new Error(errorBody);
}
} catch (e) {
paymentStatus = 'Payment failed';
console.error(e.message);
}
}

And finally, we can add this result to our page.

+page.svelte

<form on:submit|preventDefault={handlePaymentMethodSubmission}>
{#await setup()}
<p>Loading...</p>
{:catch error}
<p>{error}</p>
{/await}
<div id="card-container" />
<button>Pay $1.00</button>
</form>
{#if paymentStatus}
<div id="payment-status-container">{paymentStatus}</div>
{/if}

We’ll sprinkle in some CSS for the payment status message as well:


#payment-status-container {
width: fit-content;
font-family: Arial, Helvetica, sans-serif;
color: #ffffff;
background: #1a1a1a;
display: flex;
padding: 12px;
border-radius: 6px;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1), 0px 0px 4px rgba(0, 0, 0, 0.1);
margin: auto;
margin-top: 36px;
}

Now the page will provide feedback to our users on whether or not their payment was processed successfully.

Screenshot 2023-09-06 at 9.20.14 AM

Conclusion

So to recap, in this tutorial we covered the five steps required to build a complete end-to-end payment flow. We used the Square Web Payments SDK and the Square Payments API to accept and process a user’s payment. On top of that, all of this was wired together using Sveltekit as our framework.

This is a great start, but there’s so much more you can do with the web payments SDK. We didn’t even go into additional capabilities like custom styling and digital wallets. This tutorial is meant to provide you with a solid foundation from which you can start to add additional features. We can’t wait to see what you build from here.

Table Of Contents
View More Articles ›