From 900b816ae0b340db4344f37ba748a872a2da3742 Mon Sep 17 00:00:00 2001 From: Mythie Date: Fri, 5 May 2023 20:08:18 +1000 Subject: [PATCH] feat: stripe handlers and fetchers --- .../lib/stripe/fetchers/checkout-session.ts | 23 +++++ .../lib/stripe/fetchers/get-subscription.ts | 14 +++ .../lib/stripe/fetchers/portal-session.ts | 19 ++++ .../lib/stripe/handlers/checkout-session.ts | 91 +++++++++++++++++++ .../lib/stripe/handlers/get-subscription.ts | 63 +++++++++++++ .../lib/stripe/handlers/portal-session.ts | 54 ++++++++++- 6 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 packages/lib/stripe/fetchers/checkout-session.ts create mode 100644 packages/lib/stripe/fetchers/get-subscription.ts create mode 100644 packages/lib/stripe/fetchers/portal-session.ts create mode 100644 packages/lib/stripe/handlers/checkout-session.ts create mode 100644 packages/lib/stripe/handlers/get-subscription.ts diff --git a/packages/lib/stripe/fetchers/checkout-session.ts b/packages/lib/stripe/fetchers/checkout-session.ts new file mode 100644 index 000000000..2f8151ba6 --- /dev/null +++ b/packages/lib/stripe/fetchers/checkout-session.ts @@ -0,0 +1,23 @@ +import { CheckoutSessionRequest, CheckoutSessionResponse } from "../handlers/checkout-session" + +export type FetchCheckoutSessionOptions = CheckoutSessionRequest['body'] + +export const fetchCheckoutSession = async ({ + id, + priceId +}: FetchCheckoutSessionOptions) => { + const response = await fetch('/api/stripe/checkout-session', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + id, + priceId + }) + }); + + const json: CheckoutSessionResponse = await response.json(); + + return json; +} \ No newline at end of file diff --git a/packages/lib/stripe/fetchers/get-subscription.ts b/packages/lib/stripe/fetchers/get-subscription.ts new file mode 100644 index 000000000..874771e34 --- /dev/null +++ b/packages/lib/stripe/fetchers/get-subscription.ts @@ -0,0 +1,14 @@ +import { GetSubscriptionResponse } from "../handlers/get-subscription"; + +export const fetchSubscription = async () => { + const response = await fetch("/api/stripe/subscription", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + const json: GetSubscriptionResponse = await response.json(); + + return json; +}; diff --git a/packages/lib/stripe/fetchers/portal-session.ts b/packages/lib/stripe/fetchers/portal-session.ts new file mode 100644 index 000000000..23719c500 --- /dev/null +++ b/packages/lib/stripe/fetchers/portal-session.ts @@ -0,0 +1,19 @@ +import { PortalSessionRequest, PortalSessionResponse } from "../handlers/portal-session"; + +export type FetchPortalSessionOptions = PortalSessionRequest["body"]; + +export const fetchPortalSession = async ({ id }: FetchPortalSessionOptions) => { + const response = await fetch("/api/stripe/portal-session", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + id, + }), + }); + + const json: PortalSessionResponse = await response.json(); + + return json; +}; diff --git a/packages/lib/stripe/handlers/checkout-session.ts b/packages/lib/stripe/handlers/checkout-session.ts new file mode 100644 index 000000000..b814f0e1e --- /dev/null +++ b/packages/lib/stripe/handlers/checkout-session.ts @@ -0,0 +1,91 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import prisma from "@documenso/prisma"; +import { stripe } from "../index"; +import { getToken } from "next-auth/jwt"; + +export type CheckoutSessionRequest = { + body: { + id: string; + priceId: string; + }; +}; + +export type CheckoutSessionResponse = + | { + success: false; + message: string; + } + | { + success: true; + url: string; + }; + +export const checkoutSessionHandler = async (req: NextApiRequest, res: NextApiResponse) => { + if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) { + return res.status(500).json({ + success: false, + message: "Subscriptions are not enabled", + }); + } + + if (req.method !== "POST") { + return res.status(405).json({ + success: false, + message: "Method not allowed", + }); + } + + const token = await getToken({ + req, + }); + + if (!token || !token.email) { + return res.status(401).json({ + success: false, + message: "Unauthorized", + }); + } + + const user = await prisma.user.findFirst({ + where: { + email: token.email, + }, + }); + + if (!user) { + return res.status(404).json({ + success: false, + message: "No user found", + }); + } + + const { id, priceId } = req.body; + + if (typeof id !== "string" || typeof priceId !== "string") { + return res.status(400).json({ + success: false, + message: "No id or priceId found in request", + }); + } + + const session = await stripe.checkout.sessions.create({ + customer: id, + client_reference_id: String(user.id), + payment_method_types: ["card"], + line_items: [ + { + price: priceId, + quantity: 1, + }, + ], + mode: "subscription", + allow_promotion_codes: true, + success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/settings/billing?success=true`, + cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/settings/billing?canceled=true`, + }); + + return res.status(200).json({ + success: true, + url: session.url, + }); +}; diff --git a/packages/lib/stripe/handlers/get-subscription.ts b/packages/lib/stripe/handlers/get-subscription.ts new file mode 100644 index 000000000..0617f3cc8 --- /dev/null +++ b/packages/lib/stripe/handlers/get-subscription.ts @@ -0,0 +1,63 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import prisma from "@documenso/prisma"; +import { Subscription } from "@prisma/client"; +import { getToken } from "next-auth/jwt"; + +export type GetSubscriptionRequest = never; + +export type GetSubscriptionResponse = + | { + success: false; + message: string; + } + | { + success: true; + subscription: Subscription; + }; + +export const getSubscriptionHandler = async (req: NextApiRequest, res: NextApiResponse) => { + if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) { + return res.status(500).json({ + success: false, + message: "Subscriptions are not enabled", + }); + } + + if (req.method !== "GET") { + return res.status(405).json({ + success: false, + message: "Method not allowed", + }); + } + + const token = await getToken({ + req, + }); + + if (!token || !token.email) { + return res.status(401).json({ + success: false, + message: "Unauthorized", + }); + } + + const subscription = await prisma.subscription.findFirst({ + where: { + User: { + email: token.email, + }, + }, + }); + + if (!subscription) { + return res.status(404).json({ + success: false, + message: "No subscription found", + }); + } + + return res.status(200).json({ + success: true, + subscription, + }); +}; diff --git a/packages/lib/stripe/handlers/portal-session.ts b/packages/lib/stripe/handlers/portal-session.ts index 42151f0b8..0fff5625d 100644 --- a/packages/lib/stripe/handlers/portal-session.ts +++ b/packages/lib/stripe/handlers/portal-session.ts @@ -1 +1,53 @@ -export \ No newline at end of file +import { NextApiRequest, NextApiResponse } from "next"; +import { stripe } from "../index"; + +export type PortalSessionRequest = { + body: { + id: string; + }; +}; + +export type PortalSessionResponse = + | { + success: false; + message: string; + } + | { + success: true; + url: string; + }; + +export const portalSessionHandler = async (req: NextApiRequest, res: NextApiResponse) => { + if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) { + return res.status(500).json({ + success: false, + message: "Subscriptions are not enabled", + }); + } + + if (req.method !== "POST") { + return res.status(405).json({ + success: false, + message: "Method not allowed", + }); + } + + const { id } = req.body; + + if (typeof id !== "string") { + return res.status(400).json({ + success: false, + message: "No id found in request", + }); + } + + const session = await stripe.billingPortal.sessions.create({ + customer: id, + return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/settings/billing`, + }); + + return res.status(200).json({ + success: true, + url: session.url, + }); +};