From c94a6581be077d5c8403004f688d3213a80d80a5 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Sun, 18 Sep 2022 19:01:37 +0200 Subject: [PATCH] :sparkles: (lp) Add new pricing page --- .../BillingContent/BillingContent.tsx | 30 +-- .../CurrentSubscriptionContent.tsx | 29 +-- .../shared/ChangePlanForm/ChangePlanForm.tsx | 11 +- .../shared/ChangePlanForm/ProPlanContent.tsx | 5 +- .../ChangePlanForm/StarterPlanContent.tsx | 3 +- .../shared/ChangePlanForm/helpers.ts | 86 -------- .../ChangePlanForm/queries/updatePlan.tsx | 9 +- apps/builder/pages/api/stripe/subscription.ts | 4 - apps/builder/pages/typebots.tsx | 4 +- .../pages/typebots/[typebotId]/results.tsx | 3 - .../PricingPage/PlanComparisonTables.tsx | 115 +++++------ .../PricingPage/PricingCard/index.tsx | 19 +- apps/landing-page/package.json | 3 +- apps/landing-page/pages/pricing.tsx | 188 +++++++++++++----- package.json | 5 +- packages/typebot-js/package.json | 1 + packages/utils/pricing.ts | 84 ++++++++ pnpm-lock.yaml | 2 + 18 files changed, 346 insertions(+), 255 deletions(-) delete mode 100644 apps/builder/components/shared/ChangePlanForm/helpers.ts diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/BillingContent.tsx b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/BillingContent.tsx index 5b5d23c0e..d10af211f 100644 --- a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/BillingContent.tsx +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/BillingContent.tsx @@ -13,21 +13,23 @@ export const BillingContent = () => { if (!workspace) return null return ( - - refreshWorkspace({ - plan: Plan.FREE, - additionalChatsIndex: 0, - additionalStorageIndex: 0, - }) - } - /> - {workspace.plan !== Plan.LIFETIME && workspace.plan !== Plan.OFFERED && ( - - )} + + + refreshWorkspace({ + plan: Plan.FREE, + additionalChatsIndex: 0, + additionalStorageIndex: 0, + }) + } + /> + {workspace.plan !== Plan.LIFETIME && + workspace.plan !== Plan.OFFERED && } + + {workspace.stripeId && } ) diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/CurrentSubscriptionContent.tsx b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/CurrentSubscriptionContent.tsx index b0962ba50..854057ac7 100644 --- a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/CurrentSubscriptionContent.tsx +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/CurrentSubscriptionContent.tsx @@ -4,8 +4,8 @@ import { Link, Spinner, Stack, - Flex, Button, + Heading, } from '@chakra-ui/react' import { PlanTag } from 'components/shared/PlanTag' import { Plan } from 'db' @@ -35,15 +35,29 @@ export const CurrentSubscriptionContent = ({ setIsCancelling(false) } + const isSubscribed = (plan === Plan.STARTER || plan === Plan.PRO) && stripeId + if (isCancelling) return return ( + Subscription Current workspace subscription: + {isSubscribed && ( + + Cancel my subscription + + )} - {(plan === Plan.STARTER || plan === Plan.PRO) && stripeId && ( + {isSubscribed && ( <> @@ -59,17 +73,6 @@ export const CurrentSubscriptionContent = ({ Billing Portal - - - Cancel my subscription - - )} diff --git a/apps/builder/components/shared/ChangePlanForm/ChangePlanForm.tsx b/apps/builder/components/shared/ChangePlanForm/ChangePlanForm.tsx index d07c99add..bf0dc2243 100644 --- a/apps/builder/components/shared/ChangePlanForm/ChangePlanForm.tsx +++ b/apps/builder/components/shared/ChangePlanForm/ChangePlanForm.tsx @@ -60,16 +60,7 @@ export const ChangePlanForm = () => { return ( - + { - 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) -} diff --git a/apps/builder/components/shared/ChangePlanForm/queries/updatePlan.tsx b/apps/builder/components/shared/ChangePlanForm/queries/updatePlan.tsx index 8f1e623be..439d535fe 100644 --- a/apps/builder/components/shared/ChangePlanForm/queries/updatePlan.tsx +++ b/apps/builder/components/shared/ChangePlanForm/queries/updatePlan.tsx @@ -1,7 +1,12 @@ import { loadStripe } from '@stripe/stripe-js/pure' import { Plan, User } from 'db' -import { env, isDefined, isEmpty, sendRequest } from 'utils' -import { guessIfUserIsEuropean } from '../helpers' +import { + env, + guessIfUserIsEuropean, + isDefined, + isEmpty, + sendRequest, +} from 'utils' type UpgradeProps = { user: User diff --git a/apps/builder/pages/api/stripe/subscription.ts b/apps/builder/pages/api/stripe/subscription.ts index 00e0517c1..e1bc632af 100644 --- a/apps/builder/pages/api/stripe/subscription.ts +++ b/apps/builder/pages/api/stripe/subscription.ts @@ -155,7 +155,6 @@ const updateSubscription = async (req: NextApiRequest) => { } : undefined, ].filter(isDefined) - console.log(items) await stripe.subscriptions.update(subscription.id, { items, }) @@ -171,7 +170,6 @@ const updateSubscription = async (req: NextApiRequest) => { const cancelSubscription = (req: NextApiRequest, res: NextApiResponse) => async (userId: string) => { - console.log(req.query.stripeId, userId) const stripeId = req.query.stripeId as string | undefined if (!stripeId) return badRequest(res) if (!process.env.STRIPE_SECRET_KEY) @@ -189,9 +187,7 @@ const cancelSubscription = const existingSubscription = await stripe.subscriptions.list({ customer: workspace.stripeId, }) - console.log('yes') await stripe.subscriptions.del(existingSubscription.data[0].id) - console.log('deleted') await prisma.workspace.update({ where: { id: workspace.id }, data: { diff --git a/apps/builder/pages/typebots.tsx b/apps/builder/pages/typebots.tsx index 41e3b0d99..6b380f08d 100644 --- a/apps/builder/pages/typebots.tsx +++ b/apps/builder/pages/typebots.tsx @@ -18,12 +18,12 @@ const DashboardPage = () => { const { workspace } = useWorkspace() useEffect(() => { - const subscribePlan = query.subscribePlan as 'pro' | 'starter' | undefined + const subscribePlan = query.subscribePlan as Plan | undefined if (workspace && subscribePlan && user && workspace.plan === 'FREE') { setIsLoading(true) pay({ user, - plan: subscribePlan === 'pro' ? Plan.PRO : Plan.STARTER, + plan: subscribePlan, workspaceId: workspace.id, additionalChats: 0, additionalStorage: 0, diff --git a/apps/builder/pages/typebots/[typebotId]/results.tsx b/apps/builder/pages/typebots/[typebotId]/results.tsx index 4c9773a16..d78a7c539 100644 --- a/apps/builder/pages/typebots/[typebotId]/results.tsx +++ b/apps/builder/pages/typebots/[typebotId]/results.tsx @@ -34,8 +34,6 @@ const ResultsPage = () => { }) const { data: usageData } = useUsage(workspace?.id) - console.log(workspace?.id, usageData) - const chatsLimitPercentage = useMemo(() => { if (!usageData?.totalChatsUsed || !workspace?.plan) return 0 return Math.round( @@ -53,7 +51,6 @@ const ResultsPage = () => { ]) const storageLimitPercentage = useMemo(() => { - console.log(usageData?.totalStorageUsed) if (!usageData?.totalStorageUsed || !workspace?.plan) return 0 return Math.round( (usageData.totalStorageUsed / diff --git a/apps/landing-page/components/PricingPage/PlanComparisonTables.tsx b/apps/landing-page/components/PricingPage/PlanComparisonTables.tsx index dd846639a..382bb01c4 100644 --- a/apps/landing-page/components/PricingPage/PlanComparisonTables.tsx +++ b/apps/landing-page/components/PricingPage/PlanComparisonTables.tsx @@ -18,16 +18,19 @@ import { import { CheckIcon } from 'assets/icons/CheckIcon' import { HelpCircleIcon } from 'assets/icons/HelpCircleIcon' import { NextChakraLink } from 'components/common/nextChakraAdapters/NextChakraLink' +import { Plan } from 'db' import React from 'react' type Props = { - prices: { - personalPro: '$39' | '39€' | '' - team: '$99' | '99€' | '' - } + starterPrice: string + proPrice: string } & StackProps -export const PlanComparisonTables = ({ prices, ...props }: Props) => { +export const PlanComparisonTables = ({ + starterPrice, + proPrice, + ...props +}: Props) => { return ( @@ -37,29 +40,47 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => { Usage - Personal - Personal Pro - Team + Free + Starter + Pro - Forms + Total bots Unlimited Unlimited Unlimited - Form submissions - Unlimited - Unlimited - Unlimited + Chats + 300 / month + 2,000 / month + 10,000 / month + + + Additional Chats + + $10 per 500 + $10 per 1,000 + + + Storage + + 2 GB + 10 GB + + + Additional Storage + + $5 per 1 GB + $5 per 1 GB Members Just you - Just you - Unlimited + 2 seats + 5 seats Guests @@ -67,12 +88,6 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => { Unlimited Unlimited - - File uploads - 5 MB - Unlimited - Unlimited - @@ -83,9 +98,9 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => { Features - Personal - Personal Pro - Team + Free + Starter + Pro @@ -234,12 +249,6 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => { - - Custom domains - - Unlimited - Unlimited - { - + Custom domains - - - - - - + + Unlimited { tooltip="Analytics graph that shows your form drop-off rate, submission rate, and more." /> - - - + @@ -295,18 +295,16 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => { Support - Personal - Personal Pro - Team + Free + Starter + Pro Priority support - - - + @@ -314,9 +312,7 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => { Feature request priority - - - + @@ -344,28 +340,27 @@ export const PlanComparisonTables = ({ prices, ...props }: Props) => { - Personal Pro + Starter - {prices.personalPro}{' '} - / month + {starterPrice} / month - - Team + + Pro - {prices.team} / month + {proPrice} / month diff --git a/apps/landing-page/components/PricingPage/PricingCard/index.tsx b/apps/landing-page/components/PricingPage/PricingCard/index.tsx index c29d778db..929b4b1cd 100644 --- a/apps/landing-page/components/PricingPage/PricingCard/index.tsx +++ b/apps/landing-page/components/PricingPage/PricingCard/index.tsx @@ -13,7 +13,7 @@ import { CheckCircleIcon } from '../../../assets/icons/CheckCircleIcon' import { Card, CardProps } from './Card' export interface PricingCardData { - features: string[] + features: React.ReactNode[] name: string price: string featureLabel?: string @@ -23,10 +23,16 @@ interface PricingCardProps extends CardProps { data: PricingCardData icon?: JSX.Element button: React.ReactElement + isMostPopular?: boolean } -export const PricingCard = (props: PricingCardProps) => { - const { data, icon, button, ...rest } = props +export const PricingCard = ({ + data, + icon, + button, + isMostPopular, + ...rest +}: PricingCardProps) => { const { features, price, name } = data const accentColor = useColorModeValue('blue.500', 'white') @@ -62,7 +68,12 @@ export const PricingCard = (props: PricingCardProps) => { {data.featureLabel} )} {features.map((feature, index) => ( - + { - const [price, setPrice] = useState<{ - personalPro: '$39' | '39€' | '' - team: '$99' | '99€' | '' - }>({ - personalPro: '', - team: '', - }) + const [starterPrice, setStarterPrice] = useState('$39') + const [proPrice, setProPrice] = useState('$89') useEffect(() => { - setPrice( - navigator.languages.find((l) => l.includes('fr')) - ? { personalPro: '39€', team: '99€' } - : { personalPro: '$39', team: '$99' } - ) + if (typeof window === 'undefined') return + setStarterPrice(formatPrice(prices.STARTER)) + setProPrice(formatPrice(prices.PRO)) }, []) return ( @@ -54,13 +53,28 @@ const Pricing = () => {
- + + + Plans fit for you + + Whether you're a{' '} + + solo business owner + {' '} + or a{' '} + + growing startup + + , 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. + + + @@ -70,7 +84,7 @@ const Pricing = () => { name: 'Personal', features: [ 'Unlimited typebots', - 'Unlimited responses', + '300 chats included', 'Native integrations', 'Webhooks', 'Custom Javascript & CSS', @@ -87,58 +101,134 @@ const Pricing = () => { /> + 2 seats{' '} + included + , + <> + + 2,000 chats{' '} + included + +   + + + + + + , + <> + + 2 GB chats{' '} + included + +   + + + + + + , 'Branding removed', - 'View incomplete submissions', - 'In-depth drop off analytics', - 'Custom domains', - 'Organize typebots in folders', - 'File upload input', + 'Collect files from users', + 'Create folders', ], }} - borderWidth="3px" + borderWidth="1px" borderColor="orange.200" button={ - - Subscribe now - - - } - /> - Subscribe now } /> + + 5 seats{' '} + included + , + <> + + 10,000 chats{' '} + included + +   + + + + + + , + <> + + 10 GB chats{' '} + included + +   + + + + + + , + 'Custom domains', + 'In-depth analytics', + ], + }} + borderWidth="3px" + borderColor="blue.200" + button={ + + Subscribe now + + } + isMostPopular + /> Compare plans & features - + Frequently asked questions diff --git a/package.json b/package.json index e166ce4ed..6c72cf157 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,8 @@ "scripts": { "docker:up": "docker compose -f docker-compose.dev.yml up -d", "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 && pnpm dev:prepare && NEXT_PUBLIC_E2E_TEST=false 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", + "dev": "pnpm docker:up && 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", "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:viewer": "turbo run build --filter=viewer... && ENVSH_ENV=./apps/viewer/.env.docker ENVSH_OUTPUT=./apps/viewer/public/__env.js bash env.sh", diff --git a/packages/typebot-js/package.json b/packages/typebot-js/package.json index 8dcd10136..6d3f5fcaf 100644 --- a/packages/typebot-js/package.json +++ b/packages/typebot-js/package.json @@ -5,6 +5,7 @@ "unpkg": "dist/index.umd.min.js", "license": "AGPL-3.0-or-later", "scripts": { + "dev": "pnpm rollup -c --watch", "build": "pnpm lint && rollup -c", "lint": "eslint src --ext .ts && eslint tests --ext .ts", "test": "pnpm jest" diff --git a/packages/utils/pricing.ts b/packages/utils/pricing.ts index 4ee0e17a6..1cfb14588 100644 --- a/packages/utils/pricing.ts +++ b/packages/utils/pricing.ts @@ -83,3 +83,87 @@ export const getStorageLimit = ({ : { amount: 0 } 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) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66917303e..cc9557ff2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -264,6 +264,7 @@ importers: autoprefixer: 10.4.8 bot-engine: workspace:* cross-env: ^7.0.3 + db: workspace:* eslint: 8.23.0 eslint-config-next: 12.3.0 eslint-plugin-react: ^7.31.8 @@ -285,6 +286,7 @@ importers: '@emotion/styled': 11.10.4_fegg7422thxjtv2g43ohoqlm7a aos: 2.3.4 bot-engine: link:../../packages/bot-engine + db: link:../../packages/db focus-visible: 5.2.0 framer-motion: 7.3.2_biqbaboplfbrettd7655fr4n2y models: link:../../packages/models