diff --git a/apps/builder/src/components/inputs/Select.tsx b/apps/builder/src/components/inputs/Select.tsx index 203b8ca035..831f32972b 100644 --- a/apps/builder/src/components/inputs/Select.tsx +++ b/apps/builder/src/components/inputs/Select.tsx @@ -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 + } -type Props = { +type Props = { + 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 = ({ + isPopoverMatchingInputWidth = true, selectedItem, placeholder, items, onSelect, -}: Props) => { +}: Props) => { 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) => { + const handleKeyDown = (e: React.KeyboardEvent) => { 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 = ({ @@ -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} /> diff --git a/apps/builder/src/components/inputs/TextInput.tsx b/apps/builder/src/components/inputs/TextInput.tsx index 651ae61fe1..884c9a2b54 100644 --- a/apps/builder/src/components/inputs/TextInput.tsx +++ b/apps/builder/src/components/inputs/TextInput.tsx @@ -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(null) + useImperativeHandle(ref, () => inputRef.current) const [isTouched, setIsTouched] = useState(false) const [localValue, setLocalValue] = useState(defaultValue ?? '') const [carretPosition, setCarretPosition] = useState( @@ -128,4 +139,4 @@ export const TextInput = ({ )} ) -} +}) diff --git a/apps/builder/src/features/billing/api/procedures/createCheckoutSession.ts b/apps/builder/src/features/billing/api/procedures/createCheckoutSession.ts index 0c1327bd06..0aa3357e3f 100644 --- a/apps/builder/src/features/billing/api/procedures/createCheckoutSession.ts +++ b/apps/builder/src/features/billing/api/procedures/createCheckoutSession.ts @@ -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( diff --git a/apps/builder/src/features/billing/components/ChangePlanForm/ChangePlanForm.tsx b/apps/builder/src/features/billing/components/ChangePlanForm/ChangePlanForm.tsx index 57965d65cb..9e753a3d67 100644 --- a/apps/builder/src/features/billing/components/ChangePlanForm/ChangePlanForm.tsx +++ b/apps/builder/src/features/billing/components/ChangePlanForm/ChangePlanForm.tsx @@ -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 @@ -77,12 +78,14 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => { return ( {!workspace.stripeId && ( - setPreCheckoutPlan(undefined)} - /> + + setPreCheckoutPlan(undefined)} + /> + )} 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(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 ( - + - + + + Tax ID + +