React JS: A Detailed Stripe Subscription Example (For SAAS)

This article is going to cover a React Stripe subscription example in detail. We are going to create a ReactJS front end. This front end will talk to a NodeJS and ExpressJS-based backend. It takes you through all the “subscription and payment” based interactions the user will have as they interact with your SAAS application.

A Working Demo

To quickly get a visual overview of all the things we are going to cover in this guide, I have created a video. Once you see the video you will get a very clear idea about how this guide can help you.

Next, let’s look at how all the things you have seen in the video are actually implemented.

Step 1: Let’s Set Up A Front End & Back End & Get A Stripe Secret Key

I am going to assume that you already have a Stripe account. If you do not, creating one takes just a few minutes. Just go to the Register page, fill in your details, and you should have an account in no time. The account might be in “Test Mode” but that is all you need right now.

Getting Your Stripe Secret Key

All you need to do is go to your Stripe Dashboard and look for the section that looks like this:

Get the Stripe secret key from the Stripe Dashbaord

Next, you “Reveal” the key. And then hovering over it you can copy it. Save this key with you. You are going to need it soon. Also, it’s a “secret key”. So, as the name suggests, it is supposed to be kept a “Secret”. So, keep it safe.

Setting Up Your React Front End

For this part, you don’t need anything special. Just a simple React application. I created the demo app used in this tutorial using Create React App

There are no special libraries to be installed on the front end. All the talking to Stripe is going to happen via the backend. So, just a barebones React app is all you need.

Setting Up Your NodeJS & ExpressJS-Based Backend

Here you need to set up a new Node app and then install Express etc. Below are the rough steps:

$> mkdir strip-back-end
$> cd strip-back-end
$> npm init
$> npm install --save stripe express cors

Above, we are just creating a new folder. Doing the usual npm init to get things started and create the “package.json” etc.

Then we are installing 3 packages from NPM. Let me explain to you why we are installing each of these.

  1. stripe: Well, to talk to Stripe. This is the official Stripe package.
  2. express: This is a super lightweight way to create a web application. We will make our ReactJS front end talk to this web application.
  3. cors: We need this because the React.JS app and the backend ExpressJS app will be running on different domains. So, to allow communication between them and not have CORS issues.

So, once you have this in place, we are ready to move on.

Please Note: In this guide, I am going to assume that you know the basics of ReactJS and have used ExpressJS in the past. I will not be going into the details of these. This guide will be more focused on the Stripe side of things.

How To Create A New Customer & Get The Customer ID

The first thing that will happen as the user comes to your application and tries to use it is: You will ask the user to sign up and create an account.

So, as you do this, you will most likely capture the user’s email ID. Now, what we are going to do in this stage is, we are going to create a parallel “Customer” record in Stripe. All the things this user does, like “making a payment”, “signing up for a subscription” etc will be associated with this “Customer” record in Stripe.

Wait, why do we need to create a new customer? Why not just get the customer to subscribe?

The reason we are doing this is that once we do this, we get to use the Stripe Customer Billing Portal and other features for free. This billing portal is going to save us a huge amount of development time. In the above video, I go over what that looks like and what it allows the user to do.

Some of the features of the billing portal are:

  1. Allow the user to download old payment invoices
  2. Allow the user to look at the current plan and change it
  3. Allow the user to change the payment method etc.

Here are the official Stripe docs about the Customer Portal. Look for the button called “View Demo” to actually play with the portal and see it all in action.

Here is an image of what the billing portal looks like:

Stripe customer billing portal features when using stripe for subscriptions(Click the image to open in full size)

A Simple Node Script To Create A New Customer

Firstly, here is the link to the official docs: Customer Creation

The actual lines that create a new customer are just as follows:

// Create a Stripe Client using your "secret"
const stripe = require('stripe')('put-your-stripe-secret-here');

// Get your customer email ID (probably from the DB)
let customerEmailAddress = "customeremail@gmail.com";

// Create the customer
const customer = await stripe.customers.create({
    description: `${customerEmailAddress} via API`,
    email: customerEmailAddress
});

Now, when you create the customer, Stripe will give you back a customer object. I am going to show you the type of response you get below:

{
    id: 'cus_N2G7hYkRaHomSk',
    object: 'customer',
    address: null,
    balance: 0,
    created: 1671803272,
    currency: null,
    default_source: null,
    delinquent: false,
    description: 'customer1.email@gmail.com-12',
    discount: null,
    email: null,
    invoice_prefix: '46E9C02A',
    invoice_settings: {
      custom_fields: null,
      default_payment_method: null,
      footer: null,
      rendering_options: null
    },
    livemode: false,
    metadata: {},
    name: null,
    next_invoice_sequence: 1,
    phone: null,
    preferred_locales: [],
    shipping: null,
    tax_exempt: 'none',
    test_clock: null
  }

