diff --git a/apps/builder/src/features/billing/components/ChangePlanForm.tsx b/apps/builder/src/features/billing/components/ChangePlanForm.tsx index a136ae1c76..87d834135a 100644 --- a/apps/builder/src/features/billing/components/ChangePlanForm.tsx +++ b/apps/builder/src/features/billing/components/ChangePlanForm.tsx @@ -1,15 +1,14 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { useTranslate } from "@tolgee/react"; +import { isDefined } from "@typebot.io/lib/utils"; import { Plan } from "@typebot.io/prisma/enum"; +import { useRouter } from "next/router"; import { useState } from "react"; import { TextLink } from "@/components/TextLink"; -import { useUser } from "@/features/user/hooks/useUser"; import type { WorkspaceInApp } from "@/features/workspace/WorkspaceProvider"; import { isSelfHostedInstance } from "@/helpers/isSelfHostedInstance"; import { orpc, queryClient } from "@/lib/queryClient"; import { toast } from "@/lib/toast"; -import type { PreCheckoutDialogProps } from "./PreCheckoutDialog"; -import { PreCheckoutDialog } from "./PreCheckoutDialog"; import { ProPlanPricingCard } from "./ProPlanPricingCard"; import { StarterPlanPricingCard } from "./StarterPlanPricingCard"; import { UpgradeConfirmationDialog } from "./UpgradeConfirmationDialog"; @@ -26,11 +25,11 @@ export const ChangePlanForm = ({ excludedPlans, }: Props) => { const { t } = useTranslate(); - - const { user } = useUser(); - const [preCheckoutPlan, setPreCheckoutPlan] = - useState(); + const router = useRouter(); const [pendingUpgrade, setPendingUpgrade] = useState<"STARTER" | "PRO">(); + const [pendingCheckoutRedirect, setPendingCheckoutRedirect] = useState< + "STARTER" | "PRO" + >(); const { data, refetch } = useQuery( orpc.billing.getSubscription.queryOptions({ @@ -39,6 +38,33 @@ export const ChangePlanForm = ({ }), ); + const { data: pendingUpgradeData, isLoading: isLoadingPendingUpgrade } = + useQuery( + orpc.billing.getSubscriptionPreview.queryOptions({ + input: { + workspaceId: workspace.id, + plan: pendingUpgrade!, + }, + enabled: isDefined(pendingUpgrade), + }), + ); + + const { mutate: createCheckoutSession } = useMutation( + orpc.billing.createCheckoutSession.mutationOptions({ + onSuccess: (data) => { + router.push(data.checkoutUrl); + }, + onError: (error) => { + setPendingCheckoutRedirect(undefined); + toast({ + type: "error", + title: t("errorMessage"), + description: error.message, + }); + }, + }), + ); + const { mutateAsync: updateSubscription, status: updateSubscriptionStatus } = useMutation( orpc.billing.updateSubscription.mutationOptions({ @@ -70,8 +96,6 @@ export const ChangePlanForm = ({ ); const handlePayClick = async (plan: "STARTER" | "PRO") => { - if (!user) return; - const newSubscription = { plan, workspaceId: workspace.id, @@ -87,7 +111,12 @@ export const ChangePlanForm = ({ }); } } else { - setPreCheckoutPlan(newSubscription); + setPendingCheckoutRedirect(plan); + createCheckoutSession({ + workspaceId: workspace.id, + returnUrl: window.location.href, + plan, + }); } }; @@ -122,17 +151,10 @@ export const ChangePlanForm = ({ return (
- {!workspace.stripeId && ( - setPreCheckoutPlan(undefined)} - /> - )} setPendingUpgrade(undefined)} @@ -144,7 +166,11 @@ export const ChangePlanForm = ({ handlePayClick(Plan.STARTER)} - isLoading={updateSubscriptionStatus === "pending"} + isLoading={ + updateSubscriptionStatus === "pending" || + pendingCheckoutRedirect === "STARTER" || + (isLoadingPendingUpgrade && pendingUpgrade === Plan.STARTER) + } currency={data.subscription?.currency} /> )} @@ -153,7 +179,11 @@ export const ChangePlanForm = ({ handlePayClick(Plan.PRO)} - isLoading={updateSubscriptionStatus === "pending"} + isLoading={ + updateSubscriptionStatus === "pending" || + pendingCheckoutRedirect === "PRO" || + (isLoadingPendingUpgrade && pendingUpgrade === Plan.PRO) + } currency={data.subscription?.currency} /> )} diff --git a/apps/builder/src/features/billing/components/CurrentSubscriptionSummary.tsx b/apps/builder/src/features/billing/components/CurrentSubscriptionSummary.tsx index 3913a689ee..a54c465a94 100644 --- a/apps/builder/src/features/billing/components/CurrentSubscriptionSummary.tsx +++ b/apps/builder/src/features/billing/components/CurrentSubscriptionSummary.tsx @@ -1,6 +1,5 @@ import { useQuery } from "@tanstack/react-query"; import { useTranslate } from "@tolgee/react"; -import { Plan } from "@typebot.io/prisma/enum"; import { Alert } from "@typebot.io/ui/components/Alert"; import { TriangleAlertIcon } from "@typebot.io/ui/icons/TriangleAlertIcon"; import type { Workspace } from "@typebot.io/workspaces/schemas"; @@ -23,9 +22,7 @@ export const CurrentSubscriptionSummary = ({ workspace }: Props) => { }), ); - const isSubscribed = - (workspace.plan === Plan.STARTER || workspace.plan === Plan.PRO) && - workspace.stripeId; + const hasStripeCustomer = workspace.stripeId; return (
@@ -51,7 +48,7 @@ export const CurrentSubscriptionSummary = ({ workspace }: Props) => { )} - {isSubscribed && ( + {hasStripeCustomer && ( void; -}; - -const vatCodeLabels = taxIdTypes.map((taxIdType) => ({ - label: ( - - {taxIdType.name} ({taxIdType.code}) {taxIdType.emoji} - - ), - value: taxIdType.code, -})); - -export const PreCheckoutDialog = ({ - selectedSubscription, - existingCompany, - existingEmail, - onClose, -}: PreCheckoutDialogProps) => { - const { t } = useTranslate(); - const vatValueInputRef = React.useRef(null); - const router = useRouter(); - const { mutate: createCheckoutSession, status: createCheckoutSessionStatus } = - useMutation( - orpc.billing.createCheckoutSession.mutationOptions({ - onSuccess: ({ checkoutUrl }) => { - router.push(checkoutUrl); - }, - }), - ); - - const [customer, setCustomer] = useState({ - company: existingCompany ?? "", - email: existingEmail ?? "", - vat: { - code: undefined as (typeof vatCodeLabels)[number]["value"] | undefined, - value: "", - }, - }); - const [vatValuePlaceholder, setVatValuePlaceholder] = useState(""); - - const updateCustomerCompany = (company: string) => { - setCustomer((customer) => ({ ...customer, company })); - }; - - const updateCustomerEmail = (email: string) => { - setCustomer((customer) => ({ ...customer, email })); - }; - - const updateVatCode = (vatCode?: (typeof vatCodeLabels)[number]["value"]) => { - setCustomer((customer) => ({ - ...customer, - vat: { - ...customer.vat, - code: vatCode, - }, - })); - const vatPlaceholder = taxIdTypes.find( - (taxIdType) => taxIdType.code === vatCode, - )?.placeholder; - if (vatPlaceholder) setVatValuePlaceholder(vatPlaceholder ?? ""); - vatValueInputRef.current?.focus(); - }; - - const updateVatValue = (value: string) => { - setCustomer((customer) => ({ - ...customer, - vat: { - ...customer.vat, - value, - }, - })); - }; - - const goToCheckout = (e: FormEvent) => { - e.preventDefault(); - if (!selectedSubscription) return; - const { email, company, vat } = customer; - const vatType = taxIdTypes.find( - (taxIdType) => taxIdType.code === vat.code, - )?.type; - createCheckoutSession({ - ...selectedSubscription, - email, - company, - returnUrl: window.location.href, - vat: - vatType && vat.value ? { type: vatType, value: vat.value } : undefined, - }); - }; - - return ( - - }> -
- - - {t("billing.preCheckoutModal.companyInput.label")} - - - - - - {t("billing.preCheckoutModal.emailInput.label")} - - - - - - {t("billing.preCheckoutModal.taxId.label")} - -
- - -
-
- - -
-
-
- ); -}; diff --git a/apps/builder/src/features/billing/components/UpgradeConfirmationDialog.tsx b/apps/builder/src/features/billing/components/UpgradeConfirmationDialog.tsx index ab4c1eae09..bba87c3974 100644 --- a/apps/builder/src/features/billing/components/UpgradeConfirmationDialog.tsx +++ b/apps/builder/src/features/billing/components/UpgradeConfirmationDialog.tsx @@ -1,25 +1,23 @@ -import { useQuery } from "@tanstack/react-query"; import { useTranslate } from "@tolgee/react"; import { formatPrice } from "@typebot.io/billing/helpers/formatPrice"; -import { isDefined } from "@typebot.io/lib/utils"; import { AlertDialog } from "@typebot.io/ui/components/AlertDialog"; import { Button } from "@typebot.io/ui/components/Button"; -import { LoaderCircleIcon } from "@typebot.io/ui/icons/LoaderCircleIcon"; import { useRef, useState } from "react"; -import { orpc } from "@/lib/queryClient"; type Props = { isOpen: boolean; - workspaceId: string; targetPlan: "STARTER" | "PRO" | undefined; + amountDue: number; + currency: "eur" | "usd"; onConfirm: () => Promise | unknown; onClose: () => void; }; export const UpgradeConfirmationDialog = ({ isOpen, - workspaceId, targetPlan, + amountDue, + currency, onConfirm, onClose, }: Props) => { @@ -27,16 +25,6 @@ export const UpgradeConfirmationDialog = ({ const [confirmLoading, setConfirmLoading] = useState(false); const cancelRef = useRef(null); - const { data: preview, isLoading: isLoadingPreview } = useQuery( - orpc.billing.getSubscriptionPreview.queryOptions({ - input: { - workspaceId, - plan: targetPlan!, - }, - enabled: isOpen && isDefined(targetPlan), - }), - ); - const onConfirmClick = async () => { setConfirmLoading(true); try { @@ -56,32 +44,25 @@ export const UpgradeConfirmationDialog = ({ {t("billing.upgradeModal.title", { plan: targetPlan })}
- {isLoadingPreview ? ( -
- -

{t("billing.upgradeModal.loading")}

-
- ) : preview ? ( -
-

- {t("billing.upgradeModal.description", { - plan: targetPlan, +

+

+ {t("billing.upgradeModal.description", { + plan: targetPlan, + })} +

+
+ {t("billing.upgradeModal.amountLabel")}: + + {formatPrice(amountDue / 100, { + currency, + maxFractionDigits: 2, })} -

-
- {t("billing.upgradeModal.amountLabel")}: - - {formatPrice(preview.amountDue / 100, { - currency: preview.currency, - maxFractionDigits: 2, - })} - -
-

- {t("billing.upgradeModal.prorationNote")} -

+
- ) : null} +

+ {t("billing.upgradeModal.prorationNote")} +

+
@@ -90,7 +71,7 @@ export const UpgradeConfirmationDialog = ({ diff --git a/apps/builder/src/features/dashboard/components/DashboardPage.tsx b/apps/builder/src/features/dashboard/components/DashboardPage.tsx index 9b0f86e15f..015908071d 100644 --- a/apps/builder/src/features/dashboard/components/DashboardPage.tsx +++ b/apps/builder/src/features/dashboard/components/DashboardPage.tsx @@ -7,10 +7,6 @@ import { LoaderCircleIcon } from "@typebot.io/ui/icons/LoaderCircleIcon"; import { useRouter } from "next/router"; import { useEffect, useRef, useState } from "react"; import { Seo } from "@/components/Seo"; -import { - PreCheckoutDialog, - type PreCheckoutDialogProps, -} from "@/features/billing/components/PreCheckoutDialog"; import { FolderContent } from "@/features/folders/components/FolderContent"; import { TypebotDndProvider } from "@/features/folders/TypebotDndProvider"; import { useUser } from "@/features/user/hooks/useUser"; @@ -25,9 +21,14 @@ export const DashboardPage = () => { const router = useRouter(); const { user } = useUser(); const { workspace } = useWorkspace(); - const [preCheckoutPlan, setPreCheckoutPlan] = - useState(); const isImportingTemplateRef = useRef(false); + const { mutate: createCheckoutSession } = useMutation( + orpc.billing.createCheckoutSession.mutationOptions({ + onSuccess: (data) => { + router.push(data.checkoutUrl); + }, + }), + ); const { mutate: createCustomCheckoutSession } = useMutation( orpc.billing.createCustomCheckoutSession.mutationOptions({ onSuccess: (data) => { @@ -65,11 +66,18 @@ export const DashboardPage = () => { returnUrl: `${window.location.origin}/typebots`, }); } - if (workspace && subscribePlan && user && workspace.plan === "FREE") { + if ( + workspace && + !workspace.stripeId && + subscribePlan && + user && + workspace.plan === "FREE" + ) { setIsLoading(true); - setPreCheckoutPlan({ - plan: subscribePlan as "PRO" | "STARTER", + createCheckoutSession({ workspaceId: workspace.id, + returnUrl: `${window.location.origin}/typebots`, + plan: subscribePlan as "PRO" | "STARTER", }); } }, [createCustomCheckoutSession, router.query, user, workspace]); @@ -107,14 +115,6 @@ export const DashboardPage = () => {
- {!workspace?.stripeId && ( - setPreCheckoutPlan(undefined)} - /> - )} {isLoading ? (
diff --git a/packages/billing/src/api/handleCheckoutCancelRedirect.ts b/packages/billing/src/api/handleCheckoutCancelRedirect.ts deleted file mode 100644 index 990eb31544..0000000000 --- a/packages/billing/src/api/handleCheckoutCancelRedirect.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ORPCError } from "@orpc/server"; -import { env } from "@typebot.io/env"; -import type { User } from "@typebot.io/user/schemas"; -import Stripe from "stripe"; -import { z } from "zod"; - -export const cancelCheckoutSessionInputSchema = z.object({ - customerId: z.string(), - returnUrl: z.string(), -}); - -export const handleCheckoutCancelRedirect = async ({ - input: { returnUrl, customerId }, - context: { user }, -}: { - input: z.infer; - context: { user: Pick }; -}) => { - if (!env.STRIPE_SECRET_KEY) - throw new ORPCError("INTERNAL_SERVER_ERROR", { - message: "Stripe environment variables are missing", - }); - const stripe = new Stripe(env.STRIPE_SECRET_KEY); - - const redirectResponse = { - headers: { - location: returnUrl, - }, - }; - - const customer = await stripe.customers.retrieve(customerId); - - if ( - customer.deleted || - customer.email !== user.email || - (customer.subscriptions?.data.length ?? 0) > 0 - ) - return redirectResponse; - - const deletedCustomer = await stripe.customers.del(customerId); - - if (!deletedCustomer.deleted) - throw new ORPCError("INTERNAL_SERVER_ERROR", { - message: "Failed to delete customer", - }); - - return redirectResponse; -}; diff --git a/packages/billing/src/api/handleCreateCheckoutSession.ts b/packages/billing/src/api/handleCreateCheckoutSession.ts index 98dcc3a755..94cf40f98b 100644 --- a/packages/billing/src/api/handleCreateCheckoutSession.ts +++ b/packages/billing/src/api/handleCreateCheckoutSession.ts @@ -9,17 +9,9 @@ import { z } from "zod"; import { createCheckoutSessionUrl } from "../helpers/createCheckoutSessionUrl"; export const createCheckoutSessionInputSchema = z.object({ - email: z.string(), - company: z.string(), workspaceId: z.string(), plan: z.enum([Plan.STARTER, Plan.PRO]), returnUrl: z.string(), - vat: z - .object({ - type: z.string(), - value: z.string(), - }) - .optional(), }); export const handleCreateCheckoutSession = async ({ @@ -29,7 +21,7 @@ export const handleCreateCheckoutSession = async ({ input: z.infer; context: { user: Pick }; }) => { - const { workspaceId, returnUrl, email, company, plan, vat } = input; + const { workspaceId, returnUrl, plan } = input; if (!env.STRIPE_SECRET_KEY) throw new ORPCError("INTERNAL_SERVER_ERROR", { @@ -63,30 +55,8 @@ export const handleCreateCheckoutSession = async ({ apiVersion: "2024-09-30.acacia", }); - await prisma.user.updateMany({ - where: { - id: user.id, - }, - data: { - company, - }, - }); - - const customer = await stripe.customers.create({ - email, - name: company, - metadata: { workspaceId }, - tax_exempt: - !vat || vat.value.startsWith("FR") || vat?.type !== "eu_vat" - ? undefined - : "exempt", - tax_id_data: vat - ? [vat as Stripe.CustomerCreateParams.TaxIdDatum] - : undefined, - }); - const checkoutUrl = await createCheckoutSessionUrl(stripe)({ - customerId: customer.id, + email: user.email, userId: user.id, workspaceId, plan, diff --git a/packages/billing/src/api/handleGetSubscriptionPreview.ts b/packages/billing/src/api/handleGetSubscriptionPreview.ts index cf150f8f80..f21e9de9ee 100644 --- a/packages/billing/src/api/handleGetSubscriptionPreview.ts +++ b/packages/billing/src/api/handleGetSubscriptionPreview.ts @@ -61,18 +61,12 @@ export const handleGetSubscriptionPreview = async ({ }); const subscription = data[0] as Stripe.Subscription | undefined; - if (!subscription) { - throw new ORPCError("NOT_FOUND", { - message: "No active subscription found", - }); - } - - const currentPlanItemId = subscription.items.data.find((item) => + const currentPlanItemId = subscription?.items.data.find((item) => [env.STRIPE_STARTER_PRICE_ID, env.STRIPE_PRO_PRICE_ID].includes( item.price.id, ), )?.id; - const currentUsageItemId = subscription.items.data.find( + const currentUsageItemId = subscription?.items.data.find( (item) => item.price.id === env.STRIPE_STARTER_CHATS_PRICE_ID || item.price.id === env.STRIPE_PRO_CHATS_PRICE_ID, @@ -98,7 +92,7 @@ export const handleGetSubscriptionPreview = async ({ const upcomingInvoice = await stripe.invoices.retrieveUpcoming({ customer: workspace.stripeId, - subscription: subscription.id, + subscription: subscription?.id, subscription_items: items, subscription_proration_behavior: "always_invoice", }); diff --git a/packages/billing/src/api/handleUpdateSubscription.ts b/packages/billing/src/api/handleUpdateSubscription.ts index 6231914669..e8f6c8446d 100644 --- a/packages/billing/src/api/handleUpdateSubscription.ts +++ b/packages/billing/src/api/handleUpdateSubscription.ts @@ -8,7 +8,6 @@ import { isAdminWriteWorkspaceForbidden } from "@typebot.io/workspaces/isAdminWr import { workspaceSchema } from "@typebot.io/workspaces/schemas"; import Stripe from "stripe"; import { z } from "zod"; -import { createCheckoutSessionUrl } from "../helpers/createCheckoutSessionUrl"; export const updateSubscriptionInputSchema = z.object({ workspaceId: z.string(), @@ -23,7 +22,7 @@ export const handleUpdateSubscription = async ({ input: z.infer; context: { user: Pick }; }) => { - const { workspaceId, plan, returnUrl } = input; + const { workspaceId, plan } = input; if (!env.STRIPE_SECRET_KEY) throw new ORPCError("INTERNAL_SERVER_ERROR", { @@ -93,26 +92,26 @@ export const handleUpdateSubscription = async ({ }, ]; - if (subscription) { - if (plan === "STARTER") { - const totalChatsUsed = await prisma.result.count({ - where: { - typebot: { workspaceId }, - hasStarted: true, - createdAt: { - gte: new Date(subscription.current_period_start * 1000), - }, + if (subscription && plan === "STARTER") { + const totalChatsUsed = await prisma.result.count({ + where: { + typebot: { workspaceId }, + hasStarted: true, + createdAt: { + gte: new Date(subscription.current_period_start * 1000), }, + }, + }); + if (totalChatsUsed >= 4000) { + throw new ORPCError("BAD_REQUEST", { + message: + "You have collected more than 4000 chats during this billing cycle. You can't downgrade to the Starter.", }); - if (totalChatsUsed >= 4000) { - throw new ORPCError("BAD_REQUEST", { - message: - "You have collected more than 4000 chats during this billing cycle. You can't downgrade to the Starter.", - }); - } } + } - try { + try { + if (subscription) await stripe.subscriptions.update(subscription.id, { items, proration_behavior: "always_invoice", @@ -121,29 +120,42 @@ export const handleUpdateSubscription = async ({ reason: "explicit update", }, }); - } catch { - return { - type: "error" as const, - title: "Payment required", - description: "Check your payment method and try again.", - }; + else { + const customer = await stripe.customers.retrieve(workspace.stripeId); + if (customer.deleted) + throw new ORPCError("INTERNAL_SERVER_ERROR", { + message: "Customer deleted", + }); + if (!customer.invoice_settings.default_payment_method) { + const lastPaymentMethod = await stripe.paymentMethods.list({ + customer: workspace.stripeId, + limit: 1, + }); + const lastPaymentMethodId = lastPaymentMethod.data.at(0)?.id; + if (!lastPaymentMethodId) + throw new ORPCError("INTERNAL_SERVER_ERROR", { + message: "Last payment method not found", + }); + await stripe.customers.update(workspace.stripeId, { + invoice_settings: { + default_payment_method: lastPaymentMethodId, + }, + }); + } + await stripe.subscriptions.create({ + customer: workspace.stripeId, + items, + proration_behavior: "always_invoice", + payment_behavior: "error_if_incomplete", + }); } - } else { - const checkoutUrl = await createCheckoutSessionUrl(stripe)({ - customerId: workspace.stripeId, - userId: user.id, - workspaceId, - plan, - returnUrl, - }); - - if (!checkoutUrl) - return { - type: "error" as const, - title: "Failed to create checkout session", - }; - - return { type: "checkoutUrl" as const, checkoutUrl }; + } catch (error) { + console.error(error); + return { + type: "error" as const, + title: "Payment required", + description: "Check your payment method and try again.", + }; } const updatedWorkspace = await prisma.workspace.update({ diff --git a/packages/billing/src/api/router.ts b/packages/billing/src/api/router.ts index ae57df45da..e5388fabdd 100644 --- a/packages/billing/src/api/router.ts +++ b/packages/billing/src/api/router.ts @@ -6,10 +6,6 @@ import { workspaceSchema } from "@typebot.io/workspaces/schemas"; import { z } from "zod"; import { invoiceSchema } from "../schemas/invoice"; import { subscriptionSchema } from "../schemas/subscription"; -import { - cancelCheckoutSessionInputSchema, - handleCheckoutCancelRedirect, -} from "./handleCheckoutCancelRedirect"; import { createCheckoutSessionInputSchema, handleCreateCheckoutSession, @@ -44,8 +40,6 @@ import { updateSubscriptionInputSchema, } from "./handleUpdateSubscription"; -export const cancelCheckoutPath = "/v1/billing/checkout/cancel" as const; - export const billingRouter = { webhook: publicProcedure .route({ @@ -150,13 +144,4 @@ export const billingRouter = { ]), ) .handler(handleUpdateSubscription), - cancelCheckoutSession: authenticatedProcedure - .route({ - successStatus: 307, - outputStructure: "detailed", - method: "GET", - path: cancelCheckoutPath, - }) - .input(cancelCheckoutSessionInputSchema) - .handler(handleCheckoutCancelRedirect), }; diff --git a/packages/billing/src/helpers/createCheckoutSessionUrl.ts b/packages/billing/src/helpers/createCheckoutSessionUrl.ts index 356bea8ec9..20094cfa97 100644 --- a/packages/billing/src/helpers/createCheckoutSessionUrl.ts +++ b/packages/billing/src/helpers/createCheckoutSessionUrl.ts @@ -1,9 +1,8 @@ import { env } from "@typebot.io/env"; import type Stripe from "stripe"; -import { cancelCheckoutPath } from "../api/router"; type Props = { - customerId: string; + email: string; workspaceId: string; plan: "STARTER" | "PRO"; returnUrl: string; @@ -11,24 +10,21 @@ type Props = { }; export const createCheckoutSessionUrl = - (stripe: Stripe) => - async ({ customerId, workspaceId, plan, returnUrl }: Props) => { - const returnUrlOrigin = new URL(returnUrl).origin; - const cancelUrl = `${returnUrlOrigin}/api${cancelCheckoutPath}?returnUrl=${returnUrl}&customerId=${customerId}`; + (stripe: Stripe) => async (input: Props) => { + const { workspaceId, plan, returnUrl, email } = input; const session = await stripe.checkout.sessions.create({ success_url: `${returnUrl}?stripe=${plan}&success=true`, - cancel_url: cancelUrl, + cancel_url: `${returnUrl}?stripe=cancel`, allow_promotion_codes: true, - customer: customerId, - customer_update: { - address: "auto", - name: "never", - }, + customer_email: email, mode: "subscription", metadata: { workspaceId, plan, }, + tax_id_collection: { + enabled: true, + }, billing_address_collection: "required", automatic_tax: { enabled: true }, line_items: [