8 min read

Setting up recurring payments via subscriptions with Stripe

In this series, we’ve implemented a few different ways of charging our users using Stripe. So far, all of those cases have included single payments. With…

June 28, 2021

In this series, we’ve implemented a few different ways of charging our users using Stripe. So far, all of those cases have included single payments. With Stripe, we can also set up recurring payments using subscriptions.

Recurring payments are a popular approach nowadays in many businesses. The users save a credit card and get billed once a month, for example. In return, they get access to the platform, such as a streaming service, for example. Since it is a common use case, it is definitely worth looking into.

Creating a product#

To create a subscription, we first need to define a product. While we can do it through the API, we only need a single product for our whole application for now. Since that’s the case, we can do that through the Products dashboard.

When we click on the “Add product” button, we need to provide some basic product information. In our case, the product name is the “Monthly plan”.

The second important thing is the price information.

Since we want to implement subscriptions, we choose a recurring price billed monthly. There are more options to choose out from, though.

When we finish creating a product, Stripe redirects us to the details page. Here, we can see the information about the product we’ve just created.

The crucial part, for now, is the pricing section.

Above, we can see the id of the price we’ve set up. We need it to create subscriptions. The most straightforward way of referring to it would be to save it in the environment variables.

app.module.ts#
import { Module } from "@nestjs/common"
import { ConfigModule } from "@nestjs/config"
import * as Joi from "@hapi/joi"
@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        MONTHLY_SUBSCRIPTION_PRICE_ID: Joi.string(), // ...
      }),
    }), // ...
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}
.env#
MONTHLY_SUBSCRIPTION_PRICE_ID=price_...
# ...

Managing subscriptions#

To create a subscription for customers, they need to have a default payment method chosen.

Choosing a default payment method#

Previously, we’ve implemented the feature of saving credit cards. Now, we need to add the option to choose one of them as the default payment method. For that, we need to update the customer’s information.

stripe.service.ts#
import { Injectable } from "@nestjs/common"
import { ConfigService } from "@nestjs/config"
import Stripe from "stripe"
import StripeError from "../utils/stripeError.enum"
@Injectable()
export default class StripeService {
  private stripe: Stripe
  constructor(private configService: ConfigService) {
    this.stripe = new Stripe(configService.get("STRIPE_SECRET_KEY"), {
      apiVersion: "2020-08-27",
    })
  }
  public async setDefaultCreditCard(paymentMethodId: string, customerId: string) {
    try {
      return await this.stripe.customers.update(customerId, {
        invoice_settings: {
          default_payment_method: paymentMethodId,
        },
      })
    } catch (error) {
      if (error?.type === StripeError.InvalidRequest) {
        throw new BadRequestException("Wrong credit card chosen")
      }
      throw new InternalServerErrorException()
    }
  } // ...
}
stripeError.enum.ts#
enum StripeError {
  InvalidRequest = "StripeInvalidRequestError",
}
export default StripeError

Above, we handle a case in which a non-existent payment method is chosen or the one that belongs to another customer.

We also need to add a new route to our CreditCardsController.

creditCards.controller.ts#
import { Body, Controller, Post, Req, UseGuards, Get, HttpCode } from "@nestjs/common"
import JwtAuthenticationGuard from "../authentication/jwt-authentication.guard"
import RequestWithUser from "../authentication/requestWithUser.interface"
import StripeService from "../stripe/stripe.service"
import SetDefaultCreditCardDto from "./dto/setDefaultCreditCard.dto"
@Controller("credit-cards")
export default class CreditCardsController {
  constructor(private readonly stripeService: StripeService) {}
  @Post("default")
  @HttpCode(200)
  @UseGuards(JwtAuthenticationGuard)
  async setDefaultCard(
    @Body() creditCard: SetDefaultCreditCardDto,
    @Req() request: RequestWithUser,
  ) {
    await this.stripeService.setDefaultCreditCard(
      creditCard.paymentMethodId,
      request.user.stripeCustomerId,
    )
  } // ...
}
setDefaultCreditCard.dto.ts#
import { IsString, IsNotEmpty } from "class-validator"
export class SetDefaultCreditCardDto {
  @IsString()
  @IsNotEmpty()
  paymentMethodId: string
}
export default SetDefaultCreditCardDto

When the customers have the default payment method chosen, we can create a subscription for them.

Creating subscriptions#

To manage subscriptions, we first need to create a few methods in our StripeService:

stripe.service.ts#
import { Injectable, BadRequestException, InternalServerErrorException } from "@nestjs/common"
import StripeError from "../utils/stripeError.enum"
@Injectable()
export default class StripeService {
  // ...
  public async createSubscription(priceId: string, customerId: string) {
    try {
      return await this.stripe.subscriptions.create({
        customer: customerId,
        items: [
          {
            price: priceId,
          },
        ],
      })
    } catch (error) {
      if (error?.code === StripeError.ResourceMissing) {
        throw new BadRequestException("Credit card not set up")
      }
      throw new InternalServerErrorException()
    }
  }
  public async listSubscriptions(priceId: string, customerId: string) {
    return this.stripe.subscriptions.list({
      customer: customerId,
      price: priceId,
    })
  }
}
stripeError.enum.ts#
enum StripeError {
  InvalidRequest = "StripeInvalidRequestError",
  ResourceMissing = "resource_missing",
}
export default StripeError

