Step-By-Step Tutorial
Integrating ExactJS into Payments Page
In this step by step tutorial we will begin with a plant shop template without payments, and add ExactJS to create a fully functioning store where you Pay from Browser.
Source Code
If you would like to follow along, the fully documented source code is below.
Plant Shop Template (no ExactJS)
Prerequisites
The following prerequisites are required for the Exact Payments API:
- You have been onboarded and given an account Id
- You have generated or been given access to an Application Token
- You will need a list of test cards
Development Environment
To follow along with this tutorial, you should have on your machine:
You can verify your installations like so:
$ git --version
git version v2.37.0 (Apple Git-136)
$ node --version
v18.7.0
$ npm --version
v8.15.0
$ yarn --version
v1.22.19
NOTE: These were the versions used in testing. Other versions may produce unexpected results.
node >= 16
is a hard requirement.
Getting Started
First, lets clone the next-merchant-demo and take a look at the Plant Shop Template.
git clone [email protected]:exact-payments/next-merchant-demo.git
cd next-merchant-demo/plant-shop-template
yarn
yarn dev
If you open http://localhost:3000/ in your browser, you will now see the Exact Plant Shop Template!
Click on plants to add them to your cart, and click checkout when you are satisfied with your selection.
As you can see http://localhost:3000/checkout has not been setup. There is nowhere to input your card!
We will fix that by setting up payments through ExactJS.
.env.local
We need to have an environment setup. .env.local
will be automatically picked up by NextJS, and will be ignored by git.
NOTE: The protection of API keys/Authentication Tokens is important. Please always verify that your .gitignore
is properly configured and you are not leaking keys.
touch .env.local
Open .env.local and add the following:
P2_ACCOUNT_ID=<YOUR ACCOUNT ID HERE>
APPLICATION_TOKEN=<YOUR APPLICATION TOKEN HERE>
NEXT_PUBLIC_BASE_URL="http://localhost:3000" # this will need to be changed if deployed
About the Plant Shop
We will cover everything we need to get payments setup for our Plant Shop in this tutorial, but if you are curious about the Plant Shop, feel free to checkout the README for full documentation of the template.
Types
All necessary types are included and imported from types.ts
Adding ExactJS Script
Open pages/checkout.tsx
in your favorite Text Editor or IDE.
We will be loading ExactJS via a script tag. Add an import to next/script
:
import Script from 'next/script'
Now we will add a script tag for ExactJS in our page. We can put it right inside our main elements.
export default function Checkout() {
...
return (
<main className={styles.main}>
<Script src="https://api.exactpaysandbox.com/js/v1/exact.js" strategy="afterInteractive" onReady={onExactJSReady}/>
<div>
...
)
}
Great! You may notice that onExactJSReady has not been defined. We will need to create that function, to be called once our script is setup.
export default function Checkout() {
...
const onExactJSReady = () => {}
return (...)
}
We are now loading ExactJS into our application! Let's move on to initializing it.
Creating an Order
Before rendering your payment form for the customer, you need to send the details of the order to our servers.
Building an API route
We are now going to start building our backend. We need to communicate with the Exact Payments API, and let them know to create an order.
The url for the API is: https://api.exactpaysandbox.com/account/${process.env.P2_ACCOUNT_ID}/orders
We are going to use axios to make the POST to the API, including our authorization token in the headers.
We already have the Account ID set in .env.local
for the url, and we have an Application Token set in .env.local
for Authorization.
Data
Our data will be coming from the browser as a NextApiRequest
. For the purpose of this demo, the only info we will be sending from the browser is amount
.
Our data block will look like:
data: {
...req.body
}
Once we have made this request, we will get a response from the API. If the request was successful, the response object will contain an orderId stored as id
, and an accessToken, stored as accessToken.token
.
The access token you received when you created the Order will allow your customers to access our APIs from their browser for a limited time period. It is valid for 15 minutes and, once it has expired, further API requests will be rejected.
You can optionally re-generate an access token if you want to allow your customer more time to complete their payment.
The order access token IS NOT the authentication token used for API access.
We want to return these values from our server to our browser, so that we can use them later to initialize ExactJS. Feel free to console.log()
the response from our call to the Exact Pay API to verify everything is as expected.
pages/api/postOrders
The fully complete file:
import type { NextApiRequest, NextApiResponse } from 'next'
import axios from 'axios'
import {AxiosRequestConfig} from 'axios'
import {Data} from '../../types'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
const options : AxiosRequestConfig = {
method: 'POST',
url: `https://api.exactpaysandbox.com/account/${process.env.P2_ACCOUNT_ID}/orders`,
headers: {
accept: 'application/json',
'content-type': 'application/json',
authorization: process.env.APPLICATION_TOKEN
},
data: {
...req.body
}
};
await axios
.request(options)
.then( (response) => {
res.status(200)
console.log(response.data)
res.json({
token : response.data.accessToken.token,
orderId : response.data.id
})
})
.catch( (error) => {
console.error(error);
res.status(401)
res.end()
});
}
Add Card Component to Form
We now have an API setup to Create an Order and a Script for ExactJS. We will use both of these to create a cardComponent and add it to our form.
getTotalPrice()
We will need to know the totalPrice of the items in our cart. To get the totalPrice for this basic store, we can just multiply the number of items by 1000, since all our plants cost $10.00 today, and amount is represented in cents.
We can access the items by importing and then calling useCartState()
and accessing its items.
A method has already been included, along with a useEffect()
to prevent us from being on the checkout screen without any items.
onExactJSReady()
This function is called after we have loaded ExactJS. Let`s add a call to our backend here, since we know ExactJS will be ready to go whenever this function is called.
We will POST to api/postOrders
with data {amount: getTotalPrice()}
via axios.
import axios from 'axios'
Once we receive a response, we can use the Access Token sent back to initialize ExactJS. The order access token IS NOT the authentication token used for API access.
exact = ExactJS(response.data.token);
We are now going to add the credit card UI to the form. We will also need to provide the Order ID at this stage.
const components = exact.components({orderId: response.data.orderId});
components.addCard('cardElement', {
label: {position: "above"},
style: {
default: {
border: "1px solid #ccc",
fontSize: "14px",
},
}});
We already have an empty div in the Form with id='cardElement'
so we are telling ExactJS to attach a card component to that div. We attach some small styling to our component.
Here is what the function should now look like:
const onExactJSReady = () => {
const url = process.env.NEXT_PUBLIC_BASE_URL + '/api/postOrders'
axios.post(url, {
amount: getTotalPrice(), //Price is in cents
}).then(
(response) => {
exact = ExactJS(response.data.token)
const components = exact.components({orderId: response.data.orderId});
components.addCard('cardElement', {
label: {position: "above"},
style: {
default: {
border: "1px solid #ccc",
fontSize: "14px",
},
}});
})
}
exact.on()
exact.on()
is used to define callback behavior once payment is completed, or has failed. It takes a paymentState, and a functionCall. It calls the corresponding functionCall after the payment state is set.
The call signature of
on : (paymentState : "payment-complete" | "payment-failed", functionCall: (payload :ExactJSPayload) => void) => void,
We need to tell ExactJS what to do with our payment payload. Our payment payload contains one field: paymentId
. We attach the id to a hidden input that is already already present. We then submit the form to our backend.
exact.on("payment-complete", (payload : ExactJSPayload) => {
(document.getElementById('payment_id')! as HTMLInputElement).value = payload.paymentId;
(document.getElementById('myForm') as HTMLFormElement).submit();
});
Notice the non-null assertion and typecasting in order to access the value of the document element.
Final code
Here is the final code for onExactJSReady()
const onExactJSReady = () => {
const url = process.env.NEXT_PUBLIC_BASE_URL + '/api/postOrders'
axios.post(url, {
amount: getTotalPrice(), //Price is in cents
}).then(
(response) => {
exact = ExactJS(response.data.token)
const components = exact.components({orderId: response.data.orderId});
components.addCard('cardElement', {
label: {position: "above"},
style: {
default: {
border: "1px solid #ccc",
fontSize: "14px",
},
}});
exact.on("payment-complete", (payload : ExactJSPayload) => {
(document.getElementById('payment_id')! as HTMLInputElement).value = payload.paymentId;
(document.getElementById('myForm') as HTMLFormElement).submit();
});
exact.on("payment-failed", (payload) => {
console.debug(payload);
});
})
}
Checkpoint
Run the app and take a look at the checkout page. You will now see the ExactJS Card Component pop into the screen. Get some coffee.
Paying for Order
The card component has been attached. Lets use it to pay for the order.
handleSubmit()
We can add a onSubmit
prop to our form
<form id="myForm" action="api/notYetImplemented" method="post" onSubmit={handleSubmit}>
We will be creating a Payment ID with ExactJS and attaching it to the following
<input type="hidden" name="payment_id" id="payment_id"></input>
This is already present in the template, and we set the callback function for exact.on()
to set that value, so our handleSubmit()
function is very straightforward.
Our handleSubmit function is:
const handleSubmit = (event : FormEvent<ExactPaymentForm>) => {
event.preventDefault()
exact.payOrder()
}
We prevent the default event from executing, find our form, then call exact.payOrder()
.
We have now paid for the order with ExactJS.
Once our payment is handled, exact.on()
will be called with a paymentState
indicating whether the payment was completed succesfully.
pages/api/receivePaymentId
We have made the payment through the Exact Payments API, however it makes sense to also send the payment to our own backend.
Lets create api/receivePaymentId.ts
and change our form to point to that.
<form id="myForm" action="api/receivePaymentId" method="post" onSubmit={handleSubmit}>
Now our form with paymentId will be posted to our server. You can now delete api/notYetImplemented.ts
Here is a demo receivePaymentId.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
//HERE YOU SHOULD SAVE THE PAYMENT TO YOUR SERVER!
//We simulate this by saving the payment id to our environment
process.env.paymentId = req.body.payment_id
console.log(req.body)
res.redirect(302, `/paid`)
}
You will probably want to save paymentId
to a database in a real world application. We are just setting an environment variable and logging the request for this demo. We then redirect users to the /paid
page.
Using a paymentId
To view payment information, just make a GET request to
https://api.exactpaysandbox.com/account/{ACCOUNT_ID}/payments/{PAYMENT_ID}
pages/paid.tsx
Here is a sample demo paid page:
import Link from 'next/link'
import styles from 'styles/Home.module.css'
export default function Paid () {
return (<>
<main className={styles.main}>
<h1>
Thanks for shopping at the Exact Plant Shop <br></br>
</h1>
<h3>
The details of your payment have been sent to the server!
</h3>
<p>
Displaying payment details for demonstration
</p>
<p>
</p>
<h1>
<Link href='/'>Return to our homepage</Link>
</h1>
</main>
</>)
}
NOTE:
On our sample application, we display payment info on the [/paid
](https://github.com/exact-payments/next-merchant-demo/blob/main/pay-from-browser/pages/paid.tsx) screen. This is purely for demonstration, not included in this tutorial, and is NOT ADVISED.
Loading Screen
We now have a fully functioning ExactJS Payment Page integrated. We will add a small loading screen. Having a loading screen is important as it minimizes pop-in while ExactJS initializes, giving a cleaner experience.
We will start with our checkout page in a loading state, and update it once ExactJS has loaded.
We can create a setOrderPosted()
method and use it to set div visibility.
const setOrderPosted = () => {
(document.getElementById('hideable')! as HTMLInputElement).className = "";
(document.getElementById('loading')! as HTMLInputElement).className = styles.hidden;
}
We call this method once we have setup the Card Component in onExactJSReady()
, and we create wrapper divs with id='loading'
and id='hideable'
. We use setTimeout()
in order to add a small buffer after load. Feel free to play around with this timeout time.
For loading icon- we are using a react-loader-spinner
. You can add this to your project by running:
yarn add react-loader-spinner
Final Code for checkout.tsx
Including loading screen with MutatingDots
.
import { useCartState } from "../util/useCartState"
import styles from '../styles/Home.module.css'
import Script from 'next/script'
import axios from 'axios'
import { FormEvent, useEffect } from "react"
import { MutatingDots } from "react-loader-spinner"
import { useRouter } from "next/router"
import OrderTotal from "../components/OrderTotal"
import {Exact, ExactJSPayload, ExactPaymentForm } from '../types'
export default function Checkout() {
let exact : Exact;
const items = useCartState().items
const getTotalPrice = () => {
return items.length * 1000
}
const setOrderPosted = () => {
(document.getElementById('hideable')! as HTMLInputElement).className = "";
(document.getElementById('loading')! as HTMLInputElement).className = styles.hidden;
}
const onExactJSReady = () => {
const url = process.env.NEXT_PUBLIC_BASE_URL + '/api/postOrders'
axios.post(url, {
amount: getTotalPrice(), //Price is in cents
}).then(
(response) => {
exact = ExactJS(response.data.token)
const components = exact.components({orderId: response.data.orderId});
components.addCard('cardElement', {
label: {position: "above"},
style: {
default: {
border: "1px solid #ccc",
fontSize: "14px",
},
}});
exact.on("payment-complete", (payload : ExactJSPayload) => {
(document.getElementById('payment_id')! as HTMLInputElement).value = payload.paymentId;
(document.getElementById('myForm') as HTMLFormElement).submit();
});
exact.on("payment-failed", (payload) => {
console.debug(payload);
});
setTimeout(setOrderPosted, 1100);
})
}
const handleSubmit = (event : FormEvent<ExactPaymentForm>) => {
event.preventDefault()
exact.payOrder()
}
//Prevent checkout with empty cart
const router = useRouter()
useEffect(() => {
if (!getTotalPrice()){
router.push('/')
}
})
return (
<>
<div className={styles.checkoutdisclaimer}>
<h1>Demonstration only.</h1>
<h2><a href="https://developer.exactpay.com/docs/test-cards/" target="_blank">TEST CARDS</a></h2>
</div>
<main className={styles.main}>
<OrderTotal/>
<div id="loading">
<MutatingDots height="100" width="100" color="#4fa94d" secondaryColor= '#4fa94d' radius='12.5' ariaLabel="mutating-dots-loading"/>
<Script src="https://api.exactpaysandbox.com/js/v1/exact.js" strategy="afterInteractive" onReady={onExactJSReady}/>
</div>
<div id="hideable" className={styles.hidden}>
<form id="myForm" action="api/receivePaymentId" method="post" onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email Address</label>
<input type="email" id="email" name="email" autoComplete="email" />
</div>
<div id="cardElement" >
</div>
<div>
<label htmlFor="address">Address</label>
<input type="text" id="address" name="address" autoComplete="street-address" />
</div>
<div>
<label htmlFor ="apartment">Apartment, suite, etc.</label>
<input type="text" id="apartment" name="apartment" />
</div>
<div>
<label htmlFor="city">City</label>
<input type="text" id="city" name="city" />
</div>
<div>
<label htmlFor="province">State</label>
<input type="text" id="province" name="province" />
</div>
<div>
<label htmlFor="postcode">Postal code</label>
<input type="text" id="postcode" name="postcode" autoComplete="postal-code" />
</div>
<input type="hidden" name="payment_id" id="payment_id"></input>
<div>
<input type="submit" name="commit" value="Pay Now" data-disable-with="Pay Now" />
</div>
</form>
</div>
</main>
</>
)
}
Conclusion
🌿 We did it! Thanks for reading. Feel free to compare against our source code!
https://github.com/exact-payments/next-merchant-demo/tree/main/pay-from-browser
If you are curious about styling individual components, see Individual Components.
All customization options can be viewed here: Customization