From e7f8f51c05547ad01736d300895133902fab13c2 Mon Sep 17 00:00:00 2001 From: msukkari Date: Tue, 11 Feb 2025 19:06:40 -0800 Subject: [PATCH] save stripe session id and add manage subscription button in settings --- .../migration.sql | 2 - .../migration.sql | 2 + packages/db/prisma/schema.prisma | 22 ++++----- packages/web/src/actions.ts | 30 +++++++++++- .../web/src/app/onboard/complete/page.tsx | 6 +-- .../app/settings/billing/checkoutButton.tsx | 46 ------------------- .../billing/manageSubscriptionButton.tsx | 34 ++++++++++++++ .../web/src/app/settings/billing/page.tsx | 4 +- 8 files changed, 79 insertions(+), 67 deletions(-) delete mode 100644 packages/db/prisma/migrations/20250212023736_add_stripe_subscription_id_to_org/migration.sql create mode 100644 packages/db/prisma/migrations/20250212024846_add_stripe_session_id_to_org/migration.sql delete mode 100644 packages/web/src/app/settings/billing/checkoutButton.tsx create mode 100644 packages/web/src/app/settings/billing/manageSubscriptionButton.tsx diff --git a/packages/db/prisma/migrations/20250212023736_add_stripe_subscription_id_to_org/migration.sql b/packages/db/prisma/migrations/20250212023736_add_stripe_subscription_id_to_org/migration.sql deleted file mode 100644 index ce8061ef..00000000 --- a/packages/db/prisma/migrations/20250212023736_add_stripe_subscription_id_to_org/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Org" ADD COLUMN "subscriptionId" TEXT; diff --git a/packages/db/prisma/migrations/20250212024846_add_stripe_session_id_to_org/migration.sql b/packages/db/prisma/migrations/20250212024846_add_stripe_session_id_to_org/migration.sql new file mode 100644 index 00000000..9bea1db1 --- /dev/null +++ b/packages/db/prisma/migrations/20250212024846_add_stripe_session_id_to_org/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Org" ADD COLUMN "stripeSessionId" TEXT; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index b2974809..d01d86af 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -105,20 +105,20 @@ model Invite { } model Org { - id Int @id @default(autoincrement()) - name String - domain String @unique - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - members UserToOrg[] - connections Connection[] - repos Repo[] - secrets Secret[] + id Int @id @default(autoincrement()) + name String + domain String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + members UserToOrg[] + connections Connection[] + repos Repo[] + secrets Secret[] - subscriptionId String? + stripeSessionId String? /// List of pending invites to this organization - invites Invite[] + invites Invite[] } enum OrgRole { diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 48837154..ec5a98e2 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -98,7 +98,7 @@ export const checkIfOrgDomainExists = async (domain: string): Promise = return !!org; } -export const createOrg = async (name: string, domain: string, subscriptionId?: string): Promise<{ id: number } | ServiceError> => { +export const createOrg = async (name: string, domain: string, stripeSessionId?: string): Promise<{ id: number } | ServiceError> => { const session = await auth(); if (!session) { return notAuthenticated(); @@ -119,7 +119,7 @@ export const createOrg = async (name: string, domain: string, subscriptionId?: s data: { name, domain, - subscriptionId, + stripeSessionId, members: { create: { userId: session.user.id, @@ -422,4 +422,30 @@ export async function fetchStripeClientSecret(name: string, domain: string) { export async function fetchStripeSession(sessionId: string) { const stripeSession = await stripe.checkout.sessions.retrieve(sessionId); return stripeSession; +} + +export async function createCustomerPortalSession() { + const orgId = await getCurrentUserOrg(); + if (isServiceError(orgId)) { + return orgId; + } + + const org = await prisma.org.findUnique({ + where: { + id: orgId, + }, + }); + + if (!org || !org.stripeSessionId) { + return notFound(); + } + + const origin = (await headers()).get('origin') + const stripeSession = await fetchStripeSession(org.stripeSessionId); + const portalSession = await stripe.billingPortal.sessions.create({ + customer: stripeSession.customer as string, + return_url: `${origin}/settings/billing`, + }); + + return portalSession; } \ No newline at end of file diff --git a/packages/web/src/app/onboard/complete/page.tsx b/packages/web/src/app/onboard/complete/page.tsx index 64746282..76b45af2 100644 --- a/packages/web/src/app/onboard/complete/page.tsx +++ b/packages/web/src/app/onboard/complete/page.tsx @@ -35,14 +35,12 @@ export default async function OnboardComplete({ searchParams }: OnboardCompleteP } const stripeSession = await fetchStripeSession(sessionId); - const stripeSubscription = stripeSession.subscription; - if(stripeSession.payment_status !== "paid" || !stripeSubscription) { + if(stripeSession.payment_status !== "paid") { console.error("Invalid stripe session"); return ; } - const subscriptionId = stripeSubscription as string; - const res = await createOrg(orgName, orgDomain, subscriptionId); + const res = await createOrg(orgName, orgDomain, stripeSession.id); if (isServiceError(res)) { console.error("Failed to create org"); return ; diff --git a/packages/web/src/app/settings/billing/checkoutButton.tsx b/packages/web/src/app/settings/billing/checkoutButton.tsx deleted file mode 100644 index 88029cf4..00000000 --- a/packages/web/src/app/settings/billing/checkoutButton.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"use client" - -import { useState } from "react" -import { useRouter } from "next/navigation" -import { isServiceError } from "@/lib/utils" -import { useToast } from "@/components/hooks/use-toast" -import { Button } from "@/components/ui/button" -import { createCheckoutSession } from "../../../actions" - -export function CheckoutButton() { - const [isLoading, setIsLoading] = useState(false) - const router = useRouter() - const { toast } = useToast() - - const handleCheckoutSession = async () => { - setIsLoading(true) - try { - const session = await createCheckoutSession() - if (isServiceError(session)) { - console.log("Failed to create checkout session: ", session) - toast({ - title: "Error", - description: "Failed to create checkout session. Please try again.", - variant: "destructive", - }) - } else { - router.push(session.url!) - } - } catch (error) { - console.error("Error creating checkout session:", error) - toast({ - title: "Error", - description: "An unexpected error occurred. Please try again.", - variant: "destructive", - }) - } finally { - setIsLoading(false) - } - } - - return ( - - ) -} \ No newline at end of file diff --git a/packages/web/src/app/settings/billing/manageSubscriptionButton.tsx b/packages/web/src/app/settings/billing/manageSubscriptionButton.tsx new file mode 100644 index 00000000..bfb2de60 --- /dev/null +++ b/packages/web/src/app/settings/billing/manageSubscriptionButton.tsx @@ -0,0 +1,34 @@ +"use client" + +import { useState } from "react" +import { useRouter } from "next/navigation" +import { isServiceError } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { createCustomerPortalSession } from "../../../actions" + +export function ManageSubscriptionButton() { + const [isLoading, setIsLoading] = useState(false) + const router = useRouter() + + const redirectToCustomerPortal = async () => { + setIsLoading(true) + try { + const session = await createCustomerPortalSession() + if (isServiceError(session)) { + console.log("Failed to create portal session: ", session) + } else { + router.push(session.url!) + } + } catch (error) { + console.error("Error creating portal session:", error) + } finally { + setIsLoading(false) + } + } + + return ( + + ) +} \ No newline at end of file diff --git a/packages/web/src/app/settings/billing/page.tsx b/packages/web/src/app/settings/billing/page.tsx index 84ceaae0..6b64b5ac 100644 --- a/packages/web/src/app/settings/billing/page.tsx +++ b/packages/web/src/app/settings/billing/page.tsx @@ -1,12 +1,12 @@ 'use server' -import { CheckoutButton } from "./checkoutButton" +import { ManageSubscriptionButton } from "./manageSubscriptionButton" export default async function BillingPage() { return (

Billing

- +
); } \ No newline at end of file