🛂 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,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')}
|
||||
|
||||
Reference in New Issue
Block a user