Mastering Stripe Subscription Custom Checkout Flow: Server Side and Client Side Implementation

Development

Nikin Shan Faizal

|

January 23, 2024

|
6 min read
This article will explain how to use Stripe to create a custom subscription checkout flow. It will cover both client-side (React js) and server-side (Node js) components.

Get Started

Stripe Subscriptions is a sophisticated service provided by Stripe, a popular online payment processing platform. It lets businesses to effortlessly manage and bill their consumers on a recurring basis, generally for subscription-based products or services. Let's break down some key concepts:

Setting Up Your Stripe Account

1. Create a Stripe Account:

  • Go to the Stripe website at https://stripe.com/.
  • Click on the "Sign Up" or "Create Account" button.
  • Fill in the required information, including your email address and password.
  • Follow the on-screen instructions to complete the account setup process.

2. Navigate to the Dashboard:

  • Once your account is set up, log in to the Stripe Dashboard.

3. Activate Your Account:

  • Stripe may require you to activate your account by providing additional information, such as business details and banking information.

4. Enable Test Mode:

  • In the Dashboard, toggle to the "View test data" switch to enable Test Mode. This allows you to test transactions without processing real payments.

5. Set Up Products and Plans:

  • In the Dashboard, go to the "Products" section.

Products:

  • Click on "Add product."
  • Fill in product details, including name, description, and pricing information.

           note : you should have to add a price to save the product

  • Save the product.

Pricing :

  • Click on "Products" and select the product you created.
  • Under "Pricing," click on "Add another price."
  • Configure price details, such as billing interval, pricing, and currency.
  • Save the price.

6. Integrate Stripe with Your Website or App:

  • Follow the integration guide based on your platform to seamlessly incorporate Stripe's payment functionality.
  • Stripe provides Publishable and Secret key after activation of the account which is then used to configure stripe in frontend and backend code will be later discussed below.

Custom Checkout Flow

1. Setup Stripe

Install the Stripe client of your choice:

  • Client Side
npm install --save @stripe/react-stripe-js @stripe/stripe-js
  • Server Side
npm install stripe -- save

2. Create Plan and Pricing Model - Client Side

Plan and pricing model will be created in stripe account and will be displayed in client side with the help of priceId as shown below. After you create the prices, record the price IDs so you can use them in other steps. Price IDs look like this: price_1ORsMKDXxvvhRkJtLHm0O2yT.

Each price id represents different price plan, Its recommended to create priceId for free plan too with zero value so that you can provide free trial for 3 months or one month as per your convenience.

3. Create Customer in Stripe

  • Client Side

Stripe needs a customer for each subscription. In your application frontend, collect any necessary information from your users and pass it to the backend. Customers can be created in initial stage while registering to an account or can be created while choosing a plan based on your use case.

  • Server Side

On the server, create the Stripe customer object with the necessary information passed from the frontend.

Then the customer would be added into the stripe account as show below.

const stripe = require('stripe') ('sk_test_###############')
const customer = await stripe.customers.create({
  name: 'Jenny Rosen'
  email: 'jennyrosen@example.com'
});

4. Create an Incomplete Subscription while Customer reaches Custom Checkout form.

By a choosing a plan from step-2, customer will be directed to a checkout form where they have to enter their credit or debit card details for purchasing a subscription. Stripe often provide prebuilt checkout form but here we are discussing about how to collect payment from a custom checkout form of our design or brand.

  • Client Side

By clicking on the choose plan option, send the priceId selected by the user to backend(server side)

fetch('/create-subscription', {
  method: 'POST'
  headers: {
  'Content-Type'; 'application/json',
  },
  body: JSON.stringify({
  priceId: priceId, 
  customerId: customerId,
  }),
})

  • Server Side

Create the subscription with status incomplete using payment_behaviour=default_incomplete. Then return the client_secret from the subscription’s first payment intent to the front end to complete payment.

