Subscription Payments #1: Adding Basic and Pro subscription plans using Stripe

Subscription Payments #1: Adding Basic and Pro subscription plans using Stripe

Languages | Tools Used | Time Saved

Node.js, HTML Stripe 3 weeks → 40 mins

Goodies

Live Demo

This is the Part 1 in the series of guides on adding, managing, and keeping track of subscription payments using Stripe and Mongo for your SaaS app.

Subscription Payments #2: Keeping track of Customer Billing information using Mongo and Stripe Webhooks
Subscription Payments #3: Update and Cancel plans via a Manage Billing Screen using Stripe
Subscription Payments #4: Access premium content based on subscription plan

Every SaaS app needs subscription payments - a recurring payment. Users need to be able to change or cancel their plans, go through a trial phase to see if they even would like to pay for premium features.

In Part 1, we will:

  • Create a basic sign-in flow to keep track of users
  • Let users pay for a subscription plan: $10/month for Basic Plan or $12/month for Pro Plan
  • Present users with a Stripe Checkout screen where they can pay using their credit card
  • Add 2 weeks trial period before billing the user

At a glance

image

Setup the project

Let's start by creating new project folder. Add a package.json

{
  "name": "stripe-subscriptions-nodejs",
  "version": "1.0.0",
  "author": "sssaini",
  "description": "Add Stripe Subscriptions Payments using Node.js",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.19.0",
    "cookie-parser": "^1.4.5",
    "ejs": "^3.1.5",
    "express": "^4.17.1",
    "express-session": "^1.17.1",
    "mongoose": "^5.10.11",
    "stripe": "^8.114.0"
  }
}

Install dependencies by running,

npm install

Set up a basic Express Server

Create a new file named app.js

const bodyParser = require('body-parser')
const express = require('express')

const app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))

app.use(express.static('public'))
app.engine('html', require('ejs').renderFile)

app.get('/', async function (
  req,
  res,
  next
) {
  res.send('Hello World!')
})

const port = 4242

app.listen(port, () => console.log(`Listening on port ${port}!`))

We will use the public folder to serve any static files like images, CSS, and JS. We are also using a templating engine - EJS to make it to render information coming from server-side on the web page.

Launch the app by running,

npm start

The application should be live http://localhost:4242

Clone the UI

We need some UI to guide our user through buying the subscription. They have to log in first and if they aren't already on a plan, they can buy one.

Add a view by creating a new file, views/login.ejs

<html lang="en">
  <head>
  </head>
  <body class="text-center">
    <form class="form-login" action="/login" method="post">
  
      <h1>Log in</h1>
  <input
        type="email"
        name="email"
        placeholder="Email address"
        required
      />
      <button type="submit">
        Sign in
      </button>
    </form>

    <script
      src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
      integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
      crossorigin="anonymous"
    ></script>
  </body>
</html>

We can serve our view by editing the app.js

// ..

app.get('/', async function (
  req,
  res,
  next
) {
  res.status(200).render('login.ejs')
})

// ..

It looks pretty barebones, doesn't it? Fear not, our lovely team at SaaSBase already has you covered.

Grab the associated HTML and the CSS needed from the Github project.

image

To run the project,

npm start

The application should start running at http://localhost:4242.

Create a Stripe Customer

A Customer is one of the core resources in Stripe. Think of it like a 1:1 mapping to an individual user. Once we create a Customer using Stripe API, we get back the associated Customer API ID. This ID can be used by our application to identify and track future transactions pertaining to that customer.

We can now register our customer with Stripe using the email that they provide in the login form.

Create a new Stripe account here.

Copy in your Stripe Secret Key by going to the Stripe Dashboard > Developers > API keys or clicking here.

Create a new file called src/connect/stripe.js

const stripe = require('stripe')
const STRIPE_SECRET_KEY = 'sk_test_xxx'

const Stripe = stripe(STRIPE_SECRET_KEY, {
  apiVersion: '2020-08-27'
})

const addNewCustomer = async (email) => {
  const customer = await Stripe.customers.create({
    email,
    description: 'New Customer'
  })

  return customer
}

const getCustomerByID = async (id) => {
  const customer = await Stripe.customers.retrieve(id)
  return customer
}

module.exports = {
  addNewCustomer,
	getCustomerByID
}

In app.js

const Stripe = require('./src/connect/stripe')

//..

app.post('/login', async (req, res) => {
  const { email } = req.body
  const customer = await Stripe.addNewCustomer(email)
  res.send('customer created: ' + JSON.stringify(customer))
})

Run the application. Sign in using an email address and if the Customer was successfully created in Stripe, we will get a successful response back.

{"id":"cus_IaFpg44TGYsJNT","object":"customer","address":null,"balance":0,"created":1608145534,"currency":null,"default_source":null,"delinquent":false,"description":"SaaSBase Customer","discount":null,"email":"sukh@saasbase.dev","invoice_prefix":"756F8AF0","invoice_settings":{"custom_fields":null,"default_payment_method":null,"footer":null},"livemode":false,"metadata":{},"name":null,"next_invoice_sequence":1,"phone":null,"preferred_locales":[],"shipping":null,"tax_exempt":"none"}