As you can see, there is an “id” that you got back. The id looks something like this: cus_N2G7hYkRaHomSk. We are going to need and use this ID very soon.

Note: In order to see how this above code is used in the context of the NodeJS & ExpressJS-based server, check out this gist. I have put the entire server code there. The CORS settings and the JSON parser are all there.

But, the part specifically related to the “create_customer” route is below:

app.post('/create_customer', async (request, response) => {
    const customerEmailAddress = request.body.customerEmailId;

    const customer = await stripe.customers.create({
        description: `${customerEmailAddress} via API`,
        email: customerEmailAddress
    });

    console.log(customer);

    let theCreatedCustomerId = customer.id;

    response.send({
        customerId: theCreatedCustomerId
    });
});

As you can see, we have created a POST request, that takes the email ID from the customer and creates the customer on Stripe. Then from the response, sends the “id” of the customer back to the front end.

On the React side, here is the component that is talking to this route:

import React, { useState } from 'react';

function Stage1(props) {

    const [customerId, setCustomerId] = useState(null);
    const [customerEmailID, setCustomerEmailID] = useState(null);
    const [createCustomerButtonText, setCreateCustomerButtonText] = useState('Create Customer');

    let createCustomer = () => {

        setCreateCustomerButtonText('Talking to Stripe to create customer. Please wait..');

        let data = {
            customerEmailId: customerEmailID
        }

        fetch('http://localhost:4242/create_customer', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(data),
        })
            .then((response) => response.json())
            .then((data) => {
                setCustomerId(data.customerId);
                props.updateCustomerIdFunction(data.customerId);
                setCreateCustomerButtonText('Done! Customer Created :-)');
            });

    }

    return (
        <div className='stage'>

            <h1>Stage 1: User Gets Created</h1>

            <p>Please Fill In Your Email:</p>

            <input
                type="email"
                onChange={(event) => setCustomerEmailID(event.target.value)}
            />

            <button
                onClick={createCustomer}>{createCustomerButtonText}</button>

            {customerId != null && (<p
                id="customer_created_success_msg">
                Hey! Congrats! A user was created with the id: <strong>{customerId}</strong>
            </p>)}
        </div>);
}

export default Stage1;

As you can see, most of it is standard React code. All we are really doing is taking the email ID from the user and sending it to the backend via a POST request.

Looking At Your Customer On Your Stripe Dashboard

You just have to head over to your Stripe dashboard and click on the “Customers” tab. In the test mode, this is the URL.

(Click the image to open in full size)

The above is how your customers will look.

How To Create Products & Prices

In order for the user to have something to subscribe to, we are going to have to create products. We are going to do this step on the Stripe dashboard. It’s a simple point-and-click process that will be explained below.

Official Stripe Video

Here is the official Stripe video that goes over Stripe billing and all the options you have:

Step By Step: Setting Up Good, Better, Best Pricing For SAAS Plans

In most SAAS applications, you have a Good, Better, Best kind of pricing. Something like: $14/month for some features. $40/month for more features and then finally at $100/month for all the features.

Now, since this is the most common type of SAAS billing model, let’s look at how you would set this up on the Stripe dashboard.

Firstly, you need to go to your Products page. If you are in test mode, this is the link.

And then you need to click on the “Add Product” button. A form like the below one will open up.

Creating a SAAS reccuring billing product on Stripe

The form is pretty simple to understand. To set up a recurring billing product, you need to fill in details as I have done above. In the above example, $14 will be charged each month.

For the good, better, and best type pricing, you will create 3 such products. All the products are going to have a “price”. And all the prices are going to have a “price_id”. It looks something like this: price_1MINgESICZrnQHl2HDXHMgiU

We are going to need that price ID in the next stage. So, hold on to all of them. You can get the price ID from the product page from the section that looks like the one below:

Copy the price ID from the Pricing section for use when creating a checkout link

How To Create A “Checkout Session” So That The Customer Can Buy The Product

Now, we are going to use the prices and products we have created along with the customer to create a “checkout session”. Basically, a link via which the customer can subscribe to a plan.

A Simple Node Script To Create A Checkout Link For A Customer & Price

Once again, we will start with the official docs for creating a checkout link.

The actual code for creating a checkout session is as follows:

const session = await stripe.checkout.sessions.create({
    billing_address_collection: 'auto',
    line_items: [
        {
            price: priceId,
            quantity: 1,

        },
    ],
    mode: 'subscription',
    success_url: `http://localhost:3000/?success=true`,
    cancel_url: `http://localhost:3000?canceled=true`,
    customer: customerId
});

In the above snippet, note the following:

  1. The variable “stripe” is created using the secret key as I have shown above in the “create a customer” section. Since this is common to all interactions with the Stripe API, I will not be repeating it everywhere.
  2. The variable “priceId” on line 5 is the price that the customer wishes to pay. We got that from the above section.
  3. The variable “customerId” on line 13 is from the first section on creating a customer.
  4. The “success_url” and “cancel_url” needs to be updated as per where you would like to send the user once the payment is complete.

The result of the above is that we tell Stripe: “This customer wants to pay for this plan and get started. Please send me the link and I will send the customer over there to make the payment”.

The following is the type of response you get in the “session” variable once this is executed.

{
    id: 'cs_test_a16xwzgOIAYEuy3KGCjT7YQe7cpSWz2oyhVzjQomcW4xwMaE6ZRDF9ARr3',
    object: 'checkout.session',
    after_expiration: null,
    allow_promotion_codes: null,
    amount_subtotal: 1400,
    amount_total: 1400,
    automatic_tax: { enabled: false, status: null },
    billing_address_collection: 'auto',
    cancel_url: 'http://localhost:4242?canceled=true',
    client_reference_id: null,
    consent: null,
    consent_collection: null,
    created: 1671803550,
    currency: 'usd',
    custom_text: { shipping_address: null, submit: null },
    customer: 'cus_N2G7hYkRaHomSk',
    customer_creation: null,
    customer_details: null,
    customer_email: null,
    expires_at: 1671889950,
    invoice: null,
    invoice_creation: null,
    livemode: false,
    locale: null,
    metadata: {},
    mode: 'subscription',
    payment_intent: null,
    payment_link: null,
    payment_method_collection: 'always',
    payment_method_options: null,
    payment_method_types: [ 'card' ],
    payment_status: 'unpaid',
    phone_number_collection: { enabled: false },
    recovered_from: null,
    setup_intent: null,
    shipping_address_collection: null,
    shipping_cost: null,
    shipping_details: null,
    shipping_options: [],
    status: 'open',
    submit_type: null,
    subscription: null,
    success_url: 'http://localhost:4242/?success=true&session_id={CHECKOUT_SESSION_ID}',
    total_details: { amount_discount: 0, amount_shipping: 0, amount_tax: 0 },
    url: 'https://checkout.stripe.com/c/pay/cs_test_a16xwzgOIAYEuy3KGCjT7sadadadaZGdpYFVpZGZgbWppYWB3dic%2FcXdwYHgl'
  }

As you can see, the URL we need from Stripe is in the “url” key. So, we just have to take it out from there and send the user to this URL.

In order to see how this code is used in the context of the NodeJS & ExpressJS-based server, check out this gist. I have put the entire server code there. The CORS settings and the JSON parser are all there.

But, the part specifically related to the “create_checkout_link” route is below:

app.post('/create_checkout_link', async (request, response) => {
    const priceId = request.body.priceId;
    const customerId = request.body.customerId;

    const session = await stripe.checkout.sessions.create({
        billing_address_collection: 'auto',
        line_items: [
            {
                price: priceId,
                quantity: 1,

            },
        ],
        mode: 'subscription',
        success_url: `http://localhost:3000/?success=true`,
        cancel_url: `http://localhost:3000?canceled=true`,
        customer: customerId
    });

    console.log(session);

    response.send({
        url: session.url
    });
});

Connecting The Link To Your React UI

On the React JS side, the code is pretty simple. We are just collecting the customerId and the priceID that the user wants to go with and hitting this above route.

We get the URL from the route and we then send the user there.

Below is the ReactJS component that handles this:

function Stage2(props) {

    const plans = [
        {
            name: 'Starter',
            price_id: 'price_1MHdGSSICZrnQHl2rgnyrx4J',
            price_for_user: '$14 / month'
        },
        {
            name: 'Runner',
            price_id: 'price_1MINgESICZrnQHl2HDXHMgiU',
            price_for_user: '$40 / month'
        }
    ]

    const checkout = (priceId) => {

        let data = {
            priceId: priceId,
            customerId: props.customerId
        }

        fetch('http://localhost:4242/create_checkout_link', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(data),
        })
            .then((response) => response.json())
            .then((data) => {
                window.location.replace(data.url);
            });

    }

    return (
        <div className='stage'>

            <h1>Stage 2: User Signs Up For Subscription</h1>

            { props.customerId && (
                <p>Now that we have our customerId: <strong>{props.customerId}</strong>, lets sign up for a subscription!</p>
            ) }

            { plans.map( plan => {
                return (
                    <div className="plan">
                        <h3>{plan.name}</h3>
                        <p>Compelling reasons...</p>
                        <button onClick={ () => checkout(plan.price_id) }>Buy Now at {plan.price_for_user}</button>
                    </div>
                );
            }) }

        </div>);
}

