diff --git a/apps/builder/src/features/billing/api/createCheckoutSession.ts b/apps/builder/src/features/billing/api/createCheckoutSession.ts
index c06d83bb7..ce5a21eb3 100644
--- a/apps/builder/src/features/billing/api/createCheckoutSession.ts
+++ b/apps/builder/src/features/billing/api/createCheckoutSession.ts
@@ -1,10 +1,11 @@
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
-import { Plan, WorkspaceRole } from '@typebot.io/prisma'
+import { Plan } from '@typebot.io/prisma'
import Stripe from 'stripe'
import { z } from 'zod'
import { parseSubscriptionItems } from '../helpers/parseSubscriptionItems'
+import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
export const createCheckoutSession = authenticatedProcedure
.meta({
@@ -64,14 +65,30 @@ export const createCheckoutSession = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
- members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
+ },
+ select: {
+ stripeId: true,
+ members: {
+ select: {
+ userId: true,
+ role: true,
+ },
+ },
},
})
- if (!workspace)
+
+ if (!workspace || isAdminWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
})
+
+ if (workspace.stripeId)
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'Customer already exists, use updateSubscription endpoint.',
+ })
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
diff --git a/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts b/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts
index 0e45626b8..89d042b6f 100644
--- a/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts
+++ b/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts
@@ -1,9 +1,10 @@
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
-import { Plan, WorkspaceRole } from '@typebot.io/prisma'
+import { Plan } from '@typebot.io/prisma'
import Stripe from 'stripe'
import { z } from 'zod'
+import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
export const createCustomCheckoutSession = authenticatedProcedure
.meta({
@@ -38,15 +39,23 @@ export const createCustomCheckoutSession = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
- members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
- include: {
+ select: {
+ stripeId: true,
claimableCustomPlan: true,
+ name: true,
+ members: {
+ select: {
+ userId: true,
+ role: true,
+ },
+ },
},
})
if (
!workspace?.claimableCustomPlan ||
- workspace.claimableCustomPlan.claimedAt
+ workspace.claimableCustomPlan.claimedAt ||
+ isAdminWriteWorkspaceForbidden(workspace, user)
)
throw new TRPCError({
code: 'NOT_FOUND',
diff --git a/apps/builder/src/features/billing/api/getBillingPortalUrl.ts b/apps/builder/src/features/billing/api/getBillingPortalUrl.ts
index bcdef0941..16f08ef66 100644
--- a/apps/builder/src/features/billing/api/getBillingPortalUrl.ts
+++ b/apps/builder/src/features/billing/api/getBillingPortalUrl.ts
@@ -1,9 +1,9 @@
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
-import { WorkspaceRole } from '@typebot.io/prisma'
import Stripe from 'stripe'
import { z } from 'zod'
+import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
export const getBillingPortalUrl = authenticatedProcedure
.meta({
@@ -34,13 +34,18 @@ export const getBillingPortalUrl = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
- members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
select: {
stripeId: true,
+ members: {
+ select: {
+ userId: true,
+ role: true,
+ },
+ },
},
})
- if (!workspace?.stripeId)
+ if (!workspace?.stripeId || isAdminWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
diff --git a/apps/builder/src/features/billing/api/getSubscription.ts b/apps/builder/src/features/billing/api/getSubscription.ts
index ed27ef7db..0760937bc 100644
--- a/apps/builder/src/features/billing/api/getSubscription.ts
+++ b/apps/builder/src/features/billing/api/getSubscription.ts
@@ -1,11 +1,11 @@
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
-import { WorkspaceRole } from '@typebot.io/prisma'
import Stripe from 'stripe'
import { z } from 'zod'
import { subscriptionSchema } from '@typebot.io/schemas/features/billing/subscription'
import { priceIds } from '@typebot.io/lib/pricing'
+import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
export const getSubscription = authenticatedProcedure
.meta({
@@ -36,9 +36,21 @@ export const getSubscription = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
- members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
+ },
+ select: {
+ stripeId: true,
+ members: {
+ select: {
+ userId: true,
+ },
+ },
},
})
+ if (!workspace || isReadWorkspaceFobidden(workspace, user))
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'Workspace not found',
+ })
if (!workspace?.stripeId)
return {
subscription: null,
diff --git a/apps/builder/src/features/billing/api/listInvoices.ts b/apps/builder/src/features/billing/api/listInvoices.ts
index 8f3b4830e..33ec979c3 100644
--- a/apps/builder/src/features/billing/api/listInvoices.ts
+++ b/apps/builder/src/features/billing/api/listInvoices.ts
@@ -1,11 +1,11 @@
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
-import { WorkspaceRole } from '@typebot.io/prisma'
import Stripe from 'stripe'
import { isDefined } from '@typebot.io/lib'
import { z } from 'zod'
import { invoiceSchema } from '@typebot.io/schemas/features/billing/invoice'
+import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
export const listInvoices = authenticatedProcedure
.meta({
@@ -36,10 +36,18 @@ export const listInvoices = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
- members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
+ },
+ select: {
+ stripeId: true,
+ members: {
+ select: {
+ userId: true,
+ role: true,
+ },
+ },
},
})
- if (!workspace?.stripeId)
+ if (!workspace?.stripeId || isAdminWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
diff --git a/apps/builder/src/features/billing/api/updateSubscription.ts b/apps/builder/src/features/billing/api/updateSubscription.ts
index c4c06151a..86f97c7eb 100644
--- a/apps/builder/src/features/billing/api/updateSubscription.ts
+++ b/apps/builder/src/features/billing/api/updateSubscription.ts
@@ -2,7 +2,7 @@ import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEven
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
-import { Plan, WorkspaceRole } from '@typebot.io/prisma'
+import { Plan } from '@typebot.io/prisma'
import { workspaceSchema } from '@typebot.io/schemas'
import Stripe from 'stripe'
import { isDefined } from '@typebot.io/lib'
@@ -14,6 +14,7 @@ import {
} from '@typebot.io/lib/pricing'
import { chatPriceIds, storagePriceIds } from './getSubscription'
import { createCheckoutSessionUrl } from './createCheckoutSession'
+import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
export const updateSubscription = authenticatedProcedure
.meta({
@@ -63,10 +64,21 @@ export const updateSubscription = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
- members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
+ },
+ select: {
+ stripeId: true,
+ members: {
+ select: {
+ userId: true,
+ role: true,
+ },
+ },
},
})
- if (!workspace?.stripeId)
+ if (
+ !workspace?.stripeId ||
+ isAdminWriteWorkspaceForbidden(workspace, user)
+ )
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
diff --git a/apps/builder/src/features/billing/billing.spec.ts b/apps/builder/src/features/billing/billing.spec.ts
index f5b5d1424..7dbd5a6f4 100644
--- a/apps/builder/src/features/billing/billing.spec.ts
+++ b/apps/builder/src/features/billing/billing.spec.ts
@@ -197,7 +197,9 @@ test('plan changes should work', async ({ page }) => {
page.waitForNavigation(),
page.click('text="Billing portal"'),
])
- await expect(page.getByText('$247.00 per month')).toBeVisible()
+ await expect(page.getByText('$247.00 per month')).toBeVisible({
+ timeout: 10000,
+ })
await expect(page.getByText('(×25000)')).toBeVisible()
await expect(page.getByText('(×15)')).toBeVisible()
await expect(page.locator('text="Add payment method"')).toBeVisible()
diff --git a/apps/builder/src/features/billing/components/BillingSettingsLayout.tsx b/apps/builder/src/features/billing/components/BillingSettingsLayout.tsx
index a8d07fc35..c6ada09aa 100644
--- a/apps/builder/src/features/billing/components/BillingSettingsLayout.tsx
+++ b/apps/builder/src/features/billing/components/BillingSettingsLayout.tsx
@@ -8,7 +8,7 @@ import { UsageProgressBars } from './UsageProgressBars'
import { CurrentSubscriptionSummary } from './CurrentSubscriptionSummary'
export const BillingSettingsLayout = () => {
- const { workspace, refreshWorkspace } = useWorkspace()
+ const { workspace } = useWorkspace()
if (!workspace) return null
return (
@@ -20,10 +20,7 @@ export const BillingSettingsLayout = () => {
workspace.plan !== Plan.LIFETIME &&
workspace.plan !== Plan.UNLIMITED &&
workspace.plan !== Plan.OFFERED && (
-
+
)}
diff --git a/apps/builder/src/features/billing/components/ChangePlanForm.tsx b/apps/builder/src/features/billing/components/ChangePlanForm.tsx
index 93db69a2d..036904d7f 100644
--- a/apps/builder/src/features/billing/components/ChangePlanForm.tsx
+++ b/apps/builder/src/features/billing/components/ChangePlanForm.tsx
@@ -16,10 +16,9 @@ import { StripeClimateLogo } from './StripeClimateLogo'
type Props = {
workspace: Workspace
- onUpgradeSuccess: () => void
}
-export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
+export const ChangePlanForm = ({ workspace }: Props) => {
const scopedT = useScopedI18n('billing')
const { user } = useUser()
@@ -28,7 +27,9 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
useState()
const [isYearly, setIsYearly] = useState(true)
- const { data } = trpc.billing.getSubscription.useQuery(
+ const trpcContext = trpc.useContext()
+
+ const { data, refetch } = trpc.billing.getSubscription.useQuery(
{
workspaceId: workspace.id,
},
@@ -52,7 +53,8 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
window.location.href = checkoutUrl
return
}
- onUpgradeSuccess()
+ refetch()
+ trpcContext.workspace.getWorkspace.invalidate()
showToast({
status: 'success',
description: scopedT('updateSuccessToast.description', {
diff --git a/apps/builder/src/features/billing/components/ChangePlanModal.tsx b/apps/builder/src/features/billing/components/ChangePlanModal.tsx
index db536a571..70a36d9c7 100644
--- a/apps/builder/src/features/billing/components/ChangePlanModal.tsx
+++ b/apps/builder/src/features/billing/components/ChangePlanModal.tsx
@@ -25,7 +25,7 @@ export const ChangePlanModal = ({
type,
}: ChangePlanModalProps) => {
const t = useI18n()
- const { workspace, refreshWorkspace } = useWorkspace()
+ const { workspace } = useWorkspace()
return (
@@ -36,12 +36,7 @@ export const ChangePlanModal = ({
{t('billing.upgradeLimitLabel', { type: type })}
)}
- {workspace && (
-
- )}
+ {workspace && }
diff --git a/apps/builder/src/features/workspace/WorkspaceProvider.tsx b/apps/builder/src/features/workspace/WorkspaceProvider.tsx
index a2da3006c..805ef4f4c 100644
--- a/apps/builder/src/features/workspace/WorkspaceProvider.tsx
+++ b/apps/builder/src/features/workspace/WorkspaceProvider.tsx
@@ -25,7 +25,6 @@ const workspaceContext = createContext<{
createWorkspace: (name?: string) => Promise
updateWorkspace: (updates: { icon?: string; name?: string }) => void
deleteCurrentWorkspace: () => Promise
- refreshWorkspace: () => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
@@ -166,11 +165,6 @@ export const WorkspaceProvider = ({
await deleteWorkspaceMutation.mutateAsync({ workspaceId })
}
- const refreshWorkspace = () => {
- trpcContext.workspace.getWorkspace.invalidate()
- trpcContext.billing.getSubscription.invalidate()
- }
-
return (
{children}
diff --git a/apps/builder/src/features/workspace/helpers/isAdminWriteWorkspaceForbidden.ts b/apps/builder/src/features/workspace/helpers/isAdminWriteWorkspaceForbidden.ts
index 108219f5d..2522009d1 100644
--- a/apps/builder/src/features/workspace/helpers/isAdminWriteWorkspaceForbidden.ts
+++ b/apps/builder/src/features/workspace/helpers/isAdminWriteWorkspaceForbidden.ts
@@ -2,7 +2,7 @@ import { MemberInWorkspace, User } from '@typebot.io/prisma'
export const isAdminWriteWorkspaceForbidden = (
workspace: {
- members: MemberInWorkspace[]
+ members: Pick[]
},
user: Pick
) => {
diff --git a/apps/builder/src/pages/api/stripe/webhook.ts b/apps/builder/src/pages/api/stripe/webhook.ts
index 9f0ac4d56..0b58211c8 100644
--- a/apps/builder/src/pages/api/stripe/webhook.ts
+++ b/apps/builder/src/pages/api/stripe/webhook.ts
@@ -133,6 +133,19 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription
+ const { data } = await stripe.subscriptions.list({
+ customer: subscription.customer as string,
+ limit: 1,
+ status: 'active',
+ })
+ const existingSubscription = data[0] as
+ | Stripe.Subscription
+ | undefined
+ if (existingSubscription)
+ return res.send({
+ message:
+ 'An active subscription still exists. Skipping downgrade.',
+ })
const workspace = await prisma.workspace.update({
where: {
stripeId: subscription.customer as string,