The id is the unique ID for a Stripe Customer. From now on, we can add products, get invoices etc. all by referring to this ID on behalf of our customer.

We can also see that the newly added Customer is available on the Dashboard.

image

Perfect! We have a Customer.

Save the user session

We can save the Customer ID in the user's session for persistence.

In app.js

const session = require('express-session')

app.use(
  session({
    secret: 'secr3t',
    resave: false,
    saveUninitialized: true,
  })
)


app.post('/login', async (req, res) => {
// ..
  req.session.customerID = customer
  res.send('customer created:' + JSON.stringify(customer))
})

The express-session package saves the cookie on the client's browser as connect.sid. We can verify this on Google Chrome by opening up Developer Tools and going to Application tab > Cookies.

image

Session data is not saved in the cookie itself, just the session ID. Session data is stored server-side.

Add products to the Stripe Dashboard

We will offer two types of subscription plans - Basic for $10 and Pro for $12.

To sell a subscription plan, we need a Product.

Click on Products > Add product on the Stripe Dashboard to add a new product.

Add a Product - Basic with $10 Price and Recurring.

Add the second one as Product - Pro with $12 Price and Recurring.

image

Each Product has an associated Price API ID. Save it, we will need it in the next step.

image

Let's copy the Price API ID for both our products and add them in our app.js .

const productToPriceMap = {
  BASIC: 'price_xxx',
  PRO: 'price_xxx'
}

Create the Checkout Screen

We have a customer and we have our products. We are now ready to initiate a checkout screen where customers can enter in their credit card information and pay for the selected plan type.

We will create a Stripe checkout session on the server and send the associated Session ID back to the client. The client can use this ID to redirect to a secure checkout screen pre-filled with customer information, like their email address.

We will add JQuery and Stripe libraries.

Create two radio buttons to select the plan type and a Buy Now button in views/account.ejs

<html>
<head>
</head>
<body>
<input type="radio" id="basic" name="product" value="basic" />
        <label for="basic">Basic for $10</label><br />
        <input type="radio" id="pro" name="product" value="pro" />
        <label for="pro">Pro for $12</label><br />

<button class="btn btn-primary" id="checkout-button" type="submit">
          Buy now
        </button>

<script
      src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
      integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
      crossorigin="anonymous"
    ></script>
  <script type="text/javascript" src="https://js.stripe.com/v3/"></script>
<script type="text/javascript" src="./js/account.js"></script>
</body>
<html>

Add the script public/js/account.js

$(document).ready(function () {
  const PUBLISHABLE_KEY = 'pk_test_xxx'

  const stripe = Stripe(
    PUBLISHABLE_KEY)
  const checkoutButton = $('#checkout-button')

  checkoutButton.click(function () {
    const product = $('input[name="product"]:checked').val()

    fetch('/checkout', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        product
      })
    })
      .then((result) => result.json())
      .then(({ sessionId }) => stripe.redirectToCheckout({ sessionId }))
  })
})

The PUBLISHABLE_KEY can be found on the Stripe Dashboard > Developers or by clicking here.

We can create a matching /account GET endpoint to serve account.ejs view by modifying app.js

app.get('/account', async function (req, res) {
  res.render('account.ejs')
})

Let's modify our / POST endpoint so that it redirects to Account view on successful login. We don't implement a login system in this example, but it would be trivial to do so.

app.post('/login', async function (req, res) {	
	// ..
	req.session.email = email
  res.redirect('/account')
})

Since we want out customers to be able to try out the product for 14 days, we can add the trial period to the Checkout Session as well.

In src/connect/stripe.js

// ..
const createCheckoutSession = async (customer, price) => {
  const session = await Stripe.checkout.sessions.create({
    mode: 'subscription',
    payment_method_types: ['card'],
    customer,
    line_items: [
      {
        price,
        quantity: 1
      }
    ],
    subscription_data: {
      trial_period_days: 14
    },

    success_url: `http://localhost:4242/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `http://localhost:4242/failed`
  })

  return session
}

module.exports = {
  addNewCustomer,
  createCheckoutSession
}`

Clicking the Checkout Button makes a POST request to /checkout POST endpoint in app.js

app.post('/checkout', async (req, res) => {
  const { customer } = req.session
  const session = await Stripe.createCheckoutSession(customer, productToPriceMap.BASIC)

  console.log(session)
  res.send({ sessionId: session.id })
})

Add Success and Fail endpoints

In app.js

app.get('/success', (req, res) => {
  res.send('Payment successful')
})

app.get('/failed', (req, res) => {
  res.send('Payment failed')
})

Test out the app

npm start

Head on over to localhost:4242. Enter in your email address. A Customer record should be made with that email address. Select a plan to buy. Complete the purchase with a test credit card. Enter 4242 4242 4242 4242, any expiry date, and CVV.

Right now, our application doesn't recognize if the user already has purchased a subscription or not so that's not useful at all. In the next part, we will save customer and their purchased subscription plan information to a MongoDB so that we can show different views based on plan type.