Chapter 4: Implementing stripe subscriptions.
We keep our book up to date with recent libraries and packages.
The section below is a preview of Browser Extension Book. To read the full text, you will need to purchase the book.
In Chapter 4, you will start with the codebase in the 4-begin folder of our browser extension repo and end up with the codebase in the 4-end folder.
In this chapter, we will focus on setting up stripe subscriptions for our browser extension project. You'll learn how to configure stripe to manage payments and subscriptions directly through your extension. First, we'll start by setting up your stripe account and retrieving necessary keys.
We will cover the following topics in this chapter:
- Setting up your stripe account
- Server
- Setting up stripe in express
- Creating and understanding new files
- Modifying files and understanding the changes
- Extension
- Creating and understanding new files
- Modifying files and understanding the changes
- Visual exploration of the stripe subscription process
Setting up your stripe account link
Before you can start integrating stripe, you'll need to set up your account and retrieve your secret key. This key will allow your application to interact securely with stripe's servers.
Retrieving the stripe secret key
Create a stripe account:
- Visit Stripe's website and sign up for an account.
Access the dashboard:
- Once logged in, navigate to the Stripe dashboard.
Find the API keys:
- In the dashboard, look for the "Developers" section in the sidebar, and click on "API keys".
- Here you will find your
STRIPE_SECRET_KEY
. It is labeled as "Secret key".
Make sure to keep this key confidential and only store it in secure locations (not in your source code).
Setting environment variables
For the extension, you will need to store the STRIPE_SECRET_KEY
and other keys as environment variables. Here's how you can set them up:
# Add these to your .env file or your hosting provider's environment variable settings
STRIPE_SECRET_KEY=your_secret_key_here
STRIPE_WEBHOOK_SECRET=your_webhook_secret_here
STRIPE_PLAN1_PRICE_ID=price_id_for_plan1
STRIPE_PLAN2_PRICE_ID=price_id_for_plan2
Creating stripe prices link
We will offer two subscription options for our extension. You will need to create prices in Stripe which will be linked to subscription plans.
Using the stripe dashboard to create prices
Navigate to products:
- In your stripe dashboard, go to the "Products" section.
Create a product:
- Click on "Add product". Enter a name, description, and other details for the product. This could be the name of your extension or specific features of it.
Add pricing information:
- Once the product is created, you can add price details. Click on "Add a price" and set the billing interval (e.g., monthly, yearly) and the currency.
- After setting the details, submit to create the price.
Retrieve price IDs:
- After creating prices, you will see the price IDs listed. These are labeled as
PRICE_ID
(e.g.,price_1H8XeG2eZvKYlo2C3tzeKxdu
).
- After creating prices, you will see the price IDs listed. These are labeled as
Configuring price IDs
Store the price IDs in your environment variables as mentioned earlier.
STRIPE_PLAN1_PRICE_ID=price_id_for_your_first_plan
STRIPE_PLAN2_PRICE_ID=price_id_for_your_second_plan
Setting up stripe in express link
Installing stripe
First, ensure that the stripe package is installed in your express project. If not already installed, you can add it by running the following command:
yarn add stripe@12.18.0
Creating new files
stripe (
src/stripe.ts
)import Stripe from 'stripe'; import Users, { IUserDocument } from './models/users'; import { connectToDatabase, disconnectFromDatabase } from './mongoose'; import { requireLogin } from './utils'; import * as dotenv from 'dotenv'; import getEmailTemplate from './models/email-templates'; import sendEmail from './aws-ses'; dotenv.config(); const { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_KEY, STRIPE_PLAN1_PRICE_ID, STRIPE_PLAN2_PRICE_ID } = process.env; const stripe = new Stripe(STRIPE_SECRET_KEY, { apiVersion: '2022-11-15' }); const sendStripeEmail = async (toEmail: string, template: string) => { let emailTemplate = await getEmailTemplate(template); try { await sendEmail({ from: `AI-recruiter <${process.env.EMAIL_SUPPORT_FROM_ADDRESS}>`, to: [toEmail], cc: ['recruit@workinbiotech.com'], subject: emailTemplate.subject, body: emailTemplate.message, }); } catch (err) { console.log('Email sending error:', err); } }; const saveInvoice = async (user, event) => { const invoicePayment = event.data.object; let invoices = user.stripeInvoices || []; // delete previous entry invoices = invoices.filter(invoice => invoice.id !== invoicePayment.id); invoices.push(invoicePayment); await Users.updateOne( { _id: user._id }, { $set: { stripeInvoices: invoices } } ); return invoicePayment; }; export const webhook = async (req, res, next) => { const sig = req.headers['stripe-signature']; let rawData = ''; req.on('data', (chunk) => { // Accumulate the raw request body rawData += chunk; }); req.on('end', async () => { let event; try { event = stripe.webhooks.constructEvent(rawData, sig, STRIPE_WEBHOOK_KEY); } catch (error) { console.error('Webhook signature verification failed.', error.message); return res.sendStatus(400); } const data = event.data.object; await connectToDatabase(); // Handle specific event types switch (event.type) { case 'checkout.session.completed': if (data.mode === 'subscription') { const selector = { 'stripeCustomer.id': data.customer }; const user = await Users.findOne(selector); if (user) { await Users.updateOne( selector, { $set: { stripeCustomer: { id: data.customer }, activePricingPlan: data.metadata.plan } } ); try { await Users.subscribe({ session: { subscription: { id: data.subscription } } as any, user, }); } catch (error) { console.error('Failed to save subscription:', error); } } } if (data.mode === 'setup') { const setupIntent: any = await stripe.setupIntents.retrieve( data.setup_intent ); await stripe.customers.update(setupIntent.customer, { invoice_settings: { default_payment_method: setupIntent.payment_method, }, }); await stripe.subscriptions.update( setupIntent.metadata.subscription_id, { default_payment_method: setupIntent.payment_method, } ); } break; case 'customer.source.expiring': const expiringData = event.data.object; const user = await Users.findOne({ 'stripeCustomer.id': expiringData.customer, }); if (user) { await sendStripeEmail(user.email, 'customerSourceExpiring'); } break; case 'invoice.payment_failed': const pfselector = { 'stripeCustomer.id': event.data.object.customer }; const pfuser = await Users.findOne(pfselector); if (pfuser) { const invoicePayment = await saveInvoice(pfuser, event); await sendStripeEmail( invoicePayment.customer_email, 'paymentFailedNotification' ); } break; case 'invoice.payment_succeeded': const selector = { 'stripeCustomer.id': event.data.object.customer }; const dbuser = await Users.findOne(selector); if (dbuser) { await saveInvoice(dbuser, event); await Users.updateOne(selector, { $set: { planChangedAt: new Date() } }); } break; // Add cases for other event types you want to handle } await disconnectFromDatabase(); res.sendStatus(200); }); }; export const createSession = async ({ mode, user, plan, subscriptionId, customer, }: { mode: string; user: IUserDocument; subscriptionId?; plan?: string; customer?; }) => { const options: any = { payment_method_types: ['card'], mode, success_url: 'https://workinbiotech.com/ai-cruiter?ai-cruiter-stripe-url=success', cancel_url: 'https://workinbiotech.com/ai-cruiter?ai-cruiter-stripe-url=cancel', }; if (mode === 'subscription') { options.line_items = [ { price: plan === 'plan1' ? STRIPE_PLAN1_PRICE_ID : STRIPE_PLAN2_PRICE_ID, quantity: 1, }, ]; options.metadata = { plan }; let stripeCustomerId: string; if (user && user.stripeCustomer && user.stripeCustomer.id) { stripeCustomerId = user.stripeCustomer.id; } else { const stripeCustomer = (await stripe.customers.create({ email: user.email })); stripeCustomerId = stripeCustomer.id; await Users.updateOne({ _id: user._id }, { $set: { stripeCustomer } }); } options.customer = stripeCustomerId; } if (mode === 'setup') { options.customer = customer; options.setup_intent_data = { metadata: { customer_id: customer, subscription_id: subscriptionId, }, }; } return stripe.checkout.sessions.create(options); }; export const createSubscription = async (req, res, next) => { await requireLogin(req, res, next); await connectToDatabase(); const { plan } = req.body; let response: any; try { const session = await createSession({ mode: 'subscription', user: req.user, plan }); response = { url: session.url }; } catch (error) { console.error(error); res = { error: 'Something went wrong' }; } await disconnectFromDatabase(); if (response.error) { return res.status(500).json(); } return res.json(response); }; export const getStripeSubscription = (id: string) => stripe.subscriptions.retrieve(id); export const getSubscription = async (req, res, next) => { await requireLogin(req, res, next); await connectToDatabase(); const userId = req.user._id; const user = await Users.findOne({ _id: userId }); if (!user.stripeSubscription || !user.stripeSubscription.id) { return res.json({ subscriptionId: '' }); } const subscription = await stripe.subscriptions.retrieve( user.stripeSubscription.id ); const invoices = user.stripeInvoices || []; const customerId = user.stripeCustomer ? user.stripeCustomer.id : ''; const default_payment_method_id: any = subscription.default_payment_method; const paymentMethod = await stripe.paymentMethods.retrieve( default_payment_method_id ); const card: any = paymentMethod ? paymentMethod.card : null; await disconnectFromDatabase(); try { const session = await createSession({ mode: 'setup', user, customer: customerId, subscriptionId: subscription.id, }); card.manage_url = session.url; } catch (e) { console.log(`Error during generating manage card url ${e.message}`); } return res.json({ plan: user.activePricingPlan, subscription, invoices, card }); }; export const deleteSubscription = ({ subscriptionId, }: { subscriptionId: string; }) => { return stripe.subscriptions.del(subscriptionId); }; export const cancelSubscription = async (req, res, next) => { await requireLogin(req, res, next); await connectToDatabase(); await cancelSubscriptionHandler(req.user._id); await disconnectFromDatabase(); return res.send('ok'); }; export const cancelSubscriptionHandler = async (userId: string) => { const user = await Users.findOne({ _id: userId }); const subscription = user.stripeSubscription || {}; let deletedSubscription; if (subscription.id) { try { deletedSubscription = await deleteSubscription({ subscriptionId: subscription.id, }); } catch (e) { console.log(`Error during cancelSubscriptionHandler ${e.message}`); } } await Users.updateOne( { _id: userId }, { $set: { activePricingPlan: '', isSubscriptionActive: false, activePricingPlanWhenCancel: user.activePricingPlan, stripeSubscription: deletedSubscription } } ); };
sendStripeEmail
This asynchronous function is used for sending emails via AWS SES. It takes two parameters: the recipient's email (toEmail
) and the email template identifier (template
). It retrieves an email template, constructs the email content, and sends it to the specified recipient, handling any errors that might occur during the process.async (toEmail: string, template: string) => { let emailTemplate = await getEmailTemplate(template); ... await sendEmail(...); }
saveInvoice
This function saves a Stripe invoice to a user's document in the database. It filters out any existing invoices with the same ID to avoid duplicates and updates the user's invoice list with the new invoice data.async (user, event) => { const invoicePayment = event.data.object; ... await Users.updateOne(...); }
webhook
Handles incoming Stripe webhook events. It verifies the event signature to ensure the webhook is from Stripe, parses the event, connects to the database, and performs specific actions based on the event type (like updating user subscription data).async (req, res, next) => { ... let event = stripe.webhooks.constructEvent(rawData, sig, STRIPE_WEBHOOK_KEY); ... // Example: Handling subscription update }
cancelSubscriptionHandler
This function handles the logic for cancelling a user's subscription. It retrieves the user's information from the database, checks if there is an active subscription, and if present, deletes the subscription using Stripe's API. It updates the user's record in the database to reflect the cancellation.async (userId: string) => { const user = await Users.findOne(...); ... if (subscription.id) { deletedSubscription = await deleteSubscription(...); } ... await Users.updateOne(...); }
cancelSubscription
A wrapper function that handles the Express route logic for cancelling a subscription. It ensures the user is logged in, connects to the database, callscancelSubscriptionHandler
to process the cancellation, then closes the database connection and sends a response.async (req, res, next) => { ... await cancelSubscriptionHandler(req.user._id); ... return res.send('ok'); }
deleteSubscription
Provides a straightforward way to delete a subscription directly through Stripe's API by passing thesubscriptionId
.({ subscriptionId }: { subscriptionId: string }) => { return stripe.subscriptions.del(subscriptionId); }
Changing files
users.ts (
users.ts
)Added new stripe related fields
activePricingPlan?: 'plan1' | 'plan2'; stripeCustomer: { id: string; }; stripeSubscription: any; isSubscriptionActive: boolean; isPaymentFailed: boolean; stripeInvoices: any[];
Added new subscribe method
It handles the logic for subscribing a user based on a session object from Stripe and updates the user's subscription status in your databasepublic static async subscribe({ session, user, }: { session: Stripe.Checkout.Session; user: IUserDocument }) { if (!session.subscription) { throw new Error('Not subscribed'); } if (user.isSubscriptionActive) { throw new Error('Already subscribed.'); } const stripeSubscription = session.subscription as Stripe.Subscription; if (stripeSubscription.canceled_at) { throw new Error('Unsubscribed'); } await this.updateOne( { _id: user._id }, { stripeSubscription, isSubscriptionActive: true } ); }
Added new cancelSubscription method
It saves a user's subscription status and canceled subscription object in the databasepublic static async cancelSubscription(userId: string, subscription) { return this.updateOne( { _id: userId }, { $set: { stripeSubscription: subscription, activePricingPlan: '', isSubscriptionActive: false, } }, ) }
Here is the updated version of the users model (src/models/users.ts)*
https://github.com/async-labs/browser-extension/blob/main/chapters/4-end/server/src/models/users.ts
utils.ts
Added new function requireLogin
It is a middleware function used to secure routes by verifying that the incoming requests are from authenticated users
export const requireLogin = async (req, _res, _next) => { if (!req.headers['ai-cruiter-auth-token']) { throw new Error('Login required'); } const token = req.headers['ai-cruiter-auth-token']; try { const { user }: any = jwt.verify(token, process.env.JWT_TOKEN_SECRET || ''); req.user = user; } catch (e) { throw new Error('Login required'); } };
Here is the updated version of the utils file (src/utils.ts)*
https://github.com/async-labs/browser-extension/blob/main/chapters/4-end/server/src/utils.ts
server.ts
Created new routes for stripe
app.post('/webhook', routeErrorHandling(webhook)); app.use(express.json()); app.post('/create-subscription', routeErrorHandling(createSubscription)); app.post('/cancel-subscription', routeErrorHandling(cancelSubscription)); app.get('/get-subscription', routeErrorHandling(getSubscription));
Here is the updated version of the server file (src/server.ts)
https://github.com/async-labs/browser-extension/blob/main/chapters/4-end/server/src/server.ts
You've reached the end of the Chapter 4 preview. To continue reading, you will need to purchase the book.
We keep our book up to date with recent libraries and packages.