Enterprise Only

Integrating with Braintree Payments

Braintree Logo

Overview

In this tutorial we will look at integrating with Braintree using their hosted fields and their GraphQl endpoint.

We will be focusing solely on Card Payments in the checkout.

In this example all files will be created directly into our codebase. For the sake of reusability it's advised you create your own npm packages and import them.

How it works

Data Flow

  1. Falcon Server Configuration data (API keys etc)
  2. Falcon Client Braintree form initialized
  3. Falcon Client User inputs card data
  4. Braintree `nonce` token grabbed from Braintree
  5. Falcon Server Payment query created (using token)
  6. Braintree Payment handled
  7. Falcon Server Payment status returned

File Flow

This is an example of the data flow from a file perspective (using demo-v2):

Payment flow diagram

Useful Packages

Useful docs

Links to internal docs coming soon

Creating your integration

1. Create your Braintree account.

We are going to be using a Sandbox account for this but you could just as easily create a live account.

i. Register

Register or Login if you already have an account.

ii. Generate your keys

Braintree Admin

From the admin click on the API section. Here you'll want to create an API key & a Tokenization Key. You'll need these a little later.

2. Add your payment provider

Create your provider server/src/falcon-braintree-payments/providers/braintree.js

server/src/falcon-braintree-payments/providers/braintree.js

module.exports = class BraintreePayment {
constructor(code, config) {
this.config = config; // This is defined in our config json files
this.code = code;
}
};

We then include this in our config file. We also pass some basic config (nothing with sensitive data e.g. API keys).

server/config/default.json

{
"components": {
"payments": {
"package": "@deity/falcon-payments",
"config": {
"providers": {
"braintree": {
"package": "../../../../src/falcon-braintree-payments/providers/braintree.js",
"config": {
"title": "Braintree",
"sandboxGraphQlUrl": "https://payments.sandbox.braintree-api.com/graphql",
"graphQlUrl": "https://payments.braintree-api.com/graphql"
}
}
}
}
}
}
}

3. Pass your API details to the provider

To get this up and running we will do this in our server/config/local.json. For production config you should read these docs.

server/config/local.json

{
"components": {
"payments": {
"config": {
"providers": {
"braintree": {
"config": {
"environment": "sandbox", // Swap for production if you want to use production details
"publicKey": "YOUR_PUBLIC_KEY",
"privateKey": "YOUR_PRIVATE_KEY",
"tokenizationKey": "YOUR_TOKENIZATION_KEY"
}
}
}
}
}
}
}

You will now have access to all of these details in your provider. They will be passed as config.

4. Passing config data to the frontend (Falcon Client)

Each payment provider should include 2 methods. initialize when the provider is initialized and validate when the payment is submitted.

server/src/falcon-braintree-payments/providers/braintree.js

module.exports = class BraintreePayment {
...
async initialize(payload) {
...
}
async validate(payload) {
...
}
};

We can use the initialize method to pass data from the provider to our Falcon Client components.

server/src/falcon-braintree-payments/providers/braintree.js

module.exports = class BraintreePayment {
constructor(code, config) {
this.config = config;
this.code = code;
this.tokenizationKey = this.config.tokenizationKey;
}
// payload contains information about the payment, e.g. total, currency, customer ID
async initialize(payload) {
return {
title: this.config.title,
code: this.code,
config: {
tokenizationKey: this.tokenizationKey,
total: payload.total
}
}
}
};

5. Install the Braintree React Package

We will install this package client side. It does a lot of the heavy lifting for rendering a Braintree form with hosted fields.

react-braintree-fields

You can either install this with npm or yarn.

npm i react-braintree-fields

6. Create your component & plugin

We now need to create a frontend component for our payment method and a plugin to handle the data.

client/src/pages/shop/Checkout/components/payments/Braintree.js

import React, { useState } from 'react';
import { HostedField } from 'react-braintree-fields';
import { BraintreePlugin } from '../../../../../plugin/Braintree';
const Payment = ({ children, ...props }) => {
const [loading, setLoading] = useState(false);
const fn = () => {
setLoading(true);
return Promise.resolve();
};
return (
<BraintreePlugin {...props}>
{pay =>
children(
() =>
fn()
.then(() => pay())
.then(x => {
setLoading(false);
return x;
})
.catch(x => {
setLoading(false);
return Promise.reject(x);
}),
{ loading }
)
}
</BraintreePlugin>
);
};
// This is rendered in the payment method component
Payment.UI = () => (
<>
<HostedField
type="number"
prefill="4111 1111 1111 1111"
/>
<HostedField type="expirationMonth" />
<HostedField type="expirationYear" />
<HostedField
type="cvv"
placeholder="CVV"
/>
</>
)
export default Payment;

