Stripe, React and Serverless — Part 2
In this article series, we will talk about how we integrated Sigmetic with Stripe using React and Serverless.
In part 1, we implemented the back-end using Serverless. Here in part 2, we will integrate the front-end in React using Stripe Elements.
Prerequisites
React application
We assume that you have a front-end in React and that this is where you will have your customers subscribe to your service. One of the cool things about Stripe is, that we can collect payment information natively in the application instead of redirecting to a third-party or embedding iframe forms that look entirely different from the rest of your UI.
We will be using Stripe Elements for this. For Sigmetic, we have a page for upgrading that looks like this
With these 3 things in place, let’s get started 🔥
Stripe Manager
To make your life easier, we suggest that you write a set of utility functions that you can use to make requests to the cloud functions that we implemented in part 1.
For Sigmetic, we created a class StripeManager
with a set of static methods.
In this way, you can access the methods like StripeManager.createSubscription();
throughout the application.
You can also export the individual functions and import them like:
import * as StripeManager from '../services/StripeManager'
if you like that pattern better.
For this article, we are going with the class.
So, create a new file somewhere appropriate, like src/services/StripeManager.ts
, and create a new class definition.
class StripeManager {
}
export default StripeManager;
Database
We assume that you have a database with users. Now is the time to extend your user model to include 3 new fields:
customerID
paymentMethodID
subscriptionID
hasPaidPlan
For Sigmetic, we use GraphQL and DynamoDB.
Read more about GRADS: The Tech Stack of Sigmetic
We will mention when to write / read from the database, but we will not go into further details about the implementation in this article.
Create new customer
First, let’s create an interface representing a Stripe Customer. Create a new file src/interfaces/IStripeCustomer
:
export interface IStripeCustomer {
id: string;
object: string;
address?: any;
balance: number;
created: number;
currency: string;
default_source?: any;
delinquent: boolean;
description: string;
discount?: any;
email?: any;
invoice_prefix: string;
invoice_settings: IStripeInvoiceSettings;
livemode: boolean;
metadata: any;
name?: any;
next_invoice_sequence: number;
phone?: any;
preferred_locales: any[];
shipping?: any;
sources: IStripeSources;
subscriptions: IStripeSubscriptions;
tax_exempt: string;
tax_ids: IStripeTaxIds;
}
export interface IStripeInvoiceSettings {
custom_fields?: any;
default_payment_method?: any;
footer?: any;
}
export interface IStripeSources {
object: string;
data: any[];
has_more: boolean;
url: string;
}
export interface IStripeSubscriptions {
object: string;
data: any[];
has_more: boolean;
url: string;
}
export interface IStripeTaxIds {
object: string;
data: any[];
has_more: boolean;
url: string;
}
💡 Hint: Use json2ts to quickly create TypeScript interfaces from JSON.
Now, let’s create a method for creating a new customer
class StripeManager {
public static async createCustomer() {
try {
// Retrieve email and username of the currently logged in user.
// getUserFromDB() is *your* implemention of getting user info from the DB
const { email, username } = getUserFromDB();
if (!email || !username) {
throw Error('Email or username not found.');
}
const request = await fetch('https://your-endpoint/stripe/create-customer', {
method: 'POST',
body: JSON.stringify({
email,
username,
}),
});
const customer = (await request.json()) as IStripeCustomer;
// Update your user in DB to store the customerID
// updateUserInDB() is *your* implementation of updating a user in the DB
updateUserInDB({ customerID: customer.id });
return customer;
} catch (error) {
console.log('Failed to create customer');
console.log(error);
return null;
}
}
}
Now that we’re at it, let’s create a method for retrieving the customerID, and — if none exists — create a new one using the above method.
class StripeManager {
...
public static async getStripeCustomerID() {
// Retrieve the current customerID from the currently logged in user
// getUserFromDB() is *your* implemention of getting user info from the DB
const { customerID } = getUserFromDB();
if (!customerID) {
const customer = await this.createCustomer();
return customer?.id;
}
return customerID;
}
...
}
Create subscription
Again, let’s create a new interface for a Stripe Subscription: Create a new file src/interfaces/IStripeSubscription
:
export interface IStripeSubscription {
id: string;
object: string;
application_fee_percent?: any;
billing_cycle_anchor: number;
billing_thresholds?: any;
cancel_at?: any;
cancel_at_period_end: boolean;
canceled_at?: any;
collection_method: string;
created: number;
current_period_end: number;
current_period_start: number;
customer: string;
days_until_due?: any;
default_payment_method?: any;
default_source?: any;
default_tax_rates: any[];
discount?: any;
ended_at?: any;
items: IStripeItems;
latest_invoice?: any;
livemode: boolean;
metadata: any;
next_pending_invoice_item_invoice?: any;
pause_collection?: any;
pending_invoice_item_interval?: any;
pending_setup_intent?: any;
pending_update?: any;
quantity: number;
schedule?: any;
start_date: number;
status: string;
tax_percent?: any;
trial_end?: any;
trial_start?: any;
}
export interface IStripeRecurring {
aggregate_usage?: any;
interval: string;
interval_count: number;
trial_period_days: number;
usage_type: string;
}
export interface IStripePrice {
id: string;
object: string;
active: boolean;
billing_scheme: string;
created: number;
currency: string;
livemode: boolean;
lookup_key?: any;
metadata: any;
nickname: string;
product: string;
recurring: IStripeRecurring;
tiers?: any;
tiers_mode?: any;
transform_quantity?: any;
type: string;
unit_amount: number;
unit_amount_decimal: string;
}
export interface IStripeDatum {
id: string;
object: string;
billing_thresholds?: any;
created: number;
metadata: any;
price: IStripePrice;
quantity: number;
subscription: string;
tax_rates: any[];
}
export interface IStripeItems {
object: string;
data: IStripeDatum[];
has_more: boolean;
url: string;
}
Then, let’s create a method for creating a new subscription:
class StripeManager {
...
public static async createSubscription(customerID: string, paymentMethodID: string) {
const request = await fetch('https://your-endpoint/stripe/create-subscription', {
method: 'POST',
body: JSON.stringify({
customerID,
paymentMethodID,
}),
});
const subscription = await request.json() as IStripeSubscription;
if (subscription.status !== 'active') {
throw Error('Unable to upgrade. Please try again');
}
if (subscription.latest_invoice.payment_intent.status === 'requires_payment_method') {
throw Error('You credit card was declined. Please try again with another card.');
}
// Update your user in DB to store the subscriptionID and enable paid plan
// updateUserInDB() is *your* implementation of updating a user in the DB
updateUserInDB({
paymentMethodID,
hasPaidPlan: true,
subscriptionID: subscription.id,
});
return subscription;
}
...
}
While we’re at it — let’s add a method for updating the subscription as well.
class StripeManager {
...
public static async handleSubscription(subscriptionID: string, end: boolean) {
const request = await fetch('https://your-endpoint/stripe/handle-subscription', {
method: 'POST',
body: JSON.stringify({
subscriptionID,
end,
}),
});
return await request.json() as IStripeSubscription;
}
...
}
And finally, one for retrieving the subscription
class StripeManager {
...
public static async retrieveSubscription(subscriptionID: string) {
const request = await fetch('https://your-endpoint/stripe/retrieve-subscription', {
method: 'POST',
body: JSON.stringify({
subscriptionID,
}),
});
return await request.json() as IStripeSubscription;
}
...
}
Update payment method
Alright, now we just need a couple of more methods.
Let’s add one for updating the payment method:
class StripeManager {
...
public static async updatePaymentMethod(customerID: string, paymentMethodID: string) {
await fetch('https://your-endpoint/stripe/update-payment-method', {
method: 'POST',
body: JSON.stringify({
customerID,
paymentMethodID,
}),
});
// Update your user in DB to store the new payment method
// updateUserInDB() is *your* implementation of updating a user in the DB
updateUserInDB({ paymentMethodID });
}
...
}
We also want to be able to retrieve payment information, so let’s add a method for that as well:
class StripeManager {
...
public static async retreivePaymentInfo(paymentMethodID: string) {
try {
const request = await fetch(
'https://your-endpoint/stripe/retrieve-payment-method',
{
method: 'POST',
body: JSON.stringify({
paymentMethodID,
}),
}
);
const result = await request.json();
return {
type: result.card.brand,
digits: result.card.last4,
};
} catch (error) {
console.error(error);
return null;
}
}
...
}
Retry invoice
Alright! All that is left is the method for retrying the invoice payment.
class StripeManager {
...
public static async retryInvoice(customerID: string, paymentMethodID: string, invoiceID: string) {
const request = await fetch('https://your-endpoint/stripe/retry-invoice', {
method: 'POST',
body: JSON.stringify({
customerID,
paymentMethodID,
invoiceID,
}),
});
await request.json();
}
...
}
That’s it! 💪 The StripeManager is done!
Payment form
Now, let’s move on to the fun part: The payment form. We’re going to cut some corners here, in terms of the React code. But we’ll make sure that the point is clear.
First, you need to go to your Stripe dashboard -> Developers -> API keys and grab the public key. Let’s store that in an .env
file:
REACT_APP_STRIPE_KEY=<PUT-YOUR-KEY-HERE>
Next, let’s install a couple of Stripe dependencies:
npm install @stripe/react-stripe-js @stripe/stripe-js
Let’s create a new component for the payment form: src/component/upgrade.tsx
Now, we start by importing the needed dependencies:
import { loadStripe } from '@stripe/stripe-js';
import {
CardNumberElement,
CardExpiryElement,
CardCvcElement,
Elements,
useStripe,
useElements,
} from '@stripe/react-stripe-js';
ℹ️ You can also import a single element for the whole credit card: `import { CardElement } from '@stripe/react-stripe-js'` In this example we will use individual elements for card number, expiry date and cvc.
Let’s start creating the React component:
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_KEY || '');
const Upgrade = () => {
return (
<div>
<h1>Upgrade now</h1>
<Elements stripe={stripePromise}>
<CheckoutForm />
</Elements>
</div>
);
};
const CheckoutForm = () => {
// Not implemented yet
};
export default Upgrade;
The Upgrade
component is going the be the container of the payment form.
The form is wrapped in the Elements
component that is provided by react-stripe-react
.
We need to pass a ‘promisified’ instance on Stripe to this component.
Now, let’s create the CheckoutForm
component:
const CheckoutForm = () => {
// Include these hooks:
const stripe = useStripe();
const elements = useElements();
const [nameInput, setNameInput] = useState('');
const [retry, setRetry] = useState(!!localStorage.getItem('invoice_retry'));
const handleSubmitPayment = async () => {
// Not implemented yet
};
const handleRetryPayment = async () => {
// Not implemented yet
};
const buttonAction = retry ? handleRetryPayment : handleSubmitPayment;
return (
<div>
<input
placeholder='Name on card'
value={nameInput}
onChange={(e) => setNameInput(e.currentTarget.value)}
/>
<CardNumberElement />
<CardExpiryElement />
<CardCvcElement />
<button onClick={buttonAction}></button>
</div>
);
};
You should be able to see your payment form render 😎
Now, let’s implement that handleSubmitPayment
function:
const handleSubmitPayment = async () => {
if (!stripe || !elements) {
return;
}
try {
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: elements.getElement(CardNumberElement) as any,
billing_details: {
name: nameInput,
},
});
if (error || !paymentMethod) {
throw Error(error?.message || 'Something is not right...');
}
const customerID = await StripeManager.getStripeCustomerID();
if (!customerID) {
throw Error('Could not identify customer');
}
const paymentID = paymentMethod.id;
const subscription = await StripeManager.createSubscription(customerID, paymentID);
if (subscription.latest_invoice.payment_intent.status === 'requires_payment_method') {
setRetry(true);
localStorage.setItem('latest_invoice_id', subscription.latest_invoice.id);
throw Error('Your card was declined. Please try again or with another card');
}
if (subscription.status !== 'active') {
throw Error('Could not process payment.');
}
} catch (error) {
console.error(error);
// Let the user know that something went wrong here...
}
};
Finally, let’s implement the handleRetryPayment
function:
const handleRetryPayment = async () => {
if (!stripe || !elements) {
return;
}
const invoiceID = localStorage.getItem('latest_invoice_id');
try {
if (!invoiceID) {
throw Error('Could not process payment. Please refresh and try again.');
}
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: elements.getElement(CardNumberElement) as any,
billing_details: {
name: nameOfCard,
},
});
if (error || !paymentMethod) {
throw Error(error?.message || 'Something is not right...');
}
const customerID = await StripeManager.getStripeCustomerID();
if (!customerID) {
throw Error('Could not identify customer');
}
const paymentID = paymentMethod.id;
await StripeManager.retryInvoice(customerID, paymentID, invoiceID);
localStorage.removeItem('latest_invoice_id');
} catch (error) {
console.error(error);
// Let the user know that something went wrong here...
}
};
That’s it! 😎 The payment form should now be working! Now you just need to add some styling, maybe some extra error handling for the name-of-card input, etc.
Manage current subscription
The last thing we need to create is a way of letting the user manage their subscription. As a minimum, they should be able to cancel their subscription, see and change their payment method.
Let’s implement this in a new component. For Sigmetic, we have something that looks like this:
Let’s create a new component: src/components/Plan.tsx
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_KEY || '');
const Plan = () => {
const [cardInformation, setCardInformation] = useState<{
type: string;
digits: string;
}>();
const [subscription, setSubscription] = useState<IStripeSubscription>();
const fetchCardInformation = async () => {
try {
const info = await StripeManager.retreivePaymentInfo(user.paymentMethodID);
if (info) {
setCardInformation(info);
}
} catch (error) {
// Let the user know that something went wrong here...
}
};
const fetchSubscription = async () => {
try {
const sub = await StripeManager.retrieveSubscription(user.subscriptionID);
if (sub) {
setSubscription(sub);
}
} catch (error) {
// Let the user know that something went wrong here...
}
};
useEffect(() => {
fetchSubscription();
fetchCardInformation();
}, []);
const handleCancelSubscription = (end: boolean) => async () => {
// Not implemented yet
};
const expirationDate = new Date(subscription.current_period_end * 1000).toDateString();
const subscriptionWillEnd = subscription.cancel_at_period_end;
return (
<div>
<div>The plan will expire on: {expirationDate}</div>
<div>Card: {cardInformation.type}</div>
<div>**** **** **** {cardInformation.digits}</div>
<Elements stripe={stripePromise}>
<UpdateForm />
</Elements>
<button onClick={handleCancelSubscription(!subscriptionWillEnd)}>
{subscriptionWillEnd ? 'Continue' : 'Cancel'}
</button>
</div>
);
};
const UpdateForm = () => {
// Not implemented yet
};
export default Plan;
We skipped a lot of markup and styling here — that one will be all on you 😉
Let’s implement the handleCancelSubscription
method:
const handleCancelSubscription = (end: boolean) => async () => {
try {
const subscription = await StripeManager.handleSubscription(user.subscriptionID, end);
setSubscription(subscription);
} catch (error) {
// Let the user know that something went wrong here...
}
};
Pretty straight forward, right!
Finally, let’s implement the UpdateForm
component.
const UpdateForm = () => {
// Include these hooks:
const stripe = useStripe();
const elements = useElements();
const [nameInput, setNameInput] = useState('');
const handleUpdatePaymentMethod = async () => {
// Not implemented yet
};
return (
<div>
<input
placeholder='Name on card'
value={nameInput}
onChange={(e) => setNameInput(e.currentTarget.value)}
/>
<CardNumberElement />
<CardExpiryElement />
<CardCvcElement />
<button onClick={handleUpdatePaymentMethod}></button>
</div>
);
};
And finally, the handleUpdatePaymentMethod
function:
const handleUpdatePaymentMethod = async () => {
if (!stripe || !elements) {
return;
}
try {
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: elements.getElement(CardNumberElement) as any,
billing_details: {
name: nameOfCard,
},
});
if (error || !paymentMethod) {
throw Error(error?.message || 'Something is not right...');
}
const customerID = await StripeManager.getStripeCustomerID();
if (!customerID) {
throw Error('Could not identify customer');
}
const paymentID = paymentMethod.id;
await StripeManager.updatePaymentMethod(customerID, paymentID);
// You may want to refetch the payment method after this...
} catch (error) {
Toast.error(error.message);
}
};
Rounding up
That’s it 🎉🎉 We’re done with the integration!
Of course, we left out quite a bit of styling, database connections, etc. These are specific to your application and not overly relevant for this article.
As mentioned in the beginning — there are a lot of different ways to handle this, and this article simply explains how we handle it at Sigmetic!
Did you see something unusual, odd, or insecure? Please don’t hesitate to reach out!! We’d like to know.
Any questions or confusion? Please reach out as well — we’ll be happy to answer any questions that may have arisen 🔥