Skip to content

Instantly share code, notes, and snippets.

@rohailtaha
Last active March 16, 2024 10:08
Show Gist options
  • Save rohailtaha/19d267aea7f28244e39d6f56c5151d6f to your computer and use it in GitHub Desktop.
Save rohailtaha/19d267aea7f28244e39d6f56c5151d6f to your computer and use it in GitHub Desktop.
Stripe subscriptions in Nextjs/Nodejs with React
'use client'
import { SUBSCRIPTION_PLAN_NAME } from '@/types/types';
import { loadStripe } from '@stripe/stripe-js';
type SubscribeButtonProps = {
subscriptionPlanName: SUBSCRIPTION_PLAN_NAME;
};
function SubscribeButton({ subscriptionPlanName }: SubscribeButtonProps) {
async function handleCreateCheckoutSession() {
const STRIPE_PK = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!;
const stripe = await loadStripe(STRIPE_PK);
// some error feedback mechanism
if (!stripe) return;
const response = await fetch('/api/checkout-session', {
method: 'POST',
body: JSON.stringify({
planName: subscriptionPlanName,
}),
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
stripe.redirectToCheckout({ sessionId: data.data });
}
return (
<button
onClick={handleCreateCheckoutSession}
>
Subscribe
</button>
);
}
export default SubscribeButton;
import { getPricingPlanId } from '@/api-utils/utils';
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
export async function POST(request: Request) {
const body = await request.json();
const stripe = new Stripe(process.env.STRIPE_SECRET!);
const domain =
process.env.NODE_ENV === 'development'
? 'localhost:3000'
: process.env.DOMAIN;
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price: getPricingPlanId(body.planName),
quantity: 1,
},
],
mode: 'subscription',
success_url: `http://${domain}/billing?checkout=subscription_started`,
cancel_url: `https://${domain}/billing?checkout=subscription_failed`,
});
return NextResponse.json({ data: session.id });
}
// run this funciton when update subscription is confirmed by user
async function handleUpdate() {
try {
const response = await fetch('/api/subscription', {
method: 'PUT',
body: JSON.stringify({ subscriptionName: updateTo }),
headers: {
'Content-Type': 'application/json',
},
});
} catch (err) {
console.error(err);
}
}
// run this funciton when delete subscription is confirmed by user
async function handleDelete() {
try {
setLoading(true);
const response = await fetch('/api/subscription', {
method: 'DELETE',
});
} catch (err) {
console.error(err);
}
}
export async function PUT(request: Request) {
try {
const body = await request.json();
const token = (await getToken({ req: request as NextRequest }))!;
const subscription = (await prismaClient.subscriptions.findFirst({
where: { userId: token.id as number, stripeStatus: 'active' },
}))!;
if (!subscription) {
return jsonErrorResponse(
ERROR_TYPE.NoActiveSubscription,
'User does not have any active subscription',
422
);
}
// If there is a subscription that is pending cancelation, then don't update.
if (subscription.endsAt !== null) {
return jsonErrorResponse(
ERROR_TYPE.DisallowedSubscriptionUpdate,
'You already have a subscription that is pending cancelation. You can only update your subscription after the current subscription is cancelled at the end of the billing cycle. Or you can resume your subscription and then update.',
422
);
}
const stripe = getStripe();
const stripeSubscription = await stripe.subscriptions.retrieve(
subscription.stripeId
);
const currentSubscriptionItemId = stripeSubscription.items.data[0].id;
const updatedSubscription = await stripe.subscriptions.update(
subscription.stripeId,
{
// This proration behaviour will bill the user immediately instead of accumulating the amount
// (for updated subscription) in the next billing cycle.
proration_behavior: 'always_invoice',
items: [
{
id: currentSubscriptionItemId,
deleted: true,
},
{
price: getPriceId(body.subscriptionName),
quantity: 1,
},
],
}
);
const subscriptionItems = updatedSubscription.items.data;
await prismaClient.subscriptions.update({
where: {
stripeId: subscription.stripeId,
},
data: {
stripeStatus: updatedSubscription.status,
name: 'default',
stripePrice: subscriptionItems[0].price.id,
quantity: subscriptionItems[0].quantity,
},
});
return new Response(null, { status: 204 });
} catch (err) {
console.error(err);
return serverErrorResponse(
'There was a server error while updating the subscription. Contact the support team'
);
}
}
export async function DELETE(request: Request) {
try {
const token = (await getToken({ req: request as NextRequest }))!;
const subscription = (await prismaClient.subscriptions.findFirst({
where: { userId: token.id as number, stripeStatus: 'active' },
}))!;
if (!subscription) {
return Response.json(
{ message: 'User does not have any active subscriptions' },
{
status: 404,
}
);
}
if (subscription.endsAt !== null) {
return jsonErrorResponse(
ERROR_TYPE.DisallowedSubscriptionUpdate,
'Subscription is already marked for cancelation.',
422
);
}
const stripe = getStripe();
const updatedSubscription = await stripe.subscriptions.update(
subscription.stripeId,
{
// This only 'marks' the subscription for cancelation. The subscription will actually be
// cancelled after the current billing cycle.
cancel_at_period_end: true,
}
);
await prismaClient.subscriptions.update({
where: {
stripeId: subscription.stripeId,
},
data: {
endsAt: new Date(updatedSubscription.cancel_at! * 1000),
},
});
return new Response(null, {
status: 204,
});
} catch (err) {
console.error(err);
return serverErrorResponse(
'There was a server error while canceling the subscription. Contact the support team'
);
}
}
// run this function when resume subscription is confirmed by user
async function handleResume() {
try {
const response = await fetch('/api/subscription/resume', {
method: 'PUT',
});
} catch (err) {
console.error(err);
}
}
import {
getStripe,
jsonErrorResponse,
serverErrorResponse,
} from '@/api-utils/utils';
import prismaClient from '@/prisma/prisma-client';
import { ERROR_TYPE } from '@/types/types';
import { getToken } from 'next-auth/jwt';
import { NextRequest } from 'next/server';
export async function PUT(request: Request) {
try {
const token = (await getToken({ req: request as NextRequest }))!;
const subscription = (await prismaClient.subscriptions.findFirst({
where: { userId: token.id as number, stripeStatus: 'active' },
}))!;
if (!subscription) {
return jsonErrorResponse(
ERROR_TYPE.NoActiveSubscription,
'User does not have any active subscriptions',
422
);
}
if (subscription.endsAt === null) {
return jsonErrorResponse(
ERROR_TYPE.DisallowedSubscriptionUpdate,
'Subscription is already resumed',
422
);
}
const stripe = getStripe();
await stripe.subscriptions.update(subscription.stripeId, {
cancel_at_period_end: false,
});
await prismaClient.subscriptions.update({
where: {
stripeId: subscription.stripeId,
},
data: {
endsAt: null,
},
});
return new Response(null, { status: 204 });
} catch (err) {
console.error(err);
return serverErrorResponse(
'There was a server error while resuming the subscription. Contact the support team'
);
}
}
/**********************************************************************************************
This endpoint will give the payment that will be done at the end of the current billing cycle
i.e. for the next billing cycle
**********************************************************************************************/
import {
formatInvoiceAmount,
getStripe,
serverErrorResponse,
} from '@/api-utils/utils';
import prismaClient from '@/prisma/prisma-client';
import { getToken } from 'next-auth/jwt';
import { NextRequest } from 'next/server';
export async function GET(request: Request) {
try {
const token = (await getToken({ req: request as NextRequest }))!;
const subscription = await prismaClient.subscriptions.findFirst({
where: { userId: token.id as number, stripeStatus: 'active' },
});
if (subscription?.stripeStatus === 'active') {
const stripe = getStripe();
const stripeSubscription = await stripe.subscriptions.retrieve(
subscription.stripeId
);
const nextPaymentDate = new Date(
stripeSubscription.current_period_end * 1000
);
const user = (await prismaClient.users.findUnique({
where: {
id: token.id as number,
},
}))!;
// if subscription is marked for cancellation, then there is no upcoming invoice
if (subscription.endsAt !== null) {
return Response.json({
nextPayment: null,
});
}
const nextInvoice = await stripe.invoices.retrieveUpcoming({
customer: user.stripeId!,
subscription: subscription.stripeId,
});
return Response.json({
nextPayment: {
date: nextPaymentDate,
amount: formatInvoiceAmount(nextInvoice.amount_due),
},
});
} else {
return Response.json({
nextPayment: null,
});
}
} catch (e) {
console.error(e);
return serverErrorResponse(
'There was a server error while fetching the next payment details. Contact the support team'
);
}
}
/**********************************************************************************************
Get all paid invoices for the user
**********************************************************************************************/
import {
formatInvoiceAmount,
getStripe,
serverErrorResponse,
} from '@/api-utils/utils';
import prismaClient from '@/prisma/prisma-client';
import { getToken } from 'next-auth/jwt';
import { NextRequest } from 'next/server';
export async function GET(request: Request) {
try {
const token = (await getToken({ req: request as NextRequest }))!;
const user = (await prismaClient.users.findUnique({
where: { id: token.id as number },
}))!;
if (!user.stripeId) {
return Response.json({ invoices: null });
}
const stripe = getStripe();
const stripeInvoices = await stripe.invoices.list({
customer: user.stripeId,
status: 'paid',
});
const invoices = stripeInvoices.data.map(invoice => ({
id: invoice.id,
createdAt: new Date(invoice.created * 1000),
amount: formatInvoiceAmount(invoice.total),
status: invoice.status,
url: invoice.hosted_invoice_url,
}));
return Response.json({ invoices: invoices.length > 0 ? invoices : null });
} catch (e) {
console.error(e);
return serverErrorResponse(
'There was a server error fetching the user invoices. Contact the support team'
);
}
}
/**********************************************************************************************
We can use stripe webhook for handling asynchronous stripe events that we don't have control
over, like subscription update after end of a billing cycle, canceling a subscription at the
end of billing cycle etc.
**********************************************************************************************/
import { formatInvoiceAmount, getStripe } from '@/api-utils/utils';
import prismaClient from '@/prisma/prisma-client';
import Stripe from 'stripe';
export async function POST(request: Request) {
const stripe = getStripe();
const sig = request.headers.get('stripe-signature')!;
const payload = await request.text();
let event;
try {
event = stripe.webhooks.constructEvent(
payload,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err: any) {
console.error(err);
return new Response(`Webhook Error: ${err.message}`, {
status: 400,
});
}
if (event.type === 'checkout.session.completed') {
await handleCheckoutSessionCompletion(event);
} else if (event.type === 'customer.subscription.updated') {
await handleSubscriptionUpdate(event);
} else if (event.type === 'customer.subscription.deleted') {
await handleSubscriptionDelete(event);
} else if (event.type === 'invoice.payment_succeeded') {
await handleInvoicePaymentSuccess(event);
} else if (event.type === 'payment_method.attached') {
await handlePaymentMethodAttach(event);
}
return new Response(null, {
status: 204,
});
}
async function handlePaymentMethodAttach(
event: Stripe.PaymentMethodAttachedEvent
) {
console.log('payment_method.attached event occurred');
const stripe = getStripe();
const paymentMethod = event.data.object;
const customer = (await stripe.customers.retrieve(
paymentMethod.customer as string
)) as Stripe.Customer;
if (paymentMethod.card) {
await prismaClient.users.update({
where: {
stripeId: paymentMethod.customer as string,
email: customer.email!,
},
data: {
pmType: paymentMethod.card.brand,
pmLastFour: paymentMethod.card.last4,
pmExpiration: `${paymentMethod.card.exp_month}/${paymentMethod.card.exp_year}`,
},
});
}
}
async function handleInvoicePaymentSuccess(
event: Stripe.InvoicePaymentSucceededEvent
) {
console.log('invoice.payment_succeeded event occurred');
// create receipts for successfull invoice payment
try {
const invoice = event.data.object;
const user = (await prismaClient.users.findFirst({
where: {
stripeId: invoice.customer as string,
},
}))!;
await prismaClient.receipts.create({
data: {
userId: user.id,
providerId: invoice.id,
amount: formatInvoiceAmount(invoice.amount_due),
tax: `$${invoice.tax ?? '0.00'}`,
paidAt: new Date(invoice.status_transitions.paid_at! * 1000),
createdAt: new Date(),
},
});
} catch (err) {
console.error(err);
}
}
async function handleCheckoutSessionCompletion(
event: Stripe.CheckoutSessionCompletedEvent
) {
try {
console.log('chekout.session.completed event occurred');
// update the subscription database and also the users database (for billing details)
const sessionData = event.data.object;
const stripe = getStripe();
if (
sessionData.status === 'complete' &&
sessionData.mode === 'subscription'
) {
const customer = (await stripe.customers.retrieve(
sessionData.customer as string
)) as Stripe.Customer;
const billingCountry = customer.address?.country ?? null;
const billingState = customer.address?.state ?? null;
const billingCity = customer.address?.city ?? null;
const billingPostalCode = customer.address?.postal_code ?? null;
const billingAddress = customer.address?.line1 ?? null;
const billingAddressLine2 = customer.address?.line2 ?? null;
const user = await prismaClient.users.update({
data: {
billingCountry,
billingState,
billingCity,
billingPostalCode,
billingAddress,
billingAddressLine2,
},
where: {
email: customer.email!,
},
});
const stripeSubscription = (await stripe.subscriptions.retrieve(
sessionData.subscription as string
)) as Stripe.Subscription;
const subscriptionItems = stripeSubscription.items.data;
// create subscription
const subscription = await prismaClient.subscriptions.create({
data: {
stripeId: sessionData.subscription as string,
stripeStatus: stripeSubscription.status,
name: 'default',
userId: user.id,
stripePrice: subscriptionItems[0].price.id,
quantity: subscriptionItems[0].quantity,
createdAt: new Date(),
},
});
}
} catch (err) {
console.error(err);
}
}
async function handleSubscriptionUpdate(
event: Stripe.CustomerSubscriptionUpdatedEvent
) {
const subscription = event.data.object;
console.log('customer.subscription.updated event occurred');
try {
if (['past_due', 'canceled'].includes(subscription.status)) {
await prismaClient.subscriptions.update({
where: {
stripeId: subscription.id,
},
data: {
stripeStatus: subscription.status,
updatedAt: new Date(),
},
});
}
} catch (err) {
console.error(err);
}
}
async function handleSubscriptionDelete(
event: Stripe.CustomerSubscriptionDeletedEvent
) {
console.log('customer.subscription.deleted event occurred');
try {
// update subscription status to 'canceled'
const subscription = event.data.object;
await prismaClient.subscriptions.update({
where: {
stripeId: subscription.id,
},
data: {
stripeStatus: subscription.status,
},
});
} catch (err) {
console.error(err);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment