Recently, I worked on a Next.js project that required Braintree integration with a custom payment form using the Braintree Hosted Fields. While working on the integration, I faced a few complexities, navigated through them, and documented the successful process in this article. Let's take a look!
First of all, sign up for a Braintree Sandbox account, log in to the Sandbox, and get the merchant ID, public key, private key, and create four environment variables as follows:
BRAINTREE_MERCHANT_ID = your_merchant_id
BRAINTREE_PRIVATE_KEY = your_private_key
BRAINTREE_PUBLIC_KEY = your_public_key
NEXT_PUBLIC_BRAINTREE_ENVIRONMENT = Sandbox
After that, install braintree
and braintree-web
as dependencies, and @types/braintree
and @types/braintree-web
as dev dependencies in your Next.js project. We need these packages to work with Braintree.
Now, let's configure Braintree with the Sandbox details. To do so, within your config
directory, create a braintree.ts
file with the following code:
import braintree from 'braintree';
type Environment = 'Sandbox' | 'Production';
export const gateway = new braintree.BraintreeGateway({
environment:
braintree.Environment[process.env.BRAINTREE_ENVIRONMENT as Environment],
merchantId: process.env.BRAINTREE_MERCHANT_ID as string,
publicKey: process.env.BRAINTREE_PUBLIC_KEY as string,
privateKey: process.env.BRAINTREE_PRIVATE_KEY as string,
});
In the above code, we've created and exported a Braintree gateway
method for us to be able to use it on other modules of our app.
Next, we need an endpoint to get the Braintree client token. So, let's create a braintree-client-token
directory within the api
directory. And within the braintree-client-token
directory, create a route.ts
file with the following code:
import { gateway } from '@config/braintree';
export async function GET() {
try {
const res = await gateway.clientToken.generate({});
return Response.json({ clientToken: res.clientToken }, { status: 200 });
} catch (err) {
console.log(err);
return Response.json(
{ message: 'Error generating braintree client token' },
{ status: 500 }
);
}
}
In the above code, we've created a GET
route to get the Braintree client token. We've used the Braintree gateway
method to generate a client token and sent the token with the response.
Now, we need the user interface for the users to enter their payment details and make the payment. So, let's create a PaymentForm
component with the following code:
'use client';
import { useEffect, useState } from 'react';
import braintree, { HostedFields } from 'braintree-web';
export default function PaymentForm() {
const [hostedFields, setHostedFields] = useState<HostedFields | null>(null);
async function handlePayment() {
if (!hostedFields) return;
try {
const { nonce } = await hostedFields.tokenize();
const data = {
nonce,
id: 10000,
quantity: 1,
};
const res = await fetch(`/api/checkout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await res.json();
if (!res.ok) throw new Error(result.message);
} catch (err) {
console.log(err);
}
}
useEffect(() => {
async function initializeBraintree() {
try {
const res = await fetch('/api/braintree-client-token', {
cache: 'no-store',
});
const result = await res.json();
if (!res.ok) throw new Error(result.message);
const clientInstance = await braintree.client.create({
authorization: result.clientToken,
});
const hostedFields = await braintree.hostedFields.create({
fields: {
number: {
selector: '#card_number',
placeholder: '4111 1111 1111 1111',
},
...(process.env.NEXT_PUBLIC_BRAINTREE_ENVIRONMENT ===
'Production' && {
cvv: {
selector: '#cvv',
placeholder: '123',
},
}),
expirationDate: {
selector: '#expiration_date',
placeholder: 'Expiration',
},
},
client: clientInstance,
});
setHostedFields(hostedFields);
} catch (err) {
console.log(err);
}
}
initializeBraintree();
}, []);
return (
<form action={handlePayment}>
<div>
<label htmlFor='card_number'>Card Number</label>
<div id='card_number'></div>
</div>
{process.env.NEXT_PUBLIC_BRAINTREE_ENVIRONMENT === 'Production' && (
<div>
<label htmlFor='cvv'>CVV</label>
<div id='cvv'></div>
</div>
)}
<div>
<label htmlFor='expiration_date'>Expiration Date</label>
<div id='expiration_date'></div>
</div>
<button type='submit'>Pay</button>
</form>
);
}
In the above code, we've made the PaymentForm
component a client component to store the hostedFields
in the state and use the useEffect
hook to perform some asynchronous operations. Making it a client component also allows us to show any payment errors as alerts. If you are interested in learning about handling form errors in the Next.js app router, I have an article on Handle form errors correctly in a Next.js app built with the app router which provides useful insights.
Now, in the PaymentForm
component, we have a handlePayment
function used as the form
action to test the payment, a useEffect
hook to initialize Braintree, and necessary JSX
for the payment form user interface.
Inside the useEffect
hook, we have the initializeBraintree
function that does a few things. First, we make a request to the /api/braintree-client-token
endpoint to get the client token. Then, we create a clientInstance
with the token. After that, we create the hostedFields
and update the state with it.
Two important notes here. First, we only show the CVV field when the Braintree environment is set to Production
. This is because CVV doesn't work in the Sandbox environment. Second, we've used an empty array as the dependency of the useEffect
hook so the Braintree initialization happens only once.
Congratulations, the integration part is done! To test the integration, we need an endpoint to process the payment. So, let's create one by creating a checkout
directory within the api
directory. And within the checkout
directory, create a route.ts
file with the following code:
import { gateway } from '@config/braintree';
export async function POST(request: Request) {
const { nonce, id, quantity } = await request.json();
// Validate data
if (!nonce || !id || !quantity) {
console.log('Nonce, id or quantity is missing');
return Response.json(
{ message: 'Nonce, id or quantity is missing' },
{ status: 400 }
);
}
// Get item from the database
const item = {
id: 10000,
price: 10,
};
const totalPrice = item.price * quantity;
try {
// Create payment
const payment = await gateway.transaction.sale({
amount: totalPrice.toFixed(2),
paymentMethodNonce: nonce,
options: {
submitForSettlement: true,
},
});
if (!payment.success) {
console.log('Payment failed');
return Response.json({ message: 'Payment failed' }, { status: 500 });
}
return Response.json({ message: 'Checkout successful' }, { status: 200 });
} catch (err) {
console.log(err);
return Response.json({ message: 'Failed to checkout' }, { status: 500 });
}
}
In the above code, first, we check if all the required fields are provided. Then we do the database query to pull the necessary data to calculate the total price. After that, we create a Braintree payment
for the total amount. We check the payment
status and return a payment failed response if the payment isn't successful. Otherwise, we return a checkout success response.
Now, let's test the integration by using a Braintree test card. Enter the card details in the payment form with an expiry date and click the pay button. This should process a test payment which you can verify by logging in to your Sandbox account.
That's it! Your Next.js application now has the Braintree integration to process payments. Check out the go live article to ship the integration to production. Feel free to share this article with someone who might also find it useful. Cheers!