Set save_default_payment_method to on_subscription to save the payment method as the default for a subscription when a payment succeeds. Saving a default payment method increases the success rate of future subscription payments.

// Set your secret key. Remember to switch to your live secret key in production.
// See your keys here: https://dashboard.stripe.com/apikeys
const stripe = require('stripe')('sk_test_###################');

app.post('/create-subscription', async (req, res) => {
  const customerId = req.cookies['customer'];
  const priceId = req.body.priceId;

  try {
    // Create the subscription. Note we're expanding the Subscription's
    // latest invoice and that invoice's payment_intent
    // so we can pass it to the front end to confirm the payment
    const subscription = await stripe.subscriptions.create({
      customer: customerId,
      items: [{
        price: priceId,
      }],
      payment_behavior: 'default_incomplete',
      payment_settings: { save_default_payment_method: 'on_subscription' },
      expand: ['latest_invoice.payment_intent'],
    });

    res.send({
      subscriptionId: subscription.id,
      clientSecret: subscription.latest_invoice.payment_intent.client_secret,
    });
    } catch (error) {
    return res.status(400).send({ error: { message: error.message } });
    }
});

Here we are sending back paymentIntent-client secret and subscriptionId to frontend for completing the payment, this client secret acts as a connection to the subscription which the user has selected. At this point subscription will be inactive and will be active only once the payment is confirmed from the client side.

5. Collect Payment Information - Client Side

For collecting payment informations like credit or debit card details, cvv, expiry are collected using Stripe Elements. They securely collect all payment informations and also you can customize Elements to match the look-and-feel of your application.

  • Add Payment Element to your Checkout Form

Load Stripe by using loadStripe from "@stripe/stripe-js" and wrap your whole checkout form with Element provided by "@stripe/react-stripe-js" as shown below.

import {Elements} from '@stripe/react-stripe-js';
import {loadStripe} from '@stripe/stripe-js';

// Make sure to call 'loadStripe' outside of a component's render to avoid 
// recreating the 'Stripe' object on every render