export default Stage2;

As you would have seen in the demo video above, at this point the user will fill in some card details and proceed with checkout. It will look something like this…

A Stripe subscription payment page where the user can pay and start their SAAS subscription

How To Know When Your Customer Has Subscribed To A Plan?

Since all the signing up for the plan and making the payment etc is happening on Stripe-hosted pages, you might be wondering: “Wait, how do I know that the user actually managed to pay and I need to now actually give them access to my app?”

Well, the answer to that question is going to come in the next section on setting up a webhook. As the user interacts and pays, Stripe will keep sending you messages to a webhook URL you set up. You can read those messages and tell what has happened. Based on that you can take the appropriate actions.

How To Test Things With The Test Cards Provided By Stripe

If you set up all this, you will reach a “checkout page”. There you will be asked to fill in some Card details. Stripe has very helpfully provided a whole list of cards you can use to test things out.

You can use these cards not only to check what will happen when the user successfully manages to pay. But, you can also test out what happens when they fail to pay. As described above Stripe will send you webhook events for all the failed attempts too. You can use this to contact users who desperately want to pay but cannot manage to for some reason and offer them an alternative payment method for example.

Note: Once you make a payment and are sent back to the React App, you can try and look at the payments and the user on the Stripe dashboard. As I have shown in the demo video above, the payment and the subscription should be reflected there.

How To Set Up A Webhook For Monitoring All Events

Why You Need To Set Up A Webhook To Get All Stripe Events

As explained in the above section, the user is going to make the actual purchase on Stripe-hosted pages. So, in order to learn if the user successfully managed to pay, you need to set up a webhook. As the user interacts and pays, Stripe will keep sending you messages to a webhook URL you set up. You can read those messages and tell what has happened. Based on that you can take the appropriate actions.

Besides this, in the section below, you will see how you can easily create a Stripe-hosted billing portal. Here you can give the user the ability to change plans etc. So, as the user switches plans, your webhook URL will be hit and you will have all the information you need in order to update things at your end in terms of feature access, etc.

Steps For Setting Up Testing Of Webhook On Local

Webhooks are usually a pain to test in the dev environment. But Stripe has done an awesome job here. They have created a Stripe CLI client that you can install and then send and receive all the webhook events for your account and forward them to your localhost server.

There are the steps for doing this:

  1. Installing the Stripe CLI. Here are the official docs for all platforms.
  2. Logging in to Stripe from the CLI (also covered above)
  3. Forwarding the events to your Node and Express-based server.

As you will see in the 3rd point above when you run the command to forward the webhook events, on the CLI you will see a webhook signing secret that looks something like this:

> Ready! You are using Stripe API Version [2022-11-15]. Your webhook signing secret is whsec_43bc9fb9d0b6e1dde22a27a4533369b93e3111291d74e404db53441c62bda0ecbe8 (^C to quit)

You need to use the secret soon. This will be used to validate that the requests are really coming from Stripe. Without this, a hacker can easily hit your webhook URL and send messages that seem like they are from Stripe but simply give him access to the application for free.

Steps For Setting Up A HTTP Webhook URL

Once you graduate from local testing, the process of setting up a URL as Webhook is pretty straightforward. All you have to do is go to the webhook page. Click here for the test mode URL.

Here you need to hit the big “Add An Endpoint” URL and then fill out the form.

Create a new webhook to get Stripe Events(Click the image to open in full size)

Here again, do make sure you hold on to the endpoint secret (in the code on the right). You are going to need it in the next stage.

How To Get Webhook Events & How To Know They Are Actually From Stripe?

I am going to provide a code snippet here. In from the Node+Express Server. Please have a look at all the comments that explain all the different parts and what they are doing.

// Set up Stripe, Express, CORS etc
// Put in you secret key from stripe below
const stripe = require('stripe')('sk_test_Stripe_secret_key');
const express = require('express');
const cors = require('cors');
const app = express();
const fs = require('fs');

// This is needed because the React App
// and the Express backend are on different domains
// This is not important from the Webhook perspective
app.use(cors({
    origin: '*'
}));


// This IS IMPORTANT!!
// The Stripe package provides some code to validate the
// requests that hit the webhook URL.
// In order for that to work, the URL needs the Raw request body
// The code in the current Stripe docs does not seems to work
// but this does.
app.use(
    "/webhook",
    express.json({
      verify: (req, res, buf) => {
        req.rawBody = buf.toString();
      },
    })
  );

// Another middleware for the other routes
app.use(express.json());


