@ -9,18 +9,21 @@ import {
|
|||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
||||||
import { LimitReached } from '@/features/billing/types'
|
import { useI18n } from '@/locales'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
contentLabel: React.ReactNode
|
||||||
|
buttonLabel?: string
|
||||||
|
type?: string
|
||||||
|
} & AlertProps
|
||||||
|
|
||||||
export const UnlockPlanAlertInfo = ({
|
export const UnlockPlanAlertInfo = ({
|
||||||
contentLabel,
|
contentLabel,
|
||||||
buttonLabel = 'More info',
|
buttonLabel,
|
||||||
type,
|
type,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: Props) => {
|
||||||
contentLabel: React.ReactNode
|
const t = useI18n()
|
||||||
buttonLabel?: string
|
|
||||||
type?: LimitReached
|
|
||||||
} & AlertProps) => {
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
@ -40,7 +43,7 @@ export const UnlockPlanAlertInfo = ({
|
|||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
ml="2"
|
ml="2"
|
||||||
>
|
>
|
||||||
{buttonLabel}
|
{buttonLabel ?? t('billing.upgradeAlert.buttonDefaultLabel')}
|
||||||
</Button>
|
</Button>
|
||||||
<ChangePlanModal isOpen={isOpen} onClose={onClose} type={type} />
|
<ChangePlanModal isOpen={isOpen} onClose={onClose} type={type} />
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@ -25,10 +25,12 @@ import { useApiTokens } from '../hooks/useApiTokens'
|
|||||||
import { ApiTokenFromServer } from '../types'
|
import { ApiTokenFromServer } from '../types'
|
||||||
import { parseTimeSince } from '@/helpers/parseTimeSince'
|
import { parseTimeSince } from '@/helpers/parseTimeSince'
|
||||||
import { deleteApiTokenQuery } from '../queries/deleteApiTokenQuery'
|
import { deleteApiTokenQuery } from '../queries/deleteApiTokenQuery'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
|
|
||||||
type Props = { user: User }
|
type Props = { user: User }
|
||||||
|
|
||||||
export const ApiTokensList = ({ user }: Props) => {
|
export const ApiTokensList = ({ user }: Props) => {
|
||||||
|
const scopedT = useScopedI18n('account.apiTokens')
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
const { apiTokens, isLoading, mutate } = useApiTokens({
|
const { apiTokens, isLoading, mutate } = useApiTokens({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@ -55,13 +57,10 @@ export const ApiTokensList = ({ user }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Heading fontSize="2xl">API tokens</Heading>
|
<Heading fontSize="2xl">{scopedT('heading')}</Heading>
|
||||||
<Text>
|
<Text>{scopedT('description')}</Text>
|
||||||
These tokens allow other apps to control your whole account and
|
|
||||||
typebots. Be careful!
|
|
||||||
</Text>
|
|
||||||
<Flex justifyContent="flex-end">
|
<Flex justifyContent="flex-end">
|
||||||
<Button onClick={onCreateOpen}>Create</Button>
|
<Button onClick={onCreateOpen}>{scopedT('createButton.label')}</Button>
|
||||||
<CreateTokenModal
|
<CreateTokenModal
|
||||||
userId={user.id}
|
userId={user.id}
|
||||||
isOpen={isCreateOpen}
|
isOpen={isCreateOpen}
|
||||||
@ -74,8 +73,8 @@ export const ApiTokensList = ({ user }: Props) => {
|
|||||||
<Table>
|
<Table>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Name</Th>
|
<Th>{scopedT('table.nameHeader')}</Th>
|
||||||
<Th w="130px">Created</Th>
|
<Th w="130px">{scopedT('table.createdHeader')}</Th>
|
||||||
<Th w="0" />
|
<Th w="0" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
@ -91,7 +90,7 @@ export const ApiTokensList = ({ user }: Props) => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setDeletingId(token.id)}
|
onClick={() => setDeletingId(token.id)}
|
||||||
>
|
>
|
||||||
Delete
|
{scopedT('deleteButton.label')}
|
||||||
</Button>
|
</Button>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
@ -119,11 +118,14 @@ export const ApiTokensList = ({ user }: Props) => {
|
|||||||
onClose={() => setDeletingId(undefined)}
|
onClose={() => setDeletingId(undefined)}
|
||||||
message={
|
message={
|
||||||
<Text>
|
<Text>
|
||||||
The token <strong>{apiTokens?.find(byId(deletingId))?.name}</strong>{' '}
|
{scopedT('deleteConfirmationMessage', {
|
||||||
will be permanently deleted, are you sure you want to continue?
|
tokenName: (
|
||||||
|
<strong>{apiTokens?.find(byId(deletingId))?.name}</strong>
|
||||||
|
),
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
confirmButtonLabel="Delete"
|
confirmButtonLabel={scopedT('deleteButton.label')}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
|
@ -10,61 +10,65 @@ import Image from 'next/image'
|
|||||||
import lightModeIllustration from 'public/images/light-mode.png'
|
import lightModeIllustration from 'public/images/light-mode.png'
|
||||||
import darkModeIllustration from 'public/images/dark-mode.png'
|
import darkModeIllustration from 'public/images/dark-mode.png'
|
||||||
import systemModeIllustration from 'public/images/system-mode.png'
|
import systemModeIllustration from 'public/images/system-mode.png'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
const appearanceData = [
|
|
||||||
{
|
|
||||||
value: 'light',
|
|
||||||
label: 'Light',
|
|
||||||
image: lightModeIllustration,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'dark',
|
|
||||||
label: 'Dark',
|
|
||||||
image: darkModeIllustration,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'system',
|
|
||||||
label: 'System',
|
|
||||||
image: systemModeIllustration,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
defaultValue: string
|
defaultValue: string
|
||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppearanceRadioGroup = ({ defaultValue, onChange }: Props) => (
|
export const AppearanceRadioGroup = ({ defaultValue, onChange }: Props) => {
|
||||||
<RadioGroup onChange={onChange} defaultValue={defaultValue}>
|
const scopedT = useScopedI18n('account.preferences.appearance')
|
||||||
<HStack spacing={4} w="full" align="stretch">
|
|
||||||
{appearanceData.map((option) => (
|
|
||||||
<VStack
|
|
||||||
key={option.value}
|
|
||||||
as="label"
|
|
||||||
htmlFor={option.label}
|
|
||||||
cursor="pointer"
|
|
||||||
borderWidth="1px"
|
|
||||||
borderRadius="md"
|
|
||||||
w="full"
|
|
||||||
spacing={2}
|
|
||||||
justifyContent="space-between"
|
|
||||||
pb={6}
|
|
||||||
>
|
|
||||||
<VStack spacing={4}>
|
|
||||||
<Image
|
|
||||||
src={option.image}
|
|
||||||
alt="Theme preview"
|
|
||||||
style={{ borderRadius: '0.250rem' }}
|
|
||||||
placeholder="blur"
|
|
||||||
/>
|
|
||||||
<Stack>
|
|
||||||
<Text fontWeight="bold">{option.label}</Text>
|
|
||||||
</Stack>
|
|
||||||
</VStack>
|
|
||||||
|
|
||||||
<Radio value={option.value} id={option.label} />
|
const appearanceData = [
|
||||||
</VStack>
|
{
|
||||||
))}
|
value: 'light',
|
||||||
</HStack>
|
label: scopedT('lightLabel'),
|
||||||
</RadioGroup>
|
image: lightModeIllustration,
|
||||||
)
|
},
|
||||||
|
{
|
||||||
|
value: 'dark',
|
||||||
|
label: scopedT('darkLabel'),
|
||||||
|
image: darkModeIllustration,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'system',
|
||||||
|
label: scopedT('systemLabel'),
|
||||||
|
image: systemModeIllustration,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
<RadioGroup onChange={onChange} defaultValue={defaultValue}>
|
||||||
|
<HStack spacing={4} w="full" align="stretch">
|
||||||
|
{appearanceData.map((option) => (
|
||||||
|
<VStack
|
||||||
|
key={option.value}
|
||||||
|
as="label"
|
||||||
|
htmlFor={option.label}
|
||||||
|
cursor="pointer"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderRadius="md"
|
||||||
|
w="full"
|
||||||
|
spacing={2}
|
||||||
|
justifyContent="space-between"
|
||||||
|
pb={6}
|
||||||
|
>
|
||||||
|
<VStack spacing={4}>
|
||||||
|
<Image
|
||||||
|
src={option.image}
|
||||||
|
alt="Theme preview"
|
||||||
|
style={{ borderRadius: '0.250rem' }}
|
||||||
|
placeholder="blur"
|
||||||
|
/>
|
||||||
|
<Stack>
|
||||||
|
<Text fontWeight="bold">{option.label}</Text>
|
||||||
|
</Stack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<Radio value={option.value} id={option.label} />
|
||||||
|
</VStack>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</RadioGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { CopyButton } from '@/components/CopyButton'
|
import { CopyButton } from '@/components/CopyButton'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
@ -31,6 +32,7 @@ export const CreateTokenModal = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onNewToken,
|
onNewToken,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const scopedT = useScopedI18n('account.apiTokens.createModal')
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [newTokenValue, setNewTokenValue] = useState<string>()
|
const [newTokenValue, setNewTokenValue] = useState<string>()
|
||||||
@ -50,14 +52,14 @@ export const CreateTokenModal = ({
|
|||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{newTokenValue ? 'Token Created' : 'Create Token'}
|
{newTokenValue ? scopedT('createdHeading') : scopedT('createHeading')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
{newTokenValue ? (
|
{newTokenValue ? (
|
||||||
<ModalBody as={Stack} spacing="4">
|
<ModalBody as={Stack} spacing="4">
|
||||||
<Text>
|
<Text>
|
||||||
Please copy your token and store it in a safe place.{' '}
|
{scopedT('copyInstruction')}
|
||||||
<strong>For security reasons we cannot show it again.</strong>
|
<strong>{scopedT('securityWarning')}</strong>
|
||||||
</Text>
|
</Text>
|
||||||
<InputGroup size="md">
|
<InputGroup size="md">
|
||||||
<Input readOnly pr="4.5rem" value={newTokenValue} />
|
<Input readOnly pr="4.5rem" value={newTokenValue} />
|
||||||
@ -68,12 +70,9 @@ export const CreateTokenModal = ({
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
) : (
|
) : (
|
||||||
<ModalBody as="form" onSubmit={createToken}>
|
<ModalBody as="form" onSubmit={createToken}>
|
||||||
<Text mb="4">
|
<Text mb="4">{scopedT('nameInput.label')}</Text>
|
||||||
Enter a unique name for your token to differentiate it from other
|
|
||||||
tokens.
|
|
||||||
</Text>
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="I.e. Zapier, Github, Make.com"
|
placeholder={scopedT('nameInput.placeholder')}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
@ -82,7 +81,7 @@ export const CreateTokenModal = ({
|
|||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
{newTokenValue ? (
|
{newTokenValue ? (
|
||||||
<Button onClick={onClose} colorScheme="blue">
|
<Button onClick={onClose} colorScheme="blue">
|
||||||
Done
|
{scopedT('doneButton.label')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@ -91,7 +90,7 @@ export const CreateTokenModal = ({
|
|||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
onClick={createToken}
|
onClick={createToken}
|
||||||
>
|
>
|
||||||
Create token
|
{scopedT('createButton.label')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { MouseIcon, LaptopIcon } from '@/components/icons'
|
import { MouseIcon, LaptopIcon } from '@/components/icons'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
import {
|
import {
|
||||||
HStack,
|
HStack,
|
||||||
Radio,
|
Radio,
|
||||||
@ -9,22 +10,6 @@ import {
|
|||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { GraphNavigation } from '@typebot.io/prisma'
|
import { GraphNavigation } from '@typebot.io/prisma'
|
||||||
|
|
||||||
const graphNavigationData = [
|
|
||||||
{
|
|
||||||
value: GraphNavigation.MOUSE,
|
|
||||||
label: 'Mouse',
|
|
||||||
description:
|
|
||||||
'Move by dragging the board and zoom in/out using the scroll wheel',
|
|
||||||
icon: <MouseIcon boxSize="35px" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: GraphNavigation.TRACKPAD,
|
|
||||||
label: 'Trackpad',
|
|
||||||
description: 'Move the board using 2 fingers and zoom in/out by pinching',
|
|
||||||
icon: <LaptopIcon boxSize="35px" />,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
defaultValue: string
|
defaultValue: string
|
||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
@ -32,33 +17,50 @@ type Props = {
|
|||||||
export const GraphNavigationRadioGroup = ({
|
export const GraphNavigationRadioGroup = ({
|
||||||
defaultValue,
|
defaultValue,
|
||||||
onChange,
|
onChange,
|
||||||
}: Props) => (
|
}: Props) => {
|
||||||
<RadioGroup onChange={onChange} defaultValue={defaultValue}>
|
const scopedT = useScopedI18n('account.preferences.graphNavigation')
|
||||||
<HStack spacing={4} w="full" align="stretch">
|
const graphNavigationData = [
|
||||||
{graphNavigationData.map((option) => (
|
{
|
||||||
<VStack
|
value: GraphNavigation.MOUSE,
|
||||||
key={option.value}
|
label: scopedT('mouse.label'),
|
||||||
as="label"
|
description: scopedT('mouse.description'),
|
||||||
htmlFor={option.label}
|
icon: <MouseIcon boxSize="35px" />,
|
||||||
cursor="pointer"
|
},
|
||||||
borderWidth="1px"
|
{
|
||||||
borderRadius="md"
|
value: GraphNavigation.TRACKPAD,
|
||||||
w="full"
|
label: scopedT('trackpad.label'),
|
||||||
p="6"
|
description: scopedT('trackpad.description'),
|
||||||
spacing={6}
|
icon: <LaptopIcon boxSize="35px" />,
|
||||||
justifyContent="space-between"
|
},
|
||||||
>
|
]
|
||||||
<VStack spacing={6}>
|
return (
|
||||||
{option.icon}
|
<RadioGroup onChange={onChange} defaultValue={defaultValue}>
|
||||||
<Stack>
|
<HStack spacing={4} w="full" align="stretch">
|
||||||
<Text fontWeight="bold">{option.label}</Text>
|
{graphNavigationData.map((option) => (
|
||||||
<Text>{option.description}</Text>
|
<VStack
|
||||||
</Stack>
|
key={option.value}
|
||||||
</VStack>
|
as="label"
|
||||||
|
htmlFor={option.label}
|
||||||
|
cursor="pointer"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderRadius="md"
|
||||||
|
w="full"
|
||||||
|
p="6"
|
||||||
|
spacing={6}
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<VStack spacing={6}>
|
||||||
|
{option.icon}
|
||||||
|
<Stack>
|
||||||
|
<Text fontWeight="bold">{option.label}</Text>
|
||||||
|
<Text>{option.description}</Text>
|
||||||
|
</Stack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
<Radio value={option.value} id={option.label} />
|
<Radio value={option.value} id={option.label} />
|
||||||
</VStack>
|
</VStack>
|
||||||
))}
|
))}
|
||||||
</HStack>
|
</HStack>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
@ -5,8 +5,10 @@ import { ApiTokensList } from './ApiTokensList'
|
|||||||
import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
|
import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
|
||||||
import { useUser } from '../hooks/useUser'
|
import { useUser } from '../hooks/useUser'
|
||||||
import { TextInput } from '@/components/inputs/TextInput'
|
import { TextInput } from '@/components/inputs/TextInput'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
|
|
||||||
export const MyAccountForm = () => {
|
export const MyAccountForm = () => {
|
||||||
|
const scopedT = useScopedI18n('account.myAccount')
|
||||||
const { user, updateUser } = useUser()
|
const { user, updateUser } = useUser()
|
||||||
const [name, setName] = useState(user?.name ?? '')
|
const [name, setName] = useState(user?.name ?? '')
|
||||||
const [email, setEmail] = useState(user?.email ?? '')
|
const [email, setEmail] = useState(user?.email ?? '')
|
||||||
@ -41,10 +43,10 @@ export const MyAccountForm = () => {
|
|||||||
leftIcon={<UploadIcon />}
|
leftIcon={<UploadIcon />}
|
||||||
onFileUploaded={handleFileUploaded}
|
onFileUploaded={handleFileUploaded}
|
||||||
>
|
>
|
||||||
Change photo
|
{scopedT('changePhotoButton.label')}
|
||||||
</UploadButton>
|
</UploadButton>
|
||||||
<Text color="gray.500" fontSize="sm">
|
<Text color="gray.500" fontSize="sm">
|
||||||
.jpg or.png, max 1MB
|
{scopedT('changePhotoButton.specification')}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</HStack>
|
</HStack>
|
||||||
@ -52,17 +54,17 @@ export const MyAccountForm = () => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
defaultValue={name}
|
defaultValue={name}
|
||||||
onChange={handleNameChange}
|
onChange={handleNameChange}
|
||||||
label="Name:"
|
label={scopedT('nameInput.label')}
|
||||||
withVariableButton={false}
|
withVariableButton={false}
|
||||||
debounceTimeout={0}
|
debounceTimeout={0}
|
||||||
/>
|
/>
|
||||||
<Tooltip label="Updating email is not available. Contact the support if you want to change it.">
|
<Tooltip label={scopedT('emailInput.disabledTooltip')}>
|
||||||
<span>
|
<span>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="email"
|
type="email"
|
||||||
defaultValue={email}
|
defaultValue={email}
|
||||||
onChange={handleEmailChange}
|
onChange={handleEmailChange}
|
||||||
label="Email address:"
|
label={scopedT('emailInput.label')}
|
||||||
withVariableButton={false}
|
withVariableButton={false}
|
||||||
debounceTimeout={0}
|
debounceTimeout={0}
|
||||||
isDisabled
|
isDisabled
|
||||||
|
@ -4,8 +4,10 @@ import React, { useEffect } from 'react'
|
|||||||
import { GraphNavigationRadioGroup } from './GraphNavigationRadioGroup'
|
import { GraphNavigationRadioGroup } from './GraphNavigationRadioGroup'
|
||||||
import { AppearanceRadioGroup } from './AppearanceRadioGroup'
|
import { AppearanceRadioGroup } from './AppearanceRadioGroup'
|
||||||
import { useUser } from '../hooks/useUser'
|
import { useUser } from '../hooks/useUser'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
|
|
||||||
export const UserPreferencesForm = () => {
|
export const UserPreferencesForm = () => {
|
||||||
|
const scopedT = useScopedI18n('account.preferences')
|
||||||
const { colorMode, setColorMode } = useColorMode()
|
const { colorMode, setColorMode } = useColorMode()
|
||||||
const { user, updateUser } = useUser()
|
const { user, updateUser } = useUser()
|
||||||
|
|
||||||
@ -26,14 +28,14 @@ export const UserPreferencesForm = () => {
|
|||||||
return (
|
return (
|
||||||
<Stack spacing={12}>
|
<Stack spacing={12}>
|
||||||
<Stack spacing={6}>
|
<Stack spacing={6}>
|
||||||
<Heading size="md">Editor Navigation</Heading>
|
<Heading size="md">{scopedT('graphNavigation.heading')}</Heading>
|
||||||
<GraphNavigationRadioGroup
|
<GraphNavigationRadioGroup
|
||||||
defaultValue={user?.graphNavigation ?? GraphNavigation.TRACKPAD}
|
defaultValue={user?.graphNavigation ?? GraphNavigation.TRACKPAD}
|
||||||
onChange={changeGraphNavigation}
|
onChange={changeGraphNavigation}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack spacing={6}>
|
<Stack spacing={6}>
|
||||||
<Heading size="md">Appearance</Heading>
|
<Heading size="md">{scopedT('appearance.heading')}</Heading>
|
||||||
<AppearanceRadioGroup
|
<AppearanceRadioGroup
|
||||||
defaultValue={
|
defaultValue={
|
||||||
user?.preferredAppAppearance
|
user?.preferredAppAppearance
|
||||||
|
@ -11,12 +11,13 @@ import React from 'react'
|
|||||||
import { useAnswersCount } from '../hooks/useAnswersCount'
|
import { useAnswersCount } from '../hooks/useAnswersCount'
|
||||||
import { StatsCards } from './StatsCards'
|
import { StatsCards } from './StatsCards'
|
||||||
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
||||||
import { LimitReached } from '@/features/billing/types'
|
|
||||||
import { Graph } from '@/features/graph/components/Graph'
|
import { Graph } from '@/features/graph/components/Graph'
|
||||||
import { GraphProvider } from '@/features/graph/providers/GraphProvider'
|
import { GraphProvider } from '@/features/graph/providers/GraphProvider'
|
||||||
import { GroupsCoordinatesProvider } from '@/features/graph/providers/GroupsCoordinateProvider'
|
import { GroupsCoordinatesProvider } from '@/features/graph/providers/GroupsCoordinateProvider'
|
||||||
|
import { useI18n } from '@/locales'
|
||||||
|
|
||||||
export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
|
export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
|
||||||
|
const t = useI18n()
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
const { typebot, publishedTypebot } = useTypebot()
|
const { typebot, publishedTypebot } = useTypebot()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
@ -69,7 +70,7 @@ export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
|
|||||||
<ChangePlanModal
|
<ChangePlanModal
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
type={LimitReached.ANALYTICS}
|
type={t('billing.limitMessage.analytics')}
|
||||||
/>
|
/>
|
||||||
<StatsCards stats={stats} pos="absolute" top={10} />
|
<StatsCards stats={stats} pos="absolute" top={10} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
import {
|
import {
|
||||||
GridProps,
|
GridProps,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
@ -22,12 +23,13 @@ export const StatsCards = ({
|
|||||||
stats,
|
stats,
|
||||||
...props
|
...props
|
||||||
}: { stats?: Stats } & GridProps) => {
|
}: { stats?: Stats } & GridProps) => {
|
||||||
|
const scopedT = useScopedI18n('analytics')
|
||||||
const bg = useColorModeValue('white', 'gray.900')
|
const bg = useColorModeValue('white', 'gray.900')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleGrid columns={{ base: 1, md: 3 }} spacing="6" {...props}>
|
<SimpleGrid columns={{ base: 1, md: 3 }} spacing="6" {...props}>
|
||||||
<Stat bgColor={bg} p="4" rounded="md" boxShadow="md">
|
<Stat bgColor={bg} p="4" rounded="md" boxShadow="md">
|
||||||
<StatLabel>Views</StatLabel>
|
<StatLabel>{scopedT('viewsLabel')}</StatLabel>
|
||||||
{stats ? (
|
{stats ? (
|
||||||
<StatNumber>{stats.totalViews}</StatNumber>
|
<StatNumber>{stats.totalViews}</StatNumber>
|
||||||
) : (
|
) : (
|
||||||
@ -35,7 +37,7 @@ export const StatsCards = ({
|
|||||||
)}
|
)}
|
||||||
</Stat>
|
</Stat>
|
||||||
<Stat bgColor={bg} p="4" rounded="md" boxShadow="md">
|
<Stat bgColor={bg} p="4" rounded="md" boxShadow="md">
|
||||||
<StatLabel>Starts</StatLabel>
|
<StatLabel>{scopedT('startsLabel')}</StatLabel>
|
||||||
{stats ? (
|
{stats ? (
|
||||||
<StatNumber>{stats.totalStarts}</StatNumber>
|
<StatNumber>{stats.totalStarts}</StatNumber>
|
||||||
) : (
|
) : (
|
||||||
@ -43,7 +45,7 @@ export const StatsCards = ({
|
|||||||
)}
|
)}
|
||||||
</Stat>
|
</Stat>
|
||||||
<Stat bgColor={bg} p="4" rounded="md" boxShadow="md">
|
<Stat bgColor={bg} p="4" rounded="md" boxShadow="md">
|
||||||
<StatLabel>Completion rate</StatLabel>
|
<StatLabel>{scopedT('completionRateLabel')}</StatLabel>
|
||||||
{stats ? (
|
{stats ? (
|
||||||
<StatNumber>
|
<StatNumber>
|
||||||
{computeCompletionRate(stats.totalCompleted, stats.totalStarts)}
|
{computeCompletionRate(stats.totalCompleted, stats.totalStarts)}
|
||||||
|
@ -15,7 +15,7 @@ import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitat
|
|||||||
import { joinWorkspaces } from '@/features/auth/helpers/joinWorkspaces'
|
import { joinWorkspaces } from '@/features/auth/helpers/joinWorkspaces'
|
||||||
import { parseWorkspaceDefaultPlan } from '@/features/workspace/helpers/parseWorkspaceDefaultPlan'
|
import { parseWorkspaceDefaultPlan } from '@/features/workspace/helpers/parseWorkspaceDefaultPlan'
|
||||||
|
|
||||||
export function CustomAdapter(p: PrismaClient): Adapter {
|
export function customAdapter(p: PrismaClient): Adapter {
|
||||||
return {
|
return {
|
||||||
createUser: async (data: Omit<AdapterUser, 'id'>) => {
|
createUser: async (data: Omit<AdapterUser, 'id'>) => {
|
||||||
if (!data.email)
|
if (!data.email)
|
@ -1,24 +1,25 @@
|
|||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
import { Alert } from '@chakra-ui/react'
|
import { Alert } from '@chakra-ui/react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
error: string
|
error: string
|
||||||
}
|
}
|
||||||
const errors: Record<string, string> = {
|
|
||||||
Signin: 'Try signing with a different account.',
|
|
||||||
OAuthSignin: 'Try signing with a different account.',
|
|
||||||
OAuthCallback: 'Try signing with a different account.',
|
|
||||||
OAuthCreateAccount: 'Email not found. Try signing with a different provider.',
|
|
||||||
EmailCreateAccount: 'Try signing with a different account.',
|
|
||||||
Callback: 'Try signing with a different account.',
|
|
||||||
OAuthAccountNotLinked:
|
|
||||||
'To confirm your identity, sign in with the same account you used originally.',
|
|
||||||
CredentialsSignin:
|
|
||||||
'Sign in failed. Check the details you provided are correct.',
|
|
||||||
default: 'An error occurred. Please try again.',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SignInError = ({ error }: Props) => (
|
export const SignInError = ({ error }: Props) => {
|
||||||
<Alert status="error" variant="solid" rounded="md">
|
const scopedT = useScopedI18n('auth.error')
|
||||||
{errors[error] ?? errors[error]}
|
const errors: Record<string, string> = {
|
||||||
</Alert>
|
Signin: scopedT('default'),
|
||||||
)
|
OAuthSignin: scopedT('default'),
|
||||||
|
OAuthCallback: scopedT('default'),
|
||||||
|
OAuthCreateAccount: scopedT('email'),
|
||||||
|
EmailCreateAccount: scopedT('default'),
|
||||||
|
Callback: scopedT('default'),
|
||||||
|
OAuthAccountNotLinked: scopedT('oauthNotLinked'),
|
||||||
|
default: scopedT('unknown'),
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Alert status="error" variant="solid" rounded="md">
|
||||||
|
{errors[error] ?? errors[error]}
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -27,6 +27,7 @@ import { BuiltInProviderType } from 'next-auth/providers'
|
|||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { TextLink } from '@/components/TextLink'
|
import { TextLink } from '@/components/TextLink'
|
||||||
import { SignInError } from './SignInError'
|
import { SignInError } from './SignInError'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
defaultEmail?: string
|
defaultEmail?: string
|
||||||
@ -34,6 +35,7 @@ type Props = {
|
|||||||
export const SignInForm = ({
|
export const SignInForm = ({
|
||||||
defaultEmail,
|
defaultEmail,
|
||||||
}: Props & HTMLChakraProps<'form'>) => {
|
}: Props & HTMLChakraProps<'form'>) => {
|
||||||
|
const scopedT = useScopedI18n('auth')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { status } = useSession()
|
const { status } = useSession()
|
||||||
const [authLoading, setAuthLoading] = useState(false)
|
const [authLoading, setAuthLoading] = useState(false)
|
||||||
@ -76,8 +78,8 @@ export const SignInForm = ({
|
|||||||
})
|
})
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
showToast({
|
showToast({
|
||||||
title: 'Unauthorized',
|
title: scopedT('signinErrorToast.title'),
|
||||||
description: 'Sign ups are disabled.',
|
description: scopedT('signinErrorToast.description'),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
setIsMagicLinkSent(true)
|
setIsMagicLinkSent(true)
|
||||||
@ -89,14 +91,13 @@ export const SignInForm = ({
|
|||||||
if (hasNoAuthProvider)
|
if (hasNoAuthProvider)
|
||||||
return (
|
return (
|
||||||
<Text>
|
<Text>
|
||||||
You need to{' '}
|
{scopedT('noProvider.preLink')}{' '}
|
||||||
<TextLink
|
<TextLink
|
||||||
href="https://docs.typebot.io/self-hosting/configuration"
|
href="https://docs.typebot.io/self-hosting/configuration"
|
||||||
isExternal
|
isExternal
|
||||||
>
|
>
|
||||||
configure at least one auth provider
|
{scopedT('noProvider.link')}
|
||||||
</TextLink>{' '}
|
</TextLink>
|
||||||
(Email, Google, GitHub, Facebook or Azure AD).
|
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
@ -106,7 +107,9 @@ export const SignInForm = ({
|
|||||||
<SocialLoginButtons providers={providers} />
|
<SocialLoginButtons providers={providers} />
|
||||||
{providers?.email && (
|
{providers?.email && (
|
||||||
<>
|
<>
|
||||||
<DividerWithText mt="6">Or with your email</DividerWithText>
|
<DividerWithText mt="6">
|
||||||
|
{scopedT('orEmailLabel')}
|
||||||
|
</DividerWithText>
|
||||||
<HStack as="form" onSubmit={handleEmailSubmit}>
|
<HStack as="form" onSubmit={handleEmailSubmit}>
|
||||||
<Input
|
<Input
|
||||||
name="email"
|
name="email"
|
||||||
@ -124,7 +127,7 @@ export const SignInForm = ({
|
|||||||
}
|
}
|
||||||
isDisabled={isMagicLinkSent}
|
isDisabled={isMagicLinkSent}
|
||||||
>
|
>
|
||||||
Submit
|
{scopedT('emailSubmitButton.label')}
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
</>
|
</>
|
||||||
@ -140,10 +143,8 @@ export const SignInForm = ({
|
|||||||
<HStack>
|
<HStack>
|
||||||
<AlertIcon />
|
<AlertIcon />
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
<Text fontWeight="semibold">
|
<Text fontWeight="semibold">{scopedT('magicLink.title')}</Text>
|
||||||
A magic link email was sent. 🪄
|
<Text fontSize="sm">{scopedT('magicLink.description')}</Text>
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm">Make sure to check your SPAM folder.</Text>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Seo } from '@/components/Seo'
|
import { Seo } from '@/components/Seo'
|
||||||
import { TextLink } from '@/components/TextLink'
|
import { TextLink } from '@/components/TextLink'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
import { VStack, Heading, Text } from '@chakra-ui/react'
|
import { VStack, Heading, Text } from '@chakra-ui/react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { SignInForm } from './SignInForm'
|
import { SignInForm } from './SignInForm'
|
||||||
@ -10,26 +11,40 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SignInPage = ({ type }: Props) => {
|
export const SignInPage = ({ type }: Props) => {
|
||||||
|
const scopedT = useScopedI18n('auth')
|
||||||
const { query } = useRouter()
|
const { query } = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack spacing={4} h="100vh" justifyContent="center">
|
<VStack spacing={4} h="100vh" justifyContent="center">
|
||||||
<Seo title={type === 'signin' ? 'Sign In' : 'Register'} />
|
<Seo
|
||||||
|
title={
|
||||||
|
type === 'signin'
|
||||||
|
? scopedT('signin.heading')
|
||||||
|
: scopedT('register.heading')
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Heading
|
<Heading
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
throw new Error('Sentry is working')
|
throw new Error('Sentry is working')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{type === 'signin' ? 'Sign In' : 'Create an account'}
|
{type === 'signin'
|
||||||
|
? scopedT('signin.heading')
|
||||||
|
: scopedT('register.heading')}
|
||||||
</Heading>
|
</Heading>
|
||||||
{type === 'signin' ? (
|
{type === 'signin' ? (
|
||||||
<Text>
|
<Text>
|
||||||
Don't have an account?{' '}
|
{scopedT('signin.noAccountLabel.preLink')}{' '}
|
||||||
<TextLink href="/register">Sign up for free</TextLink>
|
<TextLink href="/register">
|
||||||
|
{scopedT('signin.noAccountLabel.link')}
|
||||||
|
</TextLink>
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text>
|
<Text>
|
||||||
Already have an account? <TextLink href="/signin">Sign in</TextLink>
|
{scopedT('register.alreadyHaveAccountLabel.preLink')}{' '}
|
||||||
|
<TextLink href="/signin">
|
||||||
|
{scopedT('register.alreadyHaveAccountLabel.link')}
|
||||||
|
</TextLink>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<SignInForm defaultEmail={query.g?.toString()} />
|
<SignInForm defaultEmail={query.g?.toString()} />
|
||||||
|
@ -15,6 +15,7 @@ import { omit } from '@typebot.io/lib'
|
|||||||
import { AzureAdLogo } from '@/components/logos/AzureAdLogo'
|
import { AzureAdLogo } from '@/components/logos/AzureAdLogo'
|
||||||
import { FacebookLogo } from '@/components/logos/FacebookLogo'
|
import { FacebookLogo } from '@/components/logos/FacebookLogo'
|
||||||
import { GitlabLogo } from '@/components/logos/GitlabLogo'
|
import { GitlabLogo } from '@/components/logos/GitlabLogo'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
providers:
|
providers:
|
||||||
@ -23,6 +24,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SocialLoginButtons = ({ providers }: Props) => {
|
export const SocialLoginButtons = ({ providers }: Props) => {
|
||||||
|
const scopedT = useScopedI18n('auth.socialLogin')
|
||||||
const { query } = useRouter()
|
const { query } = useRouter()
|
||||||
const { status } = useSession()
|
const { status } = useSession()
|
||||||
const [authLoading, setAuthLoading] =
|
const [authLoading, setAuthLoading] =
|
||||||
@ -63,7 +65,7 @@ export const SocialLoginButtons = ({ providers }: Props) => {
|
|||||||
}
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Continue with GitHub
|
{scopedT('githubButton.label')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{providers?.google && (
|
{providers?.google && (
|
||||||
@ -77,7 +79,7 @@ export const SocialLoginButtons = ({ providers }: Props) => {
|
|||||||
}
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Continue with Google
|
{scopedT('googleButton.label')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{providers?.facebook && (
|
{providers?.facebook && (
|
||||||
@ -91,7 +93,7 @@ export const SocialLoginButtons = ({ providers }: Props) => {
|
|||||||
}
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Continue with Facebook
|
{scopedT('facebookButton.label')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{providers?.gitlab && (
|
{providers?.gitlab && (
|
||||||
@ -105,7 +107,9 @@ export const SocialLoginButtons = ({ providers }: Props) => {
|
|||||||
}
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Continue with {providers.gitlab.name}
|
{scopedT('gitlabButton.label', {
|
||||||
|
gitlabProviderName: providers.gitlab.name,
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{providers?.['azure-ad'] && (
|
{providers?.['azure-ad'] && (
|
||||||
@ -119,7 +123,9 @@ export const SocialLoginButtons = ({ providers }: Props) => {
|
|||||||
}
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Continue with {providers['azure-ad'].name}
|
{scopedT('azureButton.label', {
|
||||||
|
azureProviderName: providers['azure-ad'].name,
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{providers?.['custom-oauth'] && (
|
{providers?.['custom-oauth'] && (
|
||||||
@ -131,7 +137,9 @@ export const SocialLoginButtons = ({ providers }: Props) => {
|
|||||||
}
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Continue with {providers['custom-oauth'].name}
|
{scopedT('customButton.label', {
|
||||||
|
customProviderName: providers['custom-oauth'].name,
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -193,7 +193,7 @@ test('plan changes should work', async ({ page }) => {
|
|||||||
// Go to customer portal
|
// Go to customer portal
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.waitForNavigation(),
|
page.waitForNavigation(),
|
||||||
page.click('text="Billing Portal"'),
|
page.click('text="Billing portal"'),
|
||||||
])
|
])
|
||||||
await expect(page.locator('text="Add payment method"')).toBeVisible()
|
await expect(page.locator('text="Add payment method"')).toBeVisible()
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { trpc } from '@/lib/trpc'
|
import { trpc } from '@/lib/trpc'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
import { Button, Link } from '@chakra-ui/react'
|
import { Button, Link } from '@chakra-ui/react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -7,6 +8,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const BillingPortalButton = ({ workspaceId }: Props) => {
|
export const BillingPortalButton = ({ workspaceId }: Props) => {
|
||||||
|
const scopedT = useScopedI18n('billing')
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
const { data } = trpc.billing.getBillingPortalUrl.useQuery(
|
const { data } = trpc.billing.getBillingPortalUrl.useQuery(
|
||||||
{
|
{
|
||||||
@ -22,7 +24,7 @@ export const BillingPortalButton = ({ workspaceId }: Props) => {
|
|||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<Button as={Link} href={data?.billingPortalUrl} isLoading={!data}>
|
<Button as={Link} href={data?.billingPortalUrl} isLoading={!data}>
|
||||||
Billing Portal
|
{scopedT('billingPortalButton.label')}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,10 @@ import { TextLink } from '@/components/TextLink'
|
|||||||
import { ChangePlanForm } from './ChangePlanForm'
|
import { ChangePlanForm } from './ChangePlanForm'
|
||||||
import { UsageProgressBars } from './UsageProgressBars'
|
import { UsageProgressBars } from './UsageProgressBars'
|
||||||
import { CurrentSubscriptionSummary } from './CurrentSubscriptionSummary'
|
import { CurrentSubscriptionSummary } from './CurrentSubscriptionSummary'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
|
|
||||||
export const BillingSettingsLayout = () => {
|
export const BillingSettingsLayout = () => {
|
||||||
|
const scopedT = useScopedI18n('billing')
|
||||||
const { workspace, refreshWorkspace } = useWorkspace()
|
const { workspace, refreshWorkspace } = useWorkspace()
|
||||||
|
|
||||||
if (!workspace) return null
|
if (!workspace) return null
|
||||||
@ -24,10 +26,9 @@ export const BillingSettingsLayout = () => {
|
|||||||
<HStack maxW="500px">
|
<HStack maxW="500px">
|
||||||
<StripeClimateLogo />
|
<StripeClimateLogo />
|
||||||
<Text fontSize="xs" color="gray.500">
|
<Text fontSize="xs" color="gray.500">
|
||||||
Typebot is contributing 1% of your subscription to remove CO₂ from
|
{scopedT('contribution.preLink')}{' '}
|
||||||
the atmosphere.{' '}
|
|
||||||
<TextLink href="https://climate.stripe.com/5VCRAq" isExternal>
|
<TextLink href="https://climate.stripe.com/5VCRAq" isExternal>
|
||||||
More info.
|
{scopedT('contribution.link')}
|
||||||
</TextLink>
|
</TextLink>
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
@ -11,6 +11,7 @@ import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvi
|
|||||||
import { useUser } from '@/features/account/hooks/useUser'
|
import { useUser } from '@/features/account/hooks/useUser'
|
||||||
import { StarterPlanPricingCard } from './StarterPlanPricingCard'
|
import { StarterPlanPricingCard } from './StarterPlanPricingCard'
|
||||||
import { ProPlanPricingCard } from './ProPlanPricingCard'
|
import { ProPlanPricingCard } from './ProPlanPricingCard'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workspace: Pick<Workspace, 'id' | 'stripeId' | 'plan'>
|
workspace: Pick<Workspace, 'id' | 'stripeId' | 'plan'>
|
||||||
@ -18,6 +19,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
|
export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
|
||||||
|
const scopedT = useScopedI18n('billing')
|
||||||
const { user } = useUser()
|
const { user } = useUser()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
const [preCheckoutPlan, setPreCheckoutPlan] =
|
const [preCheckoutPlan, setPreCheckoutPlan] =
|
||||||
@ -38,7 +40,7 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
|
|||||||
onUpgradeSuccess()
|
onUpgradeSuccess()
|
||||||
showToast({
|
showToast({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
description: `Workspace ${plan} plan successfully updated 🎉`,
|
description: scopedT('updateSuccessToast.description', { plan }),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -123,9 +125,9 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Text color="gray.500">
|
<Text color="gray.500">
|
||||||
Need custom limits? Specific features?{' '}
|
{scopedT('customLimit.preLink')}{' '}
|
||||||
<TextLink href={'https://typebot.io/enterprise-lead-form'} isExternal>
|
<TextLink href={'https://typebot.io/enterprise-lead-form'} isExternal>
|
||||||
Let's chat!
|
{scopedT('customLimit.link')}
|
||||||
</TextLink>
|
</TextLink>
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { AlertInfo } from '@/components/AlertInfo'
|
import { AlertInfo } from '@/components/AlertInfo'
|
||||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||||
|
import { useI18n, useScopedI18n } from '@/locales'
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
@ -10,11 +11,10 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
HStack,
|
HStack,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { LimitReached } from '../types'
|
|
||||||
import { ChangePlanForm } from './ChangePlanForm'
|
import { ChangePlanForm } from './ChangePlanForm'
|
||||||
|
|
||||||
type ChangePlanModalProps = {
|
type ChangePlanModalProps = {
|
||||||
type?: LimitReached
|
type?: string
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
@ -24,6 +24,7 @@ export const ChangePlanModal = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
type,
|
type,
|
||||||
}: ChangePlanModalProps) => {
|
}: ChangePlanModalProps) => {
|
||||||
|
const t = useI18n()
|
||||||
const { workspace, refreshWorkspace } = useWorkspace()
|
const { workspace, refreshWorkspace } = useWorkspace()
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="2xl">
|
<Modal isOpen={isOpen} onClose={onClose} size="2xl">
|
||||||
@ -32,7 +33,7 @@ export const ChangePlanModal = ({
|
|||||||
<ModalBody as={Stack} spacing="6" pt="10">
|
<ModalBody as={Stack} spacing="6" pt="10">
|
||||||
{type && (
|
{type && (
|
||||||
<AlertInfo>
|
<AlertInfo>
|
||||||
You need to upgrade your plan in order to {type}
|
{t('billing.upgradeLimitLabel', { type: type })}
|
||||||
</AlertInfo>
|
</AlertInfo>
|
||||||
)}
|
)}
|
||||||
{workspace && (
|
{workspace && (
|
||||||
@ -46,7 +47,7 @@ export const ChangePlanModal = ({
|
|||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<HStack>
|
<HStack>
|
||||||
<Button colorScheme="gray" onClick={onClose}>
|
<Button colorScheme="gray" onClick={onClose}>
|
||||||
Cancel
|
{t('cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
@ -6,6 +6,7 @@ import { PlanTag } from './PlanTag'
|
|||||||
import { BillingPortalButton } from './BillingPortalButton'
|
import { BillingPortalButton } from './BillingPortalButton'
|
||||||
import { trpc } from '@/lib/trpc'
|
import { trpc } from '@/lib/trpc'
|
||||||
import { Workspace } from '@typebot.io/schemas'
|
import { Workspace } from '@typebot.io/schemas'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workspace: Pick<Workspace, 'id' | 'plan' | 'stripeId'>
|
workspace: Pick<Workspace, 'id' | 'plan' | 'stripeId'>
|
||||||
@ -16,6 +17,7 @@ export const CurrentSubscriptionSummary = ({
|
|||||||
workspace,
|
workspace,
|
||||||
onCancelSuccess,
|
onCancelSuccess,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const scopedT = useScopedI18n('billing.currentSubscription')
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const { mutate: cancelSubscription, isLoading: isCancelling } =
|
const { mutate: cancelSubscription, isLoading: isCancelling } =
|
||||||
@ -34,9 +36,9 @@ export const CurrentSubscriptionSummary = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing="4">
|
<Stack spacing="4">
|
||||||
<Heading fontSize="3xl">Subscription</Heading>
|
<Heading fontSize="3xl">{scopedT('heading')}</Heading>
|
||||||
<HStack data-testid="current-subscription">
|
<HStack data-testid="current-subscription">
|
||||||
<Text>Current workspace subscription: </Text>
|
<Text>{scopedT('subheading')} </Text>
|
||||||
{isCancelling ? (
|
{isCancelling ? (
|
||||||
<Spinner color="gray.500" size="xs" />
|
<Spinner color="gray.500" size="xs" />
|
||||||
) : (
|
) : (
|
||||||
@ -52,7 +54,7 @@ export const CurrentSubscriptionSummary = ({
|
|||||||
cancelSubscription({ workspaceId: workspace.id })
|
cancelSubscription({ workspaceId: workspace.id })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Cancel my subscription
|
{scopedT('cancelLink')}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -62,10 +64,7 @@ export const CurrentSubscriptionSummary = ({
|
|||||||
{isSubscribed && !isCancelling && (
|
{isSubscribed && !isCancelling && (
|
||||||
<>
|
<>
|
||||||
<Stack spacing="4">
|
<Stack spacing="4">
|
||||||
<Text fontSize="sm">
|
<Text fontSize="sm">{scopedT('billingPortalDescription')}</Text>
|
||||||
Need to change payment method or billing information? Head over to
|
|
||||||
your billing portal:
|
|
||||||
</Text>
|
|
||||||
<BillingPortalButton workspaceId={workspace.id} />
|
<BillingPortalButton workspaceId={workspace.id} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
|
@ -18,12 +18,14 @@ import Link from 'next/link'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { trpc } from '@/lib/trpc'
|
import { trpc } from '@/lib/trpc'
|
||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workspaceId: string
|
workspaceId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InvoicesList = ({ workspaceId }: Props) => {
|
export const InvoicesList = ({ workspaceId }: Props) => {
|
||||||
|
const scopedT = useScopedI18n('billing.invoices')
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
const { data, status } = trpc.billing.listInvoices.useQuery(
|
const { data, status } = trpc.billing.listInvoices.useQuery(
|
||||||
{
|
{
|
||||||
@ -38,9 +40,9 @@ export const InvoicesList = ({ workspaceId }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={6}>
|
<Stack spacing={6}>
|
||||||
<Heading fontSize="3xl">Invoices</Heading>
|
<Heading fontSize="3xl">{scopedT('heading')}</Heading>
|
||||||
{data?.invoices.length === 0 && status !== 'loading' ? (
|
{data?.invoices.length === 0 && status !== 'loading' ? (
|
||||||
<Text>No invoices found for this workspace.</Text>
|
<Text>{scopedT('empty')}</Text>
|
||||||
) : (
|
) : (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
@ -48,8 +50,8 @@ export const InvoicesList = ({ workspaceId }: Props) => {
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Th w="0" />
|
<Th w="0" />
|
||||||
<Th>#</Th>
|
<Th>#</Th>
|
||||||
<Th>Paid at</Th>
|
<Th>{scopedT('paidAt')}</Th>
|
||||||
<Th>Subtotal</Th>
|
<Th>{scopedT('subtotal')}</Th>
|
||||||
<Th w="0" />
|
<Th w="0" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useI18n, useScopedI18n } from '@/locales'
|
||||||
import { Tag, TagProps, ThemeTypings } from '@chakra-ui/react'
|
import { Tag, TagProps, ThemeTypings } from '@chakra-ui/react'
|
||||||
import { Plan } from '@typebot.io/prisma'
|
import { Plan } from '@typebot.io/prisma'
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import { useRouter } from 'next/router'
|
|||||||
import React, { FormEvent, useState } from 'react'
|
import React, { FormEvent, useState } from 'react'
|
||||||
import { isDefined } from '@typebot.io/lib'
|
import { isDefined } from '@typebot.io/lib'
|
||||||
import { taxIdTypes } from '../taxIdTypes'
|
import { taxIdTypes } from '../taxIdTypes'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
|
|
||||||
export type PreCheckoutModalProps = {
|
export type PreCheckoutModalProps = {
|
||||||
selectedSubscription:
|
selectedSubscription:
|
||||||
@ -48,6 +49,7 @@ export const PreCheckoutModal = ({
|
|||||||
existingEmail,
|
existingEmail,
|
||||||
onClose,
|
onClose,
|
||||||
}: PreCheckoutModalProps) => {
|
}: PreCheckoutModalProps) => {
|
||||||
|
const scopedT = useScopedI18n('billing.preCheckoutModal')
|
||||||
const { ref } = useParentModal()
|
const { ref } = useParentModal()
|
||||||
const vatValueInputRef = React.useRef<HTMLInputElement>(null)
|
const vatValueInputRef = React.useRef<HTMLInputElement>(null)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -131,7 +133,7 @@ export const PreCheckoutModal = ({
|
|||||||
<Stack as="form" spacing="4" onSubmit={goToCheckout}>
|
<Stack as="form" spacing="4" onSubmit={goToCheckout}>
|
||||||
<TextInput
|
<TextInput
|
||||||
isRequired
|
isRequired
|
||||||
label="Company name"
|
label={scopedT('companyInput.label')}
|
||||||
defaultValue={customer.company}
|
defaultValue={customer.company}
|
||||||
onChange={updateCustomerCompany}
|
onChange={updateCustomerCompany}
|
||||||
withVariableButton={false}
|
withVariableButton={false}
|
||||||
@ -140,17 +142,17 @@ export const PreCheckoutModal = ({
|
|||||||
<TextInput
|
<TextInput
|
||||||
isRequired
|
isRequired
|
||||||
type="email"
|
type="email"
|
||||||
label="Email"
|
label={scopedT('emailInput.label')}
|
||||||
defaultValue={customer.email}
|
defaultValue={customer.email}
|
||||||
onChange={updateCustomerEmail}
|
onChange={updateCustomerEmail}
|
||||||
withVariableButton={false}
|
withVariableButton={false}
|
||||||
debounceTimeout={0}
|
debounceTimeout={0}
|
||||||
/>
|
/>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel>Tax ID</FormLabel>
|
<FormLabel>{scopedT('taxId.label')}</FormLabel>
|
||||||
<HStack>
|
<HStack>
|
||||||
<Select
|
<Select
|
||||||
placeholder="ID type"
|
placeholder={scopedT('taxId.placeholder')}
|
||||||
items={vatCodeLabels}
|
items={vatCodeLabels}
|
||||||
isPopoverMatchingInputWidth={false}
|
isPopoverMatchingInputWidth={false}
|
||||||
onSelect={updateVatType}
|
onSelect={updateVatType}
|
||||||
@ -171,7 +173,7 @@ export const PreCheckoutModal = ({
|
|||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
isDisabled={customer.company === '' || customer.email === ''}
|
isDisabled={customer.company === '' || customer.email === ''}
|
||||||
>
|
>
|
||||||
Go to checkout
|
{scopedT('submitButton.label')}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
@ -29,6 +29,7 @@ import {
|
|||||||
} from '@typebot.io/lib/pricing'
|
} from '@typebot.io/lib/pricing'
|
||||||
import { FeaturesList } from './FeaturesList'
|
import { FeaturesList } from './FeaturesList'
|
||||||
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
||||||
|
import { useI18n, useScopedI18n } from '@/locales'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialChatsLimitIndex?: number
|
initialChatsLimitIndex?: number
|
||||||
@ -48,6 +49,8 @@ export const ProPlanPricingCard = ({
|
|||||||
isLoading,
|
isLoading,
|
||||||
onPayClick,
|
onPayClick,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const t = useI18n()
|
||||||
|
const scopedT = useScopedI18n('billing.pricingCard')
|
||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] =
|
const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] =
|
||||||
useState<number>()
|
useState<number>()
|
||||||
@ -94,15 +97,15 @@ export const ProPlanPricingCard = ({
|
|||||||
)
|
)
|
||||||
return ''
|
return ''
|
||||||
if (workspace?.plan === Plan.PRO) {
|
if (workspace?.plan === Plan.PRO) {
|
||||||
if (isCurrentPlan) return 'Your current plan'
|
if (isCurrentPlan) return scopedT('upgradeButton.current')
|
||||||
|
|
||||||
if (
|
if (
|
||||||
selectedChatsLimitIndex !== initialChatsLimitIndex ||
|
selectedChatsLimitIndex !== initialChatsLimitIndex ||
|
||||||
selectedStorageLimitIndex !== initialStorageLimitIndex
|
selectedStorageLimitIndex !== initialStorageLimitIndex
|
||||||
)
|
)
|
||||||
return 'Update'
|
return t('update')
|
||||||
}
|
}
|
||||||
return 'Upgrade'
|
return t('upgrade')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePayClick = async () => {
|
const handlePayClick = async () => {
|
||||||
@ -139,15 +142,17 @@ export const ProPlanPricingCard = ({
|
|||||||
fontWeight="semibold"
|
fontWeight="semibold"
|
||||||
style={{ marginTop: 0 }}
|
style={{ marginTop: 0 }}
|
||||||
>
|
>
|
||||||
Most popular
|
{scopedT('pro.mostPopularLabel')}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Stack justifyContent="space-between" h="full">
|
<Stack justifyContent="space-between" h="full">
|
||||||
<Stack spacing="4" mt={2}>
|
<Stack spacing="4" mt={2}>
|
||||||
<Heading fontSize="2xl">
|
<Heading fontSize="2xl">
|
||||||
Upgrade to <chakra.span color="blue.400">Pro</chakra.span>
|
{scopedT('heading', {
|
||||||
|
plan: <chakra.span color="blue.400">Pro</chakra.span>,
|
||||||
|
})}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text>For agencies & growing startups.</Text>
|
<Text>{scopedT('pro.description')}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack spacing="4">
|
<Stack spacing="4">
|
||||||
<Heading>
|
<Heading>
|
||||||
@ -159,16 +164,16 @@ export const ProPlanPricingCard = ({
|
|||||||
) ?? NaN,
|
) ?? NaN,
|
||||||
currency
|
currency
|
||||||
)}
|
)}
|
||||||
<chakra.span fontSize="md">/ month</chakra.span>
|
<chakra.span fontSize="md">{scopedT('perMonth')}</chakra.span>
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text fontWeight="bold">
|
<Text fontWeight="bold">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={
|
label={
|
||||||
<FeaturesList
|
<FeaturesList
|
||||||
features={[
|
features={[
|
||||||
'Branding removed',
|
scopedT('starter.brandingRemoved'),
|
||||||
'File upload input block',
|
scopedT('starter.fileUploadBlock'),
|
||||||
'Create folders',
|
scopedT('starter.createFolders'),
|
||||||
]}
|
]}
|
||||||
spacing="0"
|
spacing="0"
|
||||||
/>
|
/>
|
||||||
@ -177,14 +182,14 @@ export const ProPlanPricingCard = ({
|
|||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<chakra.span textDecoration="underline" cursor="pointer">
|
<chakra.span textDecoration="underline" cursor="pointer">
|
||||||
Everything in Starter
|
{scopedT('pro.everythingFromStarter')}
|
||||||
</chakra.span>
|
</chakra.span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
, plus:
|
{scopedT('plus')}
|
||||||
</Text>
|
</Text>
|
||||||
<FeaturesList
|
<FeaturesList
|
||||||
features={[
|
features={[
|
||||||
'5 seats included',
|
scopedT('pro.includedSeats'),
|
||||||
<HStack key="test">
|
<HStack key="test">
|
||||||
<Text>
|
<Text>
|
||||||
<Menu>
|
<Menu>
|
||||||
@ -242,12 +247,9 @@ export const ProPlanPricingCard = ({
|
|||||||
)}
|
)}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>{' '}
|
</Menu>{' '}
|
||||||
chats/mo
|
{scopedT('chatsPerMonth')}
|
||||||
</Text>
|
</Text>
|
||||||
<MoreInfoTooltip>
|
<MoreInfoTooltip>{scopedT('chatsTooltip')}</MoreInfoTooltip>
|
||||||
A chat is counted whenever a user starts a discussion. It is
|
|
||||||
independant of the number of messages he sends and receives.
|
|
||||||
</MoreInfoTooltip>
|
|
||||||
</HStack>,
|
</HStack>,
|
||||||
<HStack key="test">
|
<HStack key="test">
|
||||||
<Text>
|
<Text>
|
||||||
@ -318,16 +320,14 @@ export const ProPlanPricingCard = ({
|
|||||||
)}
|
)}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>{' '}
|
</Menu>{' '}
|
||||||
GB of storage
|
{scopedT('storageLimit')}
|
||||||
</Text>
|
</Text>
|
||||||
<MoreInfoTooltip>
|
<MoreInfoTooltip>
|
||||||
You accumulate storage for every file that your user upload
|
{scopedT('storageLimitTooltip')}
|
||||||
into your bot. If you delete the result, it will free up the
|
|
||||||
space.
|
|
||||||
</MoreInfoTooltip>
|
</MoreInfoTooltip>
|
||||||
</HStack>,
|
</HStack>,
|
||||||
'Custom domains',
|
scopedT('pro.customDomains'),
|
||||||
'In-depth analytics',
|
scopedT('pro.analytics'),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
} from '@typebot.io/lib/pricing'
|
} from '@typebot.io/lib/pricing'
|
||||||
import { FeaturesList } from './FeaturesList'
|
import { FeaturesList } from './FeaturesList'
|
||||||
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
||||||
|
import { useI18n, useScopedI18n } from '@/locales'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialChatsLimitIndex?: number
|
initialChatsLimitIndex?: number
|
||||||
@ -44,6 +45,8 @@ export const StarterPlanPricingCard = ({
|
|||||||
currency,
|
currency,
|
||||||
onPayClick,
|
onPayClick,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const t = useI18n()
|
||||||
|
const scopedT = useScopedI18n('billing.pricingCard')
|
||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] =
|
const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] =
|
||||||
useState<number>()
|
useState<number>()
|
||||||
@ -89,17 +92,17 @@ export const StarterPlanPricingCard = ({
|
|||||||
selectedStorageLimitIndex === undefined
|
selectedStorageLimitIndex === undefined
|
||||||
)
|
)
|
||||||
return ''
|
return ''
|
||||||
if (workspace?.plan === Plan.PRO) return 'Downgrade'
|
if (workspace?.plan === Plan.PRO) return t('downgrade')
|
||||||
if (workspace?.plan === Plan.STARTER) {
|
if (workspace?.plan === Plan.STARTER) {
|
||||||
if (isCurrentPlan) return 'Your current plan'
|
if (isCurrentPlan) return scopedT('upgradeButton.current')
|
||||||
|
|
||||||
if (
|
if (
|
||||||
selectedChatsLimitIndex !== initialChatsLimitIndex ||
|
selectedChatsLimitIndex !== initialChatsLimitIndex ||
|
||||||
selectedStorageLimitIndex !== initialStorageLimitIndex
|
selectedStorageLimitIndex !== initialStorageLimitIndex
|
||||||
)
|
)
|
||||||
return 'Update'
|
return t('update')
|
||||||
}
|
}
|
||||||
return 'Upgrade'
|
return t('upgrade')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePayClick = async () => {
|
const handlePayClick = async () => {
|
||||||
@ -118,9 +121,11 @@ export const StarterPlanPricingCard = ({
|
|||||||
<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">
|
||||||
<Stack spacing="4">
|
<Stack spacing="4">
|
||||||
<Heading fontSize="2xl">
|
<Heading fontSize="2xl">
|
||||||
Upgrade to <chakra.span color="orange.400">Starter</chakra.span>
|
{scopedT('heading', {
|
||||||
|
plan: <chakra.span color="orange.400">Starter</chakra.span>,
|
||||||
|
})}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text>For individuals & small businesses.</Text>
|
<Text>{scopedT('starter.description')}</Text>
|
||||||
<Heading>
|
<Heading>
|
||||||
{formatPrice(
|
{formatPrice(
|
||||||
computePrice(
|
computePrice(
|
||||||
@ -130,11 +135,11 @@ export const StarterPlanPricingCard = ({
|
|||||||
) ?? NaN,
|
) ?? NaN,
|
||||||
currency
|
currency
|
||||||
)}
|
)}
|
||||||
<chakra.span fontSize="md">/ month</chakra.span>
|
<chakra.span fontSize="md">{scopedT('perMonth')}</chakra.span>
|
||||||
</Heading>
|
</Heading>
|
||||||
<FeaturesList
|
<FeaturesList
|
||||||
features={[
|
features={[
|
||||||
'2 seats included',
|
scopedT('starter.includedSeats'),
|
||||||
<HStack key="test">
|
<HStack key="test">
|
||||||
<Text>
|
<Text>
|
||||||
<Menu>
|
<Menu>
|
||||||
@ -194,12 +199,9 @@ export const StarterPlanPricingCard = ({
|
|||||||
)}
|
)}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>{' '}
|
</Menu>{' '}
|
||||||
chats/mo
|
{scopedT('chatsPerMonth')}
|
||||||
</Text>
|
</Text>
|
||||||
<MoreInfoTooltip>
|
<MoreInfoTooltip>{scopedT('chatsTooltip')}</MoreInfoTooltip>
|
||||||
A chat is counted whenever a user starts a discussion. It is
|
|
||||||
independant of the number of messages he sends and receives.
|
|
||||||
</MoreInfoTooltip>
|
|
||||||
</HStack>,
|
</HStack>,
|
||||||
<HStack key="test">
|
<HStack key="test">
|
||||||
<Text>
|
<Text>
|
||||||
@ -260,16 +262,15 @@ export const StarterPlanPricingCard = ({
|
|||||||
)}
|
)}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>{' '}
|
</Menu>{' '}
|
||||||
GB of storage
|
{scopedT('storageLimit')}
|
||||||
</Text>
|
</Text>
|
||||||
<MoreInfoTooltip>
|
<MoreInfoTooltip>
|
||||||
You accumulate storage for every file that your user upload into
|
{scopedT('storageLimitTooltip')}
|
||||||
your bot. If you delete the result, it will free up the space.
|
|
||||||
</MoreInfoTooltip>
|
</MoreInfoTooltip>
|
||||||
</HStack>,
|
</HStack>,
|
||||||
'Branding removed',
|
scopedT('starter.brandingRemoved'),
|
||||||
'File upload input block',
|
scopedT('starter.fileUploadBlock'),
|
||||||
'Create folders',
|
scopedT('starter.createFolders'),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -3,11 +3,12 @@ import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { isNotDefined } from '@typebot.io/lib'
|
import { isNotDefined } from '@typebot.io/lib'
|
||||||
import { ChangePlanModal } from './ChangePlanModal'
|
import { ChangePlanModal } from './ChangePlanModal'
|
||||||
import { LimitReached } from '../types'
|
import { useI18n } from '@/locales'
|
||||||
|
|
||||||
type Props = { limitReachedType?: LimitReached } & ButtonProps
|
type Props = { limitReachedType?: string } & ButtonProps
|
||||||
|
|
||||||
export const UpgradeButton = ({ limitReachedType, ...props }: Props) => {
|
export const UpgradeButton = ({ limitReachedType, ...props }: Props) => {
|
||||||
|
const t = useI18n()
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
return (
|
return (
|
||||||
@ -17,7 +18,7 @@ export const UpgradeButton = ({ limitReachedType, ...props }: Props) => {
|
|||||||
isLoading={isNotDefined(workspace)}
|
isLoading={isNotDefined(workspace)}
|
||||||
onClick={onOpen}
|
onClick={onOpen}
|
||||||
>
|
>
|
||||||
{props.children ?? 'Upgrade'}
|
{props.children ?? t('upgrade')}
|
||||||
<ChangePlanModal
|
<ChangePlanModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
@ -15,12 +15,14 @@ import { parseNumberWithCommas } from '@typebot.io/lib'
|
|||||||
import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing'
|
import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing'
|
||||||
import { defaultQueryOptions, trpc } from '@/lib/trpc'
|
import { defaultQueryOptions, trpc } from '@/lib/trpc'
|
||||||
import { storageToReadable } from '../helpers/storageToReadable'
|
import { storageToReadable } from '../helpers/storageToReadable'
|
||||||
|
import { useScopedI18n } from '@/locales'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workspace: Workspace
|
workspace: Workspace
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UsageProgressBars = ({ workspace }: Props) => {
|
export const UsageProgressBars = ({ workspace }: Props) => {
|
||||||
|
const scopedT = useScopedI18n('billing.usage')
|
||||||
const { data, isLoading } = trpc.billing.getUsage.useQuery(
|
const { data, isLoading } = trpc.billing.getUsage.useQuery(
|
||||||
{
|
{
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@ -44,12 +46,12 @@ export const UsageProgressBars = ({ workspace }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={6}>
|
<Stack spacing={6}>
|
||||||
<Heading fontSize="3xl">Usage</Heading>
|
<Heading fontSize="3xl">{scopedT('heading')}</Heading>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Flex justifyContent="space-between">
|
<Flex justifyContent="space-between">
|
||||||
<HStack>
|
<HStack>
|
||||||
<Heading fontSize="xl" as="h3">
|
<Heading fontSize="xl" as="h3">
|
||||||
Chats
|
{scopedT('chats.heading')}
|
||||||
</Heading>
|
</Heading>
|
||||||
{chatsPercentage >= 80 && (
|
{chatsPercentage >= 80 && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -58,12 +60,10 @@ export const UsageProgressBars = ({ workspace }: Props) => {
|
|||||||
p="3"
|
p="3"
|
||||||
label={
|
label={
|
||||||
<Text>
|
<Text>
|
||||||
Your typebots are popular! You will soon reach your
|
{scopedT('chats.alert.soonReach')}
|
||||||
plan's chats limit. 🚀
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
Make sure to <strong>update your plan</strong> to increase
|
{scopedT('chats.alert.updatePlan')}
|
||||||
this limit and continue chatting with your users.
|
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -73,7 +73,7 @@ export const UsageProgressBars = ({ workspace }: Props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Text fontSize="sm" fontStyle="italic" color="gray.500">
|
<Text fontSize="sm" fontStyle="italic" color="gray.500">
|
||||||
(resets on 1st of every month)
|
{scopedT('chats.resetInfo')}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ export const UsageProgressBars = ({ workspace }: Props) => {
|
|||||||
<Flex justifyContent="space-between">
|
<Flex justifyContent="space-between">
|
||||||
<HStack>
|
<HStack>
|
||||||
<Heading fontSize="xl" as="h3">
|
<Heading fontSize="xl" as="h3">
|
||||||
Storage
|
{scopedT('storage.heading')}
|
||||||
</Heading>
|
</Heading>
|
||||||
{storagePercentage >= 80 && (
|
{storagePercentage >= 80 && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -117,13 +117,10 @@ export const UsageProgressBars = ({ workspace }: Props) => {
|
|||||||
p="3"
|
p="3"
|
||||||
label={
|
label={
|
||||||
<Text>
|
<Text>
|
||||||
Your typebots are popular! You will soon reach your
|
{scopedT('storage.alert.soonReach')}
|
||||||
plan's storage limit. 🚀
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
Make sure to <strong>update your plan</strong> in order to
|
{scopedT('storage.alert.updatePlan')}
|
||||||
continue collecting uploaded files. You can also{' '}
|
|
||||||
<strong>delete files</strong> to free up space.
|
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
export enum LimitReached {
|
|
||||||
BRAND = 'remove branding',
|
|
||||||
CUSTOM_DOMAIN = 'add custom domains',
|
|
||||||
FOLDER = 'create folders',
|
|
||||||
FILE_INPUT = 'use file input blocks',
|
|
||||||
ANALYTICS = 'unlock in-depth analytics',
|
|
||||||
}
|
|
@ -3,15 +3,15 @@ import { FolderPlusIcon } from '@/components/icons'
|
|||||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||||
import { Plan } from '@typebot.io/prisma'
|
import { Plan } from '@typebot.io/prisma'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useScopedI18n } from '@/locales'
|
import { useI18n, useScopedI18n } from '@/locales'
|
||||||
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
||||||
import { LockTag } from '@/features/billing/components/LockTag'
|
import { LockTag } from '@/features/billing/components/LockTag'
|
||||||
import { isFreePlan } from '@/features/billing/helpers/isFreePlan'
|
import { isFreePlan } from '@/features/billing/helpers/isFreePlan'
|
||||||
import { LimitReached } from '@/features/billing/types'
|
|
||||||
|
|
||||||
type Props = { isLoading: boolean; onClick: () => void }
|
type Props = { isLoading: boolean; onClick: () => void }
|
||||||
|
|
||||||
export const CreateFolderButton = ({ isLoading, onClick }: Props) => {
|
export const CreateFolderButton = ({ isLoading, onClick }: Props) => {
|
||||||
|
const t = useI18n()
|
||||||
const scopedT = useScopedI18n('folders.createFolderButton')
|
const scopedT = useScopedI18n('folders.createFolderButton')
|
||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
@ -33,7 +33,7 @@ export const CreateFolderButton = ({ isLoading, onClick }: Props) => {
|
|||||||
<ChangePlanModal
|
<ChangePlanModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
type={LimitReached.FOLDER}
|
type={t('billing.limitMessage.folder')}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
@ -26,10 +26,11 @@ import { useRouter } from 'next/router'
|
|||||||
import { isNotDefined } from '@typebot.io/lib'
|
import { isNotDefined } from '@typebot.io/lib'
|
||||||
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
||||||
import { isFreePlan } from '@/features/billing/helpers/isFreePlan'
|
import { isFreePlan } from '@/features/billing/helpers/isFreePlan'
|
||||||
import { LimitReached } from '@/features/billing/types'
|
|
||||||
import { parseTimeSince } from '@/helpers/parseTimeSince'
|
import { parseTimeSince } from '@/helpers/parseTimeSince'
|
||||||
|
import { useI18n } from '@/locales'
|
||||||
|
|
||||||
export const PublishButton = (props: ButtonProps) => {
|
export const PublishButton = (props: ButtonProps) => {
|
||||||
|
const t = useI18n()
|
||||||
const warningTextColor = useColorModeValue('red.300', 'red.600')
|
const warningTextColor = useColorModeValue('red.300', 'red.600')
|
||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
const { push, query } = useRouter()
|
const { push, query } = useRouter()
|
||||||
@ -72,7 +73,7 @@ export const PublishButton = (props: ButtonProps) => {
|
|||||||
<ChangePlanModal
|
<ChangePlanModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
type={LimitReached.FILE_INPUT}
|
type={t('billing.limitMessage.fileInput')}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
|
@ -21,12 +21,13 @@ import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
|||||||
import { LockTag } from '@/features/billing/components/LockTag'
|
import { LockTag } from '@/features/billing/components/LockTag'
|
||||||
import { UpgradeButton } from '@/features/billing/components/UpgradeButton'
|
import { UpgradeButton } from '@/features/billing/components/UpgradeButton'
|
||||||
import { isProPlan } from '@/features/billing/helpers/isProPlan'
|
import { isProPlan } from '@/features/billing/helpers/isProPlan'
|
||||||
import { LimitReached } from '@/features/billing/types'
|
|
||||||
import { CustomDomainsDropdown } from '@/features/customDomains/components/CustomDomainsDropdown'
|
import { CustomDomainsDropdown } from '@/features/customDomains/components/CustomDomainsDropdown'
|
||||||
import { TypebotHeader } from '@/features/editor/components/TypebotHeader'
|
import { TypebotHeader } from '@/features/editor/components/TypebotHeader'
|
||||||
import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId'
|
import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId'
|
||||||
|
import { useI18n } from '@/locales'
|
||||||
|
|
||||||
export const SharePage = () => {
|
export const SharePage = () => {
|
||||||
|
const t = useI18n()
|
||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
const { typebot, updateTypebot, publishedTypebot } = useTypebot()
|
const { typebot, updateTypebot, publishedTypebot } = useTypebot()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
@ -129,7 +130,7 @@ export const SharePage = () => {
|
|||||||
) : (
|
) : (
|
||||||
<UpgradeButton
|
<UpgradeButton
|
||||||
colorScheme="gray"
|
colorScheme="gray"
|
||||||
limitReachedType={LimitReached.CUSTOM_DOMAIN}
|
limitReachedType={t('billing.limitMessage.customDomain')}
|
||||||
>
|
>
|
||||||
<Text mr="2">Add my domain</Text>{' '}
|
<Text mr="2">Add my domain</Text>{' '}
|
||||||
<LockTag plan={Plan.PRO} />
|
<LockTag plan={Plan.PRO} />
|
||||||
|
@ -8,7 +8,7 @@ import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
|||||||
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
||||||
import { LockTag } from '@/features/billing/components/LockTag'
|
import { LockTag } from '@/features/billing/components/LockTag'
|
||||||
import { isFreePlan } from '@/features/billing/helpers/isFreePlan'
|
import { isFreePlan } from '@/features/billing/helpers/isFreePlan'
|
||||||
import { LimitReached } from '@/features/billing/types'
|
import { useI18n } from '@/locales'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
generalSettings: GeneralSettings
|
generalSettings: GeneralSettings
|
||||||
@ -19,6 +19,7 @@ export const GeneralSettingsForm = ({
|
|||||||
generalSettings,
|
generalSettings,
|
||||||
onGeneralSettingsChange,
|
onGeneralSettingsChange,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const t = useI18n()
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
const isWorkspaceFreePlan = isFreePlan(workspace)
|
const isWorkspaceFreePlan = isFreePlan(workspace)
|
||||||
@ -53,7 +54,7 @@ export const GeneralSettingsForm = ({
|
|||||||
<ChangePlanModal
|
<ChangePlanModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
type={LimitReached.BRAND}
|
type={t('billing.limitMessage.brand')}
|
||||||
/>
|
/>
|
||||||
<Flex
|
<Flex
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
|
@ -6,6 +6,10 @@ export default {
|
|||||||
'dashboard.title': 'My typebots',
|
'dashboard.title': 'My typebots',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
errorMessage: 'An error occured',
|
errorMessage: 'An error occured',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
update: 'Update',
|
||||||
|
upgrade: 'Upgrade',
|
||||||
|
downgrade: 'Downgrade',
|
||||||
'folders.createFolderButton.label': 'Create a folder',
|
'folders.createFolderButton.label': 'Create a folder',
|
||||||
'folders.createTypebotButton.label': 'Create a typebot',
|
'folders.createTypebotButton.label': 'Create a typebot',
|
||||||
'folders.folderButton.deleteConfirmationMessage':
|
'folders.folderButton.deleteConfirmationMessage':
|
||||||
@ -19,4 +23,135 @@ export default {
|
|||||||
'Are you sure you want to delete your typebot {typebotName}?',
|
'Are you sure you want to delete your typebot {typebotName}?',
|
||||||
'folders.typebotButton.deleteConfirmationMessageWarning':
|
'folders.typebotButton.deleteConfirmationMessageWarning':
|
||||||
"All its associated data will be deleted and won't be recoverable.",
|
"All its associated data will be deleted and won't be recoverable.",
|
||||||
|
'account.apiTokens.heading': 'API tokens',
|
||||||
|
'account.apiTokens.description':
|
||||||
|
'These tokens allow other apps to control your whole account and typebots. Be careful!',
|
||||||
|
'account.apiTokens.createButton.label': 'Create',
|
||||||
|
'account.apiTokens.deleteButton.label': 'Delete',
|
||||||
|
'account.apiTokens.table.nameHeader': 'Name',
|
||||||
|
'account.apiTokens.table.createdHeader': 'Created',
|
||||||
|
'account.apiTokens.deleteConfirmationMessage':
|
||||||
|
'The token {tokenName} will be permanently revoked, are you sure you want to continue?',
|
||||||
|
'account.apiTokens.createModal.createHeading': 'Create Token',
|
||||||
|
'account.apiTokens.createModal.createdHeading': 'Token Created',
|
||||||
|
'account.apiTokens.createModal.nameInput.label':
|
||||||
|
'Enter a unique name for your token to differentiate it from other tokens.',
|
||||||
|
'account.apiTokens.createModal.nameInput.placeholder':
|
||||||
|
'I.e. Zapier, Github, Make.com',
|
||||||
|
'account.apiTokens.createModal.createButton.label': 'Create token',
|
||||||
|
'account.apiTokens.createModal.doneButton.label': 'Done',
|
||||||
|
'account.apiTokens.createModal.copyInstruction':
|
||||||
|
'Please copy your token and store it in a safe place.',
|
||||||
|
'account.apiTokens.createModal.securityWarning':
|
||||||
|
'For security reasons we cannot show it again.',
|
||||||
|
'account.preferences.graphNavigation.heading': 'Editor Navigation',
|
||||||
|
'account.preferences.graphNavigation.mouse.label': 'Mouse',
|
||||||
|
'account.preferences.graphNavigation.mouse.description':
|
||||||
|
'Move by dragging the board and zoom in/out using the scroll wheel',
|
||||||
|
'account.preferences.graphNavigation.trackpad.label': 'Trackpad',
|
||||||
|
'account.preferences.graphNavigation.trackpad.description':
|
||||||
|
'Move the board using 2 fingers and zoom in/out by pinching',
|
||||||
|
'account.preferences.appearance.heading': 'Appearance',
|
||||||
|
'account.preferences.appearance.systemLabel': 'System',
|
||||||
|
'account.preferences.appearance.lightLabel': 'Light',
|
||||||
|
'account.preferences.appearance.darkLabel': 'Dark',
|
||||||
|
'account.myAccount.changePhotoButton.label': 'Change photo',
|
||||||
|
'account.myAccount.changePhotoButton.specification': '.jpg or.png, max 1MB',
|
||||||
|
'account.myAccount.emailInput.disabledTooltip':
|
||||||
|
'Updating email is not available. Contact the support if you want to change it.',
|
||||||
|
'account.myAccount.emailInput.label': 'Email address:',
|
||||||
|
'account.myAccount.nameInput.label': 'Name:',
|
||||||
|
'analytics.viewsLabel': 'Views',
|
||||||
|
'analytics.startsLabel': 'Starts',
|
||||||
|
'analytics.completionRateLabel': 'Completion rate',
|
||||||
|
'auth.signin.heading': 'Sign In',
|
||||||
|
'auth.signin.noAccountLabel.preLink': "Don't have an account?",
|
||||||
|
'auth.signin.noAccountLabel.link': 'Sign up for free',
|
||||||
|
'auth.register.heading': 'Create an account',
|
||||||
|
'auth.register.alreadyHaveAccountLabel.preLink': 'Already have an account?',
|
||||||
|
'auth.register.alreadyHaveAccountLabel.link': 'Sign in',
|
||||||
|
'auth.error.default': 'Try signing with a different account.',
|
||||||
|
'auth.error.email': 'Email not found. Try signing with a different provider.',
|
||||||
|
'auth.error.oauthNotLinked':
|
||||||
|
'To confirm your identity, sign in with the same account you used originally.',
|
||||||
|
'auth.error.unknown': 'An error occurred. Please try again.',
|
||||||
|
'auth.signinErrorToast.title': 'Unauthorized',
|
||||||
|
'auth.signinErrorToast.description': 'Sign ups are disabled.',
|
||||||
|
'auth.noProvider.preLink': 'You need to',
|
||||||
|
'auth.noProvider.link':
|
||||||
|
'configure at least one auth provider (Email, Google, GitHub, Facebook or Azure AD).',
|
||||||
|
'auth.orEmailLabel': 'Or with your email',
|
||||||
|
'auth.emailSubmitButton.label': 'Submit',
|
||||||
|
'auth.magicLink.title': 'A magic link email was sent. 🪄',
|
||||||
|
'auth.magicLink.description': 'Make sure to check your spam folder.',
|
||||||
|
'auth.socialLogin.githubButton.label': 'Continue with GitHub',
|
||||||
|
'auth.socialLogin.googleButton.label': 'Continue with Google',
|
||||||
|
'auth.socialLogin.facebookButton.label': 'Continue with Facebook',
|
||||||
|
'auth.socialLogin.azureButton.label': 'Continue with {azureProviderName}',
|
||||||
|
'auth.socialLogin.gitlabButton.label': 'Continue with {gitlabProviderName}',
|
||||||
|
'auth.socialLogin.customButton.label': 'Continue with {customProviderName}',
|
||||||
|
'billing.billingPortalButton.label': 'Billing portal',
|
||||||
|
'billing.contribution.preLink':
|
||||||
|
'Typebot is contributing 1% of your subscription to remove CO₂ from the atmosphere.',
|
||||||
|
'billing.contribution.link': 'Learn more.',
|
||||||
|
'billing.updateSuccessToast.description':
|
||||||
|
'Workspace {plan} plan successfully updated 🎉',
|
||||||
|
'billing.customLimit.preLink': 'Need custom limits? Specific features?',
|
||||||
|
'billing.customLimit.link': "Let's chat!",
|
||||||
|
'billing.upgradeLimitLabel':
|
||||||
|
'You need to upgrade your plan in order to {type}',
|
||||||
|
'billing.currentSubscription.heading': 'Subscription',
|
||||||
|
'billing.currentSubscription.subheading': 'Current workspace subscription:',
|
||||||
|
'billing.currentSubscription.cancelLink': 'Cancel my subscription',
|
||||||
|
'billing.currentSubscription.billingPortalDescription':
|
||||||
|
'Need to change payment method or billing information? Head over to your billing portal:',
|
||||||
|
'billing.invoices.heading': 'Invoices',
|
||||||
|
'billing.invoices.empty': 'No invoices found for this workspace.',
|
||||||
|
'billing.invoices.paidAt': 'Paid at',
|
||||||
|
'billing.invoices.subtotal': 'Subtotal',
|
||||||
|
'billing.preCheckoutModal.companyInput.label': 'Company name:',
|
||||||
|
'billing.preCheckoutModal.emailInput.label': 'Email:',
|
||||||
|
'billing.preCheckoutModal.taxId.label': 'Tax ID:',
|
||||||
|
'billing.preCheckoutModal.taxId.placeholder': 'ID type',
|
||||||
|
'billing.preCheckoutModal.submitButton.label': 'Go to checkout',
|
||||||
|
'billing.pricingCard.heading': 'Upgrade to {plan}',
|
||||||
|
'billing.pricingCard.perMonth': '/ month',
|
||||||
|
'billing.pricingCard.plus': ', plus:',
|
||||||
|
'billing.pricingCard.upgradeButton.current': 'Your current plan',
|
||||||
|
'billing.pricingCard.chatsPerMonth': 'chats/mo',
|
||||||
|
'billing.pricingCard.chatsTooltip':
|
||||||
|
'A chat is counted whenever a user starts a discussion. It is independant of the number of messages he sends and receives.',
|
||||||
|
'billing.pricingCard.storageLimit': 'GB of storage',
|
||||||
|
'billing.pricingCard.storageLimitTooltip':
|
||||||
|
'You accumulate storage for every file that your user upload into your bot. If you delete the result, it will free up the space.',
|
||||||
|
'billing.pricingCard.starter.description':
|
||||||
|
'For individuals & small businesses.',
|
||||||
|
'billing.pricingCard.starter.includedSeats': '2 seats included',
|
||||||
|
'billing.pricingCard.starter.brandingRemoved': 'Branding removed',
|
||||||
|
'billing.pricingCard.starter.fileUploadBlock': 'File upload input block',
|
||||||
|
'billing.pricingCard.starter.createFolders': 'Create folders',
|
||||||
|
'billing.pricingCard.pro.mostPopularLabel': 'Most popular',
|
||||||
|
'billing.pricingCard.pro.description': 'For agencies & growing startups.',
|
||||||
|
'billing.pricingCard.pro.everythingFromStarter': 'Everything in Starter',
|
||||||
|
'billing.pricingCard.pro.includedSeats': '5 seats included',
|
||||||
|
'billing.pricingCard.pro.customDomains': 'Custom domains',
|
||||||
|
'billing.pricingCard.pro.analytics': 'In-depth analytics',
|
||||||
|
'billing.usage.heading': 'Usage',
|
||||||
|
'billing.usage.chats.heading': 'Chats',
|
||||||
|
'billing.usage.chats.alert.soonReach':
|
||||||
|
"Your typebots are popular! You will soon reach your plan's chats limit. 🚀",
|
||||||
|
'billing.usage.chats.alert.updatePlan':
|
||||||
|
'Make sure to update your plan to increase this limit and continue chatting with your users.',
|
||||||
|
'billing.usage.chats.resetInfo': '(resets on 1st of every month)',
|
||||||
|
'billing.usage.storage.heading': 'Storage',
|
||||||
|
'billing.usage.storage.alert.soonReach':
|
||||||
|
"Your typebots are popular! You will soon reach your plan's storage limit. 🚀",
|
||||||
|
'billing.usage.storage.alert.updatePlan':
|
||||||
|
'Make sure to update your plan in order to continue collecting uploaded files. You can also delete files to free up space.',
|
||||||
|
'billing.limitMessage.brand': 'remove branding',
|
||||||
|
'billing.limitMessage.customDomain': 'add custom domains',
|
||||||
|
'billing.limitMessage.analytics': 'unlock in-depth analytics',
|
||||||
|
'billing.limitMessage.fileInput': 'use file input blocks',
|
||||||
|
'billing.limitMessage.folder': 'create folders',
|
||||||
|
'billing.upgradeAlert.buttonDefaultLabel': 'More info',
|
||||||
} as const
|
} as const
|
||||||
|
@ -8,6 +8,10 @@ export default defineLocale({
|
|||||||
'dashboard.title': 'Mes typebots',
|
'dashboard.title': 'Mes typebots',
|
||||||
delete: 'Supprimer',
|
delete: 'Supprimer',
|
||||||
errorMessage: "Une erreur s'est produite",
|
errorMessage: "Une erreur s'est produite",
|
||||||
|
cancel: 'Annuler',
|
||||||
|
update: 'Mettre à jour',
|
||||||
|
upgrade: 'Upgrade',
|
||||||
|
downgrade: 'Downgrade',
|
||||||
'folders.createFolderButton.label': 'Créer un dossier',
|
'folders.createFolderButton.label': 'Créer un dossier',
|
||||||
'folders.createTypebotButton.label': 'Créer un typebot',
|
'folders.createTypebotButton.label': 'Créer un typebot',
|
||||||
'folders.folderButton.deleteConfirmationMessage':
|
'folders.folderButton.deleteConfirmationMessage':
|
||||||
@ -21,4 +25,141 @@ export default defineLocale({
|
|||||||
'Êtes-vous sûr de vouloir supprimer votre typebot {typebotName} ?',
|
'Êtes-vous sûr de vouloir supprimer votre typebot {typebotName} ?',
|
||||||
'folders.typebotButton.deleteConfirmationMessageWarning':
|
'folders.typebotButton.deleteConfirmationMessageWarning':
|
||||||
'Toutes les données associées seront supprimées et ne pourront pas être récupérées.',
|
'Toutes les données associées seront supprimées et ne pourront pas être récupérées.',
|
||||||
|
'account.apiTokens.heading': 'Tokens API',
|
||||||
|
'account.apiTokens.description':
|
||||||
|
"Ces tokens permettent à d'autres applications de contrôler votre compte et vos typebots. Soyez prudent !",
|
||||||
|
'account.apiTokens.createButton.label': 'Créer',
|
||||||
|
'account.apiTokens.deleteButton.label': 'Supprimer',
|
||||||
|
'account.apiTokens.table.nameHeader': 'Nom',
|
||||||
|
'account.apiTokens.table.createdHeader': 'Créé',
|
||||||
|
'account.apiTokens.deleteConfirmationMessage':
|
||||||
|
'Le token {tokenName} sera définitivement révoqué, êtes-vous sûr de vouloir continuer ?',
|
||||||
|
'account.apiTokens.createModal.createHeading': 'Créer un token',
|
||||||
|
'account.apiTokens.createModal.createdHeading': 'Token créé',
|
||||||
|
'account.apiTokens.createModal.nameInput.label':
|
||||||
|
'Entrez un nom unique pour votre token afin de le différencier des autres tokens.',
|
||||||
|
'account.apiTokens.createModal.nameInput.placeholder':
|
||||||
|
'Ex. Zapier, Github, Make.com',
|
||||||
|
'account.apiTokens.createModal.createButton.label': 'Créer un token',
|
||||||
|
'account.apiTokens.createModal.doneButton.label': 'Terminé',
|
||||||
|
'account.apiTokens.createModal.copyInstruction':
|
||||||
|
'Veuillez copier votre token et le stocker dans un endroit sûr.',
|
||||||
|
'account.apiTokens.createModal.securityWarning':
|
||||||
|
'Pour des raisons de sécurité, nous ne pouvons pas le montrer à nouveau.',
|
||||||
|
'account.preferences.graphNavigation.heading': "Navigation de l'éditeur",
|
||||||
|
'account.preferences.graphNavigation.mouse.label': 'Souris',
|
||||||
|
'account.preferences.graphNavigation.mouse.description':
|
||||||
|
'Déplacez en glissant et zoom en avant/arrière en utilisant la molette',
|
||||||
|
'account.preferences.graphNavigation.trackpad.label': 'Trackpad',
|
||||||
|
'account.preferences.graphNavigation.trackpad.description':
|
||||||
|
'Déplacez le board en utilisant 2 doigts et zoomer en avant/arrière en pincant',
|
||||||
|
'account.preferences.appearance.heading': 'Apparence',
|
||||||
|
'account.preferences.appearance.systemLabel': 'Système',
|
||||||
|
'account.preferences.appearance.lightLabel': 'Clair',
|
||||||
|
'account.preferences.appearance.darkLabel': 'Sombre',
|
||||||
|
'account.myAccount.changePhotoButton.label': 'Changer de photo',
|
||||||
|
'account.myAccount.changePhotoButton.specification': '.jpg ou.png, max 1MB',
|
||||||
|
'account.myAccount.emailInput.disabledTooltip':
|
||||||
|
"La mise à jour de l'adresse e-mail n'est pas disponible. Contactez le service d'assistance si vous souhaitez la modifier.",
|
||||||
|
'account.myAccount.emailInput.label': 'Adresse e-mail:',
|
||||||
|
'account.myAccount.nameInput.label': 'Nom:',
|
||||||
|
'analytics.viewsLabel': 'Vues',
|
||||||
|
'analytics.startsLabel': 'Démarrés',
|
||||||
|
'analytics.completionRateLabel': 'Taux de complétion',
|
||||||
|
'auth.signin.heading': 'Se connecter',
|
||||||
|
'auth.signin.noAccountLabel.preLink': "Vous n'avez pas de compte?",
|
||||||
|
'auth.signin.noAccountLabel.link': 'Inscrivez-vous gratuitement',
|
||||||
|
'auth.register.heading': 'Créer un compte',
|
||||||
|
'auth.register.alreadyHaveAccountLabel.preLink': 'Vous avez déjà un compte?',
|
||||||
|
'auth.register.alreadyHaveAccountLabel.link': 'Se connecter',
|
||||||
|
'auth.error.default': 'Essayez de vous connecter avec un compte différent.',
|
||||||
|
'auth.error.email':
|
||||||
|
'Email non trouvé. Essayez de vous connecter avec un fournisseur différent.',
|
||||||
|
'auth.error.oauthNotLinked':
|
||||||
|
'Pour confirmer votre identité, connectez-vous avec le même compte que vous avez utilisé à lorigine.',
|
||||||
|
'auth.error.unknown': 'Une erreur est survenue. Veuillez réessayer.',
|
||||||
|
'auth.signinErrorToast.title': 'Non autorisé',
|
||||||
|
'auth.signinErrorToast.description': 'Les inscriptions sont désactivées.',
|
||||||
|
'auth.noProvider.preLink': 'Vous avez besoin de',
|
||||||
|
'auth.noProvider.link':
|
||||||
|
"configurer au moins un fournisseur d'authentification (E-mail, Google, GitHub, Facebook ou Azure AD).",
|
||||||
|
'auth.orEmailLabel': 'Ou avec votre email',
|
||||||
|
'auth.emailSubmitButton.label': 'Se connecter',
|
||||||
|
'auth.magicLink.title':
|
||||||
|
"Un email avec un lien d'authentification a été envoyé. 🪄",
|
||||||
|
'auth.magicLink.description':
|
||||||
|
'Assurez-vous de vérifier votre dossier de spam.',
|
||||||
|
'auth.socialLogin.githubButton.label': 'Continuer avec GitHub',
|
||||||
|
'auth.socialLogin.googleButton.label': 'Continuer avec Google',
|
||||||
|
'auth.socialLogin.facebookButton.label': 'Continuer avec Facebook',
|
||||||
|
'auth.socialLogin.azureButton.label': 'Continuer avec {azureProviderName}',
|
||||||
|
'auth.socialLogin.gitlabButton.label': 'Continuer avec {gitlabProviderName}',
|
||||||
|
'auth.socialLogin.customButton.label': 'Continuer avec {customProviderName}',
|
||||||
|
'billing.billingPortalButton.label': 'Portail de facturation',
|
||||||
|
'billing.contribution.preLink':
|
||||||
|
"Typebot contribue à hauteur de 1% de votre abonnement pour éliminer le CO₂ de l'atmosphère.",
|
||||||
|
'billing.contribution.link': 'En savoir plus.',
|
||||||
|
'billing.updateSuccessToast.description':
|
||||||
|
'Votre abonnement {plan} a été mis à jour avec succès 🎉',
|
||||||
|
'billing.customLimit.preLink':
|
||||||
|
'Vous avez besoin de limites personnalisées ? De fonctionnalités spécifiques ?',
|
||||||
|
'billing.customLimit.link': 'Discutons-en!',
|
||||||
|
'billing.upgradeLimitLabel':
|
||||||
|
'Vous devez mettre à niveau votre abonnement pour {type}',
|
||||||
|
'billing.currentSubscription.heading': 'Abonnement',
|
||||||
|
'billing.currentSubscription.subheading': 'Abonnement actuel du workspace :',
|
||||||
|
'billing.currentSubscription.cancelLink': "Annuler l'abonnement",
|
||||||
|
'billing.currentSubscription.billingPortalDescription':
|
||||||
|
'Besoin de changer votre mode de paiement ou vos informations de facturation ? Rendez-vous sur votre portail de facturation :',
|
||||||
|
'billing.invoices.heading': 'Factures',
|
||||||
|
'billing.invoices.empty': 'Aucune facture trouvée pour ce workspace.',
|
||||||
|
'billing.invoices.paidAt': 'Payé le',
|
||||||
|
'billing.invoices.subtotal': 'Sous-total',
|
||||||
|
'billing.preCheckoutModal.companyInput.label': "Nom de l'entreprise :",
|
||||||
|
'billing.preCheckoutModal.emailInput.label': 'E-mail :',
|
||||||
|
'billing.preCheckoutModal.taxId.label': 'Numéro de TVA :',
|
||||||
|
'billing.preCheckoutModal.taxId.placeholder': 'Type',
|
||||||
|
'billing.preCheckoutModal.submitButton.label': 'Continuer',
|
||||||
|
'billing.pricingCard.heading': 'Passer à {plan}',
|
||||||
|
'billing.pricingCard.perMonth': '/ mois',
|
||||||
|
'billing.pricingCard.plus': ', plus :',
|
||||||
|
'billing.pricingCard.upgradeButton.current': 'Abonnement actuel',
|
||||||
|
'billing.pricingCard.chatsPerMonth': 'chats/mois',
|
||||||
|
'billing.pricingCard.chatsTooltip':
|
||||||
|
"Un chat est comptabilisé chaque fois qu'un utilisateur démarre une discussion. Il est indépendant du nombre de messages qu'il envoie et reçoit.",
|
||||||
|
'billing.pricingCard.storageLimit': 'Go de stockage',
|
||||||
|
'billing.pricingCard.storageLimitTooltip':
|
||||||
|
"Vous accumulez du stockage pour chaque fichier que votre utilisateur télécharge dans votre bot. Si vous supprimez le résultat, cela libérera de l'espace.",
|
||||||
|
'billing.pricingCard.starter.description':
|
||||||
|
'Pour les particuliers et les petites entreprises.',
|
||||||
|
'billing.pricingCard.starter.includedSeats': '2 collègues inclus',
|
||||||
|
'billing.pricingCard.starter.brandingRemoved': 'Marque enlevée',
|
||||||
|
'billing.pricingCard.starter.fileUploadBlock': "Bloc d'upload de fichier",
|
||||||
|
'billing.pricingCard.starter.createFolders': 'Créer des dossiers',
|
||||||
|
'billing.pricingCard.pro.mostPopularLabel': 'Le plus populaire',
|
||||||
|
'billing.pricingCard.pro.description':
|
||||||
|
'Pour les agences et les startups en croissance.',
|
||||||
|
'billing.pricingCard.pro.everythingFromStarter':
|
||||||
|
"Tout ce qu'il y a dans Starter",
|
||||||
|
'billing.pricingCard.pro.includedSeats': '5 collègues inclus',
|
||||||
|
'billing.pricingCard.pro.customDomains': 'Domaines personnalisés',
|
||||||
|
'billing.pricingCard.pro.analytics': 'Analyses approfondies',
|
||||||
|
'billing.usage.heading': 'Utilisation',
|
||||||
|
'billing.usage.chats.heading': 'Chats',
|
||||||
|
'billing.usage.chats.alert.soonReach':
|
||||||
|
'Vos typebots sont populaires ! Vous atteindrez bientôt la limite de chats de votre abonnement. 🚀',
|
||||||
|
'billing.usage.chats.alert.updatePlan':
|
||||||
|
'Assurez-vous de mettre à jour votre abonnement pour augmenter cette limite et continuer à discuter avec vos utilisateurs.',
|
||||||
|
'billing.usage.chats.resetInfo': '(réinitialisé le 1er de chaque mois)',
|
||||||
|
'billing.usage.storage.heading': 'Stockage',
|
||||||
|
'billing.usage.storage.alert.soonReach':
|
||||||
|
'Vos typebots sont populaires ! Vous atteindrez bientôt la limite de stockage de votre abonnement. 🚀',
|
||||||
|
'billing.usage.storage.alert.updatePlan':
|
||||||
|
"Assurez-vous de mettre à jour votre abonnement pour continuer à collecter des fichiers téléchargés. Vous pouvez également supprimer des fichiers pour libérer de l'espace.",
|
||||||
|
'billing.limitMessage.brand': 'supprimer la marque',
|
||||||
|
'billing.limitMessage.customDomain': 'ajouter des domaines personnalisés',
|
||||||
|
'billing.limitMessage.analytics': 'débloquer des analyses approfondies',
|
||||||
|
'billing.limitMessage.fileInput': 'utiliser des blocs de saisie de fichiers',
|
||||||
|
'billing.limitMessage.folder': 'créer des dossiers',
|
||||||
|
'billing.upgradeAlert.buttonDefaultLabel': "Plus d'informations",
|
||||||
})
|
})
|
||||||
|
@ -8,6 +8,10 @@ export default defineLocale({
|
|||||||
'dashboard.title': 'Meus typebots',
|
'dashboard.title': 'Meus typebots',
|
||||||
delete: 'Deletar',
|
delete: 'Deletar',
|
||||||
errorMessage: 'Ocorreu um erro',
|
errorMessage: 'Ocorreu um erro',
|
||||||
|
cancel: 'Cancelar',
|
||||||
|
update: 'Atualizar',
|
||||||
|
upgrade: 'Upgrade',
|
||||||
|
downgrade: 'Downgrade',
|
||||||
'folders.createFolderButton.label': 'Criar uma pasta',
|
'folders.createFolderButton.label': 'Criar uma pasta',
|
||||||
'folders.createTypebotButton.label': 'Criar um typebot',
|
'folders.createTypebotButton.label': 'Criar um typebot',
|
||||||
'folders.folderButton.deleteConfirmationMessage':
|
'folders.folderButton.deleteConfirmationMessage':
|
||||||
@ -21,4 +25,142 @@ export default defineLocale({
|
|||||||
'Tem certeza de que deseja excluir seu typebot {typebotName}?',
|
'Tem certeza de que deseja excluir seu typebot {typebotName}?',
|
||||||
'folders.typebotButton.deleteConfirmationMessageWarning':
|
'folders.typebotButton.deleteConfirmationMessageWarning':
|
||||||
'Todos os dados associados serão excluídos e não poderão ser recuperados.',
|
'Todos os dados associados serão excluídos e não poderão ser recuperados.',
|
||||||
|
'account.apiTokens.heading': 'Tokens de API',
|
||||||
|
'account.apiTokens.description':
|
||||||
|
'Esses tokens permitem que outros aplicativos controlem toda a sua conta e typebots. Tenha cuidado!',
|
||||||
|
'account.apiTokens.createButton.label': 'Criar',
|
||||||
|
'account.apiTokens.deleteButton.label': 'Excluir',
|
||||||
|
'account.apiTokens.table.nameHeader': 'Nome',
|
||||||
|
'account.apiTokens.table.createdHeader': 'Criado',
|
||||||
|
'account.apiTokens.deleteConfirmationMessage':
|
||||||
|
'O token {tokenName} será revogado permanentemente. Tem certeza de que deseja continuar?',
|
||||||
|
'account.apiTokens.createModal.createHeading': 'Criar Token',
|
||||||
|
'account.apiTokens.createModal.createdHeading': 'Token Criado',
|
||||||
|
'account.apiTokens.createModal.nameInput.label':
|
||||||
|
'Insira um nome único para o seu token para diferenciá-lo de outros tokens.',
|
||||||
|
'account.apiTokens.createModal.nameInput.placeholder':
|
||||||
|
'Ex. Zapier, Github, Make.com',
|
||||||
|
'account.apiTokens.createModal.createButton.label': 'Criar token',
|
||||||
|
'account.apiTokens.createModal.doneButton.label': 'Concluído',
|
||||||
|
'account.apiTokens.createModal.copyInstruction':
|
||||||
|
'Por favor, copie seu token e guarde-o em um lugar seguro.',
|
||||||
|
'account.apiTokens.createModal.securityWarning':
|
||||||
|
'Por motivos de segurança, não podemos mostrá-lo novamente.',
|
||||||
|
'account.preferences.graphNavigation.heading': 'Navegação do Editor',
|
||||||
|
'account.preferences.graphNavigation.mouse.label': 'Mouse',
|
||||||
|
'account.preferences.graphNavigation.mouse.description':
|
||||||
|
'Mova arrastando o quadro e amplie/reduza usando a roda de rolagem',
|
||||||
|
'account.preferences.graphNavigation.trackpad.label': 'Trackpad',
|
||||||
|
'account.preferences.graphNavigation.trackpad.description':
|
||||||
|
'Mova o quadro usando 2 dedos e amplie/reduza fazendo pinça',
|
||||||
|
'account.preferences.appearance.heading': 'Aparência',
|
||||||
|
'account.preferences.appearance.systemLabel': 'Sistema',
|
||||||
|
'account.preferences.appearance.lightLabel': 'Claro',
|
||||||
|
'account.preferences.appearance.darkLabel': 'Escuro',
|
||||||
|
'account.myAccount.changePhotoButton.label': 'Alterar foto',
|
||||||
|
'account.myAccount.changePhotoButton.specification':
|
||||||
|
'.jpg ou.png, máximo 1MB',
|
||||||
|
'account.myAccount.emailInput.disabledTooltip':
|
||||||
|
'A atualização do e-mail não está disponível. Entre em contato com o suporte se quiser alterá-lo.',
|
||||||
|
'account.myAccount.emailInput.label': 'Endereço de e-mail:',
|
||||||
|
'account.myAccount.nameInput.label': 'Nome:',
|
||||||
|
'analytics.viewsLabel': 'Visualizações',
|
||||||
|
'analytics.startsLabel': 'Inícios',
|
||||||
|
'analytics.completionRateLabel': 'Taxa de conclusão',
|
||||||
|
'auth.signin.heading': 'Entrar',
|
||||||
|
'auth.signin.noAccountLabel.preLink': 'Não tem uma conta?',
|
||||||
|
'auth.signin.noAccountLabel.link': 'Registre-se gratuitamente',
|
||||||
|
'auth.register.heading': 'Criar uma conta',
|
||||||
|
'auth.register.alreadyHaveAccountLabel.preLink': 'Já tem uma conta?',
|
||||||
|
'auth.register.alreadyHaveAccountLabel.link': 'Entrar',
|
||||||
|
'auth.error.default': 'Tente entrar com uma conta diferente.',
|
||||||
|
'auth.error.email':
|
||||||
|
'E-mail não encontrado. Tente entrar com um provedor diferente.',
|
||||||
|
'auth.error.oauthNotLinked':
|
||||||
|
'Para confirmar sua identidade, entre com a mesma conta que você usou originalmente.',
|
||||||
|
'auth.error.unknown': 'Ocorreu um erro. Tente novamente.',
|
||||||
|
'auth.signinErrorToast.title': 'Não autorizado',
|
||||||
|
'auth.signinErrorToast.description': 'As inscrições estão desativadas.',
|
||||||
|
'auth.noProvider.preLink': 'Você precisa',
|
||||||
|
'auth.noProvider.link':
|
||||||
|
'configurar pelo menos um provedor de autenticação (E-mail, Google, GitHub, Facebook ou Azure AD).',
|
||||||
|
'auth.orEmailLabel': 'Ou com seu email',
|
||||||
|
'auth.emailSubmitButton.label': 'Enviar',
|
||||||
|
'auth.magicLink.title': 'Um email de link mágico foi enviado. 🪄',
|
||||||
|
'auth.magicLink.description': 'Certifique-se de verificar sua pasta de spam.',
|
||||||
|
'auth.socialLogin.githubButton.label': 'Continuar com GitHub',
|
||||||
|
'auth.socialLogin.googleButton.label': 'Continuar com Google',
|
||||||
|
'auth.socialLogin.facebookButton.label': 'Continuar com Facebook',
|
||||||
|
'auth.socialLogin.azureButton.label': 'Continuar com {azureProviderName}',
|
||||||
|
'auth.socialLogin.gitlabButton.label': 'Continuar com {gitlabProviderName}',
|
||||||
|
'auth.socialLogin.customButton.label': 'Continuar com {customProviderName}',
|
||||||
|
'billing.billingPortalButton.label': 'Portal de cobrança',
|
||||||
|
'billing.contribution.preLink':
|
||||||
|
'A Typebot está contribuindo com 1% da sua assinatura para remover o CO₂ da atmosfera.',
|
||||||
|
'billing.contribution.link': 'Saiba mais.',
|
||||||
|
'billing.updateSuccessToast.description':
|
||||||
|
'Sua assinatura {plan} foi atualizada com sucesso 🎉',
|
||||||
|
'billing.customLimit.preLink':
|
||||||
|
'Precisa de limites personalizados? Recursos específicos?',
|
||||||
|
'billing.customLimit.link': 'Vamos conversar!',
|
||||||
|
'billing.upgradeLimitLabel':
|
||||||
|
'Você precisa atualizar sua assinatura para {type}',
|
||||||
|
'billing.currentSubscription.heading': 'Assinatura',
|
||||||
|
'billing.currentSubscription.subheading':
|
||||||
|
'Assinatura atual do espaço de trabalho:',
|
||||||
|
'billing.currentSubscription.cancelLink': 'Cancelar minha assinatura',
|
||||||
|
'billing.currentSubscription.billingPortalDescription':
|
||||||
|
'Precisa alterar o método de pagamento ou as informações de cobrança? Acesse seu portal de cobrança:',
|
||||||
|
'billing.invoices.heading': 'Faturas',
|
||||||
|
'billing.invoices.empty':
|
||||||
|
'Nenhuma fatura encontrada para este espaço de trabalho.',
|
||||||
|
'billing.invoices.paidAt': 'Pago em',
|
||||||
|
'billing.invoices.subtotal': 'Subtotal',
|
||||||
|
'billing.preCheckoutModal.companyInput.label': 'Nome da empresa:',
|
||||||
|
'billing.preCheckoutModal.emailInput.label': 'E-mail:',
|
||||||
|
'billing.preCheckoutModal.taxId.label': 'Identificação fiscal:',
|
||||||
|
'billing.preCheckoutModal.taxId.placeholder': 'Tipo de ID',
|
||||||
|
'billing.preCheckoutModal.submitButton.label':
|
||||||
|
'Ir para a finalização da compra',
|
||||||
|
'billing.pricingCard.heading': 'Mudar para {plan}',
|
||||||
|
'billing.pricingCard.perMonth': '/ mês',
|
||||||
|
'billing.pricingCard.plus': ', mais:',
|
||||||
|
'billing.pricingCard.upgradeButton.current': 'Sua assinatura atual',
|
||||||
|
'billing.pricingCard.chatsPerMonth': 'chats/mês',
|
||||||
|
'billing.pricingCard.chatsTooltip':
|
||||||
|
'Um chat é contado sempre que um usuário inicia uma discussão. Ele é independente do número de mensagens que ele envia e recebe.',
|
||||||
|
'billing.pricingCard.storageLimit': 'GB de armazenamento',
|
||||||
|
'billing.pricingCard.storageLimitTooltip':
|
||||||
|
'Você acumula armazenamento para cada arquivo que seu usuário carrega em seu bot. Se você excluir o resultado, ele liberará espaço.',
|
||||||
|
'billing.pricingCard.starter.description':
|
||||||
|
'Para indivíduos e pequenas empresas.',
|
||||||
|
'billing.pricingCard.starter.includedSeats': '2 assentos incluídos',
|
||||||
|
'billing.pricingCard.starter.brandingRemoved': 'Marca removida',
|
||||||
|
'billing.pricingCard.starter.fileUploadBlock': 'Bloco de envio de arquivo',
|
||||||
|
'billing.pricingCard.starter.createFolders': 'Criar pastas',
|
||||||
|
'billing.pricingCard.pro.mostPopularLabel': 'Mais popular',
|
||||||
|
'billing.pricingCard.pro.description':
|
||||||
|
'Para agências e startups em crescimento.',
|
||||||
|
'billing.pricingCard.pro.everythingFromStarter': 'Tudo em Starter',
|
||||||
|
'billing.pricingCard.pro.includedSeats': '5 assentos incluídos',
|
||||||
|
'billing.pricingCard.pro.customDomains': 'Domínios personalizados',
|
||||||
|
'billing.pricingCard.pro.analytics': 'Análises aprofundadas',
|
||||||
|
'billing.usage.heading': 'Uso',
|
||||||
|
'billing.usage.chats.heading': 'Chats',
|
||||||
|
'billing.usage.chats.alert.soonReach':
|
||||||
|
'Seus typebots são populares! Você logo alcançará o limite de chats de seu plano. 🚀',
|
||||||
|
'billing.usage.chats.alert.updatePlan':
|
||||||
|
'Certifique-se de atualizar seu plano para aumentar esse limite e continuar conversando com seus usuários.',
|
||||||
|
'billing.usage.chats.resetInfo': '(reiniciado todo dia 1)',
|
||||||
|
'billing.usage.storage.heading': 'Armazenamento',
|
||||||
|
'billing.usage.storage.alert.soonReach':
|
||||||
|
'Seus typebots são populares! Você logo alcançará o limite de armazenamento de seu plano. 🚀',
|
||||||
|
'billing.usage.storage.alert.updatePlan':
|
||||||
|
'Certifique-se de atualizar seu plano para continuar coletando arquivos enviados. Você também pode excluir arquivos para liberar espaço.',
|
||||||
|
'billing.limitMessage.brand': 'remover a marca',
|
||||||
|
'billing.limitMessage.customDomain': 'adicionar domínios personalizados',
|
||||||
|
'billing.limitMessage.analytics': 'desbloquear análises aprofundadas',
|
||||||
|
'billing.limitMessage.fileInput': 'usar blocos de envio de arquivo',
|
||||||
|
'billing.limitMessage.folder': 'criar pastas',
|
||||||
|
'billing.upgradeAlert.buttonDefaultLabel': 'Mais informações',
|
||||||
})
|
})
|
||||||
|
@ -8,12 +8,12 @@ import AzureADProvider from 'next-auth/providers/azure-ad'
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { Provider } from 'next-auth/providers'
|
import { Provider } from 'next-auth/providers'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { CustomAdapter } from './adapter'
|
import { customAdapter } from '../../../features/auth/api/customAdapter'
|
||||||
import { User } from '@typebot.io/prisma'
|
import { User } from '@typebot.io/prisma'
|
||||||
import { env, getAtPath, isDefined, isNotEmpty } from '@typebot.io/lib'
|
import { env, getAtPath, isDefined, isNotEmpty } from '@typebot.io/lib'
|
||||||
import { sendVerificationRequest } from './sendVerificationRequest'
|
|
||||||
import { mockedUser } from '@/features/auth/mockedUser'
|
import { mockedUser } from '@/features/auth/mockedUser'
|
||||||
import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitations'
|
import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitations'
|
||||||
|
import { sendVerificationRequest } from '@/features/auth/helpers/sendVerificationRequest'
|
||||||
|
|
||||||
const providers: Provider[] = []
|
const providers: Provider[] = []
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ if (isNotEmpty(process.env.CUSTOM_OAUTH_WELL_KNOWN_URL)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const authOptions: AuthOptions = {
|
export const authOptions: AuthOptions = {
|
||||||
adapter: CustomAdapter(prisma),
|
adapter: customAdapter(prisma),
|
||||||
secret: process.env.ENCRYPTION_SECRET,
|
secret: process.env.ENCRYPTION_SECRET,
|
||||||
providers,
|
providers,
|
||||||
session: {
|
session: {
|
||||||
|
Reference in New Issue
Block a user