const stripePromise = loadStripe( pk_test_###################)
export default function App(){
  const options = {
  // passing the client secret obtained from the server 
  clientSecret: '{{CLIENT_SECRET}}'
  };

return (

  <Elements stripe={stripePromise} options={options}>
 
  <CheckoutForm/>
 
  </Elements>
)}

Use PaymentElement provided by '@stripe/react-stripe-js' to display checkout form

note : If you want to customize each field like card number , Expiry or CVV, stripe provides:

  • CardElement : flexible single-line input that collects all necessary card details.
  • CardExpiryElement : Collects the card‘s expiration date.
  • CardNumberElement : Collects the card number.
import {PaymentElement} from '@stripe/react-stripe-js';
const CheckoutForm = () => {
 return(
 <form>
 <PaymentElement/> 
 <button>Submit</button>
 </form>
 )
};
export default CheckoutForm;

  • useElement and useStripe hook

To pass confidential information collected from users to stripe, the best practice is to access the elements and use it with stripe.ConfirmPayment as shown below. Here stripeHooke provides a reference to the Stripe instance passed to the Elements provider.

import {useStripe, useElements, PaymentElement} from '@stripe/react-stripe-js';
const CheckoutForm = () => {
const stripe = useStripe();
const elements = useElements () ;
const handleSubmit = async (event) => {
 // We don't want to let default form submission happen here, 
 // which would refresh the page. 

 event.preventDefault () ;

 if (!stripe || !elements) {
 // Stripe.js hasn't yet loaded.
 // Make sure to disable form submission until Stripe.js has loaded.
 return;
 }
 const result = await stripe.confirmPayment ({
 //'Elements' instance that was used to create the Payment Element 
 elements,
 confirmParams: {
 return_url: "https://example. com/order/123/complete",
 },
});

stripe.confirmPayment confirms the payment and once the payment succeeds, it redirects users to the return URL

6. Listen for Webhooks

  • Webhook Configuration
  1. Navigate to Webhooks:
  • In the Dashboard, find the "Developers" section in the left-hand sidebar.
  • Click on "Webhooks" under the "Developers" section.
  1. Create a New Endpoint:
  • Click the "Add Endpoint" button.
  • In the "Endpoint URL" field, enter the URL of the server or service that will handle the incoming webhook events. This should be an HTTPS URL.
  • Choose the events you want to be notified about by selecting them from the "Events to send" section.
  • By clicking on Add endpoint button, Stripe would provide you with a webhook secret key which is used to connect  webhook with your backend code

You will be  able to add HTTP URL in local event listener, such that you will be able to test working of webhook in local.

  • Server Side

Add this middleware  to handle incoming requests. If the original URL is "/webhook," it allows the request to pass through without parsing the JSON body. Otherwise, it uses the bodyParser middleware to parse the JSON body of the request.

// Use JSON parser for all non-webhook routes
app.use((req, res, next) => {
 if (req.originalUrl === "/webhook"){
 next();
 } else {
 bodyParser.json()(req, res, next);
 }
})

The variable endpointSecret holds the secret key associated with your webhook endpoint. This key is used to verify the authenticity of incoming webhook events.

// Set your secret key. Remember to switch to your live secret key in production!
// See your keys here: https://dashboard.stripe.com/apikeys
const Stripe = require('stripe');
const stripe = Stripe('sk_test_**************');

// If you are testing your webhook locally with the Stripe CLI you 
// can find the endpoint's secret by running stripe listen'
// Otherwise, find your endpoint's secret in your webhook settings in the Developer Dashboard
const endpointSecret = 'whsec_...';

app.post('/webhook', bodyParser.raw({type: 'application/json'}),(request, response) => {
 const sig = request.headers['stripe-signature'];
 
 let event;
 
 // Verify webhook signature and extract the event.
 // See https://stripe.com/docs/webhooks#verify-events for more information.
 try {
  event = stripe.webhooks.constructEvent(request.body, sig, endpointSecret);
 } catch (err) {
 return response.status(400).send('Webhook Error: ${err.message}');
 }   
 return res.sendStatus(400);
})

This part of the code is responsible for verifying the Stripe webhook signature and extracting the event.

In our specific context, we have already initiated the subscription process on Stripe, but it remains partial or incomplete. To finalize and update the subscription details upon payment confirmation, we rely on webhook events provided by Stripe. In particular, we are interested in the “payment_intent.succeeded” event.

If the event called “payment_intent.succeeded” is triggered, then we could update our local database as well and this could complete your whole subscription payout journey.

// This example uses Express to receive webhooks
const express = require('express');
const app = express () ;

// Match the raw body to content type application/json
// If you are using Express v4 - v4.16 you need to use body-parser, not express
app.post ('/webhook', express.json({type: 'application/json'}), (request, response) => {
 const event = request.body;
 
 // Handle the event
 switch(event.type) {
   case 'payment_intent.succeeded':
    const paymentIntent = event.data.object;
    // Then define and call a method to handle the successful payment intent.
    // handlePaymentIntentSucceeded(paymentIntent);
    break;
   case 'payment_method.attached':
    const paymentMethod = event.data.object;
    // Then define and call a method to handle the successful attachment of a PaymentMethod.
    // handlePaymentMethodAttached(paymentMethod);
    break;
    // ... handle other event types
    default:
      console.log('Unhandled event type ${event.type}');
    }
    // Return a response to acknowledge receipt of the event 
    response.json({received: true}) ;
 });
 app.listen(8000, () => console.log('Running on port 8000'));

Conclusion

We discussed whole stripe subscription checkout journey from initiation to the completion using stripe webhook. The initialization of the Stripe library is a crucial step, and it's important to replace the test secret key with the live secret key in a production environment. This ensures that real transactions are securely processed, maintaining the integrity and confidentiality of sensitive information. You can also use the help of test clock in stripe so that you could analyze the working of your subscription by providing future date in the test clock, thus make your subscription integration flawless.