From e9df0c3dd362b7fe0e51d86766032ef1e565d6a5 Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Mon, 24 Feb 2025 17:58:28 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20stripe=20portal=20session=20h?= =?UTF-8?q?elper=20to=20link=20to=20payment=20methods=20(#1576)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../portal/helpers/create-portal-session.ts | 82 ++++++++++++++++++ .../stripe/portal/payment-methods/route.ts | 3 + apps/web/src/app/api/stripe/portal/route.ts | 84 +------------------ 3 files changed, 87 insertions(+), 82 deletions(-) create mode 100644 apps/web/src/app/api/stripe/portal/helpers/create-portal-session.ts create mode 100644 apps/web/src/app/api/stripe/portal/payment-methods/route.ts diff --git a/apps/web/src/app/api/stripe/portal/helpers/create-portal-session.ts b/apps/web/src/app/api/stripe/portal/helpers/create-portal-session.ts new file mode 100644 index 000000000..4bc68f307 --- /dev/null +++ b/apps/web/src/app/api/stripe/portal/helpers/create-portal-session.ts @@ -0,0 +1,82 @@ +import { stripe } from "@rallly/billing"; +import { prisma } from "@rallly/database"; +import { absoluteUrl } from "@rallly/utils/absolute-url"; +import * as Sentry from "@sentry/nextjs"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +import { auth } from "@/next-auth"; + +export function createStripePortalSessionHandler(path: string = "") { + return async function (request: NextRequest) { + const sessionId = request.nextUrl.searchParams.get("session_id"); + const returnPath = + request.nextUrl.searchParams.get("return_path") ?? "/settings/billing"; + + let customerId: string | undefined; + + if (sessionId) { + try { + const session = await stripe.checkout.sessions.retrieve(sessionId); + if (typeof session.customer !== "string") { + Sentry.captureException(new Error("Invalid customer ID in session")); + return NextResponse.json( + { error: "Invalid customer ID in session" }, + { status: 400 }, + ); + } + customerId = session.customer; + } catch (error) { + Sentry.captureException(error); + return NextResponse.json( + { error: "Failed to retrieve session" }, + { status: 500 }, + ); + } + } else { + const userSession = await auth(); + if (!userSession?.user || userSession.user.email === null) { + const url = new URL("/login", request.url); + url.searchParams.set("redirectTo", request.nextUrl.pathname); + return NextResponse.redirect(url, 302); + } + try { + const user = await prisma.user.findUnique({ + where: { + id: userSession.user.id, + }, + select: { + customerId: true, + }, + }); + customerId = user?.customerId ?? undefined; + } catch (error) { + Sentry.captureException(error); + return NextResponse.json( + { error: "Failed to retrieve user" }, + { status: 500 }, + ); + } + } + + if (!customerId) { + return NextResponse.json({ + error: "No customer ID found", + }); + } + + try { + const portalSession = await stripe.billingPortal.sessions.create({ + customer: customerId, + return_url: returnPath ? absoluteUrl(returnPath) : undefined, + }); + return NextResponse.redirect(portalSession.url + path); + } catch (error) { + Sentry.captureException(error); + return NextResponse.json( + { error: "Failed to create portal session" }, + { status: 500 }, + ); + } + }; +} diff --git a/apps/web/src/app/api/stripe/portal/payment-methods/route.ts b/apps/web/src/app/api/stripe/portal/payment-methods/route.ts new file mode 100644 index 000000000..a61531321 --- /dev/null +++ b/apps/web/src/app/api/stripe/portal/payment-methods/route.ts @@ -0,0 +1,3 @@ +import { createStripePortalSessionHandler } from "../helpers/create-portal-session"; + +export const GET = createStripePortalSessionHandler("/payment-methods"); diff --git a/apps/web/src/app/api/stripe/portal/route.ts b/apps/web/src/app/api/stripe/portal/route.ts index ef6b88f22..a0d9201c0 100644 --- a/apps/web/src/app/api/stripe/portal/route.ts +++ b/apps/web/src/app/api/stripe/portal/route.ts @@ -1,83 +1,3 @@ -import { stripe } from "@rallly/billing"; -import { prisma } from "@rallly/database"; -import { absoluteUrl } from "@rallly/utils/absolute-url"; -import * as Sentry from "@sentry/nextjs"; -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; +import { createStripePortalSessionHandler } from "./helpers/create-portal-session"; -import { auth } from "@/next-auth"; - -export async function GET(request: NextRequest) { - const sessionId = request.nextUrl.searchParams.get("session_id"); - const returnPath = request.nextUrl.searchParams.get("return_path"); - - let customerId: string | undefined; - - if (sessionId) { - try { - const session = await stripe.checkout.sessions.retrieve(sessionId); - if (typeof session.customer !== "string") { - Sentry.captureException(new Error("Invalid customer ID in session")); - return NextResponse.json( - { error: "Invalid customer ID in session" }, - { status: 400 }, - ); - } - customerId = session.customer; - } catch (error) { - Sentry.captureException(error); - return NextResponse.json( - { error: "Failed to retrieve session" }, - { status: 500 }, - ); - } - } else { - const userSession = await auth(); - if (!userSession?.user || userSession.user.email === null) { - Sentry.captureException(new Error("User not logged in")); - return NextResponse.json( - { error: "User not logged in" }, - { status: 400 }, - ); - } - try { - const user = await prisma.user.findUnique({ - where: { - id: userSession.user.id, - }, - select: { - customerId: true, - }, - }); - customerId = user?.customerId ?? undefined; - } catch (error) { - Sentry.captureException(error); - return NextResponse.json( - { error: "Failed to retrieve user" }, - { status: 500 }, - ); - } - } - - if (!customerId) { - Sentry.captureException(new Error("Session has no customer ID")); - return NextResponse.json( - { error: "Session has no customer ID" }, - { status: 400 }, - ); - } - - try { - const portalSession = await stripe.billingPortal.sessions.create({ - customer: customerId, - return_url: returnPath ? absoluteUrl(returnPath) : undefined, - }); - return NextResponse.redirect(portalSession.url); - } catch (error) { - Sentry.captureException(error); - return NextResponse.json( - { error: "Failed to create portal session" }, - { status: 500 }, - ); - } -} +export const GET = createStripePortalSessionHandler();