✨ (lp) Add new pricing page
This commit is contained in:
committed by
Baptiste Arnaud
parent
d8b1d8ad59
commit
c94a6581be
@ -13,21 +13,23 @@ export const BillingContent = () => {
|
|||||||
if (!workspace) return null
|
if (!workspace) return null
|
||||||
return (
|
return (
|
||||||
<Stack spacing="10" w="full">
|
<Stack spacing="10" w="full">
|
||||||
<CurrentSubscriptionContent
|
|
||||||
plan={workspace.plan}
|
|
||||||
stripeId={workspace.stripeId}
|
|
||||||
onCancelSuccess={() =>
|
|
||||||
refreshWorkspace({
|
|
||||||
plan: Plan.FREE,
|
|
||||||
additionalChatsIndex: 0,
|
|
||||||
additionalStorageIndex: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<UsageContent workspace={workspace} />
|
<UsageContent workspace={workspace} />
|
||||||
{workspace.plan !== Plan.LIFETIME && workspace.plan !== Plan.OFFERED && (
|
<Stack gap="2">
|
||||||
<ChangePlanForm />
|
<CurrentSubscriptionContent
|
||||||
)}
|
plan={workspace.plan}
|
||||||
|
stripeId={workspace.stripeId}
|
||||||
|
onCancelSuccess={() =>
|
||||||
|
refreshWorkspace({
|
||||||
|
plan: Plan.FREE,
|
||||||
|
additionalChatsIndex: 0,
|
||||||
|
additionalStorageIndex: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{workspace.plan !== Plan.LIFETIME &&
|
||||||
|
workspace.plan !== Plan.OFFERED && <ChangePlanForm />}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
{workspace.stripeId && <InvoicesList workspace={workspace} />}
|
{workspace.stripeId && <InvoicesList workspace={workspace} />}
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
|
@ -4,8 +4,8 @@ import {
|
|||||||
Link,
|
Link,
|
||||||
Spinner,
|
Spinner,
|
||||||
Stack,
|
Stack,
|
||||||
Flex,
|
|
||||||
Button,
|
Button,
|
||||||
|
Heading,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { PlanTag } from 'components/shared/PlanTag'
|
import { PlanTag } from 'components/shared/PlanTag'
|
||||||
import { Plan } from 'db'
|
import { Plan } from 'db'
|
||||||
@ -35,15 +35,29 @@ export const CurrentSubscriptionContent = ({
|
|||||||
setIsCancelling(false)
|
setIsCancelling(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSubscribed = (plan === Plan.STARTER || plan === Plan.PRO) && stripeId
|
||||||
|
|
||||||
if (isCancelling) return <Spinner colorScheme="gray" />
|
if (isCancelling) return <Spinner colorScheme="gray" />
|
||||||
return (
|
return (
|
||||||
<Stack gap="2">
|
<Stack gap="2">
|
||||||
|
<Heading fontSize="3xl">Subscription</Heading>
|
||||||
<HStack>
|
<HStack>
|
||||||
<Text>Current workspace subscription: </Text>
|
<Text>Current workspace subscription: </Text>
|
||||||
<PlanTag plan={plan} />
|
<PlanTag plan={plan} />
|
||||||
|
{isSubscribed && (
|
||||||
|
<Link
|
||||||
|
as="button"
|
||||||
|
color="gray.500"
|
||||||
|
textDecor="underline"
|
||||||
|
fontSize="sm"
|
||||||
|
onClick={cancelSubscription}
|
||||||
|
>
|
||||||
|
Cancel my subscription
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{(plan === Plan.STARTER || plan === Plan.PRO) && stripeId && (
|
{isSubscribed && (
|
||||||
<>
|
<>
|
||||||
<Stack gap="1">
|
<Stack gap="1">
|
||||||
<Text fontSize="sm">
|
<Text fontSize="sm">
|
||||||
@ -59,17 +73,6 @@ export const CurrentSubscriptionContent = ({
|
|||||||
Billing Portal
|
Billing Portal
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Flex>
|
|
||||||
<Link
|
|
||||||
as="button"
|
|
||||||
color="gray.500"
|
|
||||||
textDecor="underline"
|
|
||||||
fontSize="sm"
|
|
||||||
onClick={cancelSubscription}
|
|
||||||
>
|
|
||||||
Cancel my subscription
|
|
||||||
</Link>
|
|
||||||
</Flex>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -60,16 +60,7 @@ export const ChangePlanForm = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<HStack
|
<HStack alignItems="stretch" spacing="4" w="full">
|
||||||
alignItems="stretch"
|
|
||||||
spacing="4"
|
|
||||||
w="full"
|
|
||||||
pt={
|
|
||||||
workspace?.plan === Plan.STARTER || workspace?.plan === Plan.PRO
|
|
||||||
? '10'
|
|
||||||
: '0'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StarterPlanContent
|
<StarterPlanContent
|
||||||
initialChatsLimitIndex={
|
initialChatsLimitIndex={
|
||||||
workspace?.plan === Plan.STARTER ? data?.additionalChatsIndex : 0
|
workspace?.plan === Plan.STARTER ? data?.additionalChatsIndex : 0
|
||||||
|
@ -23,10 +23,11 @@ import {
|
|||||||
getStorageLimit,
|
getStorageLimit,
|
||||||
storageLimit,
|
storageLimit,
|
||||||
parseNumberWithCommas,
|
parseNumberWithCommas,
|
||||||
|
formatPrice,
|
||||||
|
computePrice,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
||||||
import { FeaturesList } from './components/FeaturesList'
|
import { FeaturesList } from './components/FeaturesList'
|
||||||
import { computePrice, formatPrice } from './helpers'
|
|
||||||
|
|
||||||
type ProPlanContentProps = {
|
type ProPlanContentProps = {
|
||||||
initialChatsLimitIndex?: number
|
initialChatsLimitIndex?: number
|
||||||
@ -72,8 +73,6 @@ export const ProPlanContent = ({
|
|||||||
? getStorageLimit(workspace)
|
? getStorageLimit(workspace)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
console.log('workspaceChatsLimit', workspaceChatsLimit)
|
|
||||||
console.log('workspaceStorageLimit', workspace)
|
|
||||||
const isCurrentPlan =
|
const isCurrentPlan =
|
||||||
chatsLimit[Plan.PRO].totalIncluded +
|
chatsLimit[Plan.PRO].totalIncluded +
|
||||||
chatsLimit[Plan.PRO].increaseStep.amount *
|
chatsLimit[Plan.PRO].increaseStep.amount *
|
||||||
|
@ -20,10 +20,11 @@ import {
|
|||||||
getStorageLimit,
|
getStorageLimit,
|
||||||
storageLimit,
|
storageLimit,
|
||||||
parseNumberWithCommas,
|
parseNumberWithCommas,
|
||||||
|
computePrice,
|
||||||
|
formatPrice,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
||||||
import { FeaturesList } from './components/FeaturesList'
|
import { FeaturesList } from './components/FeaturesList'
|
||||||
import { computePrice, formatPrice } from './helpers'
|
|
||||||
|
|
||||||
type StarterPlanContentProps = {
|
type StarterPlanContentProps = {
|
||||||
initialChatsLimitIndex?: number
|
initialChatsLimitIndex?: number
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
import { Plan } from 'db'
|
|
||||||
import { chatsLimit, prices, storageLimit } from 'utils'
|
|
||||||
|
|
||||||
export const computePrice = (
|
|
||||||
plan: Plan,
|
|
||||||
selectedTotalChatsIndex: number,
|
|
||||||
selectedTotalStorageIndex: number
|
|
||||||
) => {
|
|
||||||
if (plan !== Plan.STARTER && plan !== Plan.PRO) return
|
|
||||||
const {
|
|
||||||
increaseStep: { price: chatsPrice },
|
|
||||||
} = chatsLimit[plan]
|
|
||||||
const {
|
|
||||||
increaseStep: { price: storagePrice },
|
|
||||||
} = storageLimit[plan]
|
|
||||||
return (
|
|
||||||
prices[plan] +
|
|
||||||
selectedTotalChatsIndex * chatsPrice +
|
|
||||||
selectedTotalStorageIndex * storagePrice
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const europeanUnionCountryCodes = [
|
|
||||||
'AT',
|
|
||||||
'BE',
|
|
||||||
'BG',
|
|
||||||
'CY',
|
|
||||||
'CZ',
|
|
||||||
'DE',
|
|
||||||
'DK',
|
|
||||||
'EE',
|
|
||||||
'ES',
|
|
||||||
'FI',
|
|
||||||
'FR',
|
|
||||||
'GR',
|
|
||||||
'HR',
|
|
||||||
'HU',
|
|
||||||
'IE',
|
|
||||||
'IT',
|
|
||||||
'LT',
|
|
||||||
'LU',
|
|
||||||
'LV',
|
|
||||||
'MT',
|
|
||||||
'NL',
|
|
||||||
'PL',
|
|
||||||
'PT',
|
|
||||||
'RO',
|
|
||||||
'SE',
|
|
||||||
'SI',
|
|
||||||
'SK',
|
|
||||||
]
|
|
||||||
|
|
||||||
const europeanUnionExclusiveLanguageCodes = [
|
|
||||||
'fr',
|
|
||||||
'de',
|
|
||||||
'it',
|
|
||||||
'el',
|
|
||||||
'pl',
|
|
||||||
'fi',
|
|
||||||
'nl',
|
|
||||||
'hr',
|
|
||||||
'cs',
|
|
||||||
'hu',
|
|
||||||
'ro',
|
|
||||||
'sl',
|
|
||||||
'sv',
|
|
||||||
'bg',
|
|
||||||
]
|
|
||||||
|
|
||||||
export const guessIfUserIsEuropean = () =>
|
|
||||||
navigator.languages.some((language) => {
|
|
||||||
const [languageCode, countryCode] = language.split('-')
|
|
||||||
return countryCode
|
|
||||||
? europeanUnionCountryCodes.includes(countryCode)
|
|
||||||
: europeanUnionExclusiveLanguageCodes.includes(languageCode)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const formatPrice = (price: number) => {
|
|
||||||
const isEuropean = guessIfUserIsEuropean()
|
|
||||||
const formatter = new Intl.NumberFormat(isEuropean ? 'fr-FR' : 'en-US', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: isEuropean ? 'EUR' : 'USD',
|
|
||||||
maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501)
|
|
||||||
})
|
|
||||||
return formatter.format(price)
|
|
||||||
}
|
|
@ -1,7 +1,12 @@
|
|||||||
import { loadStripe } from '@stripe/stripe-js/pure'
|
import { loadStripe } from '@stripe/stripe-js/pure'
|
||||||
import { Plan, User } from 'db'
|
import { Plan, User } from 'db'
|
||||||
import { env, isDefined, isEmpty, sendRequest } from 'utils'
|
import {
|
||||||
import { guessIfUserIsEuropean } from '../helpers'
|
env,
|
||||||
|
guessIfUserIsEuropean,
|
||||||
|
isDefined,
|
||||||
|
isEmpty,
|
||||||
|
sendRequest,
|
||||||
|
} from 'utils'
|
||||||
|
|
||||||
type UpgradeProps = {
|
type UpgradeProps = {
|
||||||
user: User
|
user: User
|
||||||
|
@ -155,7 +155,6 @@ const updateSubscription = async (req: NextApiRequest) => {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
].filter(isDefined)
|
].filter(isDefined)
|
||||||
console.log(items)
|
|
||||||
await stripe.subscriptions.update(subscription.id, {
|
await stripe.subscriptions.update(subscription.id, {
|
||||||
items,
|
items,
|
||||||
})
|
})
|
||||||
@ -171,7 +170,6 @@ const updateSubscription = async (req: NextApiRequest) => {
|
|||||||
|
|
||||||
const cancelSubscription =
|
const cancelSubscription =
|
||||||
(req: NextApiRequest, res: NextApiResponse) => async (userId: string) => {
|
(req: NextApiRequest, res: NextApiResponse) => async (userId: string) => {
|
||||||
console.log(req.query.stripeId, userId)
|
|
||||||
const stripeId = req.query.stripeId as string | undefined
|
const stripeId = req.query.stripeId as string | undefined
|
||||||
if (!stripeId) return badRequest(res)
|
if (!stripeId) return badRequest(res)
|
||||||
if (!process.env.STRIPE_SECRET_KEY)
|
if (!process.env.STRIPE_SECRET_KEY)
|
||||||
@ -189,9 +187,7 @@ const cancelSubscription =
|
|||||||
const existingSubscription = await stripe.subscriptions.list({
|
const existingSubscription = await stripe.subscriptions.list({
|
||||||
customer: workspace.stripeId,
|
customer: workspace.stripeId,
|
||||||
})
|
})
|
||||||
console.log('yes')
|
|
||||||
await stripe.subscriptions.del(existingSubscription.data[0].id)
|
await stripe.subscriptions.del(existingSubscription.data[0].id)
|
||||||
console.log('deleted')
|
|
||||||
await prisma.workspace.update({
|
await prisma.workspace.update({
|
||||||
where: { id: workspace.id },
|
where: { id: workspace.id },
|
||||||
data: {
|
data: {
|
||||||
|
@ -18,12 +18,12 @@ const DashboardPage = () => {
|
|||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscribePlan = query.subscribePlan as 'pro' | 'starter' | undefined
|
const subscribePlan = query.subscribePlan as Plan | undefined
|
||||||
if (workspace && subscribePlan && user && workspace.plan === 'FREE') {
|
if (workspace && subscribePlan && user && workspace.plan === 'FREE') {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
pay({
|
pay({
|
||||||
user,
|
user,
|
||||||
plan: subscribePlan === 'pro' ? Plan.PRO : Plan.STARTER,
|
plan: subscribePlan,
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
additionalChats: 0,
|
additionalChats: 0,
|
||||||
additionalStorage: 0,
|
additionalStorage: 0,
|
||||||
|
@ -34,8 +34,6 @@ const ResultsPage = () => {
|
|||||||
})
|
})
|
||||||
const { data: usageData } = useUsage(workspace?.id)
|
const { data: usageData } = useUsage(workspace?.id)
|
||||||
|
|
||||||
console.log(workspace?.id, usageData)
|
|
||||||
|
|
||||||
const chatsLimitPercentage = useMemo(() => {
|
const chatsLimitPercentage = useMemo(() => {
|
||||||
if (!usageData?.totalChatsUsed || !workspace?.plan) return 0
|
if (!usageData?.totalChatsUsed || !workspace?.plan) return 0
|
||||||
return Math.round(
|
return Math.round(
|
||||||
@ -53,7 +51,6 @@ const ResultsPage = () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
const storageLimitPercentage = useMemo(() => {
|
const storageLimitPercentage = useMemo(() => {
|
||||||
console.log(usageData?.totalStorageUsed)
|
|
||||||
if (!usageData?.totalStorageUsed || !workspace?.plan) return 0
|
if (!usageData?.totalStorageUsed || !workspace?.plan) return 0
|
||||||
return Math.round(
|
return Math.round(
|
||||||
(usageData.totalStorageUsed /
|
(usageData.totalStorageUsed /
|
||||||
|
@ -18,16 +18,19 @@ import {
|
|||||||
import { CheckIcon } from 'assets/icons/CheckIcon'
|
import { CheckIcon } from 'assets/icons/CheckIcon'
|
||||||
import { HelpCircleIcon } from 'assets/icons/HelpCircleIcon'
|
import { HelpCircleIcon } from 'assets/icons/HelpCircleIcon'
|
||||||
import { NextChakraLink } from 'components/common/nextChakraAdapters/NextChakraLink'
|
import { NextChakraLink } from 'components/common/nextChakraAdapters/NextChakraLink'
|
||||||
|
import { Plan } from 'db'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
prices: {
|
starterPrice: string
|
||||||
personalPro: '$39' | '39€' | ''
|
proPrice: string
|
||||||
team: '$99' | '99€' | ''
|
|
||||||
}
|
|
||||||
} & StackProps
|
} & StackProps
|
||||||
|
|
||||||
export const PlanComparisonTables = ({ prices, ...props }: Props) => {
|
export const PlanComparisonTables = ({
|
||||||
|
starterPrice,
|
||||||
|
proPrice,
|
||||||
|
...props
|
||||||
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
<Stack spacing="12" {...props}>
|
<Stack spacing="12" {...props}>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
@ -37,29 +40,47 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => {
|
|||||||
<Th fontWeight="bold" color="white" w="400px">
|
<Th fontWeight="bold" color="white" w="400px">
|
||||||
Usage
|
Usage
|
||||||
</Th>
|
</Th>
|
||||||
<Th>Personal</Th>
|
<Th>Free</Th>
|
||||||
<Th color="orange.200">Personal Pro</Th>
|
<Th color="orange.200">Starter</Th>
|
||||||
<Th color="purple.200">Team</Th>
|
<Th color="purple.200">Pro</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td>Forms</Td>
|
<Td>Total bots</Td>
|
||||||
<Td>Unlimited</Td>
|
<Td>Unlimited</Td>
|
||||||
<Td>Unlimited</Td>
|
<Td>Unlimited</Td>
|
||||||
<Td>Unlimited</Td>
|
<Td>Unlimited</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td>Form submissions</Td>
|
<Td>Chats</Td>
|
||||||
<Td>Unlimited</Td>
|
<Td>300 / month</Td>
|
||||||
<Td>Unlimited</Td>
|
<Td>2,000 / month</Td>
|
||||||
<Td>Unlimited</Td>
|
<Td>10,000 / month</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>Additional Chats</Td>
|
||||||
|
<Td />
|
||||||
|
<Td>$10 per 500</Td>
|
||||||
|
<Td>$10 per 1,000</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>Storage</Td>
|
||||||
|
<Td />
|
||||||
|
<Td>2 GB</Td>
|
||||||
|
<Td>10 GB</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>Additional Storage</Td>
|
||||||
|
<Td />
|
||||||
|
<Td>$5 per 1 GB</Td>
|
||||||
|
<Td>$5 per 1 GB</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td>Members</Td>
|
<Td>Members</Td>
|
||||||
<Td>Just you</Td>
|
<Td>Just you</Td>
|
||||||
<Td>Just you</Td>
|
<Td>2 seats</Td>
|
||||||
<Td>Unlimited</Td>
|
<Td>5 seats</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td>Guests</Td>
|
<Td>Guests</Td>
|
||||||
@ -67,12 +88,6 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => {
|
|||||||
<Td>Unlimited</Td>
|
<Td>Unlimited</Td>
|
||||||
<Td>Unlimited</Td>
|
<Td>Unlimited</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
<Tr>
|
|
||||||
<Td>File uploads</Td>
|
|
||||||
<Td>5 MB</Td>
|
|
||||||
<Td>Unlimited</Td>
|
|
||||||
<Td>Unlimited</Td>
|
|
||||||
</Tr>
|
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
@ -83,9 +98,9 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => {
|
|||||||
<Th fontWeight="bold" color="white" w="400px">
|
<Th fontWeight="bold" color="white" w="400px">
|
||||||
Features
|
Features
|
||||||
</Th>
|
</Th>
|
||||||
<Th>Personal</Th>
|
<Th>Free</Th>
|
||||||
<Th color="orange.200">Personal Pro</Th>
|
<Th color="orange.200">Starter</Th>
|
||||||
<Th color="purple.200">Team</Th>
|
<Th color="purple.200">Pro</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@ -234,12 +249,6 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => {
|
|||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
<Tr>
|
|
||||||
<Td>Custom domains</Td>
|
|
||||||
<Td />
|
|
||||||
<Td>Unlimited</Td>
|
|
||||||
<Td>Unlimited</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
<Tr>
|
||||||
<TdWithTooltip
|
<TdWithTooltip
|
||||||
text="Folders"
|
text="Folders"
|
||||||
@ -260,17 +269,10 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => {
|
|||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
<Tr>
|
<Tr>
|
||||||
<TdWithTooltip
|
<Td>Custom domains</Td>
|
||||||
text="Incomplete submissions"
|
|
||||||
tooltip="You get to see the form submission even if it was not fully completed by your user."
|
|
||||||
/>
|
|
||||||
<Td />
|
<Td />
|
||||||
<Td>
|
<Td />
|
||||||
<CheckIcon />
|
<Td>Unlimited</Td>
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<CheckIcon />
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
</Tr>
|
||||||
<Tr>
|
<Tr>
|
||||||
<TdWithTooltip
|
<TdWithTooltip
|
||||||
@ -278,9 +280,7 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => {
|
|||||||
tooltip="Analytics graph that shows your form drop-off rate, submission rate, and more."
|
tooltip="Analytics graph that shows your form drop-off rate, submission rate, and more."
|
||||||
/>
|
/>
|
||||||
<Td />
|
<Td />
|
||||||
<Td>
|
<Td />
|
||||||
<CheckIcon />
|
|
||||||
</Td>
|
|
||||||
<Td>
|
<Td>
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
</Td>
|
</Td>
|
||||||
@ -295,18 +295,16 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => {
|
|||||||
<Th fontWeight="bold" color="white" w="400px">
|
<Th fontWeight="bold" color="white" w="400px">
|
||||||
Support
|
Support
|
||||||
</Th>
|
</Th>
|
||||||
<Th>Personal</Th>
|
<Th>Free</Th>
|
||||||
<Th color="orange.200">Personal Pro</Th>
|
<Th color="orange.200">Starter</Th>
|
||||||
<Th color="purple.200">Team</Th>
|
<Th color="blue.200">Pro</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td>Priority support</Td>
|
<Td>Priority support</Td>
|
||||||
<Td />
|
<Td />
|
||||||
<Td>
|
<Td />
|
||||||
<CheckIcon />
|
|
||||||
</Td>
|
|
||||||
<Td>
|
<Td>
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
</Td>
|
</Td>
|
||||||
@ -314,9 +312,7 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => {
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Td>Feature request priority</Td>
|
<Td>Feature request priority</Td>
|
||||||
<Td />
|
<Td />
|
||||||
<Td>
|
<Td />
|
||||||
<CheckIcon />
|
|
||||||
</Td>
|
|
||||||
<Td>
|
<Td>
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
</Td>
|
</Td>
|
||||||
@ -344,28 +340,27 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Heading as="h3" size="md" color="orange.200">
|
<Heading as="h3" size="md" color="orange.200">
|
||||||
Personal Pro
|
Starter
|
||||||
</Heading>
|
</Heading>
|
||||||
<Heading as="h3">
|
<Heading as="h3">
|
||||||
{prices.personalPro}{' '}
|
{starterPrice} <chakra.span fontSize="lg">/ month</chakra.span>
|
||||||
<chakra.span fontSize="lg">/ month</chakra.span>
|
|
||||||
</Heading>
|
</Heading>
|
||||||
<NextChakraLink
|
<NextChakraLink
|
||||||
href="https://app.typebot.io/register?subscribePlan=pro"
|
href={`https://app.typebot.io/register?subscribePlan=${Plan.STARTER}`}
|
||||||
_hover={{ textDecor: 'none' }}
|
_hover={{ textDecor: 'none' }}
|
||||||
>
|
>
|
||||||
<Button>Subscribe</Button>
|
<Button>Subscribe</Button>
|
||||||
</NextChakraLink>
|
</NextChakraLink>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Heading as="h3" size="md" color="purple.200">
|
<Heading as="h3" size="md" color="blue.200">
|
||||||
Team
|
Pro
|
||||||
</Heading>
|
</Heading>
|
||||||
<Heading as="h3">
|
<Heading as="h3">
|
||||||
{prices.team} <chakra.span fontSize="lg">/ month</chakra.span>
|
{proPrice} <chakra.span fontSize="lg">/ month</chakra.span>
|
||||||
</Heading>
|
</Heading>
|
||||||
<NextChakraLink
|
<NextChakraLink
|
||||||
href="https://app.typebot.io/register?subscribePlan=team"
|
href={`https://app.typebot.io/register?subscribePlan=${Plan.PRO}`}
|
||||||
_hover={{ textDecor: 'none' }}
|
_hover={{ textDecor: 'none' }}
|
||||||
>
|
>
|
||||||
<Button>Subscribe</Button>
|
<Button>Subscribe</Button>
|
||||||
|
@ -13,7 +13,7 @@ import { CheckCircleIcon } from '../../../assets/icons/CheckCircleIcon'
|
|||||||
import { Card, CardProps } from './Card'
|
import { Card, CardProps } from './Card'
|
||||||
|
|
||||||
export interface PricingCardData {
|
export interface PricingCardData {
|
||||||
features: string[]
|
features: React.ReactNode[]
|
||||||
name: string
|
name: string
|
||||||
price: string
|
price: string
|
||||||
featureLabel?: string
|
featureLabel?: string
|
||||||
@ -23,10 +23,16 @@ interface PricingCardProps extends CardProps {
|
|||||||
data: PricingCardData
|
data: PricingCardData
|
||||||
icon?: JSX.Element
|
icon?: JSX.Element
|
||||||
button: React.ReactElement
|
button: React.ReactElement
|
||||||
|
isMostPopular?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PricingCard = (props: PricingCardProps) => {
|
export const PricingCard = ({
|
||||||
const { data, icon, button, ...rest } = props
|
data,
|
||||||
|
icon,
|
||||||
|
button,
|
||||||
|
isMostPopular,
|
||||||
|
...rest
|
||||||
|
}: PricingCardProps) => {
|
||||||
const { features, price, name } = data
|
const { features, price, name } = data
|
||||||
const accentColor = useColorModeValue('blue.500', 'white')
|
const accentColor = useColorModeValue('blue.500', 'white')
|
||||||
|
|
||||||
@ -62,7 +68,12 @@ export const PricingCard = (props: PricingCardProps) => {
|
|||||||
<Text fontWeight="bold">{data.featureLabel}</Text>
|
<Text fontWeight="bold">{data.featureLabel}</Text>
|
||||||
)}
|
)}
|
||||||
{features.map((feature, index) => (
|
{features.map((feature, index) => (
|
||||||
<ListItem fontWeight="medium" key={index}>
|
<ListItem
|
||||||
|
fontWeight="medium"
|
||||||
|
key={index}
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
<ListIcon
|
<ListIcon
|
||||||
fontSize="xl"
|
fontSize="xl"
|
||||||
as={CheckCircleIcon}
|
as={CheckCircleIcon}
|
||||||
|
@ -20,7 +20,8 @@
|
|||||||
"next": "12.3.0",
|
"next": "12.3.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"utils": "workspace:*"
|
"utils": "workspace:*",
|
||||||
|
"db": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.19.0",
|
"@babel/core": "7.19.0",
|
||||||
|
@ -10,7 +10,11 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Heading,
|
Heading,
|
||||||
VStack,
|
VStack,
|
||||||
|
Text,
|
||||||
|
chakra,
|
||||||
|
Tooltip,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
|
import { HelpCircleIcon } from 'assets/icons/HelpCircleIcon'
|
||||||
import { Footer } from 'components/common/Footer'
|
import { Footer } from 'components/common/Footer'
|
||||||
import { Header } from 'components/common/Header/Header'
|
import { Header } from 'components/common/Header/Header'
|
||||||
import { NextChakraLink } from 'components/common/nextChakraAdapters/NextChakraLink'
|
import { NextChakraLink } from 'components/common/nextChakraAdapters/NextChakraLink'
|
||||||
@ -20,22 +24,17 @@ import { PlanComparisonTables } from 'components/PricingPage/PlanComparisonTable
|
|||||||
import { PricingCard } from 'components/PricingPage/PricingCard'
|
import { PricingCard } from 'components/PricingPage/PricingCard'
|
||||||
import { ActionButton } from 'components/PricingPage/PricingCard/ActionButton'
|
import { ActionButton } from 'components/PricingPage/PricingCard/ActionButton'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { formatPrice, prices } from 'utils'
|
||||||
|
import { Plan } from 'db'
|
||||||
|
|
||||||
const Pricing = () => {
|
const Pricing = () => {
|
||||||
const [price, setPrice] = useState<{
|
const [starterPrice, setStarterPrice] = useState('$39')
|
||||||
personalPro: '$39' | '39€' | ''
|
const [proPrice, setProPrice] = useState('$89')
|
||||||
team: '$99' | '99€' | ''
|
|
||||||
}>({
|
|
||||||
personalPro: '',
|
|
||||||
team: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPrice(
|
if (typeof window === 'undefined') return
|
||||||
navigator.languages.find((l) => l.includes('fr'))
|
setStarterPrice(formatPrice(prices.STARTER))
|
||||||
? { personalPro: '39€', team: '99€' }
|
setProPrice(formatPrice(prices.PRO))
|
||||||
: { personalPro: '$39', team: '$99' }
|
|
||||||
)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -54,13 +53,28 @@ const Pricing = () => {
|
|||||||
<Header />
|
<Header />
|
||||||
</DarkMode>
|
</DarkMode>
|
||||||
|
|
||||||
<VStack spacing={40} w="full">
|
<VStack spacing={'24'} mt={[20, 32]} w="full">
|
||||||
|
<Stack align="center" spacing="6">
|
||||||
|
<Heading fontSize="6xl">Plans fit for you</Heading>
|
||||||
|
<Text maxW="900px" fontSize="xl" textAlign="center">
|
||||||
|
Whether you're a{' '}
|
||||||
|
<Text as="span" color="orange.200" fontWeight="bold">
|
||||||
|
solo business owner
|
||||||
|
</Text>{' '}
|
||||||
|
or a{' '}
|
||||||
|
<Text as="span" color="blue.200" fontWeight="bold">
|
||||||
|
growing startup
|
||||||
|
</Text>
|
||||||
|
, Typebot is here to help you build high-performing bots for the
|
||||||
|
right price. Pay for as little or as much usage as you need.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
direction={['column', 'row']}
|
direction={['column', 'row']}
|
||||||
alignItems={['stretch']}
|
alignItems={['stretch']}
|
||||||
spacing={10}
|
spacing={10}
|
||||||
px={[4, 0]}
|
px={[4, 0]}
|
||||||
mt={[20, 32]}
|
|
||||||
w="full"
|
w="full"
|
||||||
maxW="1200px"
|
maxW="1200px"
|
||||||
>
|
>
|
||||||
@ -70,7 +84,7 @@ const Pricing = () => {
|
|||||||
name: 'Personal',
|
name: 'Personal',
|
||||||
features: [
|
features: [
|
||||||
'Unlimited typebots',
|
'Unlimited typebots',
|
||||||
'Unlimited responses',
|
'300 chats included',
|
||||||
'Native integrations',
|
'Native integrations',
|
||||||
'Webhooks',
|
'Webhooks',
|
||||||
'Custom Javascript & CSS',
|
'Custom Javascript & CSS',
|
||||||
@ -87,58 +101,134 @@ const Pricing = () => {
|
|||||||
/>
|
/>
|
||||||
<PricingCard
|
<PricingCard
|
||||||
data={{
|
data={{
|
||||||
price: price.personalPro,
|
price: starterPrice,
|
||||||
name: 'Personal Pro',
|
name: 'Starter',
|
||||||
featureLabel: 'Everything in Personal, plus:',
|
featureLabel: 'Everything in Personal, plus:',
|
||||||
features: [
|
features: [
|
||||||
|
<Text key="seats">
|
||||||
|
<chakra.span fontWeight="bold">2 seats</chakra.span>{' '}
|
||||||
|
included
|
||||||
|
</Text>,
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
<chakra.span fontWeight="bold">2,000 chats</chakra.span>{' '}
|
||||||
|
included
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
hasArrow
|
||||||
|
placement="top"
|
||||||
|
label="A chat is counted whenever a user starts a discussion. It is
|
||||||
|
independant of the number of messages he sends and receives."
|
||||||
|
>
|
||||||
|
<chakra.span cursor="pointer" h="7">
|
||||||
|
<HelpCircleIcon />
|
||||||
|
</chakra.span>
|
||||||
|
</Tooltip>
|
||||||
|
</>,
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
<chakra.span fontWeight="bold">2 GB chats</chakra.span>{' '}
|
||||||
|
included
|
||||||
|
</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>
|
||||||
|
</>,
|
||||||
'Branding removed',
|
'Branding removed',
|
||||||
'View incomplete submissions',
|
'Collect files from users',
|
||||||
'In-depth drop off analytics',
|
'Create folders',
|
||||||
'Custom domains',
|
|
||||||
'Organize typebots in folders',
|
|
||||||
'File upload input',
|
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
borderWidth="3px"
|
borderWidth="1px"
|
||||||
borderColor="orange.200"
|
borderColor="orange.200"
|
||||||
button={
|
button={
|
||||||
<NextChakraLink
|
<NextChakraLink
|
||||||
href="https://app.typebot.io/register?subscribePlan=pro"
|
href={`https://app.typebot.io/register?subscribePlan=${Plan.STARTER}`}
|
||||||
_hover={{ textDecor: 'none' }}
|
|
||||||
>
|
|
||||||
<ActionButton colorScheme="orange">
|
|
||||||
Subscribe now
|
|
||||||
</ActionButton>
|
|
||||||
</NextChakraLink>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PricingCard
|
|
||||||
data={{
|
|
||||||
price: price.team,
|
|
||||||
name: 'Team',
|
|
||||||
featureLabel: 'Everything in Pro, plus:',
|
|
||||||
features: [
|
|
||||||
'Unlimited team members',
|
|
||||||
'Collaborative workspace',
|
|
||||||
'Custom roles',
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
borderWidth="3px"
|
|
||||||
borderColor="purple.200"
|
|
||||||
button={
|
|
||||||
<NextChakraLink
|
|
||||||
href="https://app.typebot.io/register?subscribePlan=team"
|
|
||||||
_hover={{ textDecor: 'none' }}
|
_hover={{ textDecor: 'none' }}
|
||||||
>
|
>
|
||||||
<ActionButton>Subscribe now</ActionButton>
|
<ActionButton>Subscribe now</ActionButton>
|
||||||
</NextChakraLink>
|
</NextChakraLink>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<PricingCard
|
||||||
|
data={{
|
||||||
|
price: proPrice,
|
||||||
|
name: 'Pro',
|
||||||
|
featureLabel: 'Everything in Starter, plus:',
|
||||||
|
features: [
|
||||||
|
<Text key="seats">
|
||||||
|
<chakra.span fontWeight="bold">5 seats</chakra.span>{' '}
|
||||||
|
included
|
||||||
|
</Text>,
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
<chakra.span fontWeight="bold">10,000 chats</chakra.span>{' '}
|
||||||
|
included
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
hasArrow
|
||||||
|
placement="top"
|
||||||
|
label="A chat is counted whenever a user starts a discussion. It is
|
||||||
|
independant of the number of messages he sends and receives."
|
||||||
|
>
|
||||||
|
<chakra.span cursor="pointer" h="7">
|
||||||
|
<HelpCircleIcon />
|
||||||
|
</chakra.span>
|
||||||
|
</Tooltip>
|
||||||
|
</>,
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
<chakra.span fontWeight="bold">10 GB chats</chakra.span>{' '}
|
||||||
|
included
|
||||||
|
</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>
|
||||||
|
</>,
|
||||||
|
'Custom domains',
|
||||||
|
'In-depth analytics',
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
borderWidth="3px"
|
||||||
|
borderColor="blue.200"
|
||||||
|
button={
|
||||||
|
<NextChakraLink
|
||||||
|
href={`https://app.typebot.io/register?subscribePlan=${Plan.PRO}`}
|
||||||
|
_hover={{ textDecor: 'none' }}
|
||||||
|
>
|
||||||
|
<ActionButton>Subscribe now</ActionButton>
|
||||||
|
</NextChakraLink>
|
||||||
|
}
|
||||||
|
isMostPopular
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<VStack maxW="1200px" w="full" spacing={[12, 20]} px="4">
|
<VStack maxW="1200px" w="full" spacing={[12, 20]} px="4">
|
||||||
<Stack w="full" spacing={10} display={['none', 'flex']}>
|
<Stack w="full" spacing={10} display={['none', 'flex']}>
|
||||||
<Heading>Compare plans & features</Heading>
|
<Heading>Compare plans & features</Heading>
|
||||||
<PlanComparisonTables prices={price} />
|
<PlanComparisonTables
|
||||||
|
starterPrice={starterPrice}
|
||||||
|
proPrice={proPrice}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<VStack w="full" spacing="10">
|
<VStack w="full" spacing="10">
|
||||||
<Heading textAlign="center">Frequently asked questions</Heading>
|
<Heading textAlign="center">Frequently asked questions</Heading>
|
||||||
|
@ -10,9 +10,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"docker:up": "docker compose -f docker-compose.dev.yml up -d",
|
"docker:up": "docker compose -f docker-compose.dev.yml up -d",
|
||||||
"docker:nuke": "docker compose -f docker-compose.dev.yml down --volumes --remove-orphans",
|
"docker:nuke": "docker compose -f docker-compose.dev.yml down --volumes --remove-orphans",
|
||||||
"dev:prepare": "turbo run build --scope=bot-engine --no-deps --include-dependencies && turbo run build --scope=typebot-js --no-deps",
|
"dev": "pnpm docker:up && NEXT_PUBLIC_E2E_TEST=false turbo run dev --filter=builder... --filter=viewer... --parallel --no-cache",
|
||||||
"dev": "pnpm docker:up && pnpm dev:prepare && NEXT_PUBLIC_E2E_TEST=false turbo run dev --filter=builder --filter=viewer --parallel --no-cache",
|
"dev:mocking": "pnpm docker:up && NEXT_PUBLIC_E2E_TEST=true turbo run dev --filter=builder... --filter=viewer... --parallel --no-cache",
|
||||||
"dev:mocking": "pnpm docker:up && pnpm dev:prepare && NEXT_PUBLIC_E2E_TEST=true turbo run dev --filter=builder --filter=viewer --parallel --no-cache",
|
|
||||||
"build": "pnpm docker:up && turbo run build",
|
"build": "pnpm docker:up && turbo run build",
|
||||||
"build:builder": "turbo run build --filter=builder... && ENVSH_ENV=./apps/builder/.env.docker ENVSH_OUTPUT=./apps/builder/public/__env.js bash env.sh",
|
"build:builder": "turbo run build --filter=builder... && ENVSH_ENV=./apps/builder/.env.docker ENVSH_OUTPUT=./apps/builder/public/__env.js bash env.sh",
|
||||||
"build:viewer": "turbo run build --filter=viewer... && ENVSH_ENV=./apps/viewer/.env.docker ENVSH_OUTPUT=./apps/viewer/public/__env.js bash env.sh",
|
"build:viewer": "turbo run build --filter=viewer... && ENVSH_ENV=./apps/viewer/.env.docker ENVSH_OUTPUT=./apps/viewer/public/__env.js bash env.sh",
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"unpkg": "dist/index.umd.min.js",
|
"unpkg": "dist/index.umd.min.js",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev": "pnpm rollup -c --watch",
|
||||||
"build": "pnpm lint && rollup -c",
|
"build": "pnpm lint && rollup -c",
|
||||||
"lint": "eslint src --ext .ts && eslint tests --ext .ts",
|
"lint": "eslint src --ext .ts && eslint tests --ext .ts",
|
||||||
"test": "pnpm jest"
|
"test": "pnpm jest"
|
||||||
|
@ -83,3 +83,87 @@ export const getStorageLimit = ({
|
|||||||
: { amount: 0 }
|
: { amount: 0 }
|
||||||
return totalIncluded + increaseStep.amount * additionalStorageIndex
|
return totalIncluded + increaseStep.amount * additionalStorageIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const computePrice = (
|
||||||
|
plan: Plan,
|
||||||
|
selectedTotalChatsIndex: number,
|
||||||
|
selectedTotalStorageIndex: number
|
||||||
|
) => {
|
||||||
|
if (plan !== Plan.STARTER && plan !== Plan.PRO) return
|
||||||
|
const {
|
||||||
|
increaseStep: { price: chatsPrice },
|
||||||
|
} = chatsLimit[plan]
|
||||||
|
const {
|
||||||
|
increaseStep: { price: storagePrice },
|
||||||
|
} = storageLimit[plan]
|
||||||
|
return (
|
||||||
|
prices[plan] +
|
||||||
|
selectedTotalChatsIndex * chatsPrice +
|
||||||
|
selectedTotalStorageIndex * storagePrice
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const europeanUnionCountryCodes = [
|
||||||
|
'AT',
|
||||||
|
'BE',
|
||||||
|
'BG',
|
||||||
|
'CY',
|
||||||
|
'CZ',
|
||||||
|
'DE',
|
||||||
|
'DK',
|
||||||
|
'EE',
|
||||||
|
'ES',
|
||||||
|
'FI',
|
||||||
|
'FR',
|
||||||
|
'GR',
|
||||||
|
'HR',
|
||||||
|
'HU',
|
||||||
|
'IE',
|
||||||
|
'IT',
|
||||||
|
'LT',
|
||||||
|
'LU',
|
||||||
|
'LV',
|
||||||
|
'MT',
|
||||||
|
'NL',
|
||||||
|
'PL',
|
||||||
|
'PT',
|
||||||
|
'RO',
|
||||||
|
'SE',
|
||||||
|
'SI',
|
||||||
|
'SK',
|
||||||
|
]
|
||||||
|
|
||||||
|
const europeanUnionExclusiveLanguageCodes = [
|
||||||
|
'fr',
|
||||||
|
'de',
|
||||||
|
'it',
|
||||||
|
'el',
|
||||||
|
'pl',
|
||||||
|
'fi',
|
||||||
|
'nl',
|
||||||
|
'hr',
|
||||||
|
'cs',
|
||||||
|
'hu',
|
||||||
|
'ro',
|
||||||
|
'sl',
|
||||||
|
'sv',
|
||||||
|
'bg',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const guessIfUserIsEuropean = () =>
|
||||||
|
navigator.languages.some((language) => {
|
||||||
|
const [languageCode, countryCode] = language.split('-')
|
||||||
|
return countryCode
|
||||||
|
? europeanUnionCountryCodes.includes(countryCode)
|
||||||
|
: europeanUnionExclusiveLanguageCodes.includes(languageCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const formatPrice = (price: number) => {
|
||||||
|
const isEuropean = guessIfUserIsEuropean()
|
||||||
|
const formatter = new Intl.NumberFormat(isEuropean ? 'fr-FR' : 'en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: isEuropean ? 'EUR' : 'USD',
|
||||||
|
maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501)
|
||||||
|
})
|
||||||
|
return formatter.format(price)
|
||||||
|
}
|
||||||
|
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@ -264,6 +264,7 @@ importers:
|
|||||||
autoprefixer: 10.4.8
|
autoprefixer: 10.4.8
|
||||||
bot-engine: workspace:*
|
bot-engine: workspace:*
|
||||||
cross-env: ^7.0.3
|
cross-env: ^7.0.3
|
||||||
|
db: workspace:*
|
||||||
eslint: 8.23.0
|
eslint: 8.23.0
|
||||||
eslint-config-next: 12.3.0
|
eslint-config-next: 12.3.0
|
||||||
eslint-plugin-react: ^7.31.8
|
eslint-plugin-react: ^7.31.8
|
||||||
@ -285,6 +286,7 @@ importers:
|
|||||||
'@emotion/styled': 11.10.4_fegg7422thxjtv2g43ohoqlm7a
|
'@emotion/styled': 11.10.4_fegg7422thxjtv2g43ohoqlm7a
|
||||||
aos: 2.3.4
|
aos: 2.3.4
|
||||||
bot-engine: link:../../packages/bot-engine
|
bot-engine: link:../../packages/bot-engine
|
||||||
|
db: link:../../packages/db
|
||||||
focus-visible: 5.2.0
|
focus-visible: 5.2.0
|
||||||
framer-motion: 7.3.2_biqbaboplfbrettd7655fr4n2y
|
framer-motion: 7.3.2_biqbaboplfbrettd7655fr4n2y
|
||||||
models: link:../../packages/models
|
models: link:../../packages/models
|
||||||
|
Reference in New Issue
Block a user