// The webhook route
app.post(
    '/webhook', express.raw(), (request, response) => {
        let event = request.body;
        const endpointSecret = 'whsec_REPLACE_THIS_WITH_THE_SECRET_FROM_PREVIOUS_SETPS';

        // Now this is the code
        // that validates that the request is coming from Stripe.
        // Without this a hacker can hit this webhook and pretend to be Stripe
        // and you might end up giving the hacker access to your app
        if (endpointSecret) {
            const signature = request.headers['stripe-signature'];

            try {
                event = stripe.webhooks.constructEvent(
                    request.rawBody,
                    signature,
                    endpointSecret
                );
            } catch (err) {
                console.log(`⚠️  Webhook signature verification failed.`, err.message);
                return response.sendStatus(400);
            }
        }

        // Now normally, you will read the "event.type"
        // then read the info in the event object and do something meaningfull.
        // But I am just going to store the data into a file for logging and studying
        // In a real application your business logic code comes here
        const eventType = event.type;
        const unixTimeStamp = Math.floor(Date.now() / 1000);
        const completeEventBody = event;

        let fileName = `${unixTimeStamp}-${eventType}`;
        let webhookFileContent = JSON.stringify(completeEventBody, null, 4);

        fs.appendFile(`./webhook_events/${fileName}.json`, webhookFileContent, function (err) {
            if (err) throw err;
            console.log(`Webhook event file saved with name: ${fileName}!`);
        }); 

        response.send();
    }
);

app.listen(4242);

In order to see how this code is used in the context of the NodeJS & ExpressJS-based server, check out this gist.

Here is the official doc where you can see all the events you can get at this webhook.

Since I found the official docs lacking, I have uploaded all the webhook events that are created when the user is making a Successful payment here on Github.

You can also look at all the events that hit your webhook here.

You can open up the files and have a look at all the event data and handle the events as needed.

Here is a sample event of the type: customer.subscription.created

{
    "id": "evt_1MJpLRSICZrnQHl2aXyd55fY",
    "object": "event",
    "api_version": "2022-11-15",
    "created": 1672194373,
    "data": {
        "object": {
            "id": "sub_1MJpLMSICZrnQHl2g5delzXK",
            "object": "subscription",
            "application": null,
            "application_fee_percent": null,
            "automatic_tax": {
                "enabled": false
            },
            "billing_cycle_anchor": 1672194372,
            "billing_thresholds": null,
            "cancel_at": null,
            "cancel_at_period_end": false,
            "canceled_at": null,
            "collection_method": "charge_automatically",
            "created": 1672194372,
            "currency": "usd",
            "current_period_end": 1674872772,
            "current_period_start": 1672194372,
            "customer": "cus_N3xEBwXaRDVHYL",
            "days_until_due": null,
            "default_payment_method": null,
            "default_source": null,
            "default_tax_rates": [],
            "description": null,
            "discount": null,
            "ended_at": null,
            "items": {
                "object": "list",
                "data": [
                    {
                        "id": "si_N3xFhuQUS6qrFy",
                        "object": "subscription_item",
                        "billing_thresholds": null,
                        "created": 1672194373,
                        "metadata": {},
                        "plan": {
                            "id": "price_1MHdGSSICZrnQHl2rgnyrx4J",
                            "object": "plan",
                            "active": true,
                            "aggregate_usage": null,
                            "amount": 1400,
                            "amount_decimal": "1400",
                            "billing_scheme": "per_unit",
                            "created": 1671671284,
                            "currency": "usd",
                            "interval": "month",
                            "interval_count": 1,
                            "livemode": false,
                            "metadata": {},
                            "nickname": null,
                            "product": "prod_N1gdRaZWf2vFFN",
                            "tiers_mode": null,
                            "transform_usage": null,
                            "trial_period_days": null,
                            "usage_type": "licensed"
                        },
                        "price": {
                            "id": "price_1MHdGSSICZrnQHl2rgnyrx4J",
                            "object": "price",
                            "active": true,
                            "billing_scheme": "per_unit",
                            "created": 1671671284,
                            "currency": "usd",
                            "custom_unit_amount": null,
                            "livemode": false,
                            "lookup_key": null,
                            "metadata": {},
                            "nickname": null,
                            "product": "prod_N1gdRaZWf2vFFN",
                            "recurring": {
                                "aggregate_usage": null,
                                "interval": "month",
                                "interval_count": 1,
                                "trial_period_days": null,
                                "usage_type": "licensed"
                            },
                            "tax_behavior": "unspecified",
                            "tiers_mode": null,
                            "transform_quantity": null,
                            "type": "recurring",
                            "unit_amount": 1400,
                            "unit_amount_decimal": "1400"
                        },
                        "quantity": 1,
                        "subscription": "sub_1MJpLMSICZrnQHl2g5delzXK",
                        "tax_rates": []
                    }
                ],
                "has_more": false,
                "total_count": 1,
                "url": "/v1/subscription_items?subscription=sub_1MJpLMSICZrnQHl2g5delzXK"
            },
            "latest_invoice": "in_1MJpLMSICZrnQHl2IKge5GmF",
            "livemode": false,
            "metadata": {},
            "next_pending_invoice_item_invoice": null,
            "on_behalf_of": null,
            "pause_collection": null,
            "payment_settings": {
                "payment_method_options": null,
                "payment_method_types": null,
                "save_default_payment_method": "off"
            },
            "pending_invoice_item_interval": null,
            "pending_setup_intent": null,
            "pending_update": null,
            "plan": {
                "id": "price_1MHdGSSICZrnQHl2rgnyrx4J",
                "object": "plan",
                "active": true,
                "aggregate_usage": null,
                "amount": 1400,
                "amount_decimal": "1400",
                "billing_scheme": "per_unit",
                "created": 1671671284,
                "currency": "usd",
                "interval": "month",
                "interval_count": 1,
                "livemode": false,
                "metadata": {},
                "nickname": null,
                "product": "prod_N1gdRaZWf2vFFN",
                "tiers_mode": null,
                "transform_usage": null,
                "trial_period_days": null,
                "usage_type": "licensed"
            },
            "quantity": 1,
            "schedule": null,
            "start_date": 1672194372,
            "status": "incomplete",
            "test_clock": null,
            "transfer_data": null,
            "trial_end": null,
            "trial_start": null
        }
    },
    "livemode": false,
    "pending_webhooks": 2,
    "request": {
        "id": "req_kQICUMhy5dnJlK",
        "idempotency_key": "982420a0-0870-4c5e-baf6-779f541a0fa3"
    },
    "type": "customer.subscription.created"
}