n.b. The <HostedField> components should have associated labels.

It's worth reading the package docs to see how the <Braintree> component works. The main detail we need to know about is the getTokenRef prop.

This returns a function, which when run returns a payment token (nonce) from Braintree. We need this to process transactions.

client/src/plugin/Braintree.js

import React, { useState } from 'react';
import { Braintree } from 'react-braintree-fields';
export const BraintreePlugin = props => {
const { children, config } = props;
const [tokenize, setTokenizeFunc] = useState();
// This method passes data to our validate method in our payment provider. We will use it later.
const payFn = async () => {
// @todo add error handling
return tokenize().then(payload => {
return {
total: config.total,
nonce: payload.nonce
}
});
}
return (
<>
<Braintree
authorization={config.tokenizationKey} // This key comes from our server/config
getTokenRef={ref => setTokenizeFunc(() => ref)}
>
{children(payFn)}
</Braintree>
</>
);
};

7. Include your component as a payment method

In the demo-v2 example project the payment methods are mapped to components in 2 files.

  • client/src/pages/shop/Checkout/components/PaymentMethodPicker.js
  • client/src/pages/shop/Checkout/sections/PlaceOrderSection.js

In these files methods added to your config (server/config/default.json -> payment/providers) are mapped to frontend components. We need to add Braintree to that mapping.

client/src/pages/shop/Checkout/components/PaymentMethodPicker.js

...
import { SimplePayment } from '@deity/falcon-payment-plugin';
import Braintree from './payments/Braintree';
const paymentCodeToPluginMap = {
cash: SimplePayment,
braintree: Braintree
};
...

client/src/pages/shop/Checkout/sections/PlaceOrderSection.js

const paymentCodeToPluginMap = {
braintree: `Braintree`
};

n.b. PlaceOrderSection.js maps the component based on it being within the client/src/pages/shop/checkout/payments/ directory.

If you visit the checkout now you should see Braintree as an option and should also see the form appearing as expected.

7. Create a transaction

The last step is to process the transaction. For this we will be using Braintree's GraphQL endpoint.

It's worth reading through their docs, especially if you want a more complex integration.

i. Add GraphQL Request Package

This package allows us to easily make well formatted GraphQL requests to 3rd party clients.

We install this server side.

If you're using the demo-v2 example project then you'll already have this

npm i graphql-request

ii. Connecting to the Braintree GraphQL client

server/src/falcon-braintree-payments/providers/braintree.js

const { GraphQLClient } = require('graphql-request');
module.exports = class BraintreePayment {
constructor(code, config) {
this.config = config;
this.code = code;
this.environment = this.config.environment;
this.publicKey = this.config.publicKey;
this.privateKey = this.config.privateKey;
this.tokenizationKey = this.config.tokenizationKey;
this.graphQlUrl = this.environment === 'sandbox' ? this.config.sandboxGraphQlUrl : this.config.graphQlUrl;
// This is how the client token is generated. There is also a way to get this through a GraphQL query if you prefer
this.clientToken = Buffer.from(`${this.publicKey}:${this.privateKey}`).toString('base64');
}
get gqlClient() {
return new GraphQLClient(this.graphQlUrl, {
headers: {
Authorization: `Basic ${this.clientToken}`,
'Braintree-Version': '2020-03-01'
}
});
}
};

iii. Test your connection

We can test the connection using the ping query Braintree provide. To test this I've called the ping method in the initialize step. This way we should get a console log (in the terminal window running falcon server) when initializing the payment method on the frontend (visit the checkout and select Braintree).

server/src/falcon-braintree-payments/providers/braintree.js

module.exports = class BraintreePayment {
...
async initialize(payload) {
const test = this.ping();
...
}
async ping() {
const pingQuery = 'query { ping }';
try {
const data = await this.gqlClient.request(pingQuery);
console.log(data);
} catch (errors) {
console.log(errors);
}
}
};

iiii. Create the transaction

We can now create the transaction in our validate method in the provider.

server/src/falcon-braintree-payments/providers/braintree.js

