🐛 (billing) Collect tax ID manually before checkout
This allows Typebot to always display a company name on invoices.
This commit is contained in:
@ -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}
|
||||
/>
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
548
apps/builder/src/features/billing/taxIdTypes.ts
Normal file
548
apps/builder/src/features/billing/taxIdTypes.ts
Normal 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',
|
||||
},
|
||||
]
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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 ? (
|
||||
|
Reference in New Issue
Block a user