How To Create A Link To The Billing Portal For Management Of Plans, Invoices, Billing Etc

At this point, you have a customer that has signed up for a plan. Now, your customer might ask you for an invoice. Or might want to change their plan or stop their plan etc.

As I have mentioned above, Stripe allows you to handle all this via a “Customer Billing Portal”. These are Stripe-hosted pages and using them can save you a lot of work.

Some of the features of the billing portal are:

  1. Allow the user to download old payment invoices
  2. Allow the user to look at the current plan and change it
  3. Allow the user to change the payment method etc.

Here are the official Stripe docs about the Customer Portal. Look for the button called “View Demo” to actually play with the portal and see it all in action.

Here is an image of what the billing portal looks like:

Stripe customer billing portal features when using stripe for subscriptions

(Click the image to open in full size)

First: Creating A Billing Portal Config (What the customer can do and what he cannot)

Now, before we create a billing portal link for a customer, we first need to create a “billing portal config”. Basically, we need to tell Stripe, things like: “The customer can switch between these plans and can do these things etc.”

Here are the official docs for creating a billing portal config.

Below is a small Node script for doing the same. You can just run this from the console. You do not need to do this in the context of a web app or anything.

const configuration = await stripe.billingPortal.configurations.create({
    features: {
        customer_update: {
            allowed_updates: ['name', 'email', 'tax_id', 'address', 'shipping', 'phone'],
            enabled: true,
        },
        invoice_history: { enabled: true },
        payment_method_update: { enabled: true },
        subscription_cancel: {
            enabled: true
        },
        subscription_update: {
            default_allowed_updates: [ 'price' ],
            enabled: true,
            products: [
                {
                    product: 'prod_N2Sbu7uFBrFUwK',
                    prices: [ 'price_1MINgESICZrnQHl2HDXHMgiU' ]
                },
                {
                    product: 'prod_N1gdRaZWf2vFFN',
                    prices: [ 'price_1MHdGSSICZrnQHl2rgnyrx4J' ]
                }
            ]
        }
    },
    business_profile: {
        privacy_policy_url: 'https://example.com/privacy',
        terms_of_service_url: 'https://example.com/terms',
        headline: 'SAAS Product 1 Billing Page'
    }
});

Most of the keys and values are pretty simple to understand just based on the names. Maybe the only thing interesting is the “subscription_update” key which allows you to specify which products your customers can switch between.

In case you are wondering where you can get the product IDs that look something like this: prod_N2Sbu7uFBrFUwK from, just go to the Stripe dashboard and head over to the Products tab. You should be able to copy-paste them from there.

When you run this code, Stripe gives you back a JSON object like so:

