🛂 Add new yearly plans and graduated pricing
BREAKING CHANGE: Stripe environment variables have changed. New ones are required. Check out the new Stripe configuration in the docs. Closes #457
This commit is contained in:
@@ -1,70 +0,0 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { Plan, WorkspaceRole } from '@typebot.io/prisma'
|
||||
import Stripe from 'stripe'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const cancelSubscription = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/billing/subscription',
|
||||
protect: true,
|
||||
summary: 'Cancel current subscription',
|
||||
tags: ['Billing'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
message: z.literal('success'),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { workspaceId }, ctx: { user } }) => {
|
||||
if (
|
||||
!process.env.STRIPE_SECRET_KEY ||
|
||||
!process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID ||
|
||||
!process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
|
||||
)
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Stripe environment variables are missing',
|
||||
})
|
||||
const workspace = await prisma.workspace.findFirst({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
|
||||
},
|
||||
})
|
||||
if (!workspace?.stripeId)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Workspace not found',
|
||||
})
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2022-11-15',
|
||||
})
|
||||
const currentSubscriptionId = (
|
||||
await stripe.subscriptions.list({
|
||||
customer: workspace.stripeId,
|
||||
})
|
||||
).data.shift()?.id
|
||||
if (currentSubscriptionId)
|
||||
await stripe.subscriptions.del(currentSubscriptionId)
|
||||
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspace.id },
|
||||
data: {
|
||||
plan: Plan.FREE,
|
||||
additionalChatsIndex: 0,
|
||||
additionalStorageIndex: 0,
|
||||
},
|
||||
})
|
||||
|
||||
return { message: 'success' }
|
||||
})
|
||||
@@ -33,6 +33,7 @@ export const createCheckoutSession = authenticatedProcedure
|
||||
value: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
isYearly: z.boolean(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
@@ -52,14 +53,11 @@ export const createCheckoutSession = authenticatedProcedure
|
||||
returnUrl,
|
||||
additionalChats,
|
||||
additionalStorage,
|
||||
isYearly,
|
||||
},
|
||||
ctx: { user },
|
||||
}) => {
|
||||
if (
|
||||
!process.env.STRIPE_SECRET_KEY ||
|
||||
!process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID ||
|
||||
!process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
|
||||
)
|
||||
if (!process.env.STRIPE_SECRET_KEY)
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Stripe environment variables are missing',
|
||||
@@ -120,7 +118,8 @@ export const createCheckoutSession = authenticatedProcedure
|
||||
line_items: parseSubscriptionItems(
|
||||
plan,
|
||||
additionalChats,
|
||||
additionalStorage
|
||||
additionalStorage,
|
||||
isYearly
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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'
|
||||
|
||||
export const getSubscription = authenticatedProcedure
|
||||
.meta({
|
||||
@@ -23,15 +24,11 @@ export const getSubscription = authenticatedProcedure
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
subscription: subscriptionSchema,
|
||||
subscription: subscriptionSchema.or(z.null()),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
||||
if (
|
||||
!process.env.STRIPE_SECRET_KEY ||
|
||||
!process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID ||
|
||||
!process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
|
||||
)
|
||||
if (!process.env.STRIPE_SECRET_KEY)
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Stripe environment variables are missing',
|
||||
@@ -43,10 +40,9 @@ export const getSubscription = authenticatedProcedure
|
||||
},
|
||||
})
|
||||
if (!workspace?.stripeId)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Workspace not found',
|
||||
})
|
||||
return {
|
||||
subscription: null,
|
||||
}
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2022-11-15',
|
||||
})
|
||||
@@ -58,24 +54,34 @@ export const getSubscription = authenticatedProcedure
|
||||
const subscription = subscriptions?.data.shift()
|
||||
|
||||
if (!subscription)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Subscription not found',
|
||||
})
|
||||
return {
|
||||
subscription: null,
|
||||
}
|
||||
|
||||
return {
|
||||
subscription: {
|
||||
additionalChatsIndex:
|
||||
subscription?.items.data.find(
|
||||
(item) =>
|
||||
item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID
|
||||
)?.quantity ?? 0,
|
||||
additionalStorageIndex:
|
||||
subscription.items.data.find(
|
||||
(item) =>
|
||||
item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
|
||||
)?.quantity ?? 0,
|
||||
isYearly: subscription.items.data.some((item) => {
|
||||
return (
|
||||
priceIds.STARTER.chats.yearly === item.price.id ||
|
||||
priceIds.STARTER.storage.yearly === item.price.id ||
|
||||
priceIds.PRO.chats.yearly === item.price.id ||
|
||||
priceIds.PRO.storage.yearly === item.price.id
|
||||
)
|
||||
}),
|
||||
currency: subscription.currency as 'usd' | 'eur',
|
||||
cancelDate: subscription.cancel_at
|
||||
? new Date(subscription.cancel_at * 1000)
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export const chatPriceIds = [priceIds.STARTER.chats.monthly]
|
||||
.concat(priceIds.STARTER.chats.yearly)
|
||||
.concat(priceIds.PRO.chats.monthly)
|
||||
.concat(priceIds.PRO.chats.yearly)
|
||||
|
||||
export const storagePriceIds = [priceIds.STARTER.storage.monthly]
|
||||
.concat(priceIds.STARTER.storage.yearly)
|
||||
.concat(priceIds.PRO.storage.monthly)
|
||||
.concat(priceIds.PRO.storage.yearly)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { router } from '@/helpers/server/trpc'
|
||||
import { cancelSubscription } from './cancelSubscription'
|
||||
import { createCheckoutSession } from './createCheckoutSession'
|
||||
import { getBillingPortalUrl } from './getBillingPortalUrl'
|
||||
import { getSubscription } from './getSubscription'
|
||||
@@ -10,7 +9,6 @@ import { updateSubscription } from './updateSubscription'
|
||||
export const billingRouter = router({
|
||||
getBillingPortalUrl,
|
||||
listInvoices,
|
||||
cancelSubscription,
|
||||
createCheckoutSession,
|
||||
updateSubscription,
|
||||
getSubscription,
|
||||
|
||||
@@ -7,6 +7,12 @@ import { workspaceSchema } from '@typebot.io/schemas'
|
||||
import Stripe from 'stripe'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
getChatsLimit,
|
||||
getStorageLimit,
|
||||
priceIds,
|
||||
} from '@typebot.io/lib/pricing'
|
||||
import { chatPriceIds, storagePriceIds } from './getSubscription'
|
||||
|
||||
export const updateSubscription = authenticatedProcedure
|
||||
.meta({
|
||||
@@ -25,6 +31,7 @@ export const updateSubscription = authenticatedProcedure
|
||||
additionalChats: z.number(),
|
||||
additionalStorage: z.number(),
|
||||
currency: z.enum(['usd', 'eur']),
|
||||
isYearly: z.boolean(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
@@ -40,14 +47,11 @@ export const updateSubscription = authenticatedProcedure
|
||||
additionalChats,
|
||||
additionalStorage,
|
||||
currency,
|
||||
isYearly,
|
||||
},
|
||||
ctx: { user },
|
||||
}) => {
|
||||
if (
|
||||
!process.env.STRIPE_SECRET_KEY ||
|
||||
!process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID ||
|
||||
!process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
|
||||
)
|
||||
if (!process.env.STRIPE_SECRET_KEY)
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Stripe environment variables are missing',
|
||||
@@ -70,42 +74,48 @@ export const updateSubscription = authenticatedProcedure
|
||||
customer: workspace.stripeId,
|
||||
})
|
||||
const subscription = data[0] as Stripe.Subscription | undefined
|
||||
const currentStarterPlanItemId = subscription?.items.data.find(
|
||||
(item) => item.price.id === process.env.STRIPE_STARTER_PRICE_ID
|
||||
)?.id
|
||||
const currentProPlanItemId = subscription?.items.data.find(
|
||||
(item) => item.price.id === process.env.STRIPE_PRO_PRICE_ID
|
||||
const currentPlanItemId = subscription?.items.data.find((item) =>
|
||||
[
|
||||
process.env.STRIPE_STARTER_PRODUCT_ID,
|
||||
process.env.STRIPE_PRO_PRODUCT_ID,
|
||||
].includes(item.price.product.toString())
|
||||
)?.id
|
||||
const currentAdditionalChatsItemId = subscription?.items.data.find(
|
||||
(item) => item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID
|
||||
(item) => chatPriceIds.includes(item.price.id)
|
||||
)?.id
|
||||
const currentAdditionalStorageItemId = subscription?.items.data.find(
|
||||
(item) =>
|
||||
item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
|
||||
(item) => storagePriceIds.includes(item.price.id)
|
||||
)?.id
|
||||
const frequency = isYearly ? 'yearly' : 'monthly'
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: currentStarterPlanItemId ?? currentProPlanItemId,
|
||||
price:
|
||||
plan === Plan.STARTER
|
||||
? process.env.STRIPE_STARTER_PRICE_ID
|
||||
: process.env.STRIPE_PRO_PRICE_ID,
|
||||
id: currentPlanItemId,
|
||||
price: priceIds[plan].base[frequency],
|
||||
quantity: 1,
|
||||
},
|
||||
additionalChats === 0 && !currentAdditionalChatsItemId
|
||||
? undefined
|
||||
: {
|
||||
id: currentAdditionalChatsItemId,
|
||||
price: process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID,
|
||||
quantity: additionalChats,
|
||||
price: priceIds[plan].chats[frequency],
|
||||
quantity: getChatsLimit({
|
||||
plan,
|
||||
additionalChatsIndex: additionalChats,
|
||||
customChatsLimit: null,
|
||||
}),
|
||||
deleted: subscription ? additionalChats === 0 : undefined,
|
||||
},
|
||||
additionalStorage === 0 && !currentAdditionalStorageItemId
|
||||
? undefined
|
||||
: {
|
||||
id: currentAdditionalStorageItemId,
|
||||
price: process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID,
|
||||
quantity: additionalStorage,
|
||||
price: priceIds[plan].storage[frequency],
|
||||
quantity: getStorageLimit({
|
||||
plan,
|
||||
additionalStorageIndex: additionalStorage,
|
||||
customStorageLimit: null,
|
||||
}),
|
||||
deleted: subscription ? additionalStorage === 0 : undefined,
|
||||
},
|
||||
].filter(isDefined)
|
||||
@@ -126,6 +136,9 @@ export const updateSubscription = authenticatedProcedure
|
||||
items,
|
||||
currency,
|
||||
default_payment_method: paymentMethods[0].id,
|
||||
automatic_tax: {
|
||||
enabled: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
addSubscriptionToWorkspace,
|
||||
cancelSubscription,
|
||||
createClaimableCustomPlan,
|
||||
} from '@/test/utils/databaseActions'
|
||||
import test, { expect } from '@playwright/test'
|
||||
@@ -75,7 +76,7 @@ test('should display valid usage', async ({ page }) => {
|
||||
await page.click('text="Free workspace"')
|
||||
await page.click('text=Settings & Members')
|
||||
await page.click('text=Billing & Usage')
|
||||
await expect(page.locator('text="/ 300"')).toBeVisible()
|
||||
await expect(page.locator('text="/ 200"')).toBeVisible()
|
||||
await expect(page.locator('text="Storage"')).toBeHidden()
|
||||
await page.getByText('Members', { exact: true }).click()
|
||||
await expect(
|
||||
@@ -132,6 +133,7 @@ test('plan changes should work', async ({ page }) => {
|
||||
await page.click('button >> text="3,500"')
|
||||
await page.click('button >> text="2"')
|
||||
await page.click('button >> text="4"')
|
||||
await page.locator('label span').first().click()
|
||||
await expect(page.locator('text="$73"')).toBeVisible()
|
||||
await page.click('button >> text=Upgrade >> nth=0')
|
||||
await page.getByLabel('Company name').fill('Company LLC')
|
||||
@@ -141,11 +143,11 @@ test('plan changes should work', async ({ page }) => {
|
||||
await expect(page.locator('text=$73.00 >> nth=0')).toBeVisible()
|
||||
await expect(page.locator('text=$30.00 >> nth=0')).toBeVisible()
|
||||
await expect(page.locator('text=$4.00 >> nth=0')).toBeVisible()
|
||||
await addSubscriptionToWorkspace(
|
||||
const stripeId = await addSubscriptionToWorkspace(
|
||||
planChangeWorkspaceId,
|
||||
[
|
||||
{
|
||||
price: process.env.STRIPE_STARTER_PRICE_ID,
|
||||
price: process.env.STRIPE_STARTER_MONTHLY_PRICE_ID,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
@@ -158,8 +160,8 @@ test('plan changes should work', async ({ page }) => {
|
||||
await page.click('text=Billing & Usage')
|
||||
await expect(page.locator('text="/ 2,000"')).toBeVisible()
|
||||
await expect(page.locator('text="/ 2 GB"')).toBeVisible()
|
||||
await expect(page.locator('button >> text="2,000"')).toBeVisible()
|
||||
await expect(page.locator('button >> text="2"')).toBeVisible()
|
||||
await expect(page.getByText('/ 2,000')).toBeVisible()
|
||||
await expect(page.getByText('/ 2 GB')).toBeVisible()
|
||||
await page.click('button >> text="2,000"')
|
||||
await page.click('button >> text="3,500"')
|
||||
await page.click('button >> text="2"')
|
||||
@@ -176,15 +178,15 @@ test('plan changes should work', async ({ page }) => {
|
||||
await expect(page.locator('text="$73"')).toBeVisible()
|
||||
await expect(page.locator('text="/ 3,500"')).toBeVisible()
|
||||
await expect(page.locator('text="/ 4 GB"')).toBeVisible()
|
||||
await expect(page.locator('button >> text="3,500"')).toBeVisible()
|
||||
await expect(page.locator('button >> text="4"')).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: '3,500' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: '4' })).toBeVisible()
|
||||
|
||||
// Upgrade to PRO
|
||||
await page.click('button >> text="10,000"')
|
||||
await page.click('button >> text="14,000"')
|
||||
await page.click('button >> text="25,000"')
|
||||
await page.click('button >> text="10"')
|
||||
await page.click('button >> text="12"')
|
||||
await expect(page.locator('text="$133"')).toBeVisible()
|
||||
await page.click('button >> text="15"')
|
||||
await expect(page.locator('text="$247"')).toBeVisible()
|
||||
await page.click('button >> text=Upgrade')
|
||||
await expect(
|
||||
page.locator('text="Workspace PRO plan successfully updated 🎉" >> nth=0')
|
||||
@@ -195,25 +197,20 @@ 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('(×25000)')).toBeVisible()
|
||||
await expect(page.getByText('(×15)')).toBeVisible()
|
||||
await expect(page.locator('text="Add payment method"')).toBeVisible()
|
||||
await cancelSubscription(stripeId)
|
||||
|
||||
// Cancel subscription
|
||||
await page.goto('/typebots')
|
||||
await page.click('text=Settings & Members')
|
||||
await page.click('text=Billing & Usage')
|
||||
await expect(page.locator('[data-testid="current-subscription"]')).toHaveText(
|
||||
'Current workspace subscription: ProCancel my subscription'
|
||||
)
|
||||
await page.click('button >> text="Cancel my subscription"')
|
||||
await expect(page.locator('[data-testid="current-subscription"]')).toHaveText(
|
||||
'Current workspace subscription: Free'
|
||||
)
|
||||
|
||||
// Upgrade again to PRO
|
||||
await page.getByRole('button', { name: 'Upgrade' }).nth(1).click()
|
||||
await expect(
|
||||
page.locator('text="Workspace PRO plan successfully updated 🎉" >> nth=0')
|
||||
).toBeVisible({ timeout: 20 * 1000 })
|
||||
page.getByTestId('current-subscription').getByTestId('pro-plan-tag')
|
||||
).toBeVisible()
|
||||
await expect(page.getByText('Will be cancelled on')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should display invoices', async ({ page }) => {
|
||||
@@ -228,7 +225,7 @@ test('should display invoices', async ({ page }) => {
|
||||
await page.click('text=Settings & Members')
|
||||
await page.click('text=Billing & Usage')
|
||||
await expect(page.locator('text="Invoices"')).toBeVisible()
|
||||
await expect(page.locator('tr')).toHaveCount(3)
|
||||
await expect(page.locator('tr')).toHaveCount(2)
|
||||
await expect(page.locator('text="$39.00"')).toBeVisible()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { HStack, Stack, Text } from '@chakra-ui/react'
|
||||
import { Stack } from '@chakra-ui/react'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { Plan } from '@typebot.io/prisma'
|
||||
import React from 'react'
|
||||
import { InvoicesList } from './InvoicesList'
|
||||
import { StripeClimateLogo } from './StripeClimateLogo'
|
||||
import { TextLink } from '@/components/TextLink'
|
||||
import { ChangePlanForm } from './ChangePlanForm'
|
||||
import { UsageProgressBars } from './UsageProgressBars'
|
||||
import { CurrentSubscriptionSummary } from './CurrentSubscriptionSummary'
|
||||
import { useScopedI18n } from '@/locales'
|
||||
|
||||
export const BillingSettingsLayout = () => {
|
||||
const scopedT = useScopedI18n('billing')
|
||||
const { workspace, refreshWorkspace } = useWorkspace()
|
||||
|
||||
if (!workspace) return null
|
||||
@@ -19,19 +15,7 @@ export const BillingSettingsLayout = () => {
|
||||
<Stack spacing="10" w="full">
|
||||
<UsageProgressBars workspace={workspace} />
|
||||
<Stack spacing="4">
|
||||
<CurrentSubscriptionSummary
|
||||
workspace={workspace}
|
||||
onCancelSuccess={refreshWorkspace}
|
||||
/>
|
||||
<HStack maxW="500px">
|
||||
<StripeClimateLogo />
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{scopedT('contribution.preLink')}{' '}
|
||||
<TextLink href="https://climate.stripe.com/5VCRAq" isExternal>
|
||||
{scopedT('contribution.link')}
|
||||
</TextLink>
|
||||
</Text>
|
||||
</HStack>
|
||||
<CurrentSubscriptionSummary workspace={workspace} />
|
||||
{workspace.plan !== Plan.CUSTOM &&
|
||||
workspace.plan !== Plan.LIFETIME &&
|
||||
workspace.plan !== Plan.UNLIMITED &&
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Stack, HStack, Text } from '@chakra-ui/react'
|
||||
import { Stack, HStack, Text, Switch, Tag } from '@chakra-ui/react'
|
||||
import { Plan } from '@typebot.io/prisma'
|
||||
import { TextLink } from '@/components/TextLink'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
@@ -12,22 +12,33 @@ import { useUser } from '@/features/account/hooks/useUser'
|
||||
import { StarterPlanPricingCard } from './StarterPlanPricingCard'
|
||||
import { ProPlanPricingCard } from './ProPlanPricingCard'
|
||||
import { useScopedI18n } from '@/locales'
|
||||
import { StripeClimateLogo } from './StripeClimateLogo'
|
||||
|
||||
type Props = {
|
||||
workspace: Pick<Workspace, 'id' | 'stripeId' | 'plan'>
|
||||
workspace: Workspace
|
||||
onUpgradeSuccess: () => void
|
||||
}
|
||||
|
||||
export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
|
||||
const scopedT = useScopedI18n('billing')
|
||||
|
||||
const { user } = useUser()
|
||||
const { showToast } = useToast()
|
||||
const [preCheckoutPlan, setPreCheckoutPlan] =
|
||||
useState<PreCheckoutModalProps['selectedSubscription']>()
|
||||
const [isYearly, setIsYearly] = useState(true)
|
||||
|
||||
const { data } = trpc.billing.getSubscription.useQuery({
|
||||
workspaceId: workspace.id,
|
||||
})
|
||||
const { data } = trpc.billing.getSubscription.useQuery(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
{
|
||||
onSuccess: ({ subscription }) => {
|
||||
if (isYearly === false) return
|
||||
setIsYearly(subscription?.isYearly ?? true)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const { mutate: updateSubscription, isLoading: isUpdatingSubscription } =
|
||||
trpc.billing.updateSubscription.useMutation({
|
||||
@@ -67,8 +78,9 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
|
||||
additionalChats: selectedChatsLimitIndex,
|
||||
additionalStorage: selectedStorageLimitIndex,
|
||||
currency:
|
||||
data?.subscription.currency ??
|
||||
data?.subscription?.currency ??
|
||||
(guessIfUserIsEuropean() ? 'eur' : 'usd'),
|
||||
isYearly,
|
||||
} as const
|
||||
if (workspace.stripeId) {
|
||||
updateSubscription(newSubscription)
|
||||
@@ -77,8 +89,19 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (data?.subscription?.cancelDate) return null
|
||||
|
||||
return (
|
||||
<Stack spacing={6}>
|
||||
<HStack maxW="500px">
|
||||
<StripeClimateLogo />
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{scopedT('contribution.preLink')}{' '}
|
||||
<TextLink href="https://climate.stripe.com/5VCRAq" isExternal>
|
||||
{scopedT('contribution.link')}
|
||||
</TextLink>
|
||||
</Text>
|
||||
</HStack>
|
||||
{!workspace.stripeId && (
|
||||
<ParentModalProvider>
|
||||
<PreCheckoutModal
|
||||
@@ -89,41 +112,45 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
|
||||
/>
|
||||
</ParentModalProvider>
|
||||
)}
|
||||
<HStack alignItems="stretch" spacing="4" w="full">
|
||||
<StarterPlanPricingCard
|
||||
initialChatsLimitIndex={
|
||||
workspace?.plan === Plan.STARTER
|
||||
? data?.subscription.additionalChatsIndex
|
||||
: 0
|
||||
}
|
||||
initialStorageLimitIndex={
|
||||
workspace?.plan === Plan.STARTER
|
||||
? data?.subscription.additionalStorageIndex
|
||||
: 0
|
||||
}
|
||||
onPayClick={(props) =>
|
||||
handlePayClick({ ...props, plan: Plan.STARTER })
|
||||
}
|
||||
isLoading={isUpdatingSubscription}
|
||||
currency={data?.subscription.currency}
|
||||
/>
|
||||
{data && (
|
||||
<Stack align="flex-end" spacing={6}>
|
||||
<HStack>
|
||||
<Text>Monthly</Text>
|
||||
<Switch
|
||||
isChecked={isYearly}
|
||||
onChange={() => setIsYearly(!isYearly)}
|
||||
/>
|
||||
<HStack>
|
||||
<Text>Yearly</Text>
|
||||
<Tag colorScheme="blue">16% off</Tag>
|
||||
</HStack>
|
||||
</HStack>
|
||||
<HStack alignItems="stretch" spacing="4" w="full">
|
||||
<StarterPlanPricingCard
|
||||
workspace={workspace}
|
||||
currentSubscription={{ isYearly: data.subscription?.isYearly }}
|
||||
onPayClick={(props) =>
|
||||
handlePayClick({ ...props, plan: Plan.STARTER })
|
||||
}
|
||||
isYearly={isYearly}
|
||||
isLoading={isUpdatingSubscription}
|
||||
currency={data.subscription?.currency}
|
||||
/>
|
||||
|
||||
<ProPlanPricingCard
|
||||
workspace={workspace}
|
||||
currentSubscription={{ isYearly: data.subscription?.isYearly }}
|
||||
onPayClick={(props) =>
|
||||
handlePayClick({ ...props, plan: Plan.PRO })
|
||||
}
|
||||
isYearly={isYearly}
|
||||
isLoading={isUpdatingSubscription}
|
||||
currency={data.subscription?.currency}
|
||||
/>
|
||||
</HStack>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<ProPlanPricingCard
|
||||
initialChatsLimitIndex={
|
||||
workspace?.plan === Plan.PRO
|
||||
? data?.subscription.additionalChatsIndex
|
||||
: 0
|
||||
}
|
||||
initialStorageLimitIndex={
|
||||
workspace?.plan === Plan.PRO
|
||||
? data?.subscription.additionalStorageIndex
|
||||
: 0
|
||||
}
|
||||
onPayClick={(props) => handlePayClick({ ...props, plan: Plan.PRO })}
|
||||
isLoading={isUpdatingSubscription}
|
||||
currency={data?.subscription.currency}
|
||||
/>
|
||||
</HStack>
|
||||
<Text color="gray.500">
|
||||
{scopedT('customLimit.preLink')}{' '}
|
||||
<TextLink href={'https://typebot.io/enterprise-lead-form'} isExternal>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Text, HStack, Link, Spinner, Stack, Heading } from '@chakra-ui/react'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { Text, HStack, Stack, Heading } from '@chakra-ui/react'
|
||||
import { Plan } from '@typebot.io/prisma'
|
||||
import React from 'react'
|
||||
import { PlanTag } from './PlanTag'
|
||||
@@ -10,25 +9,14 @@ import { useScopedI18n } from '@/locales'
|
||||
|
||||
type Props = {
|
||||
workspace: Pick<Workspace, 'id' | 'plan' | 'stripeId'>
|
||||
onCancelSuccess: () => void
|
||||
}
|
||||
|
||||
export const CurrentSubscriptionSummary = ({
|
||||
workspace,
|
||||
onCancelSuccess,
|
||||
}: Props) => {
|
||||
export const CurrentSubscriptionSummary = ({ workspace }: Props) => {
|
||||
const scopedT = useScopedI18n('billing.currentSubscription')
|
||||
const { showToast } = useToast()
|
||||
|
||||
const { mutate: cancelSubscription, isLoading: isCancelling } =
|
||||
trpc.billing.cancelSubscription.useMutation({
|
||||
onError: (error) => {
|
||||
showToast({
|
||||
description: error.message,
|
||||
})
|
||||
},
|
||||
onSuccess: onCancelSuccess,
|
||||
})
|
||||
const { data } = trpc.billing.getSubscription.useQuery({
|
||||
workspaceId: workspace.id,
|
||||
})
|
||||
|
||||
const isSubscribed =
|
||||
(workspace.plan === Plan.STARTER || workspace.plan === Plan.PRO) &&
|
||||
@@ -39,36 +27,15 @@ export const CurrentSubscriptionSummary = ({
|
||||
<Heading fontSize="3xl">{scopedT('heading')}</Heading>
|
||||
<HStack data-testid="current-subscription">
|
||||
<Text>{scopedT('subheading')} </Text>
|
||||
{isCancelling ? (
|
||||
<Spinner color="gray.500" size="xs" />
|
||||
) : (
|
||||
<>
|
||||
<PlanTag plan={workspace.plan} />
|
||||
{isSubscribed && (
|
||||
<Link
|
||||
as="button"
|
||||
color="gray.500"
|
||||
textDecor="underline"
|
||||
fontSize="sm"
|
||||
onClick={() =>
|
||||
cancelSubscription({ workspaceId: workspace.id })
|
||||
}
|
||||
>
|
||||
{scopedT('cancelLink')}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
<PlanTag plan={workspace.plan} />
|
||||
{data?.subscription?.cancelDate && (
|
||||
<Text fontSize="sm">
|
||||
(Will be cancelled on {data.subscription.cancelDate.toDateString()})
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{isSubscribed && !isCancelling && (
|
||||
<>
|
||||
<Stack spacing="4">
|
||||
<Text fontSize="sm">{scopedT('billingPortalDescription')}</Text>
|
||||
<BillingPortalButton workspaceId={workspace.id} />
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
{isSubscribed && <BillingPortalButton workspaceId={workspace.id} />}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export type PreCheckoutModalProps = {
|
||||
additionalChats: number
|
||||
additionalStorage: number
|
||||
currency: 'eur' | 'usd'
|
||||
isYearly: boolean
|
||||
}
|
||||
| undefined
|
||||
existingCompany?: string
|
||||
|
||||
@@ -15,10 +15,9 @@ import {
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon } from '@/components/icons'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { Plan } from '@typebot.io/prisma'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { parseNumberWithCommas } from '@typebot.io/lib'
|
||||
import { isDefined, parseNumberWithCommas } from '@typebot.io/lib'
|
||||
import {
|
||||
chatsLimit,
|
||||
computePrice,
|
||||
@@ -30,12 +29,23 @@ import {
|
||||
import { FeaturesList } from './FeaturesList'
|
||||
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
||||
import { useI18n, useScopedI18n } from '@/locales'
|
||||
import { Workspace } from '@typebot.io/schemas'
|
||||
|
||||
type Props = {
|
||||
initialChatsLimitIndex?: number
|
||||
initialStorageLimitIndex?: number
|
||||
workspace: Pick<
|
||||
Workspace,
|
||||
| 'additionalChatsIndex'
|
||||
| 'additionalStorageIndex'
|
||||
| 'plan'
|
||||
| 'customChatsLimit'
|
||||
| 'customStorageLimit'
|
||||
>
|
||||
currentSubscription: {
|
||||
isYearly?: boolean
|
||||
}
|
||||
currency?: 'usd' | 'eur'
|
||||
isLoading: boolean
|
||||
isYearly: boolean
|
||||
onPayClick: (props: {
|
||||
selectedChatsLimitIndex: number
|
||||
selectedStorageLimitIndex: number
|
||||
@@ -43,15 +53,15 @@ type Props = {
|
||||
}
|
||||
|
||||
export const ProPlanPricingCard = ({
|
||||
initialChatsLimitIndex,
|
||||
initialStorageLimitIndex,
|
||||
workspace,
|
||||
currentSubscription,
|
||||
currency,
|
||||
isLoading,
|
||||
isYearly,
|
||||
onPayClick,
|
||||
}: Props) => {
|
||||
const t = useI18n()
|
||||
const scopedT = useScopedI18n('billing.pricingCard')
|
||||
const { workspace } = useWorkspace()
|
||||
const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] =
|
||||
useState<number>()
|
||||
const [selectedStorageLimitIndex, setSelectedStorageLimitIndex] =
|
||||
@@ -59,20 +69,23 @@ export const ProPlanPricingCard = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedChatsLimitIndex === undefined &&
|
||||
initialChatsLimitIndex !== undefined
|
||||
isDefined(selectedChatsLimitIndex) ||
|
||||
isDefined(selectedStorageLimitIndex)
|
||||
)
|
||||
setSelectedChatsLimitIndex(initialChatsLimitIndex)
|
||||
if (
|
||||
selectedStorageLimitIndex === undefined &&
|
||||
initialStorageLimitIndex !== undefined
|
||||
)
|
||||
setSelectedStorageLimitIndex(initialStorageLimitIndex)
|
||||
return
|
||||
if (workspace.plan !== Plan.PRO) {
|
||||
setSelectedChatsLimitIndex(0)
|
||||
setSelectedStorageLimitIndex(0)
|
||||
return
|
||||
}
|
||||
setSelectedChatsLimitIndex(workspace.additionalChatsIndex ?? 0)
|
||||
setSelectedStorageLimitIndex(workspace.additionalStorageIndex ?? 0)
|
||||
}, [
|
||||
initialChatsLimitIndex,
|
||||
initialStorageLimitIndex,
|
||||
selectedChatsLimitIndex,
|
||||
selectedStorageLimitIndex,
|
||||
workspace.additionalChatsIndex,
|
||||
workspace.additionalStorageIndex,
|
||||
workspace?.plan,
|
||||
])
|
||||
|
||||
const workspaceChatsLimit = workspace ? getChatsLimit(workspace) : undefined
|
||||
@@ -81,14 +94,11 @@ export const ProPlanPricingCard = ({
|
||||
: undefined
|
||||
|
||||
const isCurrentPlan =
|
||||
chatsLimit[Plan.PRO].totalIncluded +
|
||||
chatsLimit[Plan.PRO].increaseStep.amount *
|
||||
(selectedChatsLimitIndex ?? 0) ===
|
||||
workspaceChatsLimit &&
|
||||
storageLimit[Plan.PRO].totalIncluded +
|
||||
storageLimit[Plan.PRO].increaseStep.amount *
|
||||
(selectedStorageLimitIndex ?? 0) ===
|
||||
workspaceStorageLimit
|
||||
chatsLimit[Plan.PRO].graduatedPrice[selectedChatsLimitIndex ?? 0]
|
||||
.totalIncluded === workspaceChatsLimit &&
|
||||
storageLimit[Plan.PRO].graduatedPrice[selectedStorageLimitIndex ?? 0]
|
||||
.totalIncluded === workspaceStorageLimit &&
|
||||
isYearly === currentSubscription?.isYearly
|
||||
|
||||
const getButtonLabel = () => {
|
||||
if (
|
||||
@@ -100,8 +110,8 @@ export const ProPlanPricingCard = ({
|
||||
if (isCurrentPlan) return scopedT('upgradeButton.current')
|
||||
|
||||
if (
|
||||
selectedChatsLimitIndex !== initialChatsLimitIndex ||
|
||||
selectedStorageLimitIndex !== initialStorageLimitIndex
|
||||
selectedChatsLimitIndex !== workspace.additionalChatsIndex ||
|
||||
selectedStorageLimitIndex !== workspace.additionalStorageIndex
|
||||
)
|
||||
return t('update')
|
||||
}
|
||||
@@ -149,7 +159,11 @@ export const ProPlanPricingCard = ({
|
||||
<Stack spacing="4" mt={2}>
|
||||
<Heading fontSize="2xl">
|
||||
{scopedT('heading', {
|
||||
plan: <chakra.span color="blue.400">Pro</chakra.span>,
|
||||
plan: (
|
||||
<chakra.span color={useColorModeValue('blue.400', 'blue.300')}>
|
||||
Pro
|
||||
</chakra.span>
|
||||
),
|
||||
})}
|
||||
</Heading>
|
||||
<Text>{scopedT('pro.description')}</Text>
|
||||
@@ -160,7 +174,8 @@ export const ProPlanPricingCard = ({
|
||||
computePrice(
|
||||
Plan.PRO,
|
||||
selectedChatsLimitIndex ?? 0,
|
||||
selectedStorageLimitIndex ?? 0
|
||||
selectedStorageLimitIndex ?? 0,
|
||||
isYearly ? 'yearly' : 'monthly'
|
||||
) ?? NaN,
|
||||
currency
|
||||
)}
|
||||
@@ -201,50 +216,21 @@ export const ProPlanPricingCard = ({
|
||||
>
|
||||
{selectedChatsLimitIndex !== undefined
|
||||
? parseNumberWithCommas(
|
||||
chatsLimit.PRO.totalIncluded +
|
||||
chatsLimit.PRO.increaseStep.amount *
|
||||
selectedChatsLimitIndex
|
||||
chatsLimit.PRO.graduatedPrice[
|
||||
selectedChatsLimitIndex
|
||||
].totalIncluded
|
||||
)
|
||||
: undefined}
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{selectedChatsLimitIndex !== 0 && (
|
||||
<MenuItem onClick={() => setSelectedChatsLimitIndex(0)}>
|
||||
{parseNumberWithCommas(chatsLimit.PRO.totalIncluded)}
|
||||
{chatsLimit.PRO.graduatedPrice.map((price, index) => (
|
||||
<MenuItem
|
||||
key={index}
|
||||
onClick={() => setSelectedChatsLimitIndex(index)}
|
||||
>
|
||||
{parseNumberWithCommas(price.totalIncluded)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedChatsLimitIndex !== 1 && (
|
||||
<MenuItem onClick={() => setSelectedChatsLimitIndex(1)}>
|
||||
{parseNumberWithCommas(
|
||||
chatsLimit.PRO.totalIncluded +
|
||||
chatsLimit.PRO.increaseStep.amount
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedChatsLimitIndex !== 2 && (
|
||||
<MenuItem onClick={() => setSelectedChatsLimitIndex(2)}>
|
||||
{parseNumberWithCommas(
|
||||
chatsLimit.PRO.totalIncluded +
|
||||
chatsLimit.PRO.increaseStep.amount * 2
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedChatsLimitIndex !== 3 && (
|
||||
<MenuItem onClick={() => setSelectedChatsLimitIndex(3)}>
|
||||
{parseNumberWithCommas(
|
||||
chatsLimit.PRO.totalIncluded +
|
||||
chatsLimit.PRO.increaseStep.amount * 3
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedChatsLimitIndex !== 4 && (
|
||||
<MenuItem onClick={() => setSelectedChatsLimitIndex(4)}>
|
||||
{parseNumberWithCommas(
|
||||
chatsLimit.PRO.totalIncluded +
|
||||
chatsLimit.PRO.increaseStep.amount * 4
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>{' '}
|
||||
{scopedT('chatsPerMonth')}
|
||||
@@ -262,62 +248,21 @@ export const ProPlanPricingCard = ({
|
||||
>
|
||||
{selectedStorageLimitIndex !== undefined
|
||||
? parseNumberWithCommas(
|
||||
storageLimit.PRO.totalIncluded +
|
||||
storageLimit.PRO.increaseStep.amount *
|
||||
selectedStorageLimitIndex
|
||||
storageLimit.PRO.graduatedPrice[
|
||||
selectedStorageLimitIndex
|
||||
].totalIncluded
|
||||
)
|
||||
: undefined}
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{selectedStorageLimitIndex !== 0 && (
|
||||
{storageLimit.PRO.graduatedPrice.map((price, index) => (
|
||||
<MenuItem
|
||||
onClick={() => setSelectedStorageLimitIndex(0)}
|
||||
key={index}
|
||||
onClick={() => setSelectedStorageLimitIndex(index)}
|
||||
>
|
||||
{parseNumberWithCommas(
|
||||
storageLimit.PRO.totalIncluded
|
||||
)}
|
||||
{parseNumberWithCommas(price.totalIncluded)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedStorageLimitIndex !== 1 && (
|
||||
<MenuItem
|
||||
onClick={() => setSelectedStorageLimitIndex(1)}
|
||||
>
|
||||
{parseNumberWithCommas(
|
||||
storageLimit.PRO.totalIncluded +
|
||||
storageLimit.PRO.increaseStep.amount
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedStorageLimitIndex !== 2 && (
|
||||
<MenuItem
|
||||
onClick={() => setSelectedStorageLimitIndex(2)}
|
||||
>
|
||||
{parseNumberWithCommas(
|
||||
storageLimit.PRO.totalIncluded +
|
||||
storageLimit.PRO.increaseStep.amount * 2
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedStorageLimitIndex !== 3 && (
|
||||
<MenuItem
|
||||
onClick={() => setSelectedStorageLimitIndex(3)}
|
||||
>
|
||||
{parseNumberWithCommas(
|
||||
storageLimit.PRO.totalIncluded +
|
||||
storageLimit.PRO.increaseStep.amount * 3
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedStorageLimitIndex !== 4 && (
|
||||
<MenuItem
|
||||
onClick={() => setSelectedStorageLimitIndex(4)}
|
||||
>
|
||||
{parseNumberWithCommas(
|
||||
storageLimit.PRO.totalIncluded +
|
||||
storageLimit.PRO.increaseStep.amount * 4
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>{' '}
|
||||
{scopedT('storageLimit')}
|
||||
|
||||
@@ -11,10 +11,9 @@ import {
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon } from '@/components/icons'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { Plan } from '@typebot.io/prisma'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { parseNumberWithCommas } from '@typebot.io/lib'
|
||||
import { isDefined, parseNumberWithCommas } from '@typebot.io/lib'
|
||||
import {
|
||||
chatsLimit,
|
||||
computePrice,
|
||||
@@ -26,12 +25,23 @@ import {
|
||||
import { FeaturesList } from './FeaturesList'
|
||||
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
||||
import { useI18n, useScopedI18n } from '@/locales'
|
||||
import { Workspace } from '@typebot.io/schemas'
|
||||
|
||||
type Props = {
|
||||
initialChatsLimitIndex?: number
|
||||
initialStorageLimitIndex?: number
|
||||
workspace: Pick<
|
||||
Workspace,
|
||||
| 'additionalChatsIndex'
|
||||
| 'additionalStorageIndex'
|
||||
| 'plan'
|
||||
| 'customChatsLimit'
|
||||
| 'customStorageLimit'
|
||||
>
|
||||
currentSubscription: {
|
||||
isYearly?: boolean
|
||||
}
|
||||
currency?: 'eur' | 'usd'
|
||||
isLoading?: boolean
|
||||
isYearly: boolean
|
||||
onPayClick: (props: {
|
||||
selectedChatsLimitIndex: number
|
||||
selectedStorageLimitIndex: number
|
||||
@@ -39,15 +49,15 @@ type Props = {
|
||||
}
|
||||
|
||||
export const StarterPlanPricingCard = ({
|
||||
initialChatsLimitIndex,
|
||||
initialStorageLimitIndex,
|
||||
workspace,
|
||||
currentSubscription,
|
||||
isLoading,
|
||||
currency,
|
||||
isYearly,
|
||||
onPayClick,
|
||||
}: Props) => {
|
||||
const t = useI18n()
|
||||
const scopedT = useScopedI18n('billing.pricingCard')
|
||||
const { workspace } = useWorkspace()
|
||||
const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] =
|
||||
useState<number>()
|
||||
const [selectedStorageLimitIndex, setSelectedStorageLimitIndex] =
|
||||
@@ -55,20 +65,23 @@ export const StarterPlanPricingCard = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedChatsLimitIndex === undefined &&
|
||||
initialChatsLimitIndex !== undefined
|
||||
isDefined(selectedChatsLimitIndex) ||
|
||||
isDefined(selectedStorageLimitIndex)
|
||||
)
|
||||
setSelectedChatsLimitIndex(initialChatsLimitIndex)
|
||||
if (
|
||||
selectedStorageLimitIndex === undefined &&
|
||||
initialStorageLimitIndex !== undefined
|
||||
)
|
||||
setSelectedStorageLimitIndex(initialStorageLimitIndex)
|
||||
return
|
||||
if (workspace.plan !== Plan.STARTER) {
|
||||
setSelectedChatsLimitIndex(0)
|
||||
setSelectedStorageLimitIndex(0)
|
||||
return
|
||||
}
|
||||
setSelectedChatsLimitIndex(workspace.additionalChatsIndex ?? 0)
|
||||
setSelectedStorageLimitIndex(workspace.additionalStorageIndex ?? 0)
|
||||
}, [
|
||||
initialChatsLimitIndex,
|
||||
initialStorageLimitIndex,
|
||||
selectedChatsLimitIndex,
|
||||
selectedStorageLimitIndex,
|
||||
workspace.additionalChatsIndex,
|
||||
workspace.additionalStorageIndex,
|
||||
workspace?.plan,
|
||||
])
|
||||
|
||||
const workspaceChatsLimit = workspace ? getChatsLimit(workspace) : undefined
|
||||
@@ -77,14 +90,11 @@ export const StarterPlanPricingCard = ({
|
||||
: undefined
|
||||
|
||||
const isCurrentPlan =
|
||||
chatsLimit[Plan.STARTER].totalIncluded +
|
||||
chatsLimit[Plan.STARTER].increaseStep.amount *
|
||||
(selectedChatsLimitIndex ?? 0) ===
|
||||
workspaceChatsLimit &&
|
||||
storageLimit[Plan.STARTER].totalIncluded +
|
||||
storageLimit[Plan.STARTER].increaseStep.amount *
|
||||
(selectedStorageLimitIndex ?? 0) ===
|
||||
workspaceStorageLimit
|
||||
chatsLimit[Plan.STARTER].graduatedPrice[selectedChatsLimitIndex ?? 0]
|
||||
.totalIncluded === workspaceChatsLimit &&
|
||||
storageLimit[Plan.STARTER].graduatedPrice[selectedStorageLimitIndex ?? 0]
|
||||
.totalIncluded === workspaceStorageLimit &&
|
||||
isYearly === currentSubscription?.isYearly
|
||||
|
||||
const getButtonLabel = () => {
|
||||
if (
|
||||
@@ -97,8 +107,9 @@ export const StarterPlanPricingCard = ({
|
||||
if (isCurrentPlan) return scopedT('upgradeButton.current')
|
||||
|
||||
if (
|
||||
selectedChatsLimitIndex !== initialChatsLimitIndex ||
|
||||
selectedStorageLimitIndex !== initialStorageLimitIndex
|
||||
selectedChatsLimitIndex !== workspace.additionalChatsIndex ||
|
||||
selectedStorageLimitIndex !== workspace.additionalStorageIndex ||
|
||||
isYearly !== currentSubscription?.isYearly
|
||||
)
|
||||
return t('update')
|
||||
}
|
||||
@@ -131,7 +142,8 @@ export const StarterPlanPricingCard = ({
|
||||
computePrice(
|
||||
Plan.STARTER,
|
||||
selectedChatsLimitIndex ?? 0,
|
||||
selectedStorageLimitIndex ?? 0
|
||||
selectedStorageLimitIndex ?? 0,
|
||||
isYearly ? 'yearly' : 'monthly'
|
||||
) ?? NaN,
|
||||
currency
|
||||
)}
|
||||
@@ -151,52 +163,21 @@ export const StarterPlanPricingCard = ({
|
||||
>
|
||||
{selectedChatsLimitIndex !== undefined
|
||||
? parseNumberWithCommas(
|
||||
chatsLimit.STARTER.totalIncluded +
|
||||
chatsLimit.STARTER.increaseStep.amount *
|
||||
selectedChatsLimitIndex
|
||||
chatsLimit.STARTER.graduatedPrice[
|
||||
selectedChatsLimitIndex
|
||||
].totalIncluded
|
||||
)
|
||||
: undefined}
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{selectedChatsLimitIndex !== 0 && (
|
||||
<MenuItem onClick={() => setSelectedChatsLimitIndex(0)}>
|
||||
{parseNumberWithCommas(
|
||||
chatsLimit.STARTER.totalIncluded
|
||||
)}
|
||||
{chatsLimit.STARTER.graduatedPrice.map((price, index) => (
|
||||
<MenuItem
|
||||
key={index}
|
||||
onClick={() => setSelectedChatsLimitIndex(index)}
|
||||
>
|
||||
{parseNumberWithCommas(price.totalIncluded)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedChatsLimitIndex !== 1 && (
|
||||
<MenuItem onClick={() => setSelectedChatsLimitIndex(1)}>
|
||||
{parseNumberWithCommas(
|
||||
chatsLimit.STARTER.totalIncluded +
|
||||
chatsLimit.STARTER.increaseStep.amount
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedChatsLimitIndex !== 2 && (
|
||||
<MenuItem onClick={() => setSelectedChatsLimitIndex(2)}>
|
||||
{parseNumberWithCommas(
|
||||
chatsLimit.STARTER.totalIncluded +
|
||||
chatsLimit.STARTER.increaseStep.amount * 2
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedChatsLimitIndex !== 3 && (
|
||||
<MenuItem onClick={() => setSelectedChatsLimitIndex(3)}>
|
||||
{parseNumberWithCommas(
|
||||
chatsLimit.STARTER.totalIncluded +
|
||||
chatsLimit.STARTER.increaseStep.amount * 3
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedChatsLimitIndex !== 4 && (
|
||||
<MenuItem onClick={() => setSelectedChatsLimitIndex(4)}>
|
||||
{parseNumberWithCommas(
|
||||
chatsLimit.STARTER.totalIncluded +
|
||||
chatsLimit.STARTER.increaseStep.amount * 4
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>{' '}
|
||||
{scopedT('chatsPerMonth')}
|
||||
@@ -214,52 +195,21 @@ export const StarterPlanPricingCard = ({
|
||||
>
|
||||
{selectedStorageLimitIndex !== undefined
|
||||
? parseNumberWithCommas(
|
||||
storageLimit.STARTER.totalIncluded +
|
||||
storageLimit.STARTER.increaseStep.amount *
|
||||
selectedStorageLimitIndex
|
||||
storageLimit.STARTER.graduatedPrice[
|
||||
selectedStorageLimitIndex
|
||||
].totalIncluded
|
||||
)
|
||||
: undefined}
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{selectedStorageLimitIndex !== 0 && (
|
||||
<MenuItem onClick={() => setSelectedStorageLimitIndex(0)}>
|
||||
{parseNumberWithCommas(
|
||||
storageLimit.STARTER.totalIncluded
|
||||
)}
|
||||
{storageLimit.STARTER.graduatedPrice.map((price, index) => (
|
||||
<MenuItem
|
||||
key={index}
|
||||
onClick={() => setSelectedStorageLimitIndex(index)}
|
||||
>
|
||||
{parseNumberWithCommas(price.totalIncluded)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedStorageLimitIndex !== 1 && (
|
||||
<MenuItem onClick={() => setSelectedStorageLimitIndex(1)}>
|
||||
{parseNumberWithCommas(
|
||||
storageLimit.STARTER.totalIncluded +
|
||||
storageLimit.STARTER.increaseStep.amount
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedStorageLimitIndex !== 2 && (
|
||||
<MenuItem onClick={() => setSelectedStorageLimitIndex(2)}>
|
||||
{parseNumberWithCommas(
|
||||
storageLimit.STARTER.totalIncluded +
|
||||
storageLimit.STARTER.increaseStep.amount * 2
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedStorageLimitIndex !== 3 && (
|
||||
<MenuItem onClick={() => setSelectedStorageLimitIndex(3)}>
|
||||
{parseNumberWithCommas(
|
||||
storageLimit.STARTER.totalIncluded +
|
||||
storageLimit.STARTER.increaseStep.amount * 3
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedStorageLimitIndex !== 4 && (
|
||||
<MenuItem onClick={() => setSelectedStorageLimitIndex(4)}>
|
||||
{parseNumberWithCommas(
|
||||
storageLimit.STARTER.totalIncluded +
|
||||
storageLimit.STARTER.increaseStep.amount * 4
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>{' '}
|
||||
{scopedT('storageLimit')}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { Plan } from '@typebot.io/prisma'
|
||||
import {
|
||||
getChatsLimit,
|
||||
getStorageLimit,
|
||||
priceIds,
|
||||
} from '@typebot.io/lib/pricing'
|
||||
|
||||
export const parseSubscriptionItems = (
|
||||
plan: Plan,
|
||||
plan: 'STARTER' | 'PRO',
|
||||
additionalChats: number,
|
||||
additionalStorage: number
|
||||
) =>
|
||||
[
|
||||
additionalStorage: number,
|
||||
isYearly: boolean
|
||||
) => {
|
||||
const frequency = isYearly ? 'yearly' : 'monthly'
|
||||
return [
|
||||
{
|
||||
price:
|
||||
plan === Plan.STARTER
|
||||
? process.env.STRIPE_STARTER_PRICE_ID
|
||||
: process.env.STRIPE_PRO_PRICE_ID,
|
||||
price: priceIds[plan].base[frequency],
|
||||
quantity: 1,
|
||||
},
|
||||
]
|
||||
@@ -18,8 +21,12 @@ export const parseSubscriptionItems = (
|
||||
additionalChats > 0
|
||||
? [
|
||||
{
|
||||
price: process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID,
|
||||
quantity: additionalChats,
|
||||
price: priceIds[plan].chats[frequency],
|
||||
quantity: getChatsLimit({
|
||||
plan,
|
||||
additionalChatsIndex: additionalChats,
|
||||
customChatsLimit: null,
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []
|
||||
@@ -28,9 +35,14 @@ export const parseSubscriptionItems = (
|
||||
additionalStorage > 0
|
||||
? [
|
||||
{
|
||||
price: process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID,
|
||||
quantity: additionalStorage,
|
||||
price: priceIds[plan].storage[frequency],
|
||||
quantity: getStorageLimit({
|
||||
plan,
|
||||
additionalStorageIndex: additionalStorage,
|
||||
customStorageLimit: null,
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,10 +26,11 @@ export const DashboardPage = () => {
|
||||
useState<PreCheckoutModalProps['selectedSubscription']>()
|
||||
|
||||
useEffect(() => {
|
||||
const { subscribePlan, chats, storage } = query as {
|
||||
const { subscribePlan, chats, storage, isYearly } = query as {
|
||||
subscribePlan: Plan | undefined
|
||||
chats: string | undefined
|
||||
storage: string | undefined
|
||||
isYearly: string | undefined
|
||||
}
|
||||
if (workspace && subscribePlan && user && workspace.plan === 'FREE') {
|
||||
setIsLoading(true)
|
||||
@@ -39,6 +40,7 @@ export const DashboardPage = () => {
|
||||
additionalChats: chats ? parseInt(chats) : 0,
|
||||
additionalStorage: storage ? parseInt(storage) : 0,
|
||||
currency: guessIfUserIsEuropean() ? 'eur' : 'usd',
|
||||
isYearly: isYearly === 'false' ? false : true,
|
||||
})
|
||||
}
|
||||
}, [query, user, workspace])
|
||||
|
||||
Reference in New Issue
Block a user