diff --git a/apps/docs/self-hosting/configuration.mdx b/apps/docs/self-hosting/configuration.mdx index 6d0a2a620d..1e0847c060 100644 --- a/apps/docs/self-hosting/configuration.mdx +++ b/apps/docs/self-hosting/configuration.mdx @@ -25,7 +25,7 @@ Parameters marked with \* are required. | DEFAULT_WORKSPACE_PLAN | FREE | Default workspace plan on user creation or when a user creates a new workspace. Possible values are `FREE`, `STARTER`, `PRO`, `LIFETIME`, `UNLIMITED`. The default plan for admin user is `UNLIMITED` | | DISABLE_SIGNUP | false | Disable new user sign ups. Invited users are still able to sign up. | | NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID | | Typebot ID used for the onboarding. Onboarding page is skipped if not provided. | -| DEBUG | false | If enabled, the server will print valuable logs to debug config issues. | +| TYPEBOT_DEBUG | false | If enabled, the server will print valuable logs to debug config issues. | | NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE | | Limits the size of each file that can be uploaded in the bots (i.e. Set `10` to limit the file upload to 10MB) | | CHAT_API_TIMEOUT | | The chat API execution timeout (in ms). It limits the chat API exection time. Useful to avoid getting stuck into an unwanted infinite loop. Note that it does not apply to known long-running blocks like OpenAI or else. | diff --git a/apps/viewer/src/pages/[[...publicId]].tsx b/apps/viewer/src/pages/[[...publicId]].tsx index dca1e749dd..c9008c62dd 100644 --- a/apps/viewer/src/pages/[[...publicId]].tsx +++ b/apps/viewer/src/pages/[[...publicId]].tsx @@ -21,7 +21,7 @@ import { } from "@/components/TypebotPageV3"; const log = (message: string) => { - if (!env.DEBUG) return; + if (!env.TYPEBOT_DEBUG) return; console.log(`[DEBUG] ${message}`); }; diff --git a/packages/billing/src/api/handleCheckoutCancelRedirect.ts b/packages/billing/src/api/handleCheckoutCancelRedirect.ts new file mode 100644 index 0000000000..990eb31544 --- /dev/null +++ b/packages/billing/src/api/handleCheckoutCancelRedirect.ts @@ -0,0 +1,48 @@ +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/router.ts b/packages/billing/src/api/router.ts index e5388fabdd..ae57df45da 100644 --- a/packages/billing/src/api/router.ts +++ b/packages/billing/src/api/router.ts @@ -6,6 +6,10 @@ 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, @@ -40,6 +44,8 @@ import { updateSubscriptionInputSchema, } from "./handleUpdateSubscription"; +export const cancelCheckoutPath = "/v1/billing/checkout/cancel" as const; + export const billingRouter = { webhook: publicProcedure .route({ @@ -144,4 +150,13 @@ 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 999aafbc7c..356bea8ec9 100644 --- a/packages/billing/src/helpers/createCheckoutSessionUrl.ts +++ b/packages/billing/src/helpers/createCheckoutSessionUrl.ts @@ -1,5 +1,6 @@ import { env } from "@typebot.io/env"; import type Stripe from "stripe"; +import { cancelCheckoutPath } from "../api/router"; type Props = { customerId: string; @@ -12,9 +13,11 @@ 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}`; const session = await stripe.checkout.sessions.create({ success_url: `${returnUrl}?stripe=${plan}&success=true`, - cancel_url: `${returnUrl}?stripe=cancel`, + cancel_url: cancelUrl, allow_promotion_codes: true, customer: customerId, customer_update: { diff --git a/packages/env/src/index.ts b/packages/env/src/index.ts index 732a06e962..491ab221fe 100644 --- a/packages/env/src/index.ts +++ b/packages/env/src/index.ts @@ -84,7 +84,7 @@ const baseEnv = { ["FREE", "STARTER", "PRO", "LIFETIME", "UNLIMITED"].includes(str), ) .default("FREE"), - DEBUG: boolean.optional().default(false), + TYPEBOT_DEBUG: boolean.optional().default(false), CHAT_API_TIMEOUT: z.coerce.number().optional(), RADAR_HIGH_RISK_KEYWORDS: z .string()