{
    id: 'bpc_1MINuXSICZrnQHl2y9mopkZ8',
    object: 'billing_portal.configuration',
    active: true,
    application: null,
    business_profile: {
      headline: null,
      privacy_policy_url: 'https://example.com/privacy',
      terms_of_service_url: 'https://example.com/terms'
    },
    created: 1671850593,
    default_return_url: null,
    features: {
      customer_update: { allowed_updates: [Array], enabled: true },
      invoice_history: { enabled: true },
      payment_method_update: { enabled: true },
      subscription_cancel: {
        cancellation_reason: [Object],
        enabled: true,
        mode: 'at_period_end',
        proration_behavior: 'none'
      },
      subscription_pause: { enabled: false },
      subscription_update: {
        default_allowed_updates: [Array],
        enabled: true,
        proration_behavior: 'none'
      }
    },
    is_default: false,
    livemode: false,
    login_page: { enabled: false, url: null },
    metadata: {},
    updated: 1671850593
}

The most important thing here is the “id”. We are going to use that next to create the Customer Billing Portal link.

A Simple Node Script To Create A Link To The Billing Portal

Now that we have the billing config ID, we can create the link using the following snippet:

const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    configuration: 'bpc_1MIO9GSICZrnQHl2SAecWs3K',
    return_url: 'http://localhost:3000/?back_from=billing',
});

You just need the customerID, which I am sure you can get from your DB at this point. The “configuration” key takes the billing config ID we created above. And the “return_url” takes the URL that we have to send the user once they are done with the billing portal.

Reacting To All The Actions The Customer Takes On The Billing Portal (Like Plan Change)

This will be done via the webhook we set up above. If the user changes the plan, we will get a hit on the webhook with an event that looks something like this:

