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…
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 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. 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 confirmS