The two methods above are quite low-level. To manage our monthly subscriptions, let’s create the SubscriptionsService:

subscriptions.service.ts#
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"
import StripeService from "../stripe/stripe.service"
import { ConfigService } from "@nestjs/config"
@Injectable()
export default class SubscriptionsService {
  constructor(
    private readonly stripeService: StripeService,
    private readonly configService: ConfigService,
  ) {}
  public async createMonthlySubscription(customerId: string) {
    const priceId = this.configService.get("MONTHLY_SUBSCRIPTION_PRICE_ID")
    const subscriptions = await this.stripeService.listSubscriptions(priceId, customerId)
    if (subscriptions.data.length) {
      throw new BadRequestException("Customer already subscribed")
    }
    return this.stripeService.createSubscription(priceId, customerId)
  }
  public async getMonthlySubscription(customerId: string) {
    const priceId = this.configService.get("MONTHLY_SUBSCRIPTION_PRICE_ID")
    const subscriptions = await this.stripeService.listSubscriptions(priceId, customerId)
    if (!subscriptions.data.length) {
      return new NotFoundException("Customer not subscribed")
    }
    return subscriptions.data[0]
  }
}

With the above logic, we allow the customers to subscribe only once and prevent Stripe from charged them too many times. The last part is to create the SubscriptionsController:

subscriptions.controller.ts#
import { Controller, Post, Req, UseGuards, Get } from "@nestjs/common"
import JwtAuthenticationGuard from "../authentication/jwt-authentication.guard"
import RequestWithUser from "../authentication/requestWithUser.interface"
import SubscriptionsService from "./subscriptions.service"
@Controller("subscriptions")
export default class SubscriptionsController {
  constructor(private readonly subscriptionsService: SubscriptionsService) {}
  @Post("monthly")
  @UseGuards(JwtAuthenticationGuard)
  async createMonthlySubscription(@Req() request: RequestWithUser) {
    return this.subscriptionsService.createMonthlySubscription(request.user.stripeCustomerId)
  }
  @Get("monthly")
  @UseGuards(JwtAuthenticationGuard)
  async getMonthlySubscription(@Req() request: RequestWithUser) {
    return this.subscriptionsService.getMonthlySubscription(request.user.stripeCustomerId)
  }
}

Confirming subscription payments#

When we go to the testing page in Stripe documentation, we can see many different testing cards to cover different cases. Some of them require additional authentication when performing payments. Let’s use the /subscriptions/monthly route that we’ve created to check the details of the created subscription.

If we see that our subscription is incomplete it means that it might require payment. Aside from the status, the Subscription also contains the latest_invoice property, which is an id.

We can pass additional properties to the stripe.subscriptions.list method to change the id to the object.

public async listSubscriptions(priceId: string, customerId: string,) {
  return this.stripe.subscriptions.list({
    customer: customerId,
    price: priceId,
    expand: ['data.latest_invoice', 'data.latest_invoice.payment_intent']
  })
}

Now, our /subscriptions/monthly endpoint responds with the details about the latest invoice, including the payment intent.

We could create separate endpoints to get the details of the invoices payment intents instead. Now, our endpoint responds with a lot of data that might not be needed. It would be a good idea to map the response from Stripe and remove unnecessary properties.

One of the properties of the payment intent is the client_secret. If the subscription status is incomplete, we need to use it on the frontend so that the user can authorize the payment.

useSubscriptionsConfirmation.tsx#
import { CardElement, useStripe } from "@stripe/react-stripe-js"
 
function useSubscriptionConfirmation() {
  const stripe = useStripe()
  const confirmSubscription = async () => {
    const subscriptionResponse = await fetch(
      `${process.env.REACT_APP_API_URL}/subscriptions/monthly`,
      {
        method: "GET",
        credentials: "include",
        headers: {
          "Content-Type": "application/json",
        },
      },
    )
    const subscriptionResponseJson = await subscriptionResponse.json()
    if (subscriptionResponseJson.status == "incomplete") {
      const secret = subscriptionResponseJson.latest_invoice.payment_intent.client_secret
      await stripe?.confirmCardPayment(secret)
    }
  }
  return {
    confirmSubscription,
  }
}
export default useSubscriptionConfirmation

When we try to confirm the payment, there is a chance that Stripe prompts the user for payment confirmation.

Creating subscriptions with trial periods#

We can create a subscription with a customer with a free trial period. To do that, we can use the trial_period_days property.

public async createSubscription(priceId: string, customerId: string,) {
  try {
    return await this.stripe.subscriptions.create({
      customer: customerId,
      items: [
        {
          price: priceId
        }
      ],
      trial_period_days: 30
    })
  } catch (error) {
    if (error?.code === StripeError.ResourceMissing) {
      throw new BadRequestException('Credit card not set up');
    }
    throw new InternalServerErrorException();
  }
}

When we do that, an invoice is still created, but for zero dollars. Once the trial is up, Stripe generates a new invoice. Three days before this happens, Stripe sends an event to our webhook endpoint. Using webhooks is a broad topic, and we will cover it separately.

Summary#

In this article, we’ve implemented subscriptions into our application. To do that, we’ve had to create a product that requires a periodical charge. We’ve also implemented a way for the users to set up their default payment method and subscribe. We’ve also covered cases such as confirming payments for subscriptions and trial periods. There is still more to cover when it comes to Stripe, so stay tuned!

Setting up recurring payments via subscriptions with Stripe | NestJS.io