(billing) Automatic usage-based billing (#924)

BREAKING CHANGE: Stripe environment variables simplified. Check out the
new configs to adapt your existing system.

Closes #906





<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
### Summary by CodeRabbit

**New Features:**
- Introduced a usage-based billing system, providing more flexibility
and options for users.
- Integrated with Stripe for a smoother and more secure payment process.
- Enhanced the user interface with improvements to the billing,
workspace, and pricing pages for a more intuitive experience.

**Improvements:**
- Simplified the billing logic, removing additional chats and yearly
billing for a more streamlined user experience.
- Updated email notifications to keep users informed about their usage
and limits.
- Improved pricing and currency formatting for better clarity and
understanding.

**Testing:**
- Updated tests and specifications to ensure the reliability of new
features and improvements.

**Note:** These changes aim to provide a more flexible and user-friendly
billing system, with clearer pricing and improved notifications. Users
should find the new system more intuitive and easier to navigate.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Baptiste Arnaud
2023-10-17 08:03:30 +02:00
committed by GitHub
parent a8c2deb258
commit 797751b418
55 changed files with 1589 additions and 1497 deletions

View File

@@ -1,9 +1,8 @@
import { Stack, HStack, Text, Switch, Tag } from '@chakra-ui/react'
import { Stack, HStack, Text } from '@chakra-ui/react'
import { Plan } from '@typebot.io/prisma'
import { TextLink } from '@/components/TextLink'
import { useToast } from '@/hooks/useToast'
import { trpc } from '@/lib/trpc'
import { guessIfUserIsEuropean } from '@typebot.io/lib/pricing'
import { Workspace } from '@typebot.io/schemas'
import { PreCheckoutModal, PreCheckoutModalProps } from './PreCheckoutModal'
import { useState } from 'react'
@@ -13,6 +12,7 @@ import { StarterPlanPricingCard } from './StarterPlanPricingCard'
import { ProPlanPricingCard } from './ProPlanPricingCard'
import { useScopedI18n } from '@/locales'
import { StripeClimateLogo } from './StripeClimateLogo'
import { guessIfUserIsEuropean } from '@typebot.io/lib/billing/guessIfUserIsEuropean'
type Props = {
workspace: Workspace
@@ -26,21 +26,12 @@ export const ChangePlanForm = ({ workspace, excludedPlans }: Props) => {
const { showToast } = useToast()
const [preCheckoutPlan, setPreCheckoutPlan] =
useState<PreCheckoutModalProps['selectedSubscription']>()
const [isYearly, setIsYearly] = useState(true)
const trpcContext = trpc.useContext()
const { data, refetch } = trpc.billing.getSubscription.useQuery(
{
workspaceId: workspace.id,
},
{
onSuccess: ({ subscription }) => {
if (isYearly === false) return
setIsYearly(subscription?.isYearly ?? true)
},
}
)
const { data, refetch } = trpc.billing.getSubscription.useQuery({
workspaceId: workspace.id,
})
const { mutate: updateSubscription, isLoading: isUpdatingSubscription } =
trpc.billing.updateSubscription.useMutation({
@@ -65,23 +56,15 @@ export const ChangePlanForm = ({ workspace, excludedPlans }: Props) => {
},
})
const handlePayClick = async ({
plan,
selectedChatsLimitIndex,
}: {
plan: 'STARTER' | 'PRO'
selectedChatsLimitIndex: number
}) => {
if (!user || selectedChatsLimitIndex === undefined) return
const handlePayClick = async (plan: 'STARTER' | 'PRO') => {
if (!user) return
const newSubscription = {
plan,
workspaceId: workspace.id,
additionalChats: selectedChatsLimitIndex,
currency:
data?.subscription?.currency ??
(guessIfUserIsEuropean() ? 'eur' : 'usd'),
isYearly,
} as const
if (workspace.stripeId) {
updateSubscription({
@@ -122,26 +105,11 @@ export const ChangePlanForm = ({ workspace, excludedPlans }: Props) => {
)}
{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">
{excludedPlans?.includes('STARTER') ? null : (
<StarterPlanPricingCard
workspace={workspace}
currentSubscription={{ isYearly: data.subscription?.isYearly }}
onPayClick={(props) =>
handlePayClick({ ...props, plan: Plan.STARTER })
}
isYearly={isYearly}
currentPlan={workspace.plan}
onPayClick={() => handlePayClick(Plan.STARTER)}
isLoading={isUpdatingSubscription}
currency={data.subscription?.currency}
/>
@@ -149,12 +117,8 @@ export const ChangePlanForm = ({ workspace, excludedPlans }: Props) => {
{excludedPlans?.includes('PRO') ? null : (
<ProPlanPricingCard
workspace={workspace}
currentSubscription={{ isYearly: data.subscription?.isYearly }}
onPayClick={(props) =>
handlePayClick({ ...props, plan: Plan.PRO })
}
isYearly={isYearly}
currentPlan={workspace.plan}
onPayClick={() => handlePayClick(Plan.PRO)}
isLoading={isUpdatingSubscription}
currency={data.subscription?.currency}
/>

View File

@@ -0,0 +1,91 @@
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
Stack,
ModalFooter,
Heading,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
} from '@chakra-ui/react'
import { proChatTiers } from '@typebot.io/lib/billing/constants'
import { formatPrice } from '@typebot.io/lib/billing/formatPrice'
type Props = {
isOpen: boolean
onClose: () => void
}
export const ChatsProTiersModal = ({ isOpen, onClose }: Props) => {
return (
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Heading size="lg">Chats pricing table</Heading>
</ModalHeader>
<ModalCloseButton />
<ModalBody as={Stack} spacing="6">
<TableContainer>
<Table variant="simple">
<Thead>
<Tr>
<Th isNumeric>Max chats</Th>
<Th isNumeric>Price per month</Th>
<Th isNumeric>Price per 1k chats</Th>
</Tr>
</Thead>
<Tbody>
{proChatTiers.map((tier, index) => {
const pricePerMonth =
proChatTiers
.slice(0, index + 1)
.reduce(
(acc, slicedTier) =>
acc + (slicedTier.flat_amount ?? 0),
0
) / 100
return (
<Tr key={tier.up_to}>
<Td isNumeric>
{tier.up_to === 'inf'
? '2,000,000+'
: tier.up_to.toLocaleString()}
</Td>
<Td isNumeric>
{index === 0 ? 'included' : formatPrice(pricePerMonth)}
</Td>
<Td isNumeric>
{index === proChatTiers.length - 1
? formatPrice(4.42, { maxFractionDigits: 2 })
: index === 0
? 'included'
: formatPrice(
(((pricePerMonth * 100) /
((tier.up_to as number) -
(proChatTiers.at(0)?.up_to as number))) *
1000) /
100,
{ maxFractionDigits: 2 }
)}
</Td>
</Tr>
)
})}
</Tbody>
</Table>
</TableContainer>
</ModalBody>
<ModalFooter />
</ModalContent>
</Modal>
)
}

View File

@@ -12,8 +12,8 @@ type FeaturesListProps = { features: (string | JSX.Element)[] } & ListProps
export const FeaturesList = ({ features, ...props }: FeaturesListProps) => (
<UnorderedList listStyleType="none" spacing={2} {...props}>
{features.map((feat, idx) => (
<Flex as={ListItem} key={idx} alignItems="center">
<ListIcon as={CheckIcon} />
<Flex as={ListItem} key={idx}>
<ListIcon as={CheckIcon} mt="1.5" />
{feat}
</Flex>
))}

View File

@@ -25,9 +25,7 @@ export type PreCheckoutModalProps = {
| {
plan: 'STARTER' | 'PRO'
workspaceId: string
additionalChats: number
currency: 'eur' | 'usd'
isYearly: boolean
}
| undefined
existingCompany?: string

View File

@@ -3,226 +3,152 @@ import {
Heading,
chakra,
HStack,
Menu,
MenuButton,
Button,
MenuList,
MenuItem,
Text,
Tooltip,
Flex,
Tag,
useColorModeValue,
useDisclosure,
} from '@chakra-ui/react'
import { ChevronLeftIcon } from '@/components/icons'
import { Plan } from '@typebot.io/prisma'
import { useEffect, useState } from 'react'
import { isDefined, parseNumberWithCommas } from '@typebot.io/lib'
import {
chatsLimit,
computePrice,
formatPrice,
getChatsLimit,
} from '@typebot.io/lib/pricing'
import { FeaturesList } from './FeaturesList'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { useI18n, useScopedI18n } from '@/locales'
import { Workspace } from '@typebot.io/schemas'
import { formatPrice } from '@typebot.io/lib/billing/formatPrice'
import { ChatsProTiersModal } from './ChatsProTiersModal'
import { prices } from '@typebot.io/lib/billing/constants'
type Props = {
workspace: Pick<
Workspace,
| 'additionalChatsIndex'
| 'plan'
| 'customChatsLimit'
| 'customStorageLimit'
| 'stripeId'
>
currentSubscription: {
isYearly?: boolean
}
currentPlan: Plan
currency?: 'usd' | 'eur'
isLoading: boolean
isYearly: boolean
onPayClick: (props: { selectedChatsLimitIndex: number }) => void
onPayClick: () => void
}
export const ProPlanPricingCard = ({
workspace,
currentSubscription,
currentPlan,
currency,
isLoading,
isYearly,
onPayClick,
}: Props) => {
const t = useI18n()
const scopedT = useScopedI18n('billing.pricingCard')
const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] =
useState<number>()
useEffect(() => {
if (isDefined(selectedChatsLimitIndex)) return
if (workspace.plan !== Plan.PRO) {
setSelectedChatsLimitIndex(0)
return
}
setSelectedChatsLimitIndex(workspace.additionalChatsIndex ?? 0)
}, [selectedChatsLimitIndex, workspace.additionalChatsIndex, workspace.plan])
const workspaceChatsLimit = workspace ? getChatsLimit(workspace) : undefined
const isCurrentPlan =
chatsLimit[Plan.PRO].graduatedPrice[selectedChatsLimitIndex ?? 0]
.totalIncluded === workspaceChatsLimit &&
isYearly === currentSubscription?.isYearly
const { isOpen, onOpen, onClose } = useDisclosure()
const getButtonLabel = () => {
if (selectedChatsLimitIndex === undefined) return ''
if (workspace?.plan === Plan.PRO) {
if (isCurrentPlan) return scopedT('upgradeButton.current')
if (selectedChatsLimitIndex !== workspace.additionalChatsIndex)
return t('update')
}
if (currentPlan === Plan.PRO) return scopedT('upgradeButton.current')
return t('upgrade')
}
const handlePayClick = async () => {
if (selectedChatsLimitIndex === undefined) return
onPayClick({
selectedChatsLimitIndex,
})
}
const price =
computePrice(
Plan.PRO,
selectedChatsLimitIndex ?? 0,
isYearly ? 'yearly' : 'monthly'
) ?? NaN
return (
<Flex
p="6"
pos="relative"
h="full"
flexDir="column"
flex="1"
flexShrink={0}
borderWidth="1px"
borderColor={useColorModeValue('blue.500', 'blue.300')}
rounded="lg"
>
<Flex justifyContent="center">
<Tag
pos="absolute"
top="-10px"
colorScheme="blue"
bg={useColorModeValue('blue.500', 'blue.400')}
variant="solid"
fontWeight="semibold"
style={{ marginTop: 0 }}
>
{scopedT('pro.mostPopularLabel')}
</Tag>
</Flex>
<Stack justifyContent="space-between" h="full">
<Stack spacing="4" mt={2}>
<Heading fontSize="2xl">
{scopedT('heading', {
plan: (
<chakra.span color={useColorModeValue('blue.400', 'blue.300')}>
Pro
</chakra.span>
),
})}
</Heading>
<Text>{scopedT('pro.description')}</Text>
</Stack>
<Stack spacing="4">
<Heading>
{formatPrice(price, currency)}
<chakra.span fontSize="md">{scopedT('perMonth')}</chakra.span>
</Heading>
<Text fontWeight="bold">
<Tooltip
label={
<FeaturesList
features={[
scopedT('starter.brandingRemoved'),
scopedT('starter.fileUploadBlock'),
scopedT('starter.createFolders'),
]}
spacing="0"
/>
}
hasArrow
placement="top"
>
<chakra.span textDecoration="underline" cursor="pointer">
{scopedT('pro.everythingFromStarter')}
</chakra.span>
</Tooltip>
{scopedT('plus')}
</Text>
<FeaturesList
features={[
scopedT('pro.includedSeats'),
<HStack key="test">
<Text>
<Menu>
<MenuButton
as={Button}
rightIcon={<ChevronLeftIcon transform="rotate(-90deg)" />}
size="sm"
isLoading={selectedChatsLimitIndex === undefined}
>
{selectedChatsLimitIndex !== undefined
? parseNumberWithCommas(
chatsLimit.PRO.graduatedPrice[
selectedChatsLimitIndex
].totalIncluded
)
: undefined}
</MenuButton>
<MenuList>
{chatsLimit.PRO.graduatedPrice.map((price, index) => (
<MenuItem
key={index}
onClick={() => setSelectedChatsLimitIndex(index)}
>
{parseNumberWithCommas(price.totalIncluded)}
</MenuItem>
))}
</MenuList>
</Menu>{' '}
{scopedT('chatsPerMonth')}
</Text>
<MoreInfoTooltip>{scopedT('chatsTooltip')}</MoreInfoTooltip>
</HStack>,
scopedT('pro.whatsAppIntegration'),
scopedT('pro.customDomains'),
scopedT('pro.analytics'),
]}
/>
<Stack spacing={3}>
{isYearly && workspace.stripeId && !isCurrentPlan && (
<Heading mt="0" fontSize="md">
You pay {formatPrice(price * 12, currency)} / year
<>
<ChatsProTiersModal isOpen={isOpen} onClose={onClose} />{' '}
<Flex
p="6"
pos="relative"
h="full"
flexDir="column"
flex="1"
flexShrink={0}
borderWidth="1px"
borderColor={useColorModeValue('blue.500', 'blue.300')}
rounded="lg"
>
<Flex justifyContent="center">
<Tag
pos="absolute"
top="-10px"
colorScheme="blue"
bg={useColorModeValue('blue.500', 'blue.400')}
variant="solid"
fontWeight="semibold"
style={{ marginTop: 0 }}
>
{scopedT('pro.mostPopularLabel')}
</Tag>
</Flex>
<Stack justifyContent="space-between" h="full">
<Stack spacing="4" mt={2}>
<Heading fontSize="2xl">
{scopedT('heading', {
plan: (
<chakra.span
color={useColorModeValue('blue.400', 'blue.300')}
>
Pro
</chakra.span>
),
})}
</Heading>
<Text>{scopedT('pro.description')}</Text>
</Stack>
<Stack spacing="8">
<Stack spacing="4">
<Heading>
{formatPrice(prices.PRO, { currency })}
<chakra.span fontSize="md">{scopedT('perMonth')}</chakra.span>
</Heading>
)}
<Text fontWeight="bold">
<Tooltip
label={
<FeaturesList
features={[
scopedT('starter.brandingRemoved'),
scopedT('starter.fileUploadBlock'),
scopedT('starter.createFolders'),
]}
spacing="0"
/>
}
hasArrow
placement="top"
>
<chakra.span textDecoration="underline" cursor="pointer">
{scopedT('pro.everythingFromStarter')}
</chakra.span>
</Tooltip>
{scopedT('plus')}
</Text>
<FeaturesList
features={[
scopedT('pro.includedSeats'),
<Stack key="starter-chats" spacing={1}>
<HStack key="test">
<Text>10,000 {scopedT('chatsPerMonth')}</Text>
<MoreInfoTooltip>
{scopedT('chatsTooltip')}
</MoreInfoTooltip>
</HStack>
<Text
fontSize="sm"
color={useColorModeValue('gray.500', 'gray.400')}
>
Extra chats:{' '}
<Button size="xs" variant="outline" onClick={onOpen}>
See tiers
</Button>
</Text>
</Stack>,
scopedT('pro.whatsAppIntegration'),
scopedT('pro.customDomains'),
scopedT('pro.analytics'),
]}
/>
</Stack>
<Button
colorScheme="blue"
variant="outline"
onClick={handlePayClick}
onClick={onPayClick}
isLoading={isLoading}
isDisabled={isCurrentPlan}
isDisabled={currentPlan === Plan.PRO}
>
{getButtonLabel()}
</Button>
</Stack>
</Stack>
</Stack>
</Flex>
</Flex>
</>
)
}

View File

@@ -3,174 +3,96 @@ import {
Heading,
chakra,
HStack,
Menu,
MenuButton,
Button,
MenuList,
MenuItem,
Text,
useColorModeValue,
} from '@chakra-ui/react'
import { ChevronLeftIcon } from '@/components/icons'
import { Plan } from '@typebot.io/prisma'
import { useEffect, useState } from 'react'
import { isDefined, parseNumberWithCommas } from '@typebot.io/lib'
import {
chatsLimit,
computePrice,
formatPrice,
getChatsLimit,
} from '@typebot.io/lib/pricing'
import { FeaturesList } from './FeaturesList'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { useI18n, useScopedI18n } from '@/locales'
import { Workspace } from '@typebot.io/schemas'
import { formatPrice } from '@typebot.io/lib/billing/formatPrice'
import { prices } from '@typebot.io/lib/billing/constants'
type Props = {
workspace: Pick<
Workspace,
| 'additionalChatsIndex'
| 'plan'
| 'customChatsLimit'
| 'customStorageLimit'
| 'stripeId'
>
currentSubscription: {
isYearly?: boolean
}
currentPlan: Plan
currency?: 'eur' | 'usd'
isLoading?: boolean
isYearly: boolean
onPayClick: (props: { selectedChatsLimitIndex: number }) => void
onPayClick: () => void
}
export const StarterPlanPricingCard = ({
workspace,
currentSubscription,
currentPlan,
isLoading,
currency,
isYearly,
onPayClick,
}: Props) => {
const t = useI18n()
const scopedT = useScopedI18n('billing.pricingCard')
const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] =
useState<number>()
useEffect(() => {
if (isDefined(selectedChatsLimitIndex)) return
if (workspace.plan !== Plan.STARTER) {
setSelectedChatsLimitIndex(0)
return
}
setSelectedChatsLimitIndex(workspace.additionalChatsIndex ?? 0)
}, [selectedChatsLimitIndex, workspace.additionalChatsIndex, workspace.plan])
const workspaceChatsLimit = workspace ? getChatsLimit(workspace) : undefined
const isCurrentPlan =
chatsLimit[Plan.STARTER].graduatedPrice[selectedChatsLimitIndex ?? 0]
.totalIncluded === workspaceChatsLimit &&
isYearly === currentSubscription?.isYearly
const getButtonLabel = () => {
if (selectedChatsLimitIndex === undefined) return ''
if (workspace?.plan === Plan.PRO) return t('downgrade')
if (workspace?.plan === Plan.STARTER) {
if (isCurrentPlan) return scopedT('upgradeButton.current')
if (
selectedChatsLimitIndex !== workspace.additionalChatsIndex ||
isYearly !== currentSubscription?.isYearly
)
return t('update')
}
if (currentPlan === Plan.PRO) return t('downgrade')
if (currentPlan === Plan.STARTER) return scopedT('upgradeButton.current')
return t('upgrade')
}
const handlePayClick = async () => {
if (selectedChatsLimitIndex === undefined) return
onPayClick({
selectedChatsLimitIndex,
})
}
const price =
computePrice(
Plan.STARTER,
selectedChatsLimitIndex ?? 0,
isYearly ? 'yearly' : 'monthly'
) ?? NaN
return (
<Stack spacing={6} p="6" rounded="lg" borderWidth="1px" flex="1" h="full">
<Stack
spacing={6}
p="6"
rounded="lg"
borderWidth="1px"
flex="1"
h="full"
justifyContent="space-between"
pt="8"
>
<Stack spacing="4">
<Heading fontSize="2xl">
{scopedT('heading', {
plan: <chakra.span color="orange.400">Starter</chakra.span>,
})}
</Heading>
<Text>{scopedT('starter.description')}</Text>
<Heading>
{formatPrice(price, currency)}
<chakra.span fontSize="md">{scopedT('perMonth')}</chakra.span>
</Heading>
<Stack>
<Stack spacing="4">
<Heading fontSize="2xl">
{scopedT('heading', {
plan: <chakra.span color="orange.400">Starter</chakra.span>,
})}
</Heading>
<Text>{scopedT('starter.description')}</Text>
</Stack>
<Heading>
{formatPrice(prices.STARTER, { currency })}
<chakra.span fontSize="md">{scopedT('perMonth')}</chakra.span>
</Heading>
</Stack>
<FeaturesList
features={[
scopedT('starter.includedSeats'),
<HStack key="test">
<Text>
<Menu>
<MenuButton
as={Button}
rightIcon={<ChevronLeftIcon transform="rotate(-90deg)" />}
size="sm"
isLoading={selectedChatsLimitIndex === undefined}
>
{selectedChatsLimitIndex !== undefined
? parseNumberWithCommas(
chatsLimit.STARTER.graduatedPrice[
selectedChatsLimitIndex
].totalIncluded
)
: undefined}
</MenuButton>
<MenuList>
{chatsLimit.STARTER.graduatedPrice.map((price, index) => (
<MenuItem
key={index}
onClick={() => setSelectedChatsLimitIndex(index)}
>
{parseNumberWithCommas(price.totalIncluded)}
</MenuItem>
))}
</MenuList>
</Menu>{' '}
{scopedT('chatsPerMonth')}
<Stack key="starter-chats" spacing={0}>
<HStack>
<Text>2,000 {scopedT('chatsPerMonth')}</Text>
<MoreInfoTooltip>{scopedT('chatsTooltip')}</MoreInfoTooltip>
</HStack>
<Text
fontSize="sm"
color={useColorModeValue('gray.500', 'gray.400')}
>
Extra chats: $10 per 500
</Text>
<MoreInfoTooltip>{scopedT('chatsTooltip')}</MoreInfoTooltip>
</HStack>,
</Stack>,
scopedT('starter.brandingRemoved'),
scopedT('starter.fileUploadBlock'),
scopedT('starter.createFolders'),
]}
/>
</Stack>
<Stack>
{isYearly && workspace.stripeId && !isCurrentPlan && (
<Heading mt="0" fontSize="md">
You pay: {formatPrice(price * 12, currency)} / year
</Heading>
)}
<Button
colorScheme="orange"
variant="outline"
onClick={handlePayClick}
isLoading={isLoading}
isDisabled={isCurrentPlan}
>
{getButtonLabel()}
</Button>
</Stack>
<Button
colorScheme="orange"
variant="outline"
onClick={onPayClick}
isLoading={isLoading}
isDisabled={currentPlan === Plan.STARTER}
>
{getButtonLabel()}
</Button>
</Stack>
)
}

View File

@@ -12,9 +12,9 @@ import { AlertIcon } from '@/components/icons'
import { Workspace } from '@typebot.io/prisma'
import React from 'react'
import { parseNumberWithCommas } from '@typebot.io/lib'
import { getChatsLimit } from '@typebot.io/lib/pricing'
import { defaultQueryOptions, trpc } from '@/lib/trpc'
import { useScopedI18n } from '@/locales'
import { getChatsLimit } from '@typebot.io/lib/billing/getChatsLimit'
type Props = {
workspace: Workspace
@@ -65,7 +65,7 @@ export const UsageProgressBars = ({ workspace }: Props) => {
</Tooltip>
)}
<Text fontSize="sm" fontStyle="italic" color="gray.500">
{scopedT('chats.resetInfo')}
(Resets on {data?.resetsAt.toLocaleDateString()})
</Text>
</HStack>
@@ -90,9 +90,8 @@ export const UsageProgressBars = ({ workspace }: Props) => {
h="5px"
value={chatsPercentage}
rounded="full"
hasStripe
isIndeterminate={isLoading}
colorScheme={totalChatsUsed >= workspaceChatsLimit ? 'red' : 'blue'}
colorScheme={'blue'}
/>
</Stack>
</Stack>