Accepting Card Payment using React
This guide shows how to build a card form to take a payment using the Payment Intents API, Dots Elements and React. You can also clone the example project and run it locally.
1. Setup your Dots Account server-side
First, create a Dots account or sign in.
2. Create a Payment Customer server-side
optional
You may skip this step if you do not intend to integrate payment method reusability for your customers.
When a payment is created, a payment method is generated and saved to the PaymentIntent object. This payment method is only capable of being used once (i.e. single-use) unless it is attached to a PaymentCustomer, in which case it becomes reusable.
Therefore, to setup future usage, create a PaymentCustomer object on your server before creating the payment and save the id
to your database. This will used in the following step. An alternative is to create a Dots User which will automatically back populates a payment customer on Dots end and use the user.id
instead of payment_customer.id
.
https://pls.senddotssandbox.com/api/v2/payment-customers/ \
-X POST \
-u "CLIENT_ID:API_KEY" \
-H "Content-Type: application/json" \
-d "{
"first_name": "John",
"last_name": "Hardy",
"email": "john_hardy@gmail.com",
...
}"
> {
"id": "0e835300-5555-4ba1-816a-0abaa8609a83",
...
}
3. Create a Payment Intent server-side
Dots uses a PaymentIntent object to represent your intent to collect payment from a customer, tracking charge attempts and payment state changes throughout the process.
Create a PaymentIntent on your server with an amount and currency. Always decide how much to charge on the server side, a trusted environment, as opposed to the client. This prevents malicious customers from being able to choose their own prices.
If you intend to save the payment method captured by this payment intent for future usage, you must pass in the customer_id
or user_id
below and set setup_future_usage
to on_session
.
https://pls.senddotssandbox.com/api/v2/payment-intents/ \
-X POST \
-u "CLIENT_ID:API_KEY" \
-H "Content-Type: application/json" \
-d "{
"amount": 1000,
"currency": "usd",
"transfer_data": {
"destination_user_id": "0a7d51ec-04df-4eb8-a2c8-d066f6d3bb81"
},
"application_fee_amount": 100 //fee to be paid to the platform
}"
> {
"client_secret": "...",
"id": "0e835300-5555-4ba1-816a-0abaa8609a83",
...
}
The returned PaymentIntent includes a client secret. The client side uses the client secret to securely complete the payment process instead of passing the entire PaymentIntent object. There are different approaches that you can use to pass the client secret to the client side.
You can retrieve the client secret from an endpoint on your server using the browser’s fetch function on the client side. This approach is generally most suitable when your client side is a single-page application, particularly one built with a modern frontend framework such as React. The following example shows how to create the server endpoint that serves the client secret:
app.post('/create-payment-intent', async (req, res) => {
const body = req.body;
const response = await axios.post(
process.env.DOTS_API_URL + '/v2/payment-intents',
{
currency: 'usd',
amount: body.amount,
},
{
withCredentials: true,
auth: {
username: process.env.DOTS_CLIENT_ID,
password: process.env.DOTS_CLIENT_SECRET,
},
}
);
res.json(response.data);
});
This example demonstrates how to fetch the client secret with TypeScript on the client side:
const createPaymentIntent = async (amount: number) => {
const res = await axios.post('/create-payment-intent', {
currency: 'usd',
amount,
});
return res.data.client_secret;
};
4. Collect card details client-side
You’re ready to collect card information on the client with Dots Elements. Elements is a set of prebuilt UI components for collecting and validating card number and expiration date.
Setup Dots Elements
Install React Dots.js and the Dots.js loader from the npm public registry.
npm install --save @dots.dev/react-dots-js @dots.dev/dots-js
Add Dots.js and Elements to your page
To use Element components, wrap the root of your React app in an Elements provider. Call loadDots with your publishable key and pass the returned Promise to the Elements provider.
import React from 'react';
import ReactDOM from 'react-dom';
import { loadDots } from '@dots.dev/dots-js';
import { Elements } from '@dots.dev/react-dots-js';
import CheckoutForm from './CheckoutForm';
// Make sure to call `loadDots` outside of a component’s render to avoid
// recreating the `Dots` object on every render.
const dotsPromise = loadDots(
'pk_prod_pMpPyn6IRnwibuUqqHaFS938RiLMj',
'sandbox'
);
function App() {
return (
<Elements dots={dotsPromise}>
<CheckoutForm />
</Elements>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
Add and configure a PaymentElement component
import { PaymentElement, useDots, useElements } from '@dots.dev/react-dots-js';
import countryList from 'react-select-country-list';
import React, { useEffect, useMemo, useState } from 'react';
export default function CheckoutForm() {
...
const {
register,
handleSubmit,
watch,
reset,
formState: { errors },
} = useForm();
const countryOptions = useMemo(() => countryList().getData(), []);
const renderForm = () => {
const fieldOptions = {
styles: {
base: {
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
fontSmoothing: 'antialiased',
color: '#32325d',
fontWeight: '400',
fontSize: '16px',
},
invalid: {
':hover': {
textDecoration: 'underline dotted red',
},
color: '#fa755a',
},
valid: {
color: '#32CD32',
},
},
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h1>
USD $
{(amount / 100).toLocaleString(navigator.language, {
minimumFractionDigits: 2,
})}{' '}
</h1>
<br />
<PaymentElement options={fieldOptions} />
<div className="sr-combo-inputs">
<div className="sr-combo-inputs-row">
<input
type="text"
placeholder="Full Name"
autoComplete="name"
className="sr-input"
{...register('name', { required: true })}
/>
<input
type="text"
placeholder="12345"
autoComplete="postal-code"
className="sr-input"
{...register('zip', { required: true })}
/>
<select
defaultValue="US"
className="sr-select"
{...register('country', { required: true })}
>
{countryOptions.map((country: any, index: number) => (
<option key={index.toString()} value={country.value}>
{country.label}
</option>
))}
</select>
</div>
</div>
{error && <div className="message sr-field-error">{error}</div>}
<button className="btn" disabled={processing}>
{processing ? 'Processing…' : 'Pay'}
</button>
</form>
);
};
...
}
Elements are completely customizable. You can style Elements to match the look and feel of your site, providing a seamless checkout experience for your customers. It’s also possible to style various input states, for example when the Element has focus.
Always collect a postal code to increase card acceptance rates and reduce fraud.
5. Submit payments to Dots client-side
Rather than sending the entire PaymentIntent object to the client, use its client secret from step 3. After you create the PaymentIntent, the API returns a unique client secret for it (the client secret is different from the secret key you use to make Dots API requests).
The client secret can complete a charge, so handle it with discretion. Don’t log it, embed it in URLs, or expose it to anyone but the customer.
To complete the payment when the user clicks, retrieve the client secret from the PaymentIntent you created in step two and call dots.confirmCardPayment with the client secret and the Element. Pass additional billing details, such as the cardholder name and address, to the billing_details hash.
To call dots.confirmCardPayment
from your payment form component, use the useDots and useElements hooks.
import React, { useEffect, useMemo, useState } from 'react';
import { PaymentElement, useDots, useElements } from '@dots.dev/react-dots-js';
import './CheckoutForm.css';
export default function CheckoutForm() {
const [error, setError] = useState(null);
const [metadata, setMetadata] = useState<any | null>();
const [succeeded, setSucceeded] = useState(false);
const [processing, setProcessing] = useState(false);
const dots = useDots();
const elements = useElements();
...
const onSubmit = async (data: any) => {
if (!dots || !elements) return;
setProcessing(true);
try {
const payload = await dots.confirmCardPayment('{CLIENT_SECRET}', {
payment_method: {
element: elements?.getElement('payment')!,
billing_details: {
name: data.name, // not required
address: {
country: data.country,
zip: data.zip,
},
},
},
});
setSucceeded(true);
setMetadata(payload);
} catch (error: any) {
setError(error.toString());
} finally {
setProcessing(false);
}
};
const renderForm = () => {...};
return (
<div className="checkout-form">
<div className="sr-payment-form">
<div className="sr-form-row" />
{renderForm()}
</div>
</div>
);
}
When the payment completes successfully, the value of the returned PaymentIntent’s status property is succeeded. Check the status of a PaymentIntent in the Dashboard or by inspecting the status property on the object. If the payment isn’t successful, inspect the returned error to determine the cause.\
6. Create future payments with reusable payment method. optional
You can create an off_session
payment by creating and confirming the same payment intent in single API call.
Save the reusable payment method
When a customer completed the first payment, a payment method will be saved to that payment intent. You can listen to the webhook event payment_intent.updated
, and retrieve the payment intent object to find the payment_method_id
. Save that id to your user on your database.
Create off session payments
Follow step 3 to create a payment intent. Instead of passing in customer_id
or user_id
, pass in the payment_method_id
and set confirm
to True
. This creates and confirm the payments immediately.
https://pls.senddotssandbox.com/api/v2/payment-intents/ \
-X POST \
-u "CLIENT_ID:API_KEY" \
-H "Content-Type: application/json" \
-d "{
"amount": 1000,
"currency": "usd",
"confirm": True
"payment_method_id": "u5835300-5555-4ba1-816a-0abaa8609a42",
"transfer_data": {
"destination_user_id": "0a7d51ec-04df-4eb8-a2c8-d066f6d3bb81"
},
"application_fee_amount": 100 //fee to be paid to the platform
}"
> {
"id": "0e835300-5555-4ba1-816a-0abaa8609a83",
"status": "succeeded",
...
}
7. Test the integration
Number | Brand | CVC | DATE |
---|---|---|---|
4111 1111 1111 1111 | Visa (Credit) | Any 3 digits | Any future dates |
4900 7700 0000 0001 | Visa (Debit) | Any 3 digits | Any future dates |
5100 4000 0000 0000 | Mastercard | Any 3 digits | Any future dates |
6011 2345 6789 0123 | Discover | Any 3 digits | Any future dates |
3701 234567 89017 | American Express | Any 3 digits | Any future dates |