6 min read
Original source

Using Stripe to save credit cards for future use

With Stripe, we can build advanced custom payment flows. In this article, we continue looking into it and save credit cards for future use. To do that, we need…

With Stripe, we can build advanced custom payment flows. In this article, we continue looking into it and save credit cards for future use. To do that, we need to have a more thorough understanding of how we can integrate Stripe with our system. Saving cards with setup intents Our goal in this article is to have payment credentials saved and ready for future payments. Imagine creating a website for an online shop. We could require the users to provide the credit card details every time they make an order, but that wouldn’t be the best user experience. Instead, we can save the credit card details in Stripe and use them later. To get us through this process, Stripe uses setup intents. We can set up a payment method without creating a charge and assign it to a customer. We need to add the above functionality to the StripeService that we’ve created in the previous part of this series.import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import Stripe from 'stripe'; @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 attachCreditCard(paymentMethodId: string, customerId: string) { return this.stripe.setupIntents.create({ customer: customerId, payment_method: paymentMethodId, }) } // ... }To use the above method, let’s create the CreditCardsController. creditCards.controller.ts import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common'; import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard'; import RequestWithUser from '../authentication/requestWithUser.interface'; import StripeService from '../stripe/stripe.service'; import AddCreditCardDto from './dto/addCreditCardDto'; @Controller('credit-cards') export default class CreditCardsController { constructor( private readonly stripeService: StripeService ) {} @Post() @UseGuards(JwtAuthenticationGuard) async addCreditCard(@Body() creditCard: AddCreditCardDto, @Req() request: RequestWithUser) { return this.stripeService.attachCreditCard(creditCard.paymentMethodId, request.user.stripeCustomerId); } } creditCards.controller.ts import { IsString, IsNotEmpty } from 'class-validator'; export class AddCreditCardDto { @IsString() @IsNotEmpty() paymentMethodId: string; } export default AddCreditCardDto;As you can see, we expect the users to send us the paymentMethodId when adding the credit card. To achieve that on the frontend side with React, we need to do it the same way as in the previous article. This time, we call the /credit-cards endpoint, though. usePaymentForm.tsx import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; import { FormEvent } from 'react'; function usePaymentForm() { const stripe = useStripe(); const elements = useElements(); const getPaymentMethodId = async () => { const cardElement = elements?.getElement(CardElement); if (!stripe || !elements || !cardElement) { return; } const stripeResponse = await stripe?.createPaymentMethod({ type: 'card', card: cardElement }); const { error, paymentMethod } = stripeResponse; if (error || !paymentMethod) { return; } return paymentMethod.id; } const handleSubmit = async (event: FormEvent) => { event.preventDefault(); const paymentMethodId = await getPaymentMethodId(); if (!paymentMethodId) { return; } fetch(`${process.env.REACT_APP_API_URL}/credit-cards`, { method: 'POST', body: JSON.stringify(({ paymentMethodId, })), credentials: 'include', headers: { 'Content-Type': 'application/json' }, }) }; return { handleSubmit } } export default usePaymentForm;There is one additional thing we should do above, though. In the previous part of this series, we’ve used the confirm: true parameter. Using it when saving a card would attempt to confirm it immediately. The above solution might work, but it is not guaranteed. The bank might require the user to perform some additional authentication on the frontend. To handle it, let’s use the client_secret Stripe provides us with when creating the payment intent. usePaymentForm.tsx import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; import { FormEvent } from 'react'; function usePaymentForm() { const stripe = useStripe(); const elements = useElements(); // ... const handleSubmit = async (event: FormEvent) => { event.preventDefault(); const paymentMethodId = await getPaymentMethodId(); if (!paymentMethodId) { return; } const response = await fetch(`${process.env.REACT_APP_API_URL}/credit-cards`, { method: 'POST', body: JSON.stringify(({ paymentMethodId, })), credentials: 'include', headers: { 'Content-Type': 'application/json' }, }) const responseJson = await response.json(); const clientSecret = responseJson.client_secret; stripe?.confirmCardSetup(clientSecret); }; return { handleSubmit } } export default usePaymentForm;When we call the stripe.confirmCardSetup on the frontend, Stripe has a chance to carry out any actions needed to confirm the card setup. Listing saved credit cards Another important part of the flow is listing the credit cards that the user saved. To implement it, let’s add a new method to the StripeService. stripe.service.ts import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import Stripe from 'stripe'; @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 listCreditCards(customerId: string) { return this.stripe.paymentMethods.list({ customer: customerId, type: 'card', }); } // ... }To use it, we also need to modify the CreditCardsController: creditCards.controller.ts import { Body, Controller, Post, Req, UseGuards, Get } from '@nestjs/common'; import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard'; import RequestWithUser from '../authentication/requestWithUser.interface'; import StripeService from '../stripe/stripe.service'; import AddCreditCardDto from './dto/addCreditCardDto'; @Controller('credit-cards') export default class CreditCardsController { constructor( private readonly stripeService: StripeService ) {} @Post() @UseGuards(JwtAuthenticationGuard) async addCreditCard(@Body() creditCard: AddCreditCardDto, @Req() request: RequestWithUser) { return this.stripeService.attachCreditCard(creditCard.paymentMethodId, request.user.stripeCustomerId); } @Get() @UseGuards(JwtAuthenticationGuard) async getCreditCards(@Req() request: RequestWithUser) { return this.stripeService.listCreditCards(request.user.stripeCustomerId); } }A significant thing to notice here is that Stripe doesn’t allow us to view all of the card’s details. For example, we can see only the last four digits of the card number, and we can’t access the CVV code. There is a great chance that our application doesn’t need to expose that much data about the credit cards. If that’s the case for you, feel free to modify the Stripe’s response in the /credit-cards endpoint. By default, the paymentMethods.list returns up to 10 credit cards. If you need more, you need to use pagination with the limit and starting_after properties. For details, check the documentation. If you want to know more about pagination in general, take a look at API with NestJS #17. Offset and keyset pagination with PostgreSQL and TypeORM Charging using saved cards The last part of the flow is charging the customer using the saved card. Let’s use the logic from the previous article, but with the  off_session parameter.public async charge(amount: number, paymentMethodId: string, customerId: string) { return this.stripe.paymentIntents.create({ amount, customer: customerId, payment_method: paymentMethodId, currency: this.configService.get('STRIPE_CURRENCY'), off_session: true, confirm: true }) }By setting off_session to true, we indicate that it occurs without the direct involvement of the customer with the use of previously collected credit card information. Let’s use the above logic in the ChargeController. creditCards.controller.ts import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common'; import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard'; import CreateChargeDto from './dto/createCharge.dto'; import RequestWithUser from '../authentication/requestWithUser.interface'; import StripeService from '../stripe/stripe.service'; @Controller('charge') export default class ChargeController { constructor( private readonly stripeService: StripeService ) {} @Post() @UseGuards(JwtAuthenticationGuard) async createCharge(@Body() charge: CreateChargeDto, @Req() request: RequestWithUser) { return this.stripeService.charge(charge.amount, charge.paymentMethodId, request.user.stripeCustomerId); } }An important change above is that we return the response from the stripeService.charge method. This is because setting confirm to true might work, but there is a chance that the bank will require additional authentication. We need to prepare for this scenario on the frontend. The above controller returns all of the data returned by Stripe. Feel free to remove unused data and send only

Using Stripe to save credit cards for future use | NestJS.io