2
0

🐛 (billing) Collect tax ID manually before checkout

This allows Typebot to always display a company name on invoices.
This commit is contained in:
Baptiste Arnaud
2023-03-07 08:01:05 +01:00
parent 67a3f42edd
commit 767a8208a8
8 changed files with 733 additions and 66 deletions

View File

@ -23,21 +23,30 @@ import { ChevronDownIcon, CloseIcon } from '../icons'
const dropdownCloseAnimationDuration = 300
type Item = string | { icon?: JSX.Element; label: string; value: string }
type Item =
| string
| {
icon?: JSX.Element
label: string
value: string
extras?: Record<string, unknown>
}
type Props = {
type Props<T extends Item> = {
isPopoverMatchingInputWidth?: boolean
selectedItem?: string
items: Item[]
items: T[]
placeholder?: string
onSelect?: (value: string | undefined) => void
onSelect?: (value: string | undefined, item?: T) => void
}
export const Select = ({
export const Select = <T extends Item>({
isPopoverMatchingInputWidth = true,
selectedItem,
placeholder,
items,
onSelect,
}: Props) => {
}: Props<T>) => {
const focusedItemBgColor = useColorModeValue('gray.200', 'gray.700')
const selectedItemBgColor = useColorModeValue('blue.50', 'blue.400')
const [isTouched, setIsTouched] = useState(false)
@ -87,20 +96,22 @@ export const Select = ({
setInputValue(e.target.value)
}
const handleItemClick = (item: Item) => () => {
const handleItemClick = (item: T) => () => {
if (!isTouched) setIsTouched(true)
setInputValue(getItemLabel(item))
onSelect?.(getItemValue(item))
onSelect?.(getItemValue(item), item)
setKeyboardFocusIndex(undefined)
closeDropwdown()
}
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && isDefined(keyboardFocusIndex)) {
e.preventDefault()
handleItemClick(filteredItems[keyboardFocusIndex])()
return setKeyboardFocusIndex(undefined)
}
if (e.key === 'ArrowDown') {
e.preventDefault()
if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0)
if (keyboardFocusIndex === filteredItems.length - 1)
return setKeyboardFocusIndex(0)
@ -111,6 +122,7 @@ export const Select = ({
return setKeyboardFocusIndex(keyboardFocusIndex + 1)
}
if (e.key === 'ArrowUp') {
e.preventDefault()
if (keyboardFocusIndex === 0 || keyboardFocusIndex === undefined)
return setKeyboardFocusIndex(filteredItems.length - 1)
itemsRef.current[keyboardFocusIndex - 1]?.scrollIntoView({
@ -140,7 +152,8 @@ export const Select = ({
<Popover
isOpen={isOpen}
initialFocusRef={inputRef}
matchWidth
matchWidth={isPopoverMatchingInputWidth}
placement="bottom-start"
offset={[0, 1]}
isLazy
>
@ -150,7 +163,7 @@ export const Select = ({
pos="absolute"
pb={2}
// We need absolute positioning the overlay match the underlying input
pt="8.5px"
pt="8px"
pl="17px"
pr={selectedItem ? 16 : 8}
w="full"
@ -173,7 +186,7 @@ export const Select = ({
onBlur={resetIsTouched}
onChange={handleInputChange}
onFocus={onOpen}
onKeyDown={handleKeyUp}
onKeyDown={handleKeyDown}
pr={selectedItem ? 16 : undefined}
/>

View File

@ -9,7 +9,14 @@ import {
InputProps,
} from '@chakra-ui/react'
import { Variable } from 'models'
import React, { ReactNode, useEffect, useRef, useState } from 'react'
import React, {
forwardRef,
ReactNode,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { env } from 'utils'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
@ -29,23 +36,27 @@ export type TextInputProps = {
'autoComplete' | 'onFocus' | 'onKeyUp' | 'type' | 'autoFocus'
>
export const TextInput = ({
type,
defaultValue,
debounceTimeout = 1000,
label,
moreInfoTooltip,
withVariableButton = true,
isRequired,
placeholder,
autoComplete,
isDisabled,
autoFocus,
onChange: _onChange,
onFocus,
onKeyUp,
}: TextInputProps) => {
export const TextInput = forwardRef(function TextInput(
{
type,
defaultValue,
debounceTimeout = 1000,
label,
moreInfoTooltip,
withVariableButton = true,
isRequired,
placeholder,
autoComplete,
isDisabled,
autoFocus,
onChange: _onChange,
onFocus,
onKeyUp,
}: TextInputProps,
ref
) {
const inputRef = useRef<HTMLInputElement | null>(null)
useImperativeHandle(ref, () => inputRef.current)
const [isTouched, setIsTouched] = useState(false)
const [localValue, setLocalValue] = useState<string>(defaultValue ?? '')
const [carretPosition, setCarretPosition] = useState<number>(
@ -128,4 +139,4 @@ export const TextInput = ({
)}
</FormControl>
)
}
})

View File

@ -27,6 +27,12 @@ export const createCheckoutSession = authenticatedProcedure
returnUrl: z.string(),
additionalChats: z.number(),
additionalStorage: z.number(),
vat: z
.object({
type: z.string(),
value: z.string(),
})
.optional(),
})
)
.output(
@ -37,6 +43,7 @@ export const createCheckoutSession = authenticatedProcedure
.mutation(
async ({
input: {
vat,
email,
company,
workspaceId,
@ -72,10 +79,22 @@ export const createCheckoutSession = authenticatedProcedure
apiVersion: '2022-11-15',
})
await prisma.user.updateMany({
where: {
id: user.id,
},
data: {
company,
},
})
const customer = await stripe.customers.create({
email,
name: company,
metadata: { workspaceId },
tax_id_data: vat
? [vat as Stripe.CustomerCreateParams.TaxIdDatum]
: undefined,
})
const session = await stripe.checkout.sessions.create({
@ -85,14 +104,11 @@ export const createCheckoutSession = authenticatedProcedure
customer: customer.id,
customer_update: {
address: 'auto',
name: 'auto',
name: 'never',
},
mode: 'subscription',
metadata: { workspaceId, plan, additionalChats, additionalStorage },
currency,
tax_id_collection: {
enabled: true,
},
billing_address_collection: 'required',
automatic_tax: { enabled: true },
line_items: parseSubscriptionItems(

View File

@ -10,6 +10,7 @@ import { guessIfUserIsEuropean } from 'utils/pricing'
import { Workspace } from 'models'
import { PreCheckoutModal, PreCheckoutModalProps } from '../PreCheckoutModal'
import { useState } from 'react'
import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider'
type Props = {
workspace: Pick<Workspace, 'id' | 'stripeId' | 'plan'>
@ -77,12 +78,14 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
return (
<Stack spacing={6}>
{!workspace.stripeId && (
<PreCheckoutModal
selectedSubscription={preCheckoutPlan}
existingEmail={user?.email ?? undefined}
existingCompany={user?.company ?? undefined}
onClose={() => setPreCheckoutPlan(undefined)}
/>
<ParentModalProvider>
<PreCheckoutModal
selectedSubscription={preCheckoutPlan}
existingEmail={user?.email ?? undefined}
existingCompany={user?.company ?? undefined}
onClose={() => setPreCheckoutPlan(undefined)}
/>
</ParentModalProvider>
)}
<HStack alignItems="stretch" spacing="4" w="full">
<StarterPlanContent

View File

@ -1,8 +1,13 @@
import { TextInput } from '@/components/inputs'
import { Select } from '@/components/inputs/Select'
import { useParentModal } from '@/features/graph'
import { useToast } from '@/hooks/useToast'
import { trpc } from '@/lib/trpc'
import {
Button,
FormControl,
FormLabel,
HStack,
Modal,
ModalBody,
ModalContent,
@ -12,6 +17,7 @@ import {
import { useRouter } from 'next/router'
import React, { FormEvent, useState } from 'react'
import { isDefined } from 'utils'
import { taxIdTypes } from '../taxIdTypes'
export type PreCheckoutModalProps = {
selectedSubscription:
@ -28,12 +34,22 @@ export type PreCheckoutModalProps = {
onClose: () => void
}
const vatCodeLabels = taxIdTypes.map((taxIdType) => ({
label: `${taxIdType.emoji} ${taxIdType.name} (${taxIdType.code})`,
value: taxIdType.type,
extras: {
placeholder: taxIdType.placeholder,
},
}))
export const PreCheckoutModal = ({
selectedSubscription,
existingCompany,
existingEmail,
onClose,
}: PreCheckoutModalProps) => {
const { ref } = useParentModal()
const vatValueInputRef = React.useRef<HTMLInputElement>(null)
const router = useRouter()
const { showToast } = useToast()
const { mutate: createCheckoutSession, isLoading: isCreatingCheckout } =
@ -51,7 +67,12 @@ export const PreCheckoutModal = ({
const [customer, setCustomer] = useState({
company: existingCompany ?? '',
email: existingEmail ?? '',
vat: {
type: undefined as string | undefined,
value: '',
},
})
const [vatValuePlaceholder, setVatValuePlaceholder] = useState('')
const updateCustomerCompany = (company: string) => {
setCustomer((customer) => ({ ...customer, company }))
@ -61,23 +82,53 @@ export const PreCheckoutModal = ({
setCustomer((customer) => ({ ...customer, email }))
}
const createCustomer = (e: FormEvent) => {
const updateVatType = (
type: string | undefined,
vatCode?: (typeof vatCodeLabels)[number]
) => {
setCustomer((customer) => ({
...customer,
vat: {
...customer.vat,
type,
},
}))
setVatValuePlaceholder(vatCode?.extras?.placeholder ?? '')
vatValueInputRef.current?.focus()
}
const updateVatValue = (value: string) => {
setCustomer((customer) => ({
...customer,
vat: {
...customer.vat,
value,
},
}))
}
const goToCheckout = (e: FormEvent) => {
e.preventDefault()
if (!selectedSubscription) return
const { email, company, vat } = customer
createCheckoutSession({
...selectedSubscription,
email: customer.email,
company: customer.company,
email,
company,
returnUrl: window.location.href,
vat:
vat.value && vat.type
? { type: vat.type, value: vat.value }
: undefined,
})
}
return (
<Modal isOpen={isDefined(selectedSubscription)} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalContent ref={ref}>
<ModalBody py="8">
<Stack as="form" onSubmit={createCustomer} spacing="4">
<Stack as="form" spacing="4" onSubmit={goToCheckout}>
<TextInput
isRequired
label="Company name"
@ -95,6 +146,25 @@ export const PreCheckoutModal = ({
withVariableButton={false}
debounceTimeout={0}
/>
<FormControl>
<FormLabel>Tax ID</FormLabel>
<HStack>
<Select
placeholder="ID type"
items={vatCodeLabels}
isPopoverMatchingInputWidth={false}
onSelect={updateVatType}
/>
<TextInput
ref={vatValueInputRef}
onChange={updateVatValue}
withVariableButton={false}
debounceTimeout={0}
placeholder={vatValuePlaceholder}
/>
</HStack>
</FormControl>
<Button
type="submit"
isLoading={isCreatingCheckout}

View File

@ -0,0 +1,548 @@
export const taxIdTypes = [
{
type: 'ae_trn',
code: 'AE TRN',
name: 'United Arab Emirates',
emoji: '🇦🇪',
placeholder: '123456789012345',
},
{
type: 'au_abn',
code: 'AU ABN',
name: 'Australia',
emoji: '🇦🇺',
placeholder: '123456789012',
},
{
type: 'au_arn',
code: 'AU ARN',
name: 'Australia',
emoji: '🇦🇺',
placeholder: '123456789012',
},
{
type: 'bg_uic',
code: 'BG UIC',
name: 'Bulgaria',
emoji: '🇧🇬',
placeholder: 'BG123456789',
},
{
type: 'br_cnpj',
code: 'BR CNPJ',
name: 'Brazil',
emoji: '🇧🇷',
placeholder: '12.345.678/0001-23',
},
{
type: 'br_cpf',
code: 'BR CPF',
name: 'Brazil',
emoji: '🇧🇷',
placeholder: '123.456.789-01',
},
{
type: 'ca_bn',
code: 'CA BN',
name: 'Canada',
emoji: '🇨🇦',
placeholder: '123456789',
},
{
type: 'ca_gst_hst',
code: 'CA GST/HST',
name: 'Canada',
emoji: '🇨🇦',
placeholder: '123456789',
},
{
type: 'ca_pst_bc',
code: 'CA PST-BC',
name: 'Canada',
emoji: '🇨🇦',
placeholder: '123456789',
},
{
type: 'ca_pst_mb',
code: 'CA PST-MB',
name: 'Canada',
emoji: '🇨🇦',
placeholder: '123456789',
},
{
type: 'ca_pst_sk',
code: 'CA PST-SK',
name: 'Canada',
emoji: '🇨🇦',
placeholder: '123456789',
},
{
type: 'ca_qst',
code: 'CA QST',
name: 'Canada',
emoji: '🇨🇦',
placeholder: '123456789',
},
{
type: 'ch_vat',
code: 'CH VAT',
name: 'Switzerland',
emoji: '🇨🇭',
placeholder: 'CHE-123.456.789 MWST',
},
{
type: 'cl_tin',
code: 'CL TIN',
name: 'Chile',
emoji: '🇨🇱',
placeholder: '12345678-9',
},
{
type: 'eg_tin',
code: 'EG TIN',
name: 'Egypt',
emoji: '🇪🇬',
placeholder: '123456789012',
},
{
type: 'es_cif',
code: 'ES CIF',
name: 'Spain',
emoji: '🇪🇸',
placeholder: 'A12345678',
},
{
type: 'eu_oss_vat',
code: 'EU OSS VAT',
name: 'European Union',
emoji: '🇪🇺',
placeholder: 'XX123456789',
},
{
type: 'eu_vat',
code: 'AT VAT',
name: 'Austria',
emoji: '🇦🇹',
placeholder: 'ATU12345678',
},
{
type: 'eu_vat',
code: 'DE VAT',
name: 'Germany',
emoji: '🇩🇪',
placeholder: 'DE123456789',
},
{
type: 'eu_vat',
code: 'BE VAT',
name: 'Belgium',
emoji: '🇧🇪',
placeholder: 'BE0123456789',
},
{
type: 'eu_vat',
code: 'BG VAT',
name: 'Bulgaria',
emoji: '🇧🇬',
placeholder: 'BG123456789',
},
{
type: 'eu_vat',
code: 'CY VAT',
name: 'Cyprus',
emoji: '🇨🇾',
placeholder: 'CY12345678L',
},
{
type: 'eu_vat',
code: 'DK VAT',
name: 'Denmark',
emoji: '🇩🇰',
placeholder: 'DK12345678',
},
{
type: 'eu_vat',
code: 'EE VAT',
name: 'Estonia',
emoji: '🇪🇪',
placeholder: 'EE123456789',
},
{
type: 'eu_vat',
code: 'GR VAT',
name: 'Greece',
emoji: '🇬🇷',
placeholder: 'EL123456789',
},
{
type: 'eu_vat',
code: 'ES VAT',
name: 'Spain',
emoji: '🇪🇸',
placeholder: 'ESX12345678',
},
{
type: 'eu_vat',
code: 'FI VAT',
name: 'Finland',
emoji: '🇫🇮',
placeholder: 'FI12345678',
},
{
type: 'eu_vat',
code: 'FR VAT',
name: 'France',
emoji: '🇫🇷',
placeholder: 'FRXX123456789',
},
{
type: 'eu_vat',
code: 'HR VAT',
name: 'Croatia',
emoji: '🇭🇷',
placeholder: 'HR12345678901',
},
{
type: 'eu_vat',
code: 'IE VAT',
name: 'Ireland',
emoji: '🇮🇪',
placeholder: 'IE1X12345X',
},
{
type: 'eu_vat',
code: 'IT VAT',
name: 'Italy',
emoji: '🇮🇹',
placeholder: 'IT12345678901',
},
{
type: 'eu_vat',
code: 'LT VAT',
name: 'Lithuania',
emoji: '🇱🇹',
placeholder: 'LT123456789',
},
{
type: 'eu_vat',
code: 'LU VAT',
name: 'Luxembourg',
emoji: '🇱🇺',
placeholder: 'LU12345678',
},
{
type: 'eu_vat',
code: 'LV VAT',
name: 'Latvia',
emoji: '🇱🇻',
placeholder: 'LV12345678901',
},
{
type: 'eu_vat',
code: 'MT VAT',
name: 'Malta',
emoji: '🇲🇹',
placeholder: 'MT12345678',
},
{
type: 'eu_vat',
code: 'NL VAT',
name: 'Netherlands',
emoji: '🇳🇱',
placeholder: 'NL123456789B01',
},
{
type: 'eu_vat',
code: 'PL VAT',
name: 'Poland',
emoji: '🇵🇱',
placeholder: 'PL1234567890',
},
{
type: 'eu_vat',
code: 'PT VAT',
name: 'Portugal',
emoji: '🇵🇹',
placeholder: 'PT123456789',
},
{
type: 'eu_vat',
code: 'RO VAT',
name: 'Romania',
emoji: '🇷🇴',
placeholder: 'RO1234567891',
},
{
type: 'eu_vat',
code: 'SE VAT',
name: 'Sweden',
emoji: '🇸🇪',
placeholder: 'SE123456789101',
},
{
type: 'eu_vat',
code: 'SI VAT',
name: 'Slovenia',
emoji: '🇸🇮',
placeholder: 'SI12345678',
},
{
type: 'eu_vat',
code: 'SK VAT',
name: 'Slovakia',
emoji: '🇸🇰',
placeholder: 'SK1234567890',
},
{
type: 'eu_vat',
code: 'XI VAT',
name: 'Northern Ireland',
emoji: '🇮🇪',
placeholder: 'XI123456789',
},
{
type: 'eu_vat',
code: 'GE VAT',
name: 'Georgia',
emoji: '🇬🇪',
placeholder: 'GE123456789',
},
{
type: 'eu_vat',
code: 'CZ VAT',
name: 'Czech Republic',
emoji: '🇨🇿',
placeholder: 'CZ12345678',
},
{
type: 'gb_vat',
code: 'GB VAT',
name: 'United Kingdom',
emoji: '🇬🇧',
placeholder: 'GB123456789',
},
{
type: 'ge_vat',
code: 'GE VAT',
name: 'Georgia',
emoji: '🇬🇪',
placeholder: 'GE123456789',
},
{
type: 'hk_br',
code: 'HK BR',
name: 'Hong Kong',
emoji: '🇭🇰',
placeholder: '12345678',
},
{
type: 'hu_tin',
code: 'HU TIN',
name: 'Hungary',
emoji: '🇭🇺',
placeholder: '12345678',
},
{
type: 'id_npwp',
code: 'ID NPWP',
name: 'Indonesia',
emoji: '🇮🇩',
placeholder: '123.456.7-8910.000',
},
{
type: 'il_vat',
code: 'IL VAT',
name: 'Israel',
emoji: '🇮🇱',
placeholder: '123456789',
},
{
type: 'in_gst',
code: 'IN GST',
name: 'India',
emoji: '🇮🇳',
placeholder: '12ABCDE1234F1Z5',
},
{
type: 'is_vat',
code: 'IS VAT',
name: 'Iceland',
emoji: '🇮🇸',
placeholder: '123456-7890',
},
{
type: 'jp_cn',
code: 'JP CN',
name: 'Japan',
emoji: '🇯🇵',
placeholder: '123456789012',
},
{
type: 'jp_rn',
code: 'JP RN',
name: 'Japan',
emoji: '🇯🇵',
placeholder: '123456789012',
},
{
type: 'jp_trn',
code: 'JP TRN',
name: 'Japan',
emoji: '🇯🇵',
placeholder: '123456789012',
},
{
type: 'ke_pin',
code: 'KE PIN',
name: 'Kenya',
emoji: '🇰🇪',
placeholder: '12345678A',
},
{
type: 'kr_brn',
code: 'KR BRN',
name: 'South Korea',
emoji: '🇰🇷',
placeholder: '1234567890',
},
{
type: 'li_uid',
code: 'LI UID',
name: 'Liechtenstein',
emoji: '🇱🇮',
placeholder: 'CHE-123.456.789',
},
{
type: 'mx_rfc',
code: 'MX RFC',
name: 'Mexico',
emoji: '🇲🇽',
placeholder: 'XAXX010101000',
},
{
type: 'my_frp',
code: 'MY FRP',
name: 'Malaysia',
emoji: '🇲🇾',
placeholder: '123456789012',
},
{
type: 'my_itn',
code: 'MY ITN',
name: 'Malaysia',
emoji: '🇲🇾',
placeholder: '123456789012',
},
{
type: 'my_sst',
code: 'MY SST',
name: 'Malaysia',
emoji: '🇲🇾',
placeholder: '123456789012',
},
{
type: 'no_vat',
code: 'NO VAT',
name: 'Norway',
emoji: '🇳🇴',
placeholder: '123456789',
},
{
type: 'nz_gst',
code: 'NZ GST',
name: 'New Zealand',
emoji: '🇳🇿',
placeholder: '123456789',
},
{
type: 'ph_tin',
code: 'PH TIN',
name: 'Philippines',
emoji: '🇵🇭',
placeholder: '123-456-789-012',
},
{
type: 'ru_inn',
code: 'RU INN',
name: 'Russia',
emoji: '🇷🇺',
placeholder: '123456789012',
},
{
type: 'ru_kpp',
code: 'RU KPP',
name: 'Russia',
emoji: '🇷🇺',
placeholder: '123456789',
},
{
type: 'sa_vat',
code: 'SA VAT',
name: 'Saudi Arabia',
emoji: '🇸🇦',
placeholder: '123456789012',
},
{
type: 'sg_gst',
code: 'SG GST',
name: 'Singapore',
emoji: '🇸🇬',
placeholder: '123456789',
},
{
type: 'sg_uen',
code: 'SG UEN',
name: 'Singapore',
emoji: '🇸🇬',
placeholder: '123456789',
},
{
type: 'si_tin',
code: 'SI TIN',
name: 'Slovenia',
emoji: '🇸🇮',
placeholder: '12345678',
},
{
type: 'th_vat',
code: 'TH VAT',
name: 'Thailand',
emoji: '🇹🇭',
placeholder: '1234567890123',
},
{
type: 'tr_tin',
code: 'TR TIN',
name: 'Turkey',
emoji: '🇹🇷',
placeholder: '1234567890',
},
{
type: 'tw_vat',
code: 'TW VAT',
name: 'Taiwan',
emoji: '🇹🇼',
placeholder: '12345678',
},
{
type: 'ua_vat',
code: 'UA VAT',
name: 'Ukraine',
emoji: '🇺🇦',
placeholder: '12345678',
},
{
type: 'us_ein',
code: 'US EIN',
name: 'United States',
emoji: '🇺🇸',
placeholder: '12-3456789',
},
{
type: 'za_vat',
code: 'ZA VAT',
name: 'South Africa',
emoji: '🇿🇦',
placeholder: '1234567890',
},
]

View File

@ -28,21 +28,24 @@ export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => {
currentWorkspaceId={typebot.workspaceId as string}
/>
)}
<GroupsDropdown
groups={
typebot &&
(options.typebotId === typebot.id || options.typebotId === 'current')
? typebot.groups
: linkedTypebots?.find(byId(options.typebotId))?.groups ?? []
}
groupId={options.groupId}
onGroupIdSelected={handleGroupIdChange}
isLoading={
linkedTypebots === undefined &&
typebot &&
typebot.id !== options.typebotId
}
/>
{options.typebotId && (
<GroupsDropdown
groups={
typebot &&
(options.typebotId === typebot.id ||
options.typebotId === 'current')
? typebot.groups
: linkedTypebots?.find(byId(options.typebotId))?.groups ?? []
}
groupId={options.groupId}
onGroupIdSelected={handleGroupIdChange}
isLoading={
linkedTypebots === undefined &&
typebot &&
typebot.id !== options.typebotId
}
/>
)}
</Stack>
)
}

View File

@ -5,6 +5,7 @@ import {
PreCheckoutModalProps,
} from '@/features/billing/components/PreCheckoutModal'
import { TypebotDndProvider, FolderContent } from '@/features/folders'
import { ParentModalProvider } from '@/features/graph'
import { useWorkspace } from '@/features/workspace'
import { Stack, VStack, Spinner, Text } from '@chakra-ui/react'
import { Plan } from 'db'
@ -44,12 +45,14 @@ export const DashboardPage = () => {
<Seo title={workspace?.name ?? 'My typebots'} />
<DashboardHeader />
{!workspace?.stripeId && (
<PreCheckoutModal
selectedSubscription={preCheckoutPlan}
existingEmail={user?.email ?? undefined}
existingCompany={workspace?.name ?? undefined}
onClose={() => setPreCheckoutPlan(undefined)}
/>
<ParentModalProvider>
<PreCheckoutModal
selectedSubscription={preCheckoutPlan}
existingEmail={user?.email ?? undefined}
existingCompany={workspace?.name ?? undefined}
onClose={() => setPreCheckoutPlan(undefined)}
/>
</ParentModalProvider>
)}
<TypebotDndProvider>
{isLoading ? (