{
    "id": "evt_1MJpMlSICZrnQHl2JK04csjC",
    "object": "event",
    "api_version": "2022-11-15",
    "created": 1672194459,
    "data": {
        "object": {
            "id": "sub_1MJpLMSICZrnQHl2g5delzXK",
            "object": "subscription",
            "application": null,
            "application_fee_percent": null,
            "automatic_tax": {
                "enabled": false
            },
            "billing_cycle_anchor": 1672194372,
            "billing_thresholds": null,
            "cancel_at": null,
            "cancel_at_period_end": false,
            "canceled_at": null,
            "collection_method": "charge_automatically",
            "created": 1672194372,
            "currency": "usd",
            "current_period_end": 1674872772,
            "current_period_start": 1672194372,
            "customer": "cus_N3xEBwXaRDVHYL",
            "days_until_due": null,
            "default_payment_method": "pm_1MJpLLSICZrnQHl2HimA2oUC",
            "default_source": null,
            "default_tax_rates": [],
            "description": null,
            "discount": null,
            "ended_at": null,
            "items": {
                "object": "list",
                "data": [
                    {
                        "id": "si_N3xFhuQUS6qrFy",
                        "object": "subscription_item",
                        "billing_thresholds": null,
                        "created": 1672194373,
                        "metadata": {},
                        "plan": {
                            "id": "price_1MINgESICZrnQHl2HDXHMgiU",
                            "object": "plan",
                            "active": true,
                            "aggregate_usage": null,
                            "amount": 4000,
                            "amount_decimal": "4000",
                            "billing_scheme": "per_unit",
                            "created": 1671849706,
                            "currency": "usd",
                            "interval": "month",
                            "interval_count": 1,
                            "livemode": false,
                            "metadata": {},
                            "nickname": null,
                            "product": "prod_N2Sbu7uFBrFUwK",
                            "tiers_mode": null,
                            "transform_usage": null,
                            "trial_period_days": null,
                            "usage_type": "licensed"
                        },
                        "price": {
                            "id": "price_1MINgESICZrnQHl2HDXHMgiU",
                            "object": "price",
                            "active": true,
                            "billing_scheme": "per_unit",
                            "created": 1671849706,
                            "currency": "usd",
                            "custom_unit_amount": null,
                            "livemode": false,
                            "lookup_key": null,
                            "metadata": {},
                            "nickname": null,
                            "product": "prod_N2Sbu7uFBrFUwK",
                            "recurring": {
                                "aggregate_usage": null,
                                "interval": "month",
                                "interval_count": 1,
                                "trial_period_days": null,
                                "usage_type": "licensed"
                            },
                            "tax_behavior": "unspecified",
                            "tiers_mode": null,
                            "transform_quantity": null,
                            "type": "recurring",
                            "unit_amount": 4000,
                            "unit_amount_decimal": "4000"
                        },
                        "quantity": 1,
                        "subscription": "sub_1MJpLMSICZrnQHl2g5delzXK",
                        "tax_rates": []
                    }
                ],
                "has_more": false,
                "total_count": 1,
                "url": "/v1/subscription_items?subscription=sub_1MJpLMSICZrnQHl2g5delzXK"
            },
            "latest_invoice": "in_1MJpLMSICZrnQHl2IKge5GmF",
            "livemode": false,
            "metadata": {},
            "next_pending_invoice_item_invoice": null,
            "on_behalf_of": null,
            "pause_collection": null,
            "payment_settings": {
                "payment_method_options": null,
                "payment_method_types": null,
                "save_default_payment_method": "off"
            },
            "pending_invoice_item_interval": null,
            "pending_setup_intent": null,
            "pending_update": null,
            "plan": {
                "id": "price_1MINgESICZrnQHl2HDXHMgiU",
                "object": "plan",
                "active": true,
                "aggregate_usage": null,
                "amount": 4000,
                "amount_decimal": "4000",
                "billing_scheme": "per_unit",
                "created": 1671849706,
                "currency": "usd",
                "interval": "month",
                "interval_count": 1,
                "livemode": false,
                "metadata": {},
                "nickname": null,
                "product": "prod_N2Sbu7uFBrFUwK",
                "tiers_mode": null,
                "transform_usage": null,
                "trial_period_days": null,
                "usage_type": "licensed"
            },
            "quantity": 1,
            "schedule": null,
            "start_date": 1672194372,
            "status": "active",
            "test_clock": null,
            "transfer_data": null,
            "trial_end": null,
            "trial_start": null
        },
        "previous_attributes": {
            "items": {
                "data": [
                    {
                        "id": "si_N3xFhuQUS6qrFy",
                        "object": "subscription_item",
                        "billing_thresholds": null,
                        "created": 1672194373,
                        "metadata": {},
                        "plan": {
                            "id": "price_1MHdGSSICZrnQHl2rgnyrx4J",
                            "object": "plan",
                            "active": true,
                            "aggregate_usage": null,
                            "amount": 1400,
                            "amount_decimal": "1400",
                            "billing_scheme": "per_unit",
                            "created": 1671671284,
                            "currency": "usd",
                            "interval": "month",
                            "interval_count": 1,
                            "livemode": false,
                            "metadata": {},
                            "nickname": null,
                            "product": "prod_N1gdRaZWf2vFFN",
                            "tiers_mode": null,
                            "transform_usage": null,
                            "trial_period_days": null,
                            "usage_type": "licensed"
                        },
                        "price": {
                            "id": "price_1MHdGSSICZrnQHl2rgnyrx4J",
                            "object": "price",
                            "active": true,
                            "billing_scheme": "per_unit",
                            "created": 1671671284,
                            "currency": "usd",
                            "custom_unit_amount": null,
                            "livemode": false,
                            "lookup_key": null,
                            "metadata": {},
                            "nickname": null,
                            "product": "prod_N1gdRaZWf2vFFN",
                            "recurring": {
                                "aggregate_usage": null,
                                "interval": "month",
                                "interval_count": 1,
                                "trial_period_days": null,
                                "usage_type": "licensed"
                            },
                            "tax_behavior": "unspecified",
                            "tiers_mode": null,
                            "transform_quantity": null,
                            "type": "recurring",
                            "unit_amount": 1400,
                            "unit_amount_decimal": "1400"
                        },
                        "quantity": 1,
                        "subscription": "sub_1MJpLMSICZrnQHl2g5delzXK",
                        "tax_rates": []
                    }
                ]
            },
            "plan": {
                "id": "price_1MHdGSSICZrnQHl2rgnyrx4J",
                "amount": 1400,
                "amount_decimal": "1400",
                "created": 1671671284,
                "product": "prod_N1gdRaZWf2vFFN"
            }
        }
    },
    "livemode": false,
    "pending_webhooks": 2,
    "request": {
        "id": null,
        "idempotency_key": "579fec61-b31a-40a4-a371-c3c28c47c26e"
    },
    "type": "customer.subscription.updated"
}

Using the information above, you should be able to get things updated on your end.

How To Handle Customers Who Are Desperately Trying To Pay But There Is Some Issue

Using The Bad Test Cards To Catch Cases You Want To Handle

Using the same Webhook infra talked about above, you can handle situations where a customer is desperately trying to pay but cannot for some reason. Maybe in that case you want to contact them and offer them another way to pay like PayPal or something.

To simulate the customer having this problem, you can use the bad cards provided by Stripe in the Stripe Testing docs.

Then using the Webhook script I have provided above, you can store all the webhook events and study them. Using this you can come up with a plan of action for how to handle these events and highlight them.

Conclusion

So, this is everything you need to know to add Stripe to your React application. I hope that this React Stripe subscription example was useful for you.