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.
1import { Module } from '@nestjs/common';
2import { ConfigModule } from '@nestjs/config';
3import * as Joi from '@hapi/joi';
4
5@Module({
6 imports: [
7 ConfigModule.forRoot({
8 validationSchema: Joi.object({
9 MONTHLY_SUBSCRIPTION_PRICE_ID: Joi.string(),
10 // ...
11 })
12 }),
13 // ...
14 ],
15 controllers: [],
16 providers: [],
17})
18export class AppModule {}1MONTHLY_SUBSCRIPTION_PRICE_ID=price_...
2# ...Managing subscriptions
To create a subscription for customers, they need to have a default payment method chosen.
Choosing a default payment method
In the previous part of this series, 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.
1import { Injectable } from '@nestjs/common';
2import { ConfigService } from '@nestjs/config';
3import Stripe from 'stripe';
4import StripeError from '../utils/stripeError.enum';
5
6@Injectable()
7export default class StripeService {
8 private stripe: Stripe;
9
10 constructor(
11 private configService: ConfigService
12 ) {
13 this.stripe = new Stripe(configService.get('STRIPE_SECRET_KEY'), {
14 apiVersion: '2020-08-27',
15 });
16 }
17
18 public async setDefaultCreditCard(paymentMethodId: string, customerId: string) {
19 try {
20 return await this.stripe.customers.update(customerId, {
21 invoice_settings: {
22 default_payment_method: paymentMethodId
23 }
24 })
25 } catch (error) {
26 if (error?.type === StripeError.InvalidRequest) {
27 throw new BadRequestException('Wrong credit card chosen');
28 }
29 throw new InternalServerErrorException();
30 }
31 }
32
33 // ...
34}1enum StripeError {
2 InvalidRequest = 'StripeInvalidRequestError'
3}
4
5export 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.
1import { Body, Controller, Post, Req, UseGuards, Get, HttpCode } from '@nestjs/common';
2import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
3import RequestWithUser from '../authentication/requestWithUser.interface';
4import StripeService from '../stripe/stripe.service';
5import SetDefaultCreditCardDto from './dto/setDefaultCreditCard.dto';
6
7@Controller('credit-cards')
8export default class CreditCardsController {
9 constructor(
10 private readonly stripeService: StripeService
11 ) {}
12
13 @Post('default')
14 @HttpCode(200)
15 @UseGuards(JwtAuthenticationGuard)
16 async setDefaultCard(@Body() creditCard: SetDefaultCreditCardDto, @Req() request: RequestWithUser) {
17 await this.stripeService.setDefaultCreditCard(creditCard.paymentMethodId, request.user.stripeCustomerId);
18 }
19
20 // ...
21}1import { IsString, IsNotEmpty } from 'class-validator';
2
3export class SetDefaultCreditCardDto {
4 @IsString()
5 @IsNotEmpty()
6 paymentMethodId: string;
7}
8
9export 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:
1import { Injectable, BadRequestException, InternalServerErrorException } from '@nestjs/common';
2import StripeError from '../utils/stripeError.enum';
3
4@Injectable()
5export default class StripeService {
6 // ...
7
8 public async createSubscription(priceId: string, customerId: string,) {
9 try {
10 return await this.stripe.subscriptions.create({
11 customer: customerId,
12 items: [
13 {
14 price: priceId
15 }
16 ]
17 })
18 } catch (error) {
19 if (error?.code === StripeError.ResourceMissing) {
20 throw new BadRequestException('Credit card not set up');
21 }
22 throw new InternalServerErrorException();
23 }
24 }
25
26 public async listSubscriptions(priceId: string, customerId: string,) {
27 return this.stripe.subscriptions.list({
28 customer: customerId,
29 price: priceId
30 })
31 }
32}1enum StripeError {
2 InvalidRequest = 'StripeInvalidRequestError',
3 ResourceMissing = 'resource_missing',
4}
5
6export default StripeError;The two methods above are quite low-level. To manage our monthly subscriptions, let’s create the SubscriptionsService:
1import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
2import StripeService from '../stripe/stripe.service';
3import { ConfigService } from '@nestjs/config';
4
5@Injectable()
6export default class SubscriptionsService {
7 constructor(
8 private readonly stripeService: StripeService,
9 private readonly configService: ConfigService
10 ) {}
11
12 public async createMonthlySubscription(customerId: string) {
13 const priceId = this.configService.get('MONTHLY_SUBSCRIPTION_PRICE_ID');
14
15 const subscriptions = await this.stripeService.listSubscriptions(priceId, customerId);
16 if (subscriptions.data.length) {
17 throw new BadRequestException('Customer already subscribed');
18 }
19 return this.stripeService.createSubscription(priceId, customerId);
20 }
21
22 public async getMonthlySubscription(customerId: string) {
23 const priceId = this.configService.get('MONTHLY_SUBSCRIPTION_PRICE_ID');
24 const subscriptions = await this.stripeService.listSubscriptions(priceId, customerId);
25
26 if (!subscriptions.data.length) {
27 return new NotFoundException('Customer not subscribed');
28 }
29 return subscriptions.data[0];
30 }
31}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:
1import { Controller, Post, Req, UseGuards, Get } from '@nestjs/common';
2import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
3import RequestWithUser from '../authentication/requestWithUser.interface';
4import SubscriptionsService from './subscriptions.service';
5
6@Controller('subscriptions')
7export default class SubscriptionsController {
8 constructor(
9 private readonly subscriptionsService: SubscriptionsService
10 ) {}
11
12 @Post('monthly')
13 @UseGuards(JwtAuthenticationGuard)
14 async createMonthlySubscription(@Req() request: RequestWithUser) {
15 return this.subscriptionsService.createMonthlySubscription(request.user.stripeCustomerId);
16 }
17
18 @Get('monthly')
19 @UseGuards(JwtAuthenticationGuard)
20 async getMonthlySubscription(@Req() request: RequestWithUser) {
21 return this.subscriptionsService.getMonthlySubscription(request.user.stripeCustomerId);
22 }
23}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.
1public async listSubscriptions(priceId: string, customerId: string,) {
2 return this.stripe.subscriptions.list({
3 customer: customerId,
4 price: priceId,
5 expand: ['data.latest_invoice', 'data.latest_invoice.payment_intent']
6 })
7}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.
1import { CardElement, useStripe } from '@stripe/react-stripe-js';
2
3function useSubscriptionConfirmation() {
4 const stripe = useStripe();
5
6 const confirmSubscription = async () => {
7 const subscriptionResponse = await fetch(`${process.env.REACT_APP_API_URL}/subscriptions/monthly`, {
8 method: 'GET',
9 credentials: 'include',
10 headers: {
11 'Content-Type': 'application/json'
12 },
13 })
14
15 const subscriptionResponseJson = await subscriptionResponse.json();
16
17 if (subscriptionResponseJson.status == 'incomplete') {
18 const secret = subscriptionResponseJson.latest_invoice.payment_intent.client_secret;
19
20 await stripe?.confirmCardPayment(secret);
21 }
22 };
23
24 return {
25 confirmSubscription
26 }
27}
28
29export 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.
1public async createSubscription(priceId: string, customerId: string,) {
2 try {
3 return await this.stripe.subscriptions.create({
4 customer: customerId,
5 items: [
6 {
7 price: priceId
8 }
9 ],
10 trial_period_days: 30
11 })
12 } catch (error) {
13 if (error?.code === StripeError.ResourceMissing) {
14 throw new BadRequestException('Credit card not set up');
15 }
16 throw new InternalServerErrorException();
17 }
18}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!