async validate(payload) {
const { nonce, total } = payload;
const transactionData = {
"input": {
"paymentMethodId": nonce,
"transaction": {
"amount": parseFloat(total).toFixed(2)
}
}
}
const createPaymentQuery = `
mutation ChargePaymentMethod($input: ChargePaymentMethodInput!) {
chargePaymentMethod(input: $input) {
transaction {
status
id
}
}
}
`;
try {
const data = await this.gqlClient.request(createPaymentQuery, transactionData);
return data.chargePaymentMethod.transaction;
} catch (errors) {
throw new Error('Braintree failed to return a response');
}
}

8. Finished

That's it, our code will take care of the rest and send you to the success page or display your error. It's worth noting we've not added much error handling, styling or testing that you will need to use this on a production environment.


Complete code.

This code is not production ready, please only use it as an example

server/src/falcon-braintree-payments/providers/braintree.js

const { GraphQLClient } = require('graphql-request');
module.exports = class BraintreePayment {
constructor(code, config) {
this.config = config;
this.code = code;
this.environment = this.config.environment;
this.publicKey = this.config.publicKey;
this.privateKey = this.config.privateKey;
this.tokenizationKey = this.config.tokenizationKey;
this.graphQlUrl = this.environment === 'sandbox' ? this.config.sandboxGraphQlUrl : this.config.graphQlUrl;
this.clientToken = Buffer.from(`${this.publicKey}:${this.privateKey}`).toString('base64');
}
get gqlClient() {
return new GraphQLClient(this.graphQlUrl, {
headers: {
Authorization: `Basic ${this.clientToken}`,
'Braintree-Version': '2020-03-01'
}
});
}
async initialize(payload) {
return {
title: this.config.title,
code: this.code,
config: {
tokenizationKey: this.tokenizationKey,
total: payload.total
}
};
}
async ping() {
const pingQuery = 'query { ping }';
try {
const data = await this.gqlClient.request(pingQuery);
console.log(data);
} catch (errors) {
console.log(errors);
}
}
// https://graphql.braintreepayments.com/guides/creating_transactions/#charging-a-payment-method
async validate(payload) {
const { nonce, total } = payload;
const transactionData = {
"input": {
"paymentMethodId": nonce,
"transaction": {
"amount": parseFloat(total).toFixed(2)
}
}
}
const createPaymentQuery = `
mutation ChargePaymentMethod($input: ChargePaymentMethodInput!) {
chargePaymentMethod(input: $input) {
transaction {
status
id
}
}
}
`;
try {
const data = await this.gqlClient.request(createPaymentQuery, transactionData);
return data.chargePaymentMethod.transaction;
} catch (errors) {
throw new Error('Braintree failed to return a response');
}
}
};

client/src/plugin/Braintree.js

import React, { useState } from 'react';
import { Braintree } from 'react-braintree-fields';
export const BraintreePlugin = props => {
const { children, config } = props;
const [tokenize, setTokenizeFunc] = useState();
const payFn = async () => {
// @todo add error handling
return tokenize().then(payload => {
return {
total: config.total,
nonce: payload.nonce
}
});
}
return (
<>
<Braintree
authorization={config.tokenizationKey}
getTokenRef={ref => setTokenizeFunc(() => ref)}
>
{children(payFn)}
</Braintree>
</>
);
};

client/src/pages/shop/Checkout/components/payments/Braintree.js

import React, { useState } from 'react';
import { HostedField } from 'react-braintree-fields';
import { BraintreePlugin } from '../../../../../plugin/Braintree';
const Payment = ({ children, ...props }) => {
const [loading, setLoading] = useState(false);
const fn = () => {
setLoading(true);
return Promise.resolve();
};
return (
<BraintreePlugin {...props}>
{pay =>
children(
() =>
fn()
.then(() => pay())
.then(x => {
setLoading(false);
return x;
})
.catch(x => {
setLoading(false);
return Promise.reject(x);
}),
{ loading }
)
}
</BraintreePlugin>
);
};
Payment.UI = () => (
<>
<HostedField
type="number"
prefill="4111 1111 1111 1111"
/>
<HostedField type="expirationMonth" />
<HostedField type="expirationYear" />
<HostedField
type="cvv"
placeholder="CVV"
/>
</>
)
export default Payment;

Ask the community. #help

If you can't find what you're looking for, the answer might be on our community slack channel. Our team keep a close eye on this and will usually get back to you within a few hours, if not straight away. If you haven't created an account yet please sign up here slack.deity.io.