♻️ Remove storage limit related code
This commit is contained in:
@@ -27,7 +27,6 @@ export const createCheckoutSession = authenticatedProcedure
|
|||||||
plan: z.enum([Plan.STARTER, Plan.PRO]),
|
plan: z.enum([Plan.STARTER, Plan.PRO]),
|
||||||
returnUrl: z.string(),
|
returnUrl: z.string(),
|
||||||
additionalChats: z.number(),
|
additionalChats: z.number(),
|
||||||
additionalStorage: z.number(),
|
|
||||||
vat: z
|
vat: z
|
||||||
.object({
|
.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
@@ -53,7 +52,6 @@ export const createCheckoutSession = authenticatedProcedure
|
|||||||
plan,
|
plan,
|
||||||
returnUrl,
|
returnUrl,
|
||||||
additionalChats,
|
additionalChats,
|
||||||
additionalStorage,
|
|
||||||
isYearly,
|
isYearly,
|
||||||
},
|
},
|
||||||
ctx: { user },
|
ctx: { user },
|
||||||
@@ -119,7 +117,6 @@ export const createCheckoutSession = authenticatedProcedure
|
|||||||
plan,
|
plan,
|
||||||
returnUrl,
|
returnUrl,
|
||||||
additionalChats,
|
additionalChats,
|
||||||
additionalStorage,
|
|
||||||
isYearly,
|
isYearly,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -142,7 +139,6 @@ type Props = {
|
|||||||
plan: 'STARTER' | 'PRO'
|
plan: 'STARTER' | 'PRO'
|
||||||
returnUrl: string
|
returnUrl: string
|
||||||
additionalChats: number
|
additionalChats: number
|
||||||
additionalStorage: number
|
|
||||||
isYearly: boolean
|
isYearly: boolean
|
||||||
userId: string
|
userId: string
|
||||||
}
|
}
|
||||||
@@ -156,7 +152,6 @@ export const createCheckoutSessionUrl =
|
|||||||
plan,
|
plan,
|
||||||
returnUrl,
|
returnUrl,
|
||||||
additionalChats,
|
additionalChats,
|
||||||
additionalStorage,
|
|
||||||
isYearly,
|
isYearly,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const session = await stripe.checkout.sessions.create({
|
const session = await stripe.checkout.sessions.create({
|
||||||
@@ -173,17 +168,11 @@ export const createCheckoutSessionUrl =
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
plan,
|
plan,
|
||||||
additionalChats,
|
additionalChats,
|
||||||
additionalStorage,
|
|
||||||
},
|
},
|
||||||
currency,
|
currency,
|
||||||
billing_address_collection: 'required',
|
billing_address_collection: 'required',
|
||||||
automatic_tax: { enabled: true },
|
automatic_tax: { enabled: true },
|
||||||
line_items: parseSubscriptionItems(
|
line_items: parseSubscriptionItems(plan, additionalChats, isYearly),
|
||||||
plan,
|
|
||||||
additionalChats,
|
|
||||||
additionalStorage,
|
|
||||||
isYearly
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return session.url
|
return session.url
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ export const getUsage = authenticatedProcedure
|
|||||||
workspaceId: z.string(),
|
workspaceId: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.output(
|
.output(z.object({ totalChatsUsed: z.number() }))
|
||||||
z.object({ totalChatsUsed: z.number(), totalStorageUsed: z.number() })
|
|
||||||
)
|
|
||||||
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
||||||
const workspace = await prisma.workspace.findFirst({
|
const workspace = await prisma.workspace.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@@ -55,20 +53,8 @@ export const getUsage = authenticatedProcedure
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const {
|
|
||||||
_sum: { storageUsed: totalStorageUsed },
|
|
||||||
} = await prisma.answer.aggregate({
|
|
||||||
where: {
|
|
||||||
storageUsed: { gt: 0 },
|
|
||||||
result: {
|
|
||||||
typebotId: { in: workspace.typebots.map((typebot) => typebot.id) },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_sum: { storageUsed: true },
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalChatsUsed,
|
totalChatsUsed,
|
||||||
totalStorageUsed: totalStorageUsed ?? 0,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { workspaceSchema } from '@typebot.io/schemas'
|
|||||||
import Stripe from 'stripe'
|
import Stripe from 'stripe'
|
||||||
import { isDefined } from '@typebot.io/lib'
|
import { isDefined } from '@typebot.io/lib'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing'
|
import { getChatsLimit } from '@typebot.io/lib/pricing'
|
||||||
import { chatPriceIds, storagePriceIds } from './getSubscription'
|
import { chatPriceIds } from './getSubscription'
|
||||||
import { createCheckoutSessionUrl } from './createCheckoutSession'
|
import { createCheckoutSessionUrl } from './createCheckoutSession'
|
||||||
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
|
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
|
||||||
import { getUsage } from '@typebot.io/lib/api/getUsage'
|
import { getUsage } from '@typebot.io/lib/api/getUsage'
|
||||||
@@ -31,7 +31,6 @@ export const updateSubscription = authenticatedProcedure
|
|||||||
workspaceId: z.string(),
|
workspaceId: z.string(),
|
||||||
plan: z.enum([Plan.STARTER, Plan.PRO]),
|
plan: z.enum([Plan.STARTER, Plan.PRO]),
|
||||||
additionalChats: z.number(),
|
additionalChats: z.number(),
|
||||||
additionalStorage: z.number(),
|
|
||||||
currency: z.enum(['usd', 'eur']),
|
currency: z.enum(['usd', 'eur']),
|
||||||
isYearly: z.boolean(),
|
isYearly: z.boolean(),
|
||||||
})
|
})
|
||||||
@@ -48,7 +47,6 @@ export const updateSubscription = authenticatedProcedure
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
plan,
|
plan,
|
||||||
additionalChats,
|
additionalChats,
|
||||||
additionalStorage,
|
|
||||||
currency,
|
currency,
|
||||||
isYearly,
|
isYearly,
|
||||||
returnUrl,
|
returnUrl,
|
||||||
@@ -100,9 +98,6 @@ export const updateSubscription = authenticatedProcedure
|
|||||||
const currentAdditionalChatsItemId = subscription?.items.data.find(
|
const currentAdditionalChatsItemId = subscription?.items.data.find(
|
||||||
(item) => chatPriceIds.includes(item.price.id)
|
(item) => chatPriceIds.includes(item.price.id)
|
||||||
)?.id
|
)?.id
|
||||||
const currentAdditionalStorageItemId = subscription?.items.data.find(
|
|
||||||
(item) => storagePriceIds.includes(item.price.id)
|
|
||||||
)?.id
|
|
||||||
const frequency = isYearly ? 'yearly' : 'monthly'
|
const frequency = isYearly ? 'yearly' : 'monthly'
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
@@ -123,18 +118,6 @@ export const updateSubscription = authenticatedProcedure
|
|||||||
}),
|
}),
|
||||||
deleted: subscription ? additionalChats === 0 : undefined,
|
deleted: subscription ? additionalChats === 0 : undefined,
|
||||||
},
|
},
|
||||||
additionalStorage === 0 && !currentAdditionalStorageItemId
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
id: currentAdditionalStorageItemId,
|
|
||||||
price: priceIds[plan].storage[frequency],
|
|
||||||
quantity: getStorageLimit({
|
|
||||||
plan,
|
|
||||||
additionalStorageIndex: additionalStorage,
|
|
||||||
customStorageLimit: null,
|
|
||||||
}),
|
|
||||||
deleted: subscription ? additionalStorage === 0 : undefined,
|
|
||||||
},
|
|
||||||
].filter(isDefined)
|
].filter(isDefined)
|
||||||
|
|
||||||
if (subscription) {
|
if (subscription) {
|
||||||
@@ -151,7 +134,6 @@ export const updateSubscription = authenticatedProcedure
|
|||||||
plan,
|
plan,
|
||||||
returnUrl,
|
returnUrl,
|
||||||
additionalChats,
|
additionalChats,
|
||||||
additionalStorage,
|
|
||||||
isYearly,
|
isYearly,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -175,7 +157,6 @@ export const updateSubscription = authenticatedProcedure
|
|||||||
data: {
|
data: {
|
||||||
plan,
|
plan,
|
||||||
additionalChatsIndex: additionalChats,
|
additionalChatsIndex: additionalChats,
|
||||||
additionalStorageIndex: additionalStorage,
|
|
||||||
isQuarantined,
|
isQuarantined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -188,7 +169,6 @@ export const updateSubscription = authenticatedProcedure
|
|||||||
data: {
|
data: {
|
||||||
plan,
|
plan,
|
||||||
additionalChatsIndex: additionalChats,
|
additionalChatsIndex: additionalChats,
|
||||||
additionalStorageIndex: additionalStorage,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ test('should display valid usage', async ({ page }) => {
|
|||||||
await injectFakeResults({
|
await injectFakeResults({
|
||||||
count: 10,
|
count: 10,
|
||||||
typebotId: usageTypebotId,
|
typebotId: usageTypebotId,
|
||||||
fakeStorage: 1100 * 1024 * 1024,
|
|
||||||
})
|
})
|
||||||
await page.click('text=Free workspace')
|
await page.click('text=Free workspace')
|
||||||
await page.click('text="Usage Workspace"')
|
await page.click('text="Usage Workspace"')
|
||||||
@@ -101,7 +100,6 @@ test('should display valid usage', async ({ page }) => {
|
|||||||
await injectFakeResults({
|
await injectFakeResults({
|
||||||
typebotId: usageTypebotId,
|
typebotId: usageTypebotId,
|
||||||
count: 1090,
|
count: 1090,
|
||||||
fakeStorage: 1200 * 1024 * 1024,
|
|
||||||
})
|
})
|
||||||
await page.click('text="Settings"')
|
await page.click('text="Settings"')
|
||||||
await page.click('text="Billing & Usage"')
|
await page.click('text="Billing & Usage"')
|
||||||
@@ -140,7 +138,7 @@ test('plan changes should work', async ({ page }) => {
|
|||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{ plan: Plan.STARTER, additionalChatsIndex: 0, additionalStorageIndex: 0 }
|
{ plan: Plan.STARTER, additionalChatsIndex: 0 }
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update plan with additional quotas
|
// Update plan with additional quotas
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ export const ChangePlanForm = ({ workspace }: Props) => {
|
|||||||
plan,
|
plan,
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
additionalChats: selectedChatsLimitIndex,
|
additionalChats: selectedChatsLimitIndex,
|
||||||
additionalStorage: selectedStorageLimitIndex,
|
|
||||||
currency:
|
currency:
|
||||||
data?.subscription?.currency ??
|
data?.subscription?.currency ??
|
||||||
(guessIfUserIsEuropean() ? 'eur' : 'usd'),
|
(guessIfUserIsEuropean() ? 'eur' : 'usd'),
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export type PreCheckoutModalProps = {
|
|||||||
plan: 'STARTER' | 'PRO'
|
plan: 'STARTER' | 'PRO'
|
||||||
workspaceId: string
|
workspaceId: string
|
||||||
additionalChats: number
|
additionalChats: number
|
||||||
additionalStorage: number
|
|
||||||
currency: 'eur' | 'usd'
|
currency: 'eur' | 'usd'
|
||||||
isYearly: boolean
|
isYearly: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import {
|
|||||||
computePrice,
|
computePrice,
|
||||||
formatPrice,
|
formatPrice,
|
||||||
getChatsLimit,
|
getChatsLimit,
|
||||||
getStorageLimit,
|
|
||||||
storageLimit,
|
|
||||||
} from '@typebot.io/lib/pricing'
|
} from '@typebot.io/lib/pricing'
|
||||||
import { FeaturesList } from './FeaturesList'
|
import { FeaturesList } from './FeaturesList'
|
||||||
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
||||||
@@ -35,7 +33,6 @@ type Props = {
|
|||||||
workspace: Pick<
|
workspace: Pick<
|
||||||
Workspace,
|
Workspace,
|
||||||
| 'additionalChatsIndex'
|
| 'additionalChatsIndex'
|
||||||
| 'additionalStorageIndex'
|
|
||||||
| 'plan'
|
| 'plan'
|
||||||
| 'customChatsLimit'
|
| 'customChatsLimit'
|
||||||
| 'customStorageLimit'
|
| 'customStorageLimit'
|
||||||
@@ -80,25 +77,18 @@ export const ProPlanPricingCard = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setSelectedChatsLimitIndex(workspace.additionalChatsIndex ?? 0)
|
setSelectedChatsLimitIndex(workspace.additionalChatsIndex ?? 0)
|
||||||
setSelectedStorageLimitIndex(workspace.additionalStorageIndex ?? 0)
|
|
||||||
}, [
|
}, [
|
||||||
selectedChatsLimitIndex,
|
selectedChatsLimitIndex,
|
||||||
selectedStorageLimitIndex,
|
selectedStorageLimitIndex,
|
||||||
workspace.additionalChatsIndex,
|
workspace.additionalChatsIndex,
|
||||||
workspace.additionalStorageIndex,
|
|
||||||
workspace?.plan,
|
workspace?.plan,
|
||||||
])
|
])
|
||||||
|
|
||||||
const workspaceChatsLimit = workspace ? getChatsLimit(workspace) : undefined
|
const workspaceChatsLimit = workspace ? getChatsLimit(workspace) : undefined
|
||||||
const workspaceStorageLimit = workspace
|
|
||||||
? getStorageLimit(workspace)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const isCurrentPlan =
|
const isCurrentPlan =
|
||||||
chatsLimit[Plan.PRO].graduatedPrice[selectedChatsLimitIndex ?? 0]
|
chatsLimit[Plan.PRO].graduatedPrice[selectedChatsLimitIndex ?? 0]
|
||||||
.totalIncluded === workspaceChatsLimit &&
|
.totalIncluded === workspaceChatsLimit &&
|
||||||
storageLimit[Plan.PRO].graduatedPrice[selectedStorageLimitIndex ?? 0]
|
|
||||||
.totalIncluded === workspaceStorageLimit &&
|
|
||||||
isYearly === currentSubscription?.isYearly
|
isYearly === currentSubscription?.isYearly
|
||||||
|
|
||||||
const getButtonLabel = () => {
|
const getButtonLabel = () => {
|
||||||
@@ -110,10 +100,7 @@ export const ProPlanPricingCard = ({
|
|||||||
if (workspace?.plan === Plan.PRO) {
|
if (workspace?.plan === Plan.PRO) {
|
||||||
if (isCurrentPlan) return scopedT('upgradeButton.current')
|
if (isCurrentPlan) return scopedT('upgradeButton.current')
|
||||||
|
|
||||||
if (
|
if (selectedChatsLimitIndex !== workspace.additionalChatsIndex)
|
||||||
selectedChatsLimitIndex !== workspace.additionalChatsIndex ||
|
|
||||||
selectedStorageLimitIndex !== workspace.additionalStorageIndex
|
|
||||||
)
|
|
||||||
return t('update')
|
return t('update')
|
||||||
}
|
}
|
||||||
return t('upgrade')
|
return t('upgrade')
|
||||||
@@ -135,7 +122,6 @@ export const ProPlanPricingCard = ({
|
|||||||
computePrice(
|
computePrice(
|
||||||
Plan.PRO,
|
Plan.PRO,
|
||||||
selectedChatsLimitIndex ?? 0,
|
selectedChatsLimitIndex ?? 0,
|
||||||
selectedStorageLimitIndex ?? 0,
|
|
||||||
isYearly ? 'yearly' : 'monthly'
|
isYearly ? 'yearly' : 'monthly'
|
||||||
) ?? NaN
|
) ?? NaN
|
||||||
|
|
||||||
@@ -238,40 +224,6 @@ export const ProPlanPricingCard = ({
|
|||||||
</Text>
|
</Text>
|
||||||
<MoreInfoTooltip>{scopedT('chatsTooltip')}</MoreInfoTooltip>
|
<MoreInfoTooltip>{scopedT('chatsTooltip')}</MoreInfoTooltip>
|
||||||
</HStack>,
|
</HStack>,
|
||||||
<HStack key="test">
|
|
||||||
<Text>
|
|
||||||
<Menu>
|
|
||||||
<MenuButton
|
|
||||||
as={Button}
|
|
||||||
rightIcon={<ChevronLeftIcon transform="rotate(-90deg)" />}
|
|
||||||
size="sm"
|
|
||||||
isLoading={selectedStorageLimitIndex === undefined}
|
|
||||||
>
|
|
||||||
{selectedStorageLimitIndex !== undefined
|
|
||||||
? parseNumberWithCommas(
|
|
||||||
storageLimit.PRO.graduatedPrice[
|
|
||||||
selectedStorageLimitIndex
|
|
||||||
].totalIncluded
|
|
||||||
)
|
|
||||||
: undefined}
|
|
||||||
</MenuButton>
|
|
||||||
<MenuList>
|
|
||||||
{storageLimit.PRO.graduatedPrice.map((price, index) => (
|
|
||||||
<MenuItem
|
|
||||||
key={index}
|
|
||||||
onClick={() => setSelectedStorageLimitIndex(index)}
|
|
||||||
>
|
|
||||||
{parseNumberWithCommas(price.totalIncluded)}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</MenuList>
|
|
||||||
</Menu>{' '}
|
|
||||||
{scopedT('storageLimit')}
|
|
||||||
</Text>
|
|
||||||
<MoreInfoTooltip>
|
|
||||||
{scopedT('storageLimitTooltip')}
|
|
||||||
</MoreInfoTooltip>
|
|
||||||
</HStack>,
|
|
||||||
scopedT('pro.customDomains'),
|
scopedT('pro.customDomains'),
|
||||||
scopedT('pro.analytics'),
|
scopedT('pro.analytics'),
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ import {
|
|||||||
computePrice,
|
computePrice,
|
||||||
formatPrice,
|
formatPrice,
|
||||||
getChatsLimit,
|
getChatsLimit,
|
||||||
getStorageLimit,
|
|
||||||
storageLimit,
|
|
||||||
} from '@typebot.io/lib/pricing'
|
} from '@typebot.io/lib/pricing'
|
||||||
import { FeaturesList } from './FeaturesList'
|
import { FeaturesList } from './FeaturesList'
|
||||||
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
||||||
@@ -31,7 +29,6 @@ type Props = {
|
|||||||
workspace: Pick<
|
workspace: Pick<
|
||||||
Workspace,
|
Workspace,
|
||||||
| 'additionalChatsIndex'
|
| 'additionalChatsIndex'
|
||||||
| 'additionalStorageIndex'
|
|
||||||
| 'plan'
|
| 'plan'
|
||||||
| 'customChatsLimit'
|
| 'customChatsLimit'
|
||||||
| 'customStorageLimit'
|
| 'customStorageLimit'
|
||||||
@@ -76,25 +73,18 @@ export const StarterPlanPricingCard = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setSelectedChatsLimitIndex(workspace.additionalChatsIndex ?? 0)
|
setSelectedChatsLimitIndex(workspace.additionalChatsIndex ?? 0)
|
||||||
setSelectedStorageLimitIndex(workspace.additionalStorageIndex ?? 0)
|
|
||||||
}, [
|
}, [
|
||||||
selectedChatsLimitIndex,
|
selectedChatsLimitIndex,
|
||||||
selectedStorageLimitIndex,
|
selectedStorageLimitIndex,
|
||||||
workspace.additionalChatsIndex,
|
workspace.additionalChatsIndex,
|
||||||
workspace.additionalStorageIndex,
|
|
||||||
workspace?.plan,
|
workspace?.plan,
|
||||||
])
|
])
|
||||||
|
|
||||||
const workspaceChatsLimit = workspace ? getChatsLimit(workspace) : undefined
|
const workspaceChatsLimit = workspace ? getChatsLimit(workspace) : undefined
|
||||||
const workspaceStorageLimit = workspace
|
|
||||||
? getStorageLimit(workspace)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const isCurrentPlan =
|
const isCurrentPlan =
|
||||||
chatsLimit[Plan.STARTER].graduatedPrice[selectedChatsLimitIndex ?? 0]
|
chatsLimit[Plan.STARTER].graduatedPrice[selectedChatsLimitIndex ?? 0]
|
||||||
.totalIncluded === workspaceChatsLimit &&
|
.totalIncluded === workspaceChatsLimit &&
|
||||||
storageLimit[Plan.STARTER].graduatedPrice[selectedStorageLimitIndex ?? 0]
|
|
||||||
.totalIncluded === workspaceStorageLimit &&
|
|
||||||
isYearly === currentSubscription?.isYearly
|
isYearly === currentSubscription?.isYearly
|
||||||
|
|
||||||
const getButtonLabel = () => {
|
const getButtonLabel = () => {
|
||||||
@@ -109,7 +99,6 @@ export const StarterPlanPricingCard = ({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
selectedChatsLimitIndex !== workspace.additionalChatsIndex ||
|
selectedChatsLimitIndex !== workspace.additionalChatsIndex ||
|
||||||
selectedStorageLimitIndex !== workspace.additionalStorageIndex ||
|
|
||||||
isYearly !== currentSubscription?.isYearly
|
isYearly !== currentSubscription?.isYearly
|
||||||
)
|
)
|
||||||
return t('update')
|
return t('update')
|
||||||
@@ -133,7 +122,6 @@ export const StarterPlanPricingCard = ({
|
|||||||
computePrice(
|
computePrice(
|
||||||
Plan.STARTER,
|
Plan.STARTER,
|
||||||
selectedChatsLimitIndex ?? 0,
|
selectedChatsLimitIndex ?? 0,
|
||||||
selectedStorageLimitIndex ?? 0,
|
|
||||||
isYearly ? 'yearly' : 'monthly'
|
isYearly ? 'yearly' : 'monthly'
|
||||||
) ?? NaN
|
) ?? NaN
|
||||||
|
|
||||||
@@ -185,40 +173,6 @@ export const StarterPlanPricingCard = ({
|
|||||||
</Text>
|
</Text>
|
||||||
<MoreInfoTooltip>{scopedT('chatsTooltip')}</MoreInfoTooltip>
|
<MoreInfoTooltip>{scopedT('chatsTooltip')}</MoreInfoTooltip>
|
||||||
</HStack>,
|
</HStack>,
|
||||||
<HStack key="test">
|
|
||||||
<Text>
|
|
||||||
<Menu>
|
|
||||||
<MenuButton
|
|
||||||
as={Button}
|
|
||||||
rightIcon={<ChevronLeftIcon transform="rotate(-90deg)" />}
|
|
||||||
size="sm"
|
|
||||||
isLoading={selectedStorageLimitIndex === undefined}
|
|
||||||
>
|
|
||||||
{selectedStorageLimitIndex !== undefined
|
|
||||||
? parseNumberWithCommas(
|
|
||||||
storageLimit.STARTER.graduatedPrice[
|
|
||||||
selectedStorageLimitIndex
|
|
||||||
].totalIncluded
|
|
||||||
)
|
|
||||||
: undefined}
|
|
||||||
</MenuButton>
|
|
||||||
<MenuList>
|
|
||||||
{storageLimit.STARTER.graduatedPrice.map((price, index) => (
|
|
||||||
<MenuItem
|
|
||||||
key={index}
|
|
||||||
onClick={() => setSelectedStorageLimitIndex(index)}
|
|
||||||
>
|
|
||||||
{parseNumberWithCommas(price.totalIncluded)}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</MenuList>
|
|
||||||
</Menu>{' '}
|
|
||||||
{scopedT('storageLimit')}
|
|
||||||
</Text>
|
|
||||||
<MoreInfoTooltip>
|
|
||||||
{scopedT('storageLimitTooltip')}
|
|
||||||
</MoreInfoTooltip>
|
|
||||||
</HStack>,
|
|
||||||
scopedT('starter.brandingRemoved'),
|
scopedT('starter.brandingRemoved'),
|
||||||
scopedT('starter.fileUploadBlock'),
|
scopedT('starter.fileUploadBlock'),
|
||||||
scopedT('starter.createFolders'),
|
scopedT('starter.createFolders'),
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing'
|
import { getChatsLimit } from '@typebot.io/lib/pricing'
|
||||||
import { priceIds } from '@typebot.io/lib/api/pricing'
|
import { priceIds } from '@typebot.io/lib/api/pricing'
|
||||||
|
|
||||||
export const parseSubscriptionItems = (
|
export const parseSubscriptionItems = (
|
||||||
plan: 'STARTER' | 'PRO',
|
plan: 'STARTER' | 'PRO',
|
||||||
additionalChats: number,
|
additionalChats: number,
|
||||||
additionalStorage: number,
|
|
||||||
isYearly: boolean
|
isYearly: boolean
|
||||||
) => {
|
) => {
|
||||||
const frequency = isYearly ? 'yearly' : 'monthly'
|
const frequency = isYearly ? 'yearly' : 'monthly'
|
||||||
@@ -13,33 +12,18 @@ export const parseSubscriptionItems = (
|
|||||||
price: priceIds[plan].base[frequency],
|
price: priceIds[plan].base[frequency],
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
]
|
].concat(
|
||||||
.concat(
|
additionalChats > 0
|
||||||
additionalChats > 0
|
? [
|
||||||
? [
|
{
|
||||||
{
|
price: priceIds[plan].chats[frequency],
|
||||||
price: priceIds[plan].chats[frequency],
|
quantity: getChatsLimit({
|
||||||
quantity: getChatsLimit({
|
plan,
|
||||||
plan,
|
additionalChatsIndex: additionalChats,
|
||||||
additionalChatsIndex: additionalChats,
|
customChatsLimit: null,
|
||||||
customChatsLimit: null,
|
}),
|
||||||
}),
|
},
|
||||||
},
|
]
|
||||||
]
|
: []
|
||||||
: []
|
)
|
||||||
)
|
|
||||||
.concat(
|
|
||||||
additionalStorage > 0
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
price: priceIds[plan].storage[frequency],
|
|
||||||
quantity: getStorageLimit({
|
|
||||||
plan,
|
|
||||||
additionalStorageIndex: additionalStorage,
|
|
||||||
customStorageLimit: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,10 @@ export const DashboardPage = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { subscribePlan, chats, storage, isYearly, claimCustomPlan } =
|
const { subscribePlan, chats, isYearly, claimCustomPlan } =
|
||||||
router.query as {
|
router.query as {
|
||||||
subscribePlan: Plan | undefined
|
subscribePlan: Plan | undefined
|
||||||
chats: string | undefined
|
chats: string | undefined
|
||||||
storage: string | undefined
|
|
||||||
isYearly: string | undefined
|
isYearly: string | undefined
|
||||||
claimCustomPlan: string | undefined
|
claimCustomPlan: string | undefined
|
||||||
}
|
}
|
||||||
@@ -55,7 +54,6 @@ export const DashboardPage = () => {
|
|||||||
plan: subscribePlan as 'PRO' | 'STARTER',
|
plan: subscribePlan as 'PRO' | 'STARTER',
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
additionalChats: chats ? parseInt(chats) : 0,
|
additionalChats: chats ? parseInt(chats) : 0,
|
||||||
additionalStorage: storage ? parseInt(storage) : 0,
|
|
||||||
currency: guessIfUserIsEuropean() ? 'eur' : 'usd',
|
currency: guessIfUserIsEuropean() ? 'eur' : 'usd',
|
||||||
isYearly: isYearly === 'false' ? false : true,
|
isYearly: isYearly === 'false' ? false : true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -47,15 +47,13 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
| {
|
| {
|
||||||
plan: 'STARTER' | 'PRO'
|
plan: 'STARTER' | 'PRO'
|
||||||
additionalChats: string
|
additionalChats: string
|
||||||
additionalStorage: string
|
|
||||||
workspaceId: string
|
workspaceId: string
|
||||||
userId: string
|
userId: string
|
||||||
}
|
}
|
||||||
| { claimableCustomPlanId: string; userId: string }
|
| { claimableCustomPlanId: string; userId: string }
|
||||||
if ('plan' in metadata) {
|
if ('plan' in metadata) {
|
||||||
const { workspaceId, plan, additionalChats, additionalStorage } =
|
const { workspaceId, plan, additionalChats } = metadata
|
||||||
metadata
|
if (!workspaceId || !plan || !additionalChats)
|
||||||
if (!workspaceId || !plan || !additionalChats || !additionalStorage)
|
|
||||||
return res
|
return res
|
||||||
.status(500)
|
.status(500)
|
||||||
.send({ message: `Couldn't retrieve valid metadata` })
|
.send({ message: `Couldn't retrieve valid metadata` })
|
||||||
@@ -65,7 +63,6 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
plan,
|
plan,
|
||||||
stripeId: session.customer as string,
|
stripeId: session.customer as string,
|
||||||
additionalChatsIndex: parseInt(additionalChats),
|
additionalChatsIndex: parseInt(additionalChats),
|
||||||
additionalStorageIndex: parseInt(additionalStorage),
|
|
||||||
isQuarantined: false,
|
isQuarantined: false,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
@@ -88,7 +85,6 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
data: {
|
data: {
|
||||||
plan,
|
plan,
|
||||||
additionalChatsIndex: parseInt(additionalChats),
|
additionalChatsIndex: parseInt(additionalChats),
|
||||||
additionalStorageIndex: parseInt(additionalStorage),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -124,7 +120,6 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
data: {
|
data: {
|
||||||
plan: Plan.CUSTOM,
|
plan: Plan.CUSTOM,
|
||||||
additionalChatsIndex: 0,
|
additionalChatsIndex: 0,
|
||||||
additionalStorageIndex: 0,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -154,7 +149,6 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
data: {
|
data: {
|
||||||
plan: Plan.FREE,
|
plan: Plan.FREE,
|
||||||
additionalChatsIndex: 0,
|
additionalChatsIndex: 0,
|
||||||
additionalStorageIndex: 0,
|
|
||||||
customChatsLimit: null,
|
customChatsLimit: null,
|
||||||
customStorageLimit: null,
|
customStorageLimit: null,
|
||||||
customSeatsLimit: null,
|
customSeatsLimit: null,
|
||||||
@@ -179,7 +173,6 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
data: {
|
data: {
|
||||||
plan: Plan.FREE,
|
plan: Plan.FREE,
|
||||||
additionalChatsIndex: 0,
|
additionalChatsIndex: 0,
|
||||||
additionalStorageIndex: 0,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ const stripe = new Stripe(env.STRIPE_SECRET_KEY ?? '', {
|
|||||||
export const addSubscriptionToWorkspace = async (
|
export const addSubscriptionToWorkspace = async (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
items: Stripe.SubscriptionCreateParams.Item[],
|
items: Stripe.SubscriptionCreateParams.Item[],
|
||||||
metadata: Pick<
|
metadata: Pick<Workspace, 'additionalChatsIndex' | 'plan'>
|
||||||
Workspace,
|
|
||||||
'additionalChatsIndex' | 'additionalStorageIndex' | 'plan'
|
|
||||||
>
|
|
||||||
) => {
|
) => {
|
||||||
const { id: stripeId } = await stripe.customers.create({
|
const { id: stripeId } = await stripe.customers.create({
|
||||||
email: 'test-user@gmail.com',
|
email: 'test-user@gmail.com',
|
||||||
|
|||||||
@@ -232,15 +232,11 @@
|
|||||||
},
|
},
|
||||||
"additionalChatsIndex": {
|
"additionalChatsIndex": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
|
||||||
"additionalStorageIndex": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"plan",
|
"plan",
|
||||||
"additionalChatsIndex",
|
"additionalChatsIndex"
|
||||||
"additionalStorageIndex"
|
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@@ -320,21 +316,13 @@
|
|||||||
"chatsLimit": {
|
"chatsLimit": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"storageLimit": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"totalChatsUsed": {
|
"totalChatsUsed": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
|
||||||
"totalStorageUsed": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"chatsLimit",
|
"chatsLimit",
|
||||||
"storageLimit",
|
"totalChatsUsed"
|
||||||
"totalChatsUsed",
|
|
||||||
"totalStorageUsed"
|
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@@ -30391,9 +30379,6 @@
|
|||||||
"additionalChats": {
|
"additionalChats": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"additionalStorage": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"vat": {
|
"vat": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -30422,7 +30407,6 @@
|
|||||||
"plan",
|
"plan",
|
||||||
"returnUrl",
|
"returnUrl",
|
||||||
"additionalChats",
|
"additionalChats",
|
||||||
"additionalStorage",
|
|
||||||
"isYearly"
|
"isYearly"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -30492,9 +30476,6 @@
|
|||||||
"additionalChats": {
|
"additionalChats": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"additionalStorage": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"currency": {
|
"currency": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -30511,7 +30492,6 @@
|
|||||||
"workspaceId",
|
"workspaceId",
|
||||||
"plan",
|
"plan",
|
||||||
"additionalChats",
|
"additionalChats",
|
||||||
"additionalStorage",
|
|
||||||
"currency",
|
"currency",
|
||||||
"isYearly"
|
"isYearly"
|
||||||
],
|
],
|
||||||
@@ -30767,14 +30747,10 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"totalChatsUsed": {
|
"totalChatsUsed": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
|
||||||
"totalStorageUsed": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"totalChatsUsed",
|
"totalChatsUsed"
|
||||||
"totalStorageUsed"
|
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
formatPrice,
|
formatPrice,
|
||||||
prices,
|
prices,
|
||||||
seatsLimit,
|
seatsLimit,
|
||||||
storageLimit,
|
|
||||||
} from '@typebot.io/lib/pricing'
|
} from '@typebot.io/lib/pricing'
|
||||||
import { parseNumberWithCommas } from '@typebot.io/lib'
|
import { parseNumberWithCommas } from '@typebot.io/lib'
|
||||||
|
|
||||||
@@ -85,22 +84,6 @@ export const PlanComparisonTables = () => (
|
|||||||
<Td>2 GB</Td>
|
<Td>2 GB</Td>
|
||||||
<Td>10 GB</Td>
|
<Td>10 GB</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
<Tr>
|
|
||||||
<Td>Additional Storage</Td>
|
|
||||||
<Td />
|
|
||||||
<Td>
|
|
||||||
{formatPrice(storageLimit.STARTER.graduatedPrice[1].price)} per{' '}
|
|
||||||
{storageLimit.STARTER.graduatedPrice[1].totalIncluded -
|
|
||||||
storageLimit.STARTER.graduatedPrice[0].totalIncluded}{' '}
|
|
||||||
GB
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
{formatPrice(storageLimit.PRO.graduatedPrice[1].price)} per{' '}
|
|
||||||
{storageLimit.PRO.graduatedPrice[1].totalIncluded -
|
|
||||||
storageLimit.PRO.graduatedPrice[0].totalIncluded}{' '}
|
|
||||||
GB
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td>Members</Td>
|
<Td>Members</Td>
|
||||||
<Td>Just you</Td>
|
<Td>Just you</Td>
|
||||||
|
|||||||
@@ -15,12 +15,7 @@ import { Plan } from '@typebot.io/prisma'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { parseNumberWithCommas } from '@typebot.io/lib'
|
import { parseNumberWithCommas } from '@typebot.io/lib'
|
||||||
import {
|
import { chatsLimit, computePrice, seatsLimit } from '@typebot.io/lib/pricing'
|
||||||
chatsLimit,
|
|
||||||
computePrice,
|
|
||||||
seatsLimit,
|
|
||||||
storageLimit,
|
|
||||||
} from '@typebot.io/lib/pricing'
|
|
||||||
import { PricingCard } from './PricingCard'
|
import { PricingCard } from './PricingCard'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -30,14 +25,11 @@ type Props = {
|
|||||||
export const ProPlanCard = ({ isYearly }: Props) => {
|
export const ProPlanCard = ({ isYearly }: Props) => {
|
||||||
const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] =
|
const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] =
|
||||||
useState<number>(0)
|
useState<number>(0)
|
||||||
const [selectedStorageLimitIndex, setSelectedStorageLimitIndex] =
|
|
||||||
useState<number>(0)
|
|
||||||
|
|
||||||
const price =
|
const price =
|
||||||
computePrice(
|
computePrice(
|
||||||
Plan.PRO,
|
Plan.PRO,
|
||||||
selectedChatsLimitIndex ?? 0,
|
selectedChatsLimitIndex ?? 0,
|
||||||
selectedStorageLimitIndex ?? 0,
|
|
||||||
isYearly ? 'yearly' : 'monthly'
|
isYearly ? 'yearly' : 'monthly'
|
||||||
) ?? NaN
|
) ?? NaN
|
||||||
|
|
||||||
@@ -93,46 +85,6 @@ export const ProPlanCard = ({ isYearly }: Props) => {
|
|||||||
</chakra.span>
|
</chakra.span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</HStack>,
|
</HStack>,
|
||||||
<HStack key="storage" spacing={1.5}>
|
|
||||||
<Menu>
|
|
||||||
<MenuButton
|
|
||||||
as={Button}
|
|
||||||
rightIcon={<ChevronDownIcon />}
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
isLoading={selectedStorageLimitIndex === undefined}
|
|
||||||
>
|
|
||||||
{selectedStorageLimitIndex !== undefined
|
|
||||||
? parseNumberWithCommas(
|
|
||||||
storageLimit.PRO.graduatedPrice[selectedStorageLimitIndex]
|
|
||||||
.totalIncluded
|
|
||||||
)
|
|
||||||
: undefined}
|
|
||||||
</MenuButton>
|
|
||||||
<MenuList>
|
|
||||||
{storageLimit.PRO.graduatedPrice.map((price, index) => (
|
|
||||||
<MenuItem
|
|
||||||
key={index}
|
|
||||||
onClick={() => setSelectedStorageLimitIndex(index)}
|
|
||||||
>
|
|
||||||
{parseNumberWithCommas(price.totalIncluded)}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</MenuList>
|
|
||||||
</Menu>{' '}
|
|
||||||
<Text>GB of storage</Text>
|
|
||||||
<Tooltip
|
|
||||||
hasArrow
|
|
||||||
placement="top"
|
|
||||||
label="You accumulate storage for every file that your user upload
|
|
||||||
into your bot. If you delete the result, it will free up the
|
|
||||||
space."
|
|
||||||
>
|
|
||||||
<chakra.span cursor="pointer" h="7">
|
|
||||||
<HelpCircleIcon />
|
|
||||||
</chakra.span>
|
|
||||||
</Tooltip>
|
|
||||||
</HStack>,
|
|
||||||
'Custom domains',
|
'Custom domains',
|
||||||
'In-depth analytics',
|
'In-depth analytics',
|
||||||
],
|
],
|
||||||
@@ -142,7 +94,7 @@ export const ProPlanCard = ({ isYearly }: Props) => {
|
|||||||
button={
|
button={
|
||||||
<Button
|
<Button
|
||||||
as={Link}
|
as={Link}
|
||||||
href={`https://app.typebot.io/register?subscribePlan=${Plan.PRO}&chats=${selectedChatsLimitIndex}&storage=${selectedStorageLimitIndex}&isYearly=${isYearly}`}
|
href={`https://app.typebot.io/register?subscribePlan=${Plan.PRO}&chats=${selectedChatsLimitIndex}&isYearly=${isYearly}`}
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
size="lg"
|
size="lg"
|
||||||
w="full"
|
w="full"
|
||||||
|
|||||||
@@ -15,12 +15,7 @@ import { Plan } from '@typebot.io/prisma'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { parseNumberWithCommas } from '@typebot.io/lib'
|
import { parseNumberWithCommas } from '@typebot.io/lib'
|
||||||
import {
|
import { chatsLimit, computePrice, seatsLimit } from '@typebot.io/lib/pricing'
|
||||||
chatsLimit,
|
|
||||||
computePrice,
|
|
||||||
seatsLimit,
|
|
||||||
storageLimit,
|
|
||||||
} from '@typebot.io/lib/pricing'
|
|
||||||
import { PricingCard } from './PricingCard'
|
import { PricingCard } from './PricingCard'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -29,14 +24,11 @@ type Props = {
|
|||||||
export const StarterPlanCard = ({ isYearly }: Props) => {
|
export const StarterPlanCard = ({ isYearly }: Props) => {
|
||||||
const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] =
|
const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] =
|
||||||
useState<number>(0)
|
useState<number>(0)
|
||||||
const [selectedStorageLimitIndex, setSelectedStorageLimitIndex] =
|
|
||||||
useState<number>(0)
|
|
||||||
|
|
||||||
const price =
|
const price =
|
||||||
computePrice(
|
computePrice(
|
||||||
Plan.STARTER,
|
Plan.STARTER,
|
||||||
selectedChatsLimitIndex ?? 0,
|
selectedChatsLimitIndex ?? 0,
|
||||||
selectedStorageLimitIndex ?? 0,
|
|
||||||
isYearly ? 'yearly' : 'monthly'
|
isYearly ? 'yearly' : 'monthly'
|
||||||
) ?? NaN
|
) ?? NaN
|
||||||
|
|
||||||
@@ -90,48 +82,6 @@ export const StarterPlanCard = ({ isYearly }: Props) => {
|
|||||||
</chakra.span>
|
</chakra.span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</HStack>,
|
</HStack>,
|
||||||
<HStack key="storage" spacing={1.5}>
|
|
||||||
<Menu>
|
|
||||||
<MenuButton
|
|
||||||
as={Button}
|
|
||||||
rightIcon={<ChevronDownIcon />}
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
isLoading={selectedStorageLimitIndex === undefined}
|
|
||||||
colorScheme="orange"
|
|
||||||
>
|
|
||||||
{selectedStorageLimitIndex !== undefined
|
|
||||||
? parseNumberWithCommas(
|
|
||||||
storageLimit.STARTER.graduatedPrice[
|
|
||||||
selectedStorageLimitIndex
|
|
||||||
].totalIncluded
|
|
||||||
)
|
|
||||||
: undefined}
|
|
||||||
</MenuButton>
|
|
||||||
<MenuList>
|
|
||||||
{storageLimit.STARTER.graduatedPrice.map((price, index) => (
|
|
||||||
<MenuItem
|
|
||||||
key={index}
|
|
||||||
onClick={() => setSelectedStorageLimitIndex(index)}
|
|
||||||
>
|
|
||||||
{parseNumberWithCommas(price.totalIncluded)}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</MenuList>
|
|
||||||
</Menu>{' '}
|
|
||||||
<Text>GB of storage</Text>
|
|
||||||
<Tooltip
|
|
||||||
hasArrow
|
|
||||||
placement="top"
|
|
||||||
label="You accumulate storage for every file that your user upload
|
|
||||||
into your bot. If you delete the result, it will free up the
|
|
||||||
space."
|
|
||||||
>
|
|
||||||
<chakra.span cursor="pointer" h="7">
|
|
||||||
<HelpCircleIcon />
|
|
||||||
</chakra.span>
|
|
||||||
</Tooltip>
|
|
||||||
</HStack>,
|
|
||||||
'Branding removed',
|
'Branding removed',
|
||||||
'Collect files from users',
|
'Collect files from users',
|
||||||
'Create folders',
|
'Create folders',
|
||||||
@@ -142,7 +92,7 @@ export const StarterPlanCard = ({ isYearly }: Props) => {
|
|||||||
button={
|
button={
|
||||||
<Button
|
<Button
|
||||||
as={Link}
|
as={Link}
|
||||||
href={`https://app.typebot.io/register?subscribePlan=${Plan.STARTER}&chats=${selectedChatsLimitIndex}&storage=${selectedStorageLimitIndex}&isYearly=${isYearly}`}
|
href={`https://app.typebot.io/register?subscribePlan=${Plan.STARTER}&chats=${selectedChatsLimitIndex}&isYearly=${isYearly}`}
|
||||||
colorScheme="orange"
|
colorScheme="orange"
|
||||||
size="lg"
|
size="lg"
|
||||||
w="full"
|
w="full"
|
||||||
|
|||||||
@@ -3,17 +3,10 @@ import { createId } from '@paralleldrive/cuid2'
|
|||||||
import { parse } from 'papaparse'
|
import { parse } from 'papaparse'
|
||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
import { isDefined } from '@typebot.io/lib'
|
import { isDefined } from '@typebot.io/lib'
|
||||||
import {
|
import { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions'
|
||||||
createWorkspaces,
|
|
||||||
importTypebotInDatabase,
|
|
||||||
injectFakeResults,
|
|
||||||
} from '@typebot.io/lib/playwright/databaseActions'
|
|
||||||
import { getTestAsset } from '@/test/utils/playwright'
|
import { getTestAsset } from '@/test/utils/playwright'
|
||||||
import { Plan } from '@typebot.io/prisma'
|
|
||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
|
|
||||||
const THREE_GIGABYTES = 3 * 1024 * 1024 * 1024
|
|
||||||
|
|
||||||
test('should work as expected', async ({ page, browser }) => {
|
test('should work as expected', async ({ page, browser }) => {
|
||||||
const typebotId = createId()
|
const typebotId = createId()
|
||||||
await importTypebotInDatabase(getTestAsset('typebots/fileUpload.json'), {
|
await importTypebotInDatabase(getTestAsset('typebots/fileUpload.json'), {
|
||||||
@@ -75,22 +68,3 @@ test('should work as expected', async ({ page, browser }) => {
|
|||||||
await page2.goto(urls[0])
|
await page2.goto(urls[0])
|
||||||
await expect(page2.locator('pre')).toBeHidden()
|
await expect(page2.locator('pre')).toBeHidden()
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Storage limit is reached', () => {
|
|
||||||
const typebotId = createId()
|
|
||||||
const workspaceId = createId()
|
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
|
||||||
await createWorkspaces([{ id: workspaceId, plan: Plan.STARTER }])
|
|
||||||
await importTypebotInDatabase(getTestAsset('typebots/fileUpload.json'), {
|
|
||||||
id: typebotId,
|
|
||||||
publicId: `${typebotId}-public`,
|
|
||||||
workspaceId,
|
|
||||||
})
|
|
||||||
await injectFakeResults({
|
|
||||||
typebotId,
|
|
||||||
count: 20,
|
|
||||||
fakeStorage: THREE_GIGABYTES,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import test, { expect } from '@playwright/test'
|
import test, { expect } from '@playwright/test'
|
||||||
import { createId } from '@paralleldrive/cuid2'
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
import { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions'
|
import { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions'
|
||||||
import { getTestAsset } from '@/test/utils/playwright'
|
|
||||||
import { SmtpCredentials } from '@typebot.io/schemas'
|
import { SmtpCredentials } from '@typebot.io/schemas'
|
||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
import { createSmtpCredentials } from './utils/databaseActions'
|
import { createSmtpCredentials } from './utils/databaseActions'
|
||||||
|
import { getTestAsset } from './utils/playwright'
|
||||||
|
|
||||||
export const mockSmtpCredentials: SmtpCredentials['data'] = {
|
export const mockSmtpCredentials: SmtpCredentials['data'] = {
|
||||||
from: {
|
from: {
|
||||||
|
|||||||
@@ -226,7 +226,6 @@ const saveAnswer =
|
|||||||
groupId: block.groupId,
|
groupId: block.groupId,
|
||||||
content: reply,
|
content: reply,
|
||||||
variableId: block.options.variableId,
|
variableId: block.options.variableId,
|
||||||
storageUsed: 0,
|
|
||||||
},
|
},
|
||||||
reply,
|
reply,
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ export const getUsage =
|
|||||||
select: { id: true },
|
select: { id: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
const [
|
const [totalChatsUsed] = await Promise.all([
|
||||||
totalChatsUsed,
|
|
||||||
{
|
|
||||||
_sum: { storageUsed: totalStorageUsed },
|
|
||||||
},
|
|
||||||
] = await Promise.all([
|
|
||||||
prisma.result.count({
|
prisma.result.count({
|
||||||
where: {
|
where: {
|
||||||
typebotId: { in: typebots.map((typebot) => typebot.id) },
|
typebotId: { in: typebots.map((typebot) => typebot.id) },
|
||||||
@@ -34,19 +29,9 @@ export const getUsage =
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.answer.aggregate({
|
|
||||||
where: {
|
|
||||||
storageUsed: { gt: 0 },
|
|
||||||
result: {
|
|
||||||
typebotId: { in: typebots.map((typebot) => typebot.id) },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_sum: { storageUsed: true },
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalChatsUsed,
|
totalChatsUsed,
|
||||||
totalStorageUsed: totalStorageUsed ?? 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ type CreateFakeResultsProps = {
|
|||||||
count: number
|
count: number
|
||||||
customResultIdPrefix?: string
|
customResultIdPrefix?: string
|
||||||
isChronological?: boolean
|
isChronological?: boolean
|
||||||
fakeStorage?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const injectFakeResults = async ({
|
export const injectFakeResults = async ({
|
||||||
@@ -30,7 +29,6 @@ export const injectFakeResults = async ({
|
|||||||
customResultIdPrefix,
|
customResultIdPrefix,
|
||||||
typebotId,
|
typebotId,
|
||||||
isChronological,
|
isChronological,
|
||||||
fakeStorage,
|
|
||||||
}: CreateFakeResultsProps) => {
|
}: CreateFakeResultsProps) => {
|
||||||
const resultIdPrefix = customResultIdPrefix ?? createId()
|
const resultIdPrefix = customResultIdPrefix ?? createId()
|
||||||
await prisma.result.createMany({
|
await prisma.result.createMany({
|
||||||
@@ -53,17 +51,13 @@ export const injectFakeResults = async ({
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
return createAnswers({ fakeStorage, resultIdPrefix, count })
|
return createAnswers({ resultIdPrefix, count })
|
||||||
}
|
}
|
||||||
|
|
||||||
const createAnswers = ({
|
const createAnswers = ({
|
||||||
count,
|
count,
|
||||||
resultIdPrefix,
|
resultIdPrefix,
|
||||||
fakeStorage,
|
}: { resultIdPrefix: string } & Pick<CreateFakeResultsProps, 'count'>) => {
|
||||||
}: { resultIdPrefix: string } & Pick<
|
|
||||||
CreateFakeResultsProps,
|
|
||||||
'fakeStorage' | 'count'
|
|
||||||
>) => {
|
|
||||||
return prisma.answer.createMany({
|
return prisma.answer.createMany({
|
||||||
data: [
|
data: [
|
||||||
...Array.from(Array(count)).map((_, idx) => ({
|
...Array.from(Array(count)).map((_, idx) => ({
|
||||||
@@ -71,7 +65,6 @@ const createAnswers = ({
|
|||||||
content: `content${idx}`,
|
content: `content${idx}`,
|
||||||
blockId: 'block1',
|
blockId: 'block1',
|
||||||
groupId: 'group1',
|
groupId: 'group1',
|
||||||
storageUsed: fakeStorage ? Math.round(fakeStorage / count) : null,
|
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -47,54 +47,6 @@ export const chatsLimit = {
|
|||||||
[Plan.UNLIMITED]: { totalIncluded: infinity },
|
[Plan.UNLIMITED]: { totalIncluded: infinity },
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const storageLimit = {
|
|
||||||
[Plan.FREE]: { totalIncluded: 0 },
|
|
||||||
[Plan.STARTER]: {
|
|
||||||
graduatedPrice: [
|
|
||||||
{ totalIncluded: 2, price: 0 },
|
|
||||||
{
|
|
||||||
totalIncluded: 3,
|
|
||||||
price: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
totalIncluded: 4,
|
|
||||||
price: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
totalIncluded: 5,
|
|
||||||
price: 6,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
[Plan.PRO]: {
|
|
||||||
graduatedPrice: [
|
|
||||||
{ totalIncluded: 10, price: 0 },
|
|
||||||
{
|
|
||||||
totalIncluded: 15,
|
|
||||||
price: 8,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
totalIncluded: 25,
|
|
||||||
price: 24,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
totalIncluded: 40,
|
|
||||||
price: 49,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
[Plan.CUSTOM]: {
|
|
||||||
totalIncluded: 2,
|
|
||||||
increaseStep: {
|
|
||||||
amount: 1,
|
|
||||||
price: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[Plan.OFFERED]: { totalIncluded: 2 },
|
|
||||||
[Plan.LIFETIME]: { totalIncluded: 10 },
|
|
||||||
[Plan.UNLIMITED]: { totalIncluded: infinity },
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const seatsLimit = {
|
export const seatsLimit = {
|
||||||
[Plan.FREE]: { totalIncluded: 1 },
|
[Plan.FREE]: { totalIncluded: 1 },
|
||||||
[Plan.STARTER]: {
|
[Plan.STARTER]: {
|
||||||
@@ -124,22 +76,6 @@ export const getChatsLimit = ({
|
|||||||
return totalIncluded
|
return totalIncluded
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStorageLimit = ({
|
|
||||||
plan,
|
|
||||||
additionalStorageIndex,
|
|
||||||
customStorageLimit,
|
|
||||||
}: Pick<
|
|
||||||
Workspace,
|
|
||||||
'additionalStorageIndex' | 'plan' | 'customStorageLimit'
|
|
||||||
>) => {
|
|
||||||
if (customStorageLimit) return customStorageLimit
|
|
||||||
const totalIncluded =
|
|
||||||
plan === Plan.STARTER || plan === Plan.PRO
|
|
||||||
? storageLimit[plan].graduatedPrice[additionalStorageIndex].totalIncluded
|
|
||||||
: storageLimit[plan].totalIncluded
|
|
||||||
return totalIncluded
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getSeatsLimit = ({
|
export const getSeatsLimit = ({
|
||||||
plan,
|
plan,
|
||||||
customSeatsLimit,
|
customSeatsLimit,
|
||||||
@@ -164,14 +100,12 @@ export const isSeatsLimitReached = ({
|
|||||||
export const computePrice = (
|
export const computePrice = (
|
||||||
plan: Plan,
|
plan: Plan,
|
||||||
selectedTotalChatsIndex: number,
|
selectedTotalChatsIndex: number,
|
||||||
selectedTotalStorageIndex: number,
|
|
||||||
frequency: 'monthly' | 'yearly'
|
frequency: 'monthly' | 'yearly'
|
||||||
) => {
|
) => {
|
||||||
if (plan !== Plan.STARTER && plan !== Plan.PRO) return
|
if (plan !== Plan.STARTER && plan !== Plan.PRO) return
|
||||||
const price =
|
const price =
|
||||||
prices[plan] +
|
prices[plan] +
|
||||||
chatsLimit[plan].graduatedPrice[selectedTotalChatsIndex].price +
|
chatsLimit[plan].graduatedPrice[selectedTotalChatsIndex].price
|
||||||
storageLimit[plan].graduatedPrice[selectedTotalStorageIndex].price
|
|
||||||
return frequency === 'monthly' ? price : price - price * 0.16
|
return frequency === 'monthly' ? price : price - price * 0.16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ const subscriptionUpdatedEventSchema = workspaceEvent.merge(
|
|||||||
data: z.object({
|
data: z.object({
|
||||||
plan: z.nativeEnum(Plan),
|
plan: z.nativeEnum(Plan),
|
||||||
additionalChatsIndex: z.number(),
|
additionalChatsIndex: z.number(),
|
||||||
additionalStorageIndex: z.number(),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -83,9 +82,7 @@ const workspaceLimitReachedEventSchema = workspaceEvent.merge(
|
|||||||
name: z.literal('Workspace limit reached'),
|
name: z.literal('Workspace limit reached'),
|
||||||
data: z.object({
|
data: z.object({
|
||||||
chatsLimit: z.number(),
|
chatsLimit: z.number(),
|
||||||
storageLimit: z.number(),
|
|
||||||
totalChatsUsed: z.number(),
|
totalChatsUsed: z.number(),
|
||||||
totalStorageUsed: z.number(),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
WorkspaceRole,
|
WorkspaceRole,
|
||||||
} from '@typebot.io/prisma'
|
} from '@typebot.io/prisma'
|
||||||
import { isDefined } from '@typebot.io/lib'
|
import { isDefined } from '@typebot.io/lib'
|
||||||
import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing'
|
import { getChatsLimit } from '@typebot.io/lib/pricing'
|
||||||
import { promptAndSetEnvironment } from './utils'
|
import { promptAndSetEnvironment } from './utils'
|
||||||
import { TelemetryEvent } from '@typebot.io/schemas/features/telemetry'
|
import { TelemetryEvent } from '@typebot.io/schemas/features/telemetry'
|
||||||
import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEvent'
|
import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEvent'
|
||||||
@@ -141,14 +141,9 @@ const sendAlertIfLimitReached = async (
|
|||||||
if (taggedWorkspaces.includes(workspace.id) || workspace.isQuarantined)
|
if (taggedWorkspaces.includes(workspace.id) || workspace.isQuarantined)
|
||||||
continue
|
continue
|
||||||
taggedWorkspaces.push(workspace.id)
|
taggedWorkspaces.push(workspace.id)
|
||||||
const { totalChatsUsed, totalStorageUsed } = await getUsage(workspace.id)
|
const { totalChatsUsed } = await getUsage(workspace.id)
|
||||||
const totalStorageUsedInGb = totalStorageUsed / 1024 / 1024 / 1024
|
|
||||||
const chatsLimit = getChatsLimit(workspace)
|
const chatsLimit = getChatsLimit(workspace)
|
||||||
const storageLimit = getStorageLimit(workspace)
|
if (chatsLimit > 0 && totalChatsUsed >= chatsLimit) {
|
||||||
if (
|
|
||||||
(chatsLimit > 0 && totalChatsUsed >= chatsLimit) ||
|
|
||||||
(storageLimit > 0 && totalStorageUsedInGb >= storageLimit)
|
|
||||||
) {
|
|
||||||
events.push(
|
events.push(
|
||||||
...workspace.members
|
...workspace.members
|
||||||
.filter((member) => member.role === WorkspaceRole.ADMIN)
|
.filter((member) => member.role === WorkspaceRole.ADMIN)
|
||||||
@@ -160,9 +155,7 @@ const sendAlertIfLimitReached = async (
|
|||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
data: {
|
data: {
|
||||||
totalChatsUsed,
|
totalChatsUsed,
|
||||||
totalStorageUsed: totalStorageUsedInGb,
|
|
||||||
chatsLimit,
|
chatsLimit,
|
||||||
storageLimit,
|
|
||||||
},
|
},
|
||||||
} satisfies TelemetryEvent)
|
} satisfies TelemetryEvent)
|
||||||
)
|
)
|
||||||
@@ -186,12 +179,7 @@ const getUsage = async (workspaceId: string) => {
|
|||||||
select: { id: true },
|
select: { id: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
const [
|
const [totalChatsUsed] = await Promise.all([
|
||||||
totalChatsUsed,
|
|
||||||
{
|
|
||||||
_sum: { storageUsed: totalStorageUsed },
|
|
||||||
},
|
|
||||||
] = await Promise.all([
|
|
||||||
prisma.result.count({
|
prisma.result.count({
|
||||||
where: {
|
where: {
|
||||||
typebotId: { in: typebots.map((typebot) => typebot.id) },
|
typebotId: { in: typebots.map((typebot) => typebot.id) },
|
||||||
@@ -202,20 +190,10 @@ const getUsage = async (workspaceId: string) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.answer.aggregate({
|
|
||||||
where: {
|
|
||||||
storageUsed: { gt: 0 },
|
|
||||||
result: {
|
|
||||||
typebotId: { in: typebots.map((typebot) => typebot.id) },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_sum: { storageUsed: true },
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalChatsUsed,
|
totalChatsUsed,
|
||||||
totalStorageUsed: totalStorageUsed ?? 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"jsx": "preserve"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user