feat(editor): ✨ Payment input
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
|||||||
ChatIcon,
|
ChatIcon,
|
||||||
CheckSquareIcon,
|
CheckSquareIcon,
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
|
CreditCardIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
EmailIcon,
|
EmailIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
@@ -62,6 +63,8 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
|
|||||||
return <PhoneIcon color="orange.500" {...props} />
|
return <PhoneIcon color="orange.500" {...props} />
|
||||||
case InputStepType.CHOICE:
|
case InputStepType.CHOICE:
|
||||||
return <CheckSquareIcon color="orange.500" {...props} />
|
return <CheckSquareIcon color="orange.500" {...props} />
|
||||||
|
case InputStepType.PAYMENT:
|
||||||
|
return <CreditCardIcon color="orange.500" {...props} />
|
||||||
case LogicStepType.SET_VARIABLE:
|
case LogicStepType.SET_VARIABLE:
|
||||||
return <EditIcon color="purple.500" {...props} />
|
return <EditIcon color="purple.500" {...props} />
|
||||||
case LogicStepType.CONDITION:
|
case LogicStepType.CONDITION:
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export const StepTypeLabel = ({ type }: Props) => {
|
|||||||
return <Text>Phone</Text>
|
return <Text>Phone</Text>
|
||||||
case InputStepType.CHOICE:
|
case InputStepType.CHOICE:
|
||||||
return <Text>Button</Text>
|
return <Text>Button</Text>
|
||||||
|
case InputStepType.PAYMENT:
|
||||||
|
return <Text>Payment</Text>
|
||||||
case LogicStepType.SET_VARIABLE:
|
case LogicStepType.SET_VARIABLE:
|
||||||
return <Text>Set variable</Text>
|
return <Text>Set variable</Text>
|
||||||
case LogicStepType.CONDITION:
|
case LogicStepType.CONDITION:
|
||||||
|
|||||||
@@ -7,15 +7,13 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Image,
|
Image,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
Tooltip,
|
|
||||||
HStack,
|
HStack,
|
||||||
Text,
|
Text,
|
||||||
Flex,
|
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { ImageUploadContent } from 'components/shared/ImageUploadContent'
|
import { ImageUploadContent } from 'components/shared/ImageUploadContent'
|
||||||
import { Input, Textarea } from 'components/shared/Textbox'
|
import { Input, Textarea } from 'components/shared/Textbox'
|
||||||
import { CodeEditor } from 'components/shared/CodeEditor'
|
import { CodeEditor } from 'components/shared/CodeEditor'
|
||||||
import { HelpCircleIcon } from 'assets/icons'
|
import { MoreInfoTooltip } from 'components/shared/MoreInfoTooltip'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
typebotName: string
|
typebotName: string
|
||||||
@@ -113,17 +111,10 @@ export const MetadataForm = ({
|
|||||||
<Stack>
|
<Stack>
|
||||||
<HStack as={FormLabel} mb="0" htmlFor="head">
|
<HStack as={FormLabel} mb="0" htmlFor="head">
|
||||||
<Text>Custom head code:</Text>
|
<Text>Custom head code:</Text>
|
||||||
<Tooltip
|
<MoreInfoTooltip>
|
||||||
label={
|
Will be pasted at the bottom of the header section, just above the
|
||||||
'Will be pasted at the bottom of the header section, just above the closing head tag. Only `meta` and `script` tags are allowed.'
|
closing head tag. Only `meta` and `script` tags are allowed.
|
||||||
}
|
</MoreInfoTooltip>
|
||||||
placement="top"
|
|
||||||
hasArrow
|
|
||||||
>
|
|
||||||
<Flex cursor="pointer">
|
|
||||||
<HelpCircleIcon />
|
|
||||||
</Flex>
|
|
||||||
</Tooltip>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
id="head"
|
id="head"
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { CodeSettings } from './bodies/CodeSettings'
|
|||||||
import { ConditionSettingsBody } from './bodies/ConditionSettingsBody'
|
import { ConditionSettingsBody } from './bodies/ConditionSettingsBody'
|
||||||
import { GoogleAnalyticsSettings } from './bodies/GoogleAnalyticsSettings'
|
import { GoogleAnalyticsSettings } from './bodies/GoogleAnalyticsSettings'
|
||||||
import { GoogleSheetsSettingsBody } from './bodies/GoogleSheetsSettingsBody'
|
import { GoogleSheetsSettingsBody } from './bodies/GoogleSheetsSettingsBody'
|
||||||
|
import { PaymentSettings } from './bodies/PaymentSettings'
|
||||||
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
|
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
|
||||||
import { RedirectSettings } from './bodies/RedirectSettings'
|
import { RedirectSettings } from './bodies/RedirectSettings'
|
||||||
import { SendEmailSettings } from './bodies/SendEmailSettings'
|
import { SendEmailSettings } from './bodies/SendEmailSettings'
|
||||||
@@ -155,6 +156,14 @@ export const StepSettings = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case InputStepType.PAYMENT: {
|
||||||
|
return (
|
||||||
|
<PaymentSettings
|
||||||
|
options={step.options}
|
||||||
|
onOptionsChange={handleOptionsChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
case LogicStepType.SET_VARIABLE: {
|
case LogicStepType.SET_VARIABLE: {
|
||||||
return (
|
return (
|
||||||
<SetVariableSettings
|
<SetVariableSettings
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
useDisclosure,
|
||||||
|
Text,
|
||||||
|
Select,
|
||||||
|
HStack,
|
||||||
|
Accordion,
|
||||||
|
AccordionButton,
|
||||||
|
AccordionIcon,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionPanel,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { CredentialsDropdown } from 'components/shared/CredentialsDropdown'
|
||||||
|
import { DropdownList } from 'components/shared/DropdownList'
|
||||||
|
import { Input } from 'components/shared/Textbox'
|
||||||
|
import { CredentialsType, PaymentInputOptions, PaymentProvider } from 'models'
|
||||||
|
import React, { ChangeEvent, useState } from 'react'
|
||||||
|
import { currencies } from './currencies'
|
||||||
|
import { StripeConfigModal } from './StripeConfigModal'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
options: PaymentInputOptions
|
||||||
|
onOptionsChange: (options: PaymentInputOptions) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
|
const [refreshCredentialsKey, setRefreshCredentialsKey] = useState(0)
|
||||||
|
|
||||||
|
const handleProviderChange = (provider: PaymentProvider) => {
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
provider,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCredentialsSelect = (credentialsId?: string) => {
|
||||||
|
setRefreshCredentialsKey(refreshCredentialsKey + 1)
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
credentialsId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAmountChange = (amount?: string) =>
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
amount,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleCurrencyChange = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
currency: e.target.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleNameChange = (name: string) =>
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
additionalInformation: { ...options.additionalInformation, name },
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleEmailChange = (email: string) =>
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
additionalInformation: { ...options.additionalInformation, email },
|
||||||
|
})
|
||||||
|
|
||||||
|
const handlePhoneNumberChange = (phoneNumber: string) =>
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
additionalInformation: { ...options.additionalInformation, phoneNumber },
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleButtonLabelChange = (button: string) =>
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
labels: { button },
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<Stack>
|
||||||
|
<Text>Provider:</Text>
|
||||||
|
<DropdownList
|
||||||
|
onItemSelect={handleProviderChange}
|
||||||
|
items={Object.values(PaymentProvider)}
|
||||||
|
currentItem={options.provider}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Text>Account:</Text>
|
||||||
|
<CredentialsDropdown
|
||||||
|
type={CredentialsType.STRIPE}
|
||||||
|
currentCredentialsId={options.credentialsId}
|
||||||
|
onCredentialsSelect={handleCredentialsSelect}
|
||||||
|
onCreateNewClick={onOpen}
|
||||||
|
refreshDropdownKey={refreshCredentialsKey}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<HStack>
|
||||||
|
<Stack>
|
||||||
|
<Text>Price amount:</Text>
|
||||||
|
<Input
|
||||||
|
onChange={handleAmountChange}
|
||||||
|
defaultValue={options.amount}
|
||||||
|
placeholder="30.00"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Text>Currency:</Text>
|
||||||
|
<Select
|
||||||
|
placeholder="Select option"
|
||||||
|
value={options.currency.toLowerCase()}
|
||||||
|
onChange={handleCurrencyChange}
|
||||||
|
>
|
||||||
|
{currencies.map((currency) => (
|
||||||
|
<option value={currency.code.toLowerCase()} key={currency.code}>
|
||||||
|
{currency.code}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Stack>
|
||||||
|
</HStack>
|
||||||
|
<Stack>
|
||||||
|
<Text>Button label:</Text>
|
||||||
|
<Input
|
||||||
|
onChange={handleButtonLabelChange}
|
||||||
|
defaultValue={options.labels.button}
|
||||||
|
placeholder="Pay"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Accordion allowToggle>
|
||||||
|
<AccordionItem>
|
||||||
|
<AccordionButton justifyContent="space-between">
|
||||||
|
Additional information
|
||||||
|
<AccordionIcon />
|
||||||
|
</AccordionButton>
|
||||||
|
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||||
|
<Stack>
|
||||||
|
<Text>Name:</Text>
|
||||||
|
<Input
|
||||||
|
defaultValue={options.additionalInformation?.name ?? ''}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
placeholder="John Smith"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Text>Email:</Text>
|
||||||
|
<Input
|
||||||
|
defaultValue={options.additionalInformation?.email ?? ''}
|
||||||
|
onChange={handleEmailChange}
|
||||||
|
placeholder="john@gmail.com"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Text>Phone number:</Text>
|
||||||
|
<Input
|
||||||
|
defaultValue={options.additionalInformation?.phoneNumber ?? ''}
|
||||||
|
onChange={handlePhoneNumberChange}
|
||||||
|
placeholder="+33XXXXXXXXX"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<StripeConfigModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
onNewCredentials={handleCredentialsSelect}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
Button,
|
||||||
|
useToast,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
HStack,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { useUser } from 'contexts/UserContext'
|
||||||
|
import { CredentialsType, StripeCredentialsData } from 'models'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||||
|
import { Input } from 'components/shared/Textbox'
|
||||||
|
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
|
||||||
|
import { MoreInfoTooltip } from 'components/shared/MoreInfoTooltip'
|
||||||
|
import { ExternalLinkIcon } from 'assets/icons'
|
||||||
|
import { createCredentials } from 'services/credentials'
|
||||||
|
import { omit } from 'utils'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onNewCredentials: (id: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StripeConfigModal = ({
|
||||||
|
isOpen,
|
||||||
|
onNewCredentials,
|
||||||
|
onClose,
|
||||||
|
}: Props) => {
|
||||||
|
const { user } = useUser()
|
||||||
|
const { workspace } = useWorkspace()
|
||||||
|
const [isCreating, setIsCreating] = useState(false)
|
||||||
|
const toast = useToast({
|
||||||
|
position: 'top-right',
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
const [stripeConfig, setStripeConfig] = useState<
|
||||||
|
StripeCredentialsData & { name: string }
|
||||||
|
>({
|
||||||
|
name: '',
|
||||||
|
live: { publicKey: '', secretKey: '' },
|
||||||
|
test: { publicKey: '', secretKey: '' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleNameChange = (name: string) =>
|
||||||
|
setStripeConfig({
|
||||||
|
...stripeConfig,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handlePublicKeyChange = (publicKey: string) =>
|
||||||
|
setStripeConfig({
|
||||||
|
...stripeConfig,
|
||||||
|
live: { ...stripeConfig.live, publicKey },
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSecretKeyChange = (secretKey: string) =>
|
||||||
|
setStripeConfig({
|
||||||
|
...stripeConfig,
|
||||||
|
live: { ...stripeConfig.live, secretKey },
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleTestPublicKeyChange = (publicKey: string) =>
|
||||||
|
setStripeConfig({
|
||||||
|
...stripeConfig,
|
||||||
|
test: { ...stripeConfig.test, publicKey },
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleTestSecretKeyChange = (secretKey: string) =>
|
||||||
|
setStripeConfig({
|
||||||
|
...stripeConfig,
|
||||||
|
test: { ...stripeConfig.test, secretKey },
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleCreateClick = async () => {
|
||||||
|
if (!user?.email || !workspace?.id) return
|
||||||
|
setIsCreating(true)
|
||||||
|
const { data, error } = await createCredentials({
|
||||||
|
data: omit(stripeConfig, 'name'),
|
||||||
|
name: stripeConfig.name,
|
||||||
|
type: CredentialsType.STRIPE,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
})
|
||||||
|
setIsCreating(false)
|
||||||
|
if (error) return toast({ title: error.name, description: error.message })
|
||||||
|
if (!data?.credentials)
|
||||||
|
return toast({ description: "Credentials wasn't created" })
|
||||||
|
onNewCredentials(data.credentials.id)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>Connect Stripe account</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<Stack as="form" spacing={4}>
|
||||||
|
<FormControl isRequired>
|
||||||
|
<FormLabel>Account name:</FormLabel>
|
||||||
|
<Input
|
||||||
|
onChange={handleNameChange}
|
||||||
|
placeholder="Typebot"
|
||||||
|
withVariableButton={false}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Stack>
|
||||||
|
<FormLabel>
|
||||||
|
Test keys:{' '}
|
||||||
|
<MoreInfoTooltip>
|
||||||
|
Will be used when previewing the bot.
|
||||||
|
</MoreInfoTooltip>
|
||||||
|
</FormLabel>
|
||||||
|
<HStack>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
onChange={handleTestPublicKeyChange}
|
||||||
|
placeholder="pk_test_..."
|
||||||
|
withVariableButton={false}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
onChange={handleTestSecretKeyChange}
|
||||||
|
placeholder="sk_test_..."
|
||||||
|
withVariableButton={false}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<FormLabel>Live keys:</FormLabel>
|
||||||
|
<HStack>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
onChange={handlePublicKeyChange}
|
||||||
|
placeholder="pk_live_..."
|
||||||
|
withVariableButton={false}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
onChange={handleSecretKeyChange}
|
||||||
|
placeholder="sk_live_..."
|
||||||
|
withVariableButton={false}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
(You can find your keys{' '}
|
||||||
|
<NextChakraLink
|
||||||
|
href="https://dashboard.stripe.com/apikeys"
|
||||||
|
isExternal
|
||||||
|
textDecor="underline"
|
||||||
|
>
|
||||||
|
here <ExternalLinkIcon />
|
||||||
|
</NextChakraLink>
|
||||||
|
)
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
onClick={handleCreateClick}
|
||||||
|
isDisabled={
|
||||||
|
stripeConfig.live.publicKey === '' ||
|
||||||
|
stripeConfig.name === '' ||
|
||||||
|
stripeConfig.live.secretKey === ''
|
||||||
|
}
|
||||||
|
isLoading={isCreating}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,545 @@
|
|||||||
|
// The STRIPE-supported currencies, sorted by code
|
||||||
|
// https://gist.github.com/chrisdavies/9e3f00889fb764013339632bd3f2a71b
|
||||||
|
|
||||||
|
export const currencies = [
|
||||||
|
{
|
||||||
|
code: 'AED',
|
||||||
|
description: 'United Arab Emirates Dirham',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'AFN',
|
||||||
|
description: 'Afghan Afghani**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ALL',
|
||||||
|
description: 'Albanian Lek',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'AMD',
|
||||||
|
description: 'Armenian Dram',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ANG',
|
||||||
|
description: 'Netherlands Antillean Gulden',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'AOA',
|
||||||
|
description: 'Angolan Kwanza**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ARS',
|
||||||
|
description: 'Argentine Peso**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'AUD',
|
||||||
|
description: 'Australian Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'AWG',
|
||||||
|
description: 'Aruban Florin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'AZN',
|
||||||
|
description: 'Azerbaijani Manat',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'BAM',
|
||||||
|
description: 'Bosnia & Herzegovina Convertible Mark',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'BBD',
|
||||||
|
description: 'Barbadian Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'BDT',
|
||||||
|
description: 'Bangladeshi Taka',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'BGN',
|
||||||
|
description: 'Bulgarian Lev',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'BIF',
|
||||||
|
description: 'Burundian Franc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'BMD',
|
||||||
|
description: 'Bermudian Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'BND',
|
||||||
|
description: 'Brunei Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'BOB',
|
||||||
|
description: 'Bolivian Boliviano**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'BRL',
|
||||||
|
description: 'Brazilian Real**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'BSD',
|
||||||
|
description: 'Bahamian Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'BWP',
|
||||||
|
description: 'Botswana Pula',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'BZD',
|
||||||
|
description: 'Belize Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'CAD',
|
||||||
|
description: 'Canadian Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'CDF',
|
||||||
|
description: 'Congolese Franc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'CHF',
|
||||||
|
description: 'Swiss Franc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'CLP',
|
||||||
|
description: 'Chilean Peso**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'CNY',
|
||||||
|
description: 'Chinese Renminbi Yuan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'COP',
|
||||||
|
description: 'Colombian Peso**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'CRC',
|
||||||
|
description: 'Costa Rican Colón**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'CVE',
|
||||||
|
description: 'Cape Verdean Escudo**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'CZK',
|
||||||
|
description: 'Czech Koruna**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'DJF',
|
||||||
|
description: 'Djiboutian Franc**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'DKK',
|
||||||
|
description: 'Danish Krone',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'DOP',
|
||||||
|
description: 'Dominican Peso',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'DZD',
|
||||||
|
description: 'Algerian Dinar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'EGP',
|
||||||
|
description: 'Egyptian Pound',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ETB',
|
||||||
|
description: 'Ethiopian Birr',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'EUR',
|
||||||
|
description: 'Euro',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'FJD',
|
||||||
|
description: 'Fijian Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'FKP',
|
||||||
|
description: 'Falkland Islands Pound**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'GBP',
|
||||||
|
description: 'British Pound',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'GEL',
|
||||||
|
description: 'Georgian Lari',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'GIP',
|
||||||
|
description: 'Gibraltar Pound',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'GMD',
|
||||||
|
description: 'Gambian Dalasi',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'GNF',
|
||||||
|
description: 'Guinean Franc**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'GTQ',
|
||||||
|
description: 'Guatemalan Quetzal**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'GYD',
|
||||||
|
description: 'Guyanese Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'HKD',
|
||||||
|
description: 'Hong Kong Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'HNL',
|
||||||
|
description: 'Honduran Lempira**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'HRK',
|
||||||
|
description: 'Croatian Kuna',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'HTG',
|
||||||
|
description: 'Haitian Gourde',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'HUF',
|
||||||
|
description: 'Hungarian Forint**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'IDR',
|
||||||
|
description: 'Indonesian Rupiah',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ILS',
|
||||||
|
description: 'Israeli New Sheqel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'INR',
|
||||||
|
description: 'Indian Rupee**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ISK',
|
||||||
|
description: 'Icelandic Króna',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'JMD',
|
||||||
|
description: 'Jamaican Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'JPY',
|
||||||
|
description: 'Japanese Yen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'KES',
|
||||||
|
description: 'Kenyan Shilling',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'KGS',
|
||||||
|
description: 'Kyrgyzstani Som',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'KHR',
|
||||||
|
description: 'Cambodian Riel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'KMF',
|
||||||
|
description: 'Comorian Franc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'KRW',
|
||||||
|
description: 'South Korean Won',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'KYD',
|
||||||
|
description: 'Cayman Islands Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'KZT',
|
||||||
|
description: 'Kazakhstani Tenge',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'LAK',
|
||||||
|
description: 'Lao Kip**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'LBP',
|
||||||
|
description: 'Lebanese Pound',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'LKR',
|
||||||
|
description: 'Sri Lankan Rupee',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'LRD',
|
||||||
|
description: 'Liberian Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'LSL',
|
||||||
|
description: 'Lesotho Loti',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'MAD',
|
||||||
|
description: 'Moroccan Dirham',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'MDL',
|
||||||
|
description: 'Moldovan Leu',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'MGA',
|
||||||
|
description: 'Malagasy Ariary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'MKD',
|
||||||
|
description: 'Macedonian Denar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'MNT',
|
||||||
|
description: 'Mongolian Tögrög',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'MOP',
|
||||||
|
description: 'Macanese Pataca',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'MRO',
|
||||||
|
description: 'Mauritanian Ouguiya',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'MUR',
|
||||||
|
description: 'Mauritian Rupee**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'MVR',
|
||||||
|
description: 'Maldivian Rufiyaa',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'MWK',
|
||||||
|
description: 'Malawian Kwacha',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'MXN',
|
||||||
|
description: 'Mexican Peso**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'MYR',
|
||||||
|
description: 'Malaysian Ringgit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'MZN',
|
||||||
|
description: 'Mozambican Metical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'NAD',
|
||||||
|
description: 'Namibian Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'NGN',
|
||||||
|
description: 'Nigerian Naira',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'NIO',
|
||||||
|
description: 'Nicaraguan Córdoba**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'NOK',
|
||||||
|
description: 'Norwegian Krone',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'NPR',
|
||||||
|
description: 'Nepalese Rupee',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'NZD',
|
||||||
|
description: 'New Zealand Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'PAB',
|
||||||
|
description: 'Panamanian Balboa**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'PEN',
|
||||||
|
description: 'Peruvian Nuevo Sol**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'PGK',
|
||||||
|
description: 'Papua New Guinean Kina',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'PHP',
|
||||||
|
description: 'Philippine Peso',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'PKR',
|
||||||
|
description: 'Pakistani Rupee',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'PLN',
|
||||||
|
description: 'Polish Złoty',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'PYG',
|
||||||
|
description: 'Paraguayan Guaraní**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'QAR',
|
||||||
|
description: 'Qatari Riyal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'RON',
|
||||||
|
description: 'Romanian Leu',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'RSD',
|
||||||
|
description: 'Serbian Dinar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'RUB',
|
||||||
|
description: 'Russian Ruble',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'RWF',
|
||||||
|
description: 'Rwandan Franc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'SAR',
|
||||||
|
description: 'Saudi Riyal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'SBD',
|
||||||
|
description: 'Solomon Islands Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'SCR',
|
||||||
|
description: 'Seychellois Rupee',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'SEK',
|
||||||
|
description: 'Swedish Krona',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'SGD',
|
||||||
|
description: 'Singapore Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'SHP',
|
||||||
|
description: 'Saint Helenian Pound**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'SLL',
|
||||||
|
description: 'Sierra Leonean Leone',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'SOS',
|
||||||
|
description: 'Somali Shilling',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'SRD',
|
||||||
|
description: 'Surinamese Dollar**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'STD',
|
||||||
|
description: 'São Tomé and Príncipe Dobra',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'SVC',
|
||||||
|
description: 'Salvadoran Colón**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'SZL',
|
||||||
|
description: 'Swazi Lilangeni',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'THB',
|
||||||
|
description: 'Thai Baht',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'TJS',
|
||||||
|
description: 'Tajikistani Somoni',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'TOP',
|
||||||
|
description: 'Tongan Paʻanga',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'TRY',
|
||||||
|
description: 'Turkish Lira',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'TTD',
|
||||||
|
description: 'Trinidad and Tobago Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'TWD',
|
||||||
|
description: 'New Taiwan Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'TZS',
|
||||||
|
description: 'Tanzanian Shilling',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'UAH',
|
||||||
|
description: 'Ukrainian Hryvnia',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'UGX',
|
||||||
|
description: 'Ugandan Shilling',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'USD',
|
||||||
|
description: 'United States Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'UYU',
|
||||||
|
description: 'Uruguayan Peso**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'UZS',
|
||||||
|
description: 'Uzbekistani Som',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'VND',
|
||||||
|
description: 'Vietnamese Đồng',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'VUV',
|
||||||
|
description: 'Vanuatu Vatu',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'WST',
|
||||||
|
description: 'Samoan Tala',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'XAF',
|
||||||
|
description: 'Central African Cfa Franc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'XCD',
|
||||||
|
description: 'East Caribbean Dollar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'XOF',
|
||||||
|
description: 'West African Cfa Franc**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'XPF',
|
||||||
|
description: 'Cfp Franc**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'YER',
|
||||||
|
description: 'Yemeni Rial',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ZAR',
|
||||||
|
description: 'South African Rand',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ZMW',
|
||||||
|
description: 'Zambian Kwacha',
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PaymentSettings } from './PaymentSettings'
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from './contents'
|
} from './contents'
|
||||||
import { ConfigureContent } from './contents/ConfigureContent'
|
import { ConfigureContent } from './contents/ConfigureContent'
|
||||||
import { ImageBubbleContent } from './contents/ImageBubbleContent'
|
import { ImageBubbleContent } from './contents/ImageBubbleContent'
|
||||||
|
import { PaymentInputContent } from './contents/PaymentInputContent'
|
||||||
import { PlaceholderContent } from './contents/PlaceholderContent'
|
import { PlaceholderContent } from './contents/PlaceholderContent'
|
||||||
import { SendEmailContent } from './contents/SendEmailContent'
|
import { SendEmailContent } from './contents/SendEmailContent'
|
||||||
import { TypebotLinkContent } from './contents/TypebotLinkContent'
|
import { TypebotLinkContent } from './contents/TypebotLinkContent'
|
||||||
@@ -68,6 +69,9 @@ export const StepNodeContent = ({ step, indices }: Props) => {
|
|||||||
case InputStepType.CHOICE: {
|
case InputStepType.CHOICE: {
|
||||||
return <ItemNodesList step={step} indices={indices} />
|
return <ItemNodesList step={step} indices={indices} />
|
||||||
}
|
}
|
||||||
|
case InputStepType.PAYMENT: {
|
||||||
|
return <PaymentInputContent step={step} />
|
||||||
|
}
|
||||||
case LogicStepType.SET_VARIABLE: {
|
case LogicStepType.SET_VARIABLE: {
|
||||||
return <SetVariableContent step={step} />
|
return <SetVariableContent step={step} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Text } from '@chakra-ui/react'
|
||||||
|
import { PaymentInputStep } from 'models'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
step: PaymentInputStep
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaymentInputContent = ({ step }: Props) => {
|
||||||
|
if (
|
||||||
|
!step.options.amount ||
|
||||||
|
!step.options.credentialsId ||
|
||||||
|
!step.options.currency
|
||||||
|
)
|
||||||
|
return <Text color="gray.500">Configure...</Text>
|
||||||
|
return (
|
||||||
|
<Text noOfLines={0} pr="6">
|
||||||
|
Collect {step.options.amount} {step.options.currency}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
apps/builder/components/shared/MoreInfoTooltip.tsx
Normal file
17
apps/builder/components/shared/MoreInfoTooltip.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Tooltip, chakra } from '@chakra-ui/react'
|
||||||
|
import { HelpCircleIcon } from 'assets/icons'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MoreInfoTooltip = ({ children }: Props) => {
|
||||||
|
return (
|
||||||
|
<Tooltip label={children}>
|
||||||
|
<chakra.span cursor="pointer">
|
||||||
|
<HelpCircleIcon />
|
||||||
|
</chakra.span>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -44,7 +44,7 @@ export const SmartNumberInput = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NumberInput onChange={handleValueChange} value={currentValue} {...props}>
|
<NumberInput onChange={handleValueChange} value={currentValue} {...props}>
|
||||||
<NumberInputField />
|
<NumberInputField placeholder={props.placeholder} />
|
||||||
<NumberInputStepper>
|
<NumberInputStepper>
|
||||||
<NumberIncrementStepper />
|
<NumberIncrementStepper />
|
||||||
<NumberDecrementStepper />
|
<NumberDecrementStepper />
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import {
|
import { FormLabel, HStack, Switch, SwitchProps } from '@chakra-ui/react'
|
||||||
chakra,
|
|
||||||
FormLabel,
|
|
||||||
HStack,
|
|
||||||
Switch,
|
|
||||||
SwitchProps,
|
|
||||||
Tooltip,
|
|
||||||
} from '@chakra-ui/react'
|
|
||||||
import { HelpCircleIcon } from 'assets/icons'
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import { MoreInfoTooltip } from './MoreInfoTooltip'
|
||||||
|
|
||||||
type SwitchWithLabelProps = {
|
type SwitchWithLabelProps = {
|
||||||
id: string
|
id: string
|
||||||
@@ -38,11 +31,7 @@ export const SwitchWithLabel = ({
|
|||||||
{label}
|
{label}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
{moreInfoContent && (
|
{moreInfoContent && (
|
||||||
<Tooltip label={moreInfoContent}>
|
<MoreInfoTooltip>{moreInfoContent}</MoreInfoTooltip>
|
||||||
<chakra.span cursor="pointer">
|
|
||||||
<HelpCircleIcon />
|
|
||||||
</chakra.span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"papaparse": "^5.3.2",
|
"papaparse": "^5.3.2",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
|
"qs": "^6.10.3",
|
||||||
"react": "^18.1.0",
|
"react": "^18.1.0",
|
||||||
"react-dom": "^18.1.0",
|
"react-dom": "^18.1.0",
|
||||||
"react-draggable": "^4.4.5",
|
"react-draggable": "^4.4.5",
|
||||||
@@ -73,15 +74,13 @@
|
|||||||
"slate-history": "^0.66.0",
|
"slate-history": "^0.66.0",
|
||||||
"slate-hyperscript": "^0.77.0",
|
"slate-hyperscript": "^0.77.0",
|
||||||
"slate-react": "^0.79.0",
|
"slate-react": "^0.79.0",
|
||||||
"stripe": "^9.1.0",
|
|
||||||
"styled-components": "^5.3.5",
|
"styled-components": "^5.3.5",
|
||||||
"svg-round-corners": "^0.3.0",
|
"svg-round-corners": "^0.3.0",
|
||||||
"swr": "^1.3.0",
|
"swr": "^1.3.0",
|
||||||
"tinycolor2": "^1.4.2",
|
"tinycolor2": "^1.4.2",
|
||||||
"typebot-js": "*",
|
"typebot-js": "*",
|
||||||
"use-debounce": "^8.0.1",
|
"use-debounce": "^8.0.1",
|
||||||
"utils": "*",
|
"utils": "*"
|
||||||
"qs": "^6.10.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.22.0",
|
"@playwright/test": "^1.22.0",
|
||||||
@@ -96,6 +95,7 @@
|
|||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@types/papaparse": "^5.3.2",
|
"@types/papaparse": "^5.3.2",
|
||||||
"@types/prettier": "^2.6.1",
|
"@types/prettier": "^2.6.1",
|
||||||
|
"@types/qs": "^6.9.7",
|
||||||
"@types/react": "^18.0.9",
|
"@types/react": "^18.0.9",
|
||||||
"@types/react-table": "^7.7.12",
|
"@types/react-table": "^7.7.12",
|
||||||
"@types/tinycolor2": "^1.4.3",
|
"@types/tinycolor2": "^1.4.3",
|
||||||
@@ -104,7 +104,6 @@
|
|||||||
"eslint": "<8.0.0",
|
"eslint": "<8.0.0",
|
||||||
"eslint-config-next": "12.1.6",
|
"eslint-config-next": "12.1.6",
|
||||||
"msw": "^0.39.2",
|
"msw": "^0.39.2",
|
||||||
"typescript": "^4.6.4",
|
"typescript": "^4.6.4"
|
||||||
"@types/qs": "^6.9.7"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,7 @@ SMTP_HOST=smtp.ethereal.email
|
|||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=true
|
SMTP_SECURE=true
|
||||||
SMTP_USERNAME=tobin.tillman65@ethereal.email
|
SMTP_USERNAME=tobin.tillman65@ethereal.email
|
||||||
SMTP_PASSWORD=Ty9BcwCBrK6w8AG2hx
|
SMTP_PASSWORD=Ty9BcwCBrK6w8AG2hx
|
||||||
|
|
||||||
|
STRIPE_TEST_PUBLIC_KEY=
|
||||||
|
STRIPE_TEST_SECRET_KEY=
|
||||||
@@ -5,3 +5,8 @@ export const deleteButtonInConfirmDialog = (page: Page) =>
|
|||||||
|
|
||||||
export const typebotViewer = (page: Page) =>
|
export const typebotViewer = (page: Page) =>
|
||||||
page.frameLocator('#typebot-iframe')
|
page.frameLocator('#typebot-iframe')
|
||||||
|
|
||||||
|
export const stripePaymentForm = (page: Page) =>
|
||||||
|
page
|
||||||
|
.frameLocator('#typebot-iframe')
|
||||||
|
.frameLocator('[title="Secure payment input frame"]')
|
||||||
|
|||||||
73
apps/builder/playwright/tests/inputs/payment.spec.ts
Normal file
73
apps/builder/playwright/tests/inputs/payment.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import test, { expect } from '@playwright/test'
|
||||||
|
import {
|
||||||
|
createTypebots,
|
||||||
|
parseDefaultBlockWithStep,
|
||||||
|
} from '../../services/database'
|
||||||
|
import { defaultPaymentInputOptions, InputStepType } from 'models'
|
||||||
|
import cuid from 'cuid'
|
||||||
|
import { stripePaymentForm, typebotViewer } from '../../services/selectorUtils'
|
||||||
|
|
||||||
|
test.describe('Payment input step', () => {
|
||||||
|
test('Can configure Stripe account', async ({ page }) => {
|
||||||
|
const typebotId = cuid()
|
||||||
|
await createTypebots([
|
||||||
|
{
|
||||||
|
id: typebotId,
|
||||||
|
...parseDefaultBlockWithStep({
|
||||||
|
type: InputStepType.PAYMENT,
|
||||||
|
options: defaultPaymentInputOptions,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
await page.click('text=Configure...')
|
||||||
|
await page.click('text=Select an account')
|
||||||
|
await page.click('text=Connect new')
|
||||||
|
await page.fill('[placeholder="Typebot"]', 'My Stripe Account')
|
||||||
|
await page.fill(
|
||||||
|
'[placeholder="sk_test_..."]',
|
||||||
|
process.env.STRIPE_TEST_SECRET_KEY ?? ''
|
||||||
|
)
|
||||||
|
await page.fill(
|
||||||
|
'[placeholder="sk_live_..."]',
|
||||||
|
process.env.STRIPE_TEST_SECRET_KEY ?? ''
|
||||||
|
)
|
||||||
|
await page.fill(
|
||||||
|
'[placeholder="pk_test_..."]',
|
||||||
|
process.env.STRIPE_TEST_PUBLIC_KEY ?? ''
|
||||||
|
)
|
||||||
|
await page.fill(
|
||||||
|
'[placeholder="pk_live_..."]',
|
||||||
|
process.env.STRIPE_TEST_PUBLIC_KEY ?? ''
|
||||||
|
)
|
||||||
|
await expect(page.locator('button >> text="Connect"')).toBeEnabled()
|
||||||
|
await page.click('button >> text="Connect"')
|
||||||
|
await expect(page.locator('text="Secret test key:"')).toBeHidden()
|
||||||
|
await expect(page.locator('text="My Stripe Account"')).toBeVisible()
|
||||||
|
await page.fill('[placeholder="30.00"] >> nth=-1', '30.00')
|
||||||
|
await page.click('text=Additional information')
|
||||||
|
await page.fill('[placeholder="John Smith"]', 'Baptiste')
|
||||||
|
await page.fill('[placeholder="john@gmail.com"]', 'baptiste@typebot.io')
|
||||||
|
await expect(page.locator('text="Phone number:"')).toBeVisible()
|
||||||
|
|
||||||
|
await page.click('text=Preview')
|
||||||
|
await stripePaymentForm(page)
|
||||||
|
.locator(`[placeholder="1234 1234 1234 1234"]`)
|
||||||
|
.fill('4000000000000002')
|
||||||
|
await stripePaymentForm(page)
|
||||||
|
.locator(`[placeholder="MM / YY"]`)
|
||||||
|
.fill('12 / 25')
|
||||||
|
await stripePaymentForm(page).locator(`[placeholder="CVC"]`).fill('240')
|
||||||
|
await stripePaymentForm(page).locator(`[placeholder="90210"]`).fill('90210')
|
||||||
|
await typebotViewer(page).locator(`text="Pay 30$"`).click()
|
||||||
|
await expect(
|
||||||
|
typebotViewer(page).locator(`text="Your card was declined."`)
|
||||||
|
).toBeVisible()
|
||||||
|
await stripePaymentForm(page)
|
||||||
|
.locator(`[placeholder="1234 1234 1234 1234"]`)
|
||||||
|
.fill('4242424242424242')
|
||||||
|
await typebotViewer(page).locator(`text="Pay 30$"`).click()
|
||||||
|
await expect(typebotViewer(page).locator(`text="Success"`)).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { User } from 'db'
|
import { User } from 'db'
|
||||||
import { loadStripe } from '@stripe/stripe-js'
|
import { loadStripe } from '@stripe/stripe-js/pure'
|
||||||
import { sendRequest } from 'utils'
|
import { sendRequest } from 'utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
defaultEmbedBubbleContent,
|
defaultEmbedBubbleContent,
|
||||||
ChoiceInputStep,
|
ChoiceInputStep,
|
||||||
ConditionStep,
|
ConditionStep,
|
||||||
|
defaultPaymentInputOptions,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { Typebot } from 'models'
|
import { Typebot } from 'models'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
@@ -320,6 +321,8 @@ const parseDefaultStepOptions = (type: StepWithOptionsType): StepOptions => {
|
|||||||
return defaultUrlInputOptions
|
return defaultUrlInputOptions
|
||||||
case InputStepType.CHOICE:
|
case InputStepType.CHOICE:
|
||||||
return defaultChoiceInputOptions
|
return defaultChoiceInputOptions
|
||||||
|
case InputStepType.PAYMENT:
|
||||||
|
return defaultPaymentInputOptions
|
||||||
case LogicStepType.SET_VARIABLE:
|
case LogicStepType.SET_VARIABLE:
|
||||||
return defaultSetVariablesOptions
|
return defaultSetVariablesOptions
|
||||||
case LogicStepType.REDIRECT:
|
case LogicStepType.REDIRECT:
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"react": "^18.1.0",
|
"react": "^18.1.0",
|
||||||
"react-dom": "^18.1.0",
|
"react-dom": "^18.1.0",
|
||||||
"sanitize-html": "^2.7.0",
|
"sanitize-html": "^2.7.0",
|
||||||
|
"stripe": "^9.1.0",
|
||||||
"utils": "*"
|
"utils": "*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@types/react": "^18.0.9",
|
"@types/react": "^18.0.9",
|
||||||
"@types/sanitize-html": "^2.6.2",
|
"@types/sanitize-html": "^2.6.2",
|
||||||
|
"@types/stripe": "^8.0.417",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.23.0",
|
"@typescript-eslint/eslint-plugin": "^5.23.0",
|
||||||
"eslint": "<8.0.0",
|
"eslint": "<8.0.0",
|
||||||
"eslint-config-next": "12.1.6",
|
"eslint-config-next": "12.1.6",
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import {
|
||||||
|
badRequest,
|
||||||
|
decrypt,
|
||||||
|
forbidden,
|
||||||
|
initMiddleware,
|
||||||
|
methodNotAllowed,
|
||||||
|
} from 'utils'
|
||||||
|
import Stripe from 'stripe'
|
||||||
|
|
||||||
|
import Cors from 'cors'
|
||||||
|
import { withSentry } from '@sentry/nextjs'
|
||||||
|
import { PaymentInputOptions, StripeCredentialsData, Variable } from 'models'
|
||||||
|
import prisma from 'libs/prisma'
|
||||||
|
import { parseVariables } from 'bot-engine'
|
||||||
|
|
||||||
|
const cors = initMiddleware(Cors())
|
||||||
|
|
||||||
|
const currencySymbols: { [key: string]: string } = {
|
||||||
|
USD: '$',
|
||||||
|
EUR: '€',
|
||||||
|
CRC: '₡',
|
||||||
|
GBP: '£',
|
||||||
|
ILS: '₪',
|
||||||
|
INR: '₹',
|
||||||
|
JPY: '¥',
|
||||||
|
KRW: '₩',
|
||||||
|
NGN: '₦',
|
||||||
|
PHP: '₱',
|
||||||
|
PLN: 'zł',
|
||||||
|
PYG: '₲',
|
||||||
|
THB: '฿',
|
||||||
|
UAH: '₴',
|
||||||
|
VND: '₫',
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
await cors(req, res)
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const { inputOptions, isPreview, variables } = (
|
||||||
|
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||||
|
) as {
|
||||||
|
inputOptions: PaymentInputOptions
|
||||||
|
isPreview: boolean
|
||||||
|
variables: Variable[]
|
||||||
|
}
|
||||||
|
if (!inputOptions.credentialsId) return forbidden(res)
|
||||||
|
const stripeKeys = await getStripeInfo(inputOptions.credentialsId)
|
||||||
|
if (!stripeKeys) return forbidden(res)
|
||||||
|
const stripe = new Stripe(
|
||||||
|
isPreview && stripeKeys?.test?.secretKey
|
||||||
|
? stripeKeys.test.secretKey
|
||||||
|
: stripeKeys.live.secretKey,
|
||||||
|
{ apiVersion: '2020-08-27' }
|
||||||
|
)
|
||||||
|
console.log(variables, inputOptions)
|
||||||
|
const amount = Math.round(
|
||||||
|
Number(parseVariables(variables)(inputOptions.amount)) * 100
|
||||||
|
)
|
||||||
|
if (isNaN(amount)) return badRequest(res)
|
||||||
|
// Create a PaymentIntent with the order amount and currency
|
||||||
|
const paymentIntent = await stripe.paymentIntents.create({
|
||||||
|
amount,
|
||||||
|
currency: inputOptions.currency,
|
||||||
|
receipt_email: inputOptions.additionalInformation?.email,
|
||||||
|
automatic_payment_methods: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
clientSecret: paymentIntent.client_secret,
|
||||||
|
publicKey:
|
||||||
|
isPreview && stripeKeys.test?.publicKey
|
||||||
|
? stripeKeys.test.publicKey
|
||||||
|
: stripeKeys.live.publicKey,
|
||||||
|
amountLabel: `${amount / 100}${currencySymbols[inputOptions.currency]}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return methodNotAllowed(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStripeInfo = async (
|
||||||
|
credentialsId: string
|
||||||
|
): Promise<StripeCredentialsData | undefined> => {
|
||||||
|
const credentials = await prisma.credentials.findUnique({
|
||||||
|
where: { id: credentialsId },
|
||||||
|
})
|
||||||
|
if (!credentials) return
|
||||||
|
return decrypt(credentials.data, credentials.iv) as StripeCredentialsData
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withSentry(handler)
|
||||||
@@ -15,7 +15,9 @@ export const parseSampleResult =
|
|||||||
typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>,
|
typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>,
|
||||||
linkedTypebots: (Typebot | PublicTypebot)[]
|
linkedTypebots: (Typebot | PublicTypebot)[]
|
||||||
) =>
|
) =>
|
||||||
async (currentBlockId: string): Promise<Record<string, string>> => {
|
async (
|
||||||
|
currentBlockId: string
|
||||||
|
): Promise<Record<string, string | boolean | undefined>> => {
|
||||||
const header = parseResultHeader({
|
const header = parseResultHeader({
|
||||||
blocks: [...typebot.blocks, ...linkedTypebots.flatMap((t) => t.blocks)],
|
blocks: [...typebot.blocks, ...linkedTypebots.flatMap((t) => t.blocks)],
|
||||||
variables: [
|
variables: [
|
||||||
@@ -83,7 +85,7 @@ const parseBlocksResultSample = (
|
|||||||
inputSteps: InputStep[],
|
inputSteps: InputStep[],
|
||||||
header: ResultHeaderCell[]
|
header: ResultHeaderCell[]
|
||||||
) =>
|
) =>
|
||||||
header.reduce<Record<string, string>>((steps, cell) => {
|
header.reduce<Record<string, string | boolean | undefined>>((steps, cell) => {
|
||||||
const inputStep = inputSteps.find((step) => step.id === cell.stepId)
|
const inputStep = inputSteps.find((step) => step.id === cell.stepId)
|
||||||
if (isNotDefined(inputStep)) {
|
if (isNotDefined(inputStep)) {
|
||||||
if (cell.variableId)
|
if (cell.variableId)
|
||||||
|
|||||||
12
packages/bot-engine/lib/stripe.ts
Normal file
12
packages/bot-engine/lib/stripe.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export const initStripe = (document: Document): Promise<void> =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
const existingScript = document.getElementById('stripe-script')
|
||||||
|
if (existingScript) return resolve()
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = 'https://js.stripe.com/v3'
|
||||||
|
script.id = 'stripe-script'
|
||||||
|
document.body.appendChild(script)
|
||||||
|
script.onload = () => {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"module": "dist/esm/index.js",
|
"module": "dist/esm/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@stripe/react-stripe-js": "^1.8.0",
|
||||||
"db": "*",
|
"db": "*",
|
||||||
"models": "*",
|
"models": "*",
|
||||||
"qs": "^6.10.3",
|
"qs": "^6.10.3",
|
||||||
@@ -20,11 +21,17 @@
|
|||||||
"@rollup/plugin-json": "^4.1.0",
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||||
"@rollup/plugin-typescript": "^8.3.2",
|
"@rollup/plugin-typescript": "^8.3.2",
|
||||||
|
"@types/qs": "^6.9.7",
|
||||||
"@types/react": "^18.0.9",
|
"@types/react": "^18.0.9",
|
||||||
"@types/react-phone-number-input": "^3.0.13",
|
"@types/react-phone-number-input": "^3.0.13",
|
||||||
"@types/react-scroll": "^1.8.3",
|
"@types/react-scroll": "^1.8.3",
|
||||||
"@types/react-transition-group": "^4.4.4",
|
"@types/react-transition-group": "^4.4.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.23.0",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
|
"eslint": "<8.0.0",
|
||||||
|
"eslint-config-next": "12.1.6",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss": "^8.4.13",
|
"postcss": "^8.4.13",
|
||||||
"rollup": "^2.72.1",
|
"rollup": "^2.72.1",
|
||||||
@@ -33,14 +40,8 @@
|
|||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"tailwindcss": "^3.0.24",
|
"tailwindcss": "^3.0.24",
|
||||||
"typescript": "^4.6.4",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.23.0",
|
|
||||||
"eslint": "<8.0.0",
|
|
||||||
"eslint-config-next": "12.1.6",
|
|
||||||
"eslint-config-prettier": "^8.5.0",
|
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0",
|
||||||
"@types/qs": "^6.9.7"
|
"typescript": "^4.6.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.1.0"
|
"react": "^18.1.0"
|
||||||
|
|||||||
@@ -183,6 +183,10 @@ textarea {
|
|||||||
box-shadow: 0 2px 6px -1px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.typebot-input-error-message {
|
||||||
|
color: var(--typebot-input-color);
|
||||||
|
}
|
||||||
|
|
||||||
.typebot-button > .send-icon {
|
.typebot-button > .send-icon {
|
||||||
fill: var(--typebot-button-color);
|
fill: var(--typebot-button-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { ChoiceForm } from './inputs/ChoiceForm'
|
|||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import { parseVariables } from '../../../services/variable'
|
import { parseVariables } from '../../../services/variable'
|
||||||
import { isInputValid } from 'services/inputs'
|
import { isInputValid } from 'services/inputs'
|
||||||
|
import { PaymentForm } from './inputs/PaymentForm'
|
||||||
|
|
||||||
export const InputChatStep = ({
|
export const InputChatStep = ({
|
||||||
step,
|
step,
|
||||||
@@ -107,5 +108,12 @@ const Input = ({
|
|||||||
return <DateForm options={step.options} onSubmit={onSubmit} />
|
return <DateForm options={step.options} onSubmit={onSubmit} />
|
||||||
case InputStepType.CHOICE:
|
case InputStepType.CHOICE:
|
||||||
return <ChoiceForm step={step} onSubmit={onSubmit} />
|
return <ChoiceForm step={step} onSubmit={onSubmit} />
|
||||||
|
case InputStepType.PAYMENT:
|
||||||
|
return (
|
||||||
|
<PaymentForm
|
||||||
|
options={step.options}
|
||||||
|
onSuccess={() => onSubmit('Success')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { PaymentInputOptions, PaymentProvider } from 'models'
|
||||||
|
import React from 'react'
|
||||||
|
import { StripePaymentForm } from './StripePaymentForm'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSuccess: () => void
|
||||||
|
options: PaymentInputOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaymentForm = ({ onSuccess, options }: Props): JSX.Element => {
|
||||||
|
switch (options.provider) {
|
||||||
|
case PaymentProvider.STRIPE:
|
||||||
|
return <StripePaymentForm onSuccess={onSuccess} options={options} />
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import React, { FormEvent, useEffect, useState } from 'react'
|
||||||
|
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js'
|
||||||
|
import { Elements } from '@stripe/react-stripe-js'
|
||||||
|
import { createPaymentIntent } from 'services/stripe'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
|
import { PaymentInputOptions, Variable } from 'models'
|
||||||
|
import { SendButton } from '../SendButton'
|
||||||
|
import { useFrame } from 'react-frame-component'
|
||||||
|
import { initStripe } from '../../../../../../lib/stripe'
|
||||||
|
import { parseVariables } from 'services/variable'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
options: PaymentInputOptions
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StripePaymentForm = ({ options, onSuccess }: Props) => {
|
||||||
|
const {
|
||||||
|
apiHost,
|
||||||
|
isPreview,
|
||||||
|
typebot: { variables },
|
||||||
|
} = useTypebot()
|
||||||
|
const { window: frameWindow, document: frameDocument } = useFrame()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const [stripe, setStripe] = useState<any>(null)
|
||||||
|
const [clientSecret, setClientSecret] = useState('')
|
||||||
|
const [amountLabel, setAmountLabel] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
;(async () => {
|
||||||
|
const { data, error } = await createPaymentIntent({
|
||||||
|
apiHost,
|
||||||
|
isPreview,
|
||||||
|
variables,
|
||||||
|
inputOptions: options,
|
||||||
|
})
|
||||||
|
if (error || !data) return console.error(error)
|
||||||
|
await initStripe(frameDocument)
|
||||||
|
setStripe(frameWindow.Stripe(data.publicKey))
|
||||||
|
setClientSecret(data.clientSecret)
|
||||||
|
setAmountLabel(data.amountLabel)
|
||||||
|
})()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!stripe || !clientSecret) return <></>
|
||||||
|
return (
|
||||||
|
<Elements stripe={stripe} options={{ clientSecret }}>
|
||||||
|
<CheckoutForm
|
||||||
|
onSuccess={onSuccess}
|
||||||
|
clientSecret={clientSecret}
|
||||||
|
amountLabel={amountLabel}
|
||||||
|
options={options}
|
||||||
|
variables={variables}
|
||||||
|
viewerHost={apiHost}
|
||||||
|
/>
|
||||||
|
</Elements>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CheckoutForm = ({
|
||||||
|
onSuccess,
|
||||||
|
clientSecret,
|
||||||
|
amountLabel,
|
||||||
|
options,
|
||||||
|
variables,
|
||||||
|
viewerHost,
|
||||||
|
}: {
|
||||||
|
onSuccess: () => void
|
||||||
|
clientSecret: string
|
||||||
|
amountLabel: string
|
||||||
|
options: PaymentInputOptions
|
||||||
|
variables: Variable[]
|
||||||
|
viewerHost: string
|
||||||
|
}) => {
|
||||||
|
const [ignoreFirstPaymentIntentCall, setIgnoreFirstPaymentIntentCall] =
|
||||||
|
useState(true)
|
||||||
|
|
||||||
|
const stripe = useStripe()
|
||||||
|
const elements = useElements()
|
||||||
|
|
||||||
|
const [message, setMessage] = useState<string>()
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stripe || !clientSecret) return
|
||||||
|
|
||||||
|
if (ignoreFirstPaymentIntentCall)
|
||||||
|
return setIgnoreFirstPaymentIntentCall(false)
|
||||||
|
|
||||||
|
stripe.retrievePaymentIntent(clientSecret).then(({ paymentIntent }) => {
|
||||||
|
switch (paymentIntent?.status) {
|
||||||
|
case 'succeeded':
|
||||||
|
setMessage('Payment succeeded!')
|
||||||
|
break
|
||||||
|
case 'processing':
|
||||||
|
setMessage('Your payment is processing.')
|
||||||
|
break
|
||||||
|
case 'requires_payment_method':
|
||||||
|
setMessage('Your payment was not successful, please try again.')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
setMessage('Something went wrong.')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [stripe, clientSecret])
|
||||||
|
|
||||||
|
const handleSubmit = async (e: FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!stripe || !elements) return
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const { error } = await stripe.confirmPayment({
|
||||||
|
elements,
|
||||||
|
confirmParams: {
|
||||||
|
// Make sure to change this to your payment completion page
|
||||||
|
return_url: viewerHost,
|
||||||
|
payment_method_data: {
|
||||||
|
billing_details: {
|
||||||
|
name: options.additionalInformation?.name
|
||||||
|
? parseVariables(variables)(options.additionalInformation?.name)
|
||||||
|
: undefined,
|
||||||
|
email: options.additionalInformation?.email
|
||||||
|
? parseVariables(variables)(options.additionalInformation?.email)
|
||||||
|
: undefined,
|
||||||
|
phone: options.additionalInformation?.phoneNumber
|
||||||
|
? parseVariables(variables)(
|
||||||
|
options.additionalInformation?.phoneNumber
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
redirect: 'if_required',
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
if (!error) return onSuccess()
|
||||||
|
if (error.type === 'validation_error') return
|
||||||
|
if (error?.type === 'card_error') return setMessage(error.message)
|
||||||
|
setMessage('An unexpected error occured.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
id="payment-form"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex flex-col rounded-lg p-4 typebot-input w-full items-center"
|
||||||
|
>
|
||||||
|
<PaymentElement id="payment-element" className="w-full" />
|
||||||
|
<SendButton
|
||||||
|
label={`${options.labels.button} ${amountLabel}`}
|
||||||
|
isDisabled={isLoading || !stripe || !elements}
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="mt-4 w-full max-w-lg"
|
||||||
|
disableIcon
|
||||||
|
/>
|
||||||
|
|
||||||
|
{message && (
|
||||||
|
<div
|
||||||
|
id="payment-message"
|
||||||
|
className="typebot-input-error-message mt-4 text-center"
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PaymentForm } from './PaymentForm'
|
||||||
@@ -4,25 +4,55 @@ import { SendIcon } from '../../../../assets/icons'
|
|||||||
type SendButtonProps = {
|
type SendButtonProps = {
|
||||||
label: string
|
label: string
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
|
isLoading?: boolean
|
||||||
|
disableIcon?: boolean
|
||||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>
|
} & React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||||
|
|
||||||
export const SendButton = ({
|
export const SendButton = ({
|
||||||
label,
|
label,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
|
isLoading,
|
||||||
|
disableIcon,
|
||||||
...props
|
...props
|
||||||
}: SendButtonProps) => {
|
}: SendButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isDisabled}
|
disabled={isDisabled || isLoading}
|
||||||
{...props}
|
{...props}
|
||||||
className={
|
className={
|
||||||
'py-2 px-4 font-semibold rounded-md text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 typebot-button ' +
|
'py-2 px-4 justify-center font-semibold rounded-md text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 typebot-button ' +
|
||||||
props.className
|
props.className
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="hidden xs:flex">{label}</span>
|
{isLoading && (
|
||||||
<SendIcon className="send-icon flex xs:hidden" />
|
<svg
|
||||||
|
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
<span className={'xs:flex ' + (disableIcon ? '' : 'hidden')}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<SendIcon
|
||||||
|
className={'send-icon flex ' + (disableIcon ? 'hidden' : 'xs:hidden')}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export const TextInput = ({ step, value, onChange }: TextInputProps) => {
|
|||||||
style={{ appearance: 'auto' }}
|
style={{ appearance: 'auto' }}
|
||||||
min={step.options?.min}
|
min={step.options?.min}
|
||||||
max={step.options?.max}
|
max={step.options?.max}
|
||||||
step={step.options?.step}
|
step={step.options?.step ?? 'any'}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
21
packages/bot-engine/src/services/stripe.ts
Normal file
21
packages/bot-engine/src/services/stripe.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { PaymentInputOptions, Variable } from 'models'
|
||||||
|
import { sendRequest } from 'utils'
|
||||||
|
|
||||||
|
export const createPaymentIntent = ({
|
||||||
|
apiHost,
|
||||||
|
isPreview,
|
||||||
|
inputOptions,
|
||||||
|
variables,
|
||||||
|
}: {
|
||||||
|
inputOptions: PaymentInputOptions
|
||||||
|
apiHost: string
|
||||||
|
variables: Variable[]
|
||||||
|
isPreview: boolean
|
||||||
|
}) =>
|
||||||
|
sendRequest<{ clientSecret: string; publicKey: string; amountLabel: string }>(
|
||||||
|
{
|
||||||
|
url: `${apiHost}/api/integrations/stripe/createPaymentIntent`,
|
||||||
|
method: 'POST',
|
||||||
|
body: { inputOptions, isPreview, variables },
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import { Credentials as CredentialsFromPrisma } from 'db'
|
import { Credentials as CredentialsFromPrisma } from 'db'
|
||||||
|
|
||||||
export type Credentials = SmtpCredentials | GoogleSheetsCredentials
|
export type Credentials =
|
||||||
|
| SmtpCredentials
|
||||||
|
| GoogleSheetsCredentials
|
||||||
|
| StripeCredentials
|
||||||
|
|
||||||
export type CredentialsBase = Omit<CredentialsFromPrisma, 'data' | 'type'>
|
export type CredentialsBase = Omit<CredentialsFromPrisma, 'data' | 'type'>
|
||||||
|
|
||||||
export enum CredentialsType {
|
export enum CredentialsType {
|
||||||
GOOGLE_SHEETS = 'google sheets',
|
GOOGLE_SHEETS = 'google sheets',
|
||||||
SMTP = 'smtp',
|
SMTP = 'smtp',
|
||||||
|
STRIPE = 'stripe',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SmtpCredentials = CredentialsBase & {
|
export type SmtpCredentials = CredentialsBase & {
|
||||||
@@ -19,6 +23,11 @@ export type GoogleSheetsCredentials = CredentialsBase & {
|
|||||||
data: GoogleSheetsCredentialsData
|
data: GoogleSheetsCredentialsData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StripeCredentials = CredentialsBase & {
|
||||||
|
type: CredentialsType.STRIPE
|
||||||
|
data: StripeCredentialsData
|
||||||
|
}
|
||||||
|
|
||||||
export type GoogleSheetsCredentialsData = {
|
export type GoogleSheetsCredentialsData = {
|
||||||
refresh_token?: string | null
|
refresh_token?: string | null
|
||||||
expiry_date?: number | null
|
expiry_date?: number | null
|
||||||
@@ -36,3 +45,14 @@ export type SmtpCredentialsData = {
|
|||||||
port: number
|
port: number
|
||||||
from: { email?: string; name?: string }
|
from: { email?: string; name?: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StripeCredentialsData = {
|
||||||
|
live: {
|
||||||
|
secretKey: string
|
||||||
|
publicKey: string
|
||||||
|
}
|
||||||
|
test?: {
|
||||||
|
secretKey?: string
|
||||||
|
publicKey?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type InputStep =
|
|||||||
| DateInputStep
|
| DateInputStep
|
||||||
| PhoneNumberInputStep
|
| PhoneNumberInputStep
|
||||||
| ChoiceInputStep
|
| ChoiceInputStep
|
||||||
|
| PaymentInputStep
|
||||||
|
|
||||||
export enum InputStepType {
|
export enum InputStepType {
|
||||||
TEXT = 'text input',
|
TEXT = 'text input',
|
||||||
@@ -18,6 +19,7 @@ export enum InputStepType {
|
|||||||
DATE = 'date input',
|
DATE = 'date input',
|
||||||
PHONE = 'phone number input',
|
PHONE = 'phone number input',
|
||||||
CHOICE = 'choice input',
|
CHOICE = 'choice input',
|
||||||
|
PAYMENT = 'payment input',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InputStepOptions =
|
export type InputStepOptions =
|
||||||
@@ -28,6 +30,7 @@ export type InputStepOptions =
|
|||||||
| UrlInputOptions
|
| UrlInputOptions
|
||||||
| PhoneNumberInputOptions
|
| PhoneNumberInputOptions
|
||||||
| ChoiceInputOptions
|
| ChoiceInputOptions
|
||||||
|
| PaymentInputOptions
|
||||||
|
|
||||||
export type TextInputStep = StepBase & {
|
export type TextInputStep = StepBase & {
|
||||||
type: InputStepType.TEXT
|
type: InputStepType.TEXT
|
||||||
@@ -70,6 +73,18 @@ export type ButtonItem = ItemBase & {
|
|||||||
content?: string
|
content?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PaymentInputStep = StepBase & {
|
||||||
|
type: InputStepType.PAYMENT
|
||||||
|
options: PaymentInputOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreditCardDetails = {
|
||||||
|
number: string
|
||||||
|
exp_month: string
|
||||||
|
exp_year: string
|
||||||
|
cvc: string
|
||||||
|
}
|
||||||
|
|
||||||
type OptionBase = { variableId?: string }
|
type OptionBase = { variableId?: string }
|
||||||
|
|
||||||
type InputTextOptionsBase = {
|
type InputTextOptionsBase = {
|
||||||
@@ -115,6 +130,23 @@ export type NumberInputOptions = OptionBase &
|
|||||||
step?: number
|
step?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PaymentProvider {
|
||||||
|
STRIPE = 'Stripe',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaymentInputOptions = OptionBase & {
|
||||||
|
provider: PaymentProvider
|
||||||
|
amount?: string
|
||||||
|
currency: string
|
||||||
|
credentialsId?: string
|
||||||
|
additionalInformation?: {
|
||||||
|
name?: string
|
||||||
|
email?: string
|
||||||
|
phoneNumber?: string
|
||||||
|
}
|
||||||
|
labels: { button: string }
|
||||||
|
}
|
||||||
|
|
||||||
const defaultButtonLabel = 'Send'
|
const defaultButtonLabel = 'Send'
|
||||||
|
|
||||||
export const defaultTextInputOptions: TextInputOptions = {
|
export const defaultTextInputOptions: TextInputOptions = {
|
||||||
@@ -163,3 +195,9 @@ export const defaultChoiceInputOptions: ChoiceInputOptions = {
|
|||||||
buttonLabel: defaultButtonLabel,
|
buttonLabel: defaultButtonLabel,
|
||||||
isMultipleChoice: false,
|
isMultipleChoice: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defaultPaymentInputOptions: PaymentInputOptions = {
|
||||||
|
provider: PaymentProvider.STRIPE,
|
||||||
|
labels: { button: 'Pay' },
|
||||||
|
currency: 'USD',
|
||||||
|
}
|
||||||
|
|||||||
16
yarn.lock
16
yarn.lock
@@ -3622,6 +3622,13 @@
|
|||||||
eval "^0.1.8"
|
eval "^0.1.8"
|
||||||
webpack-sources "^1.4.3"
|
webpack-sources "^1.4.3"
|
||||||
|
|
||||||
|
"@stripe/react-stripe-js@^1.8.0":
|
||||||
|
version "1.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@stripe/react-stripe-js/-/react-stripe-js-1.8.0.tgz#b07f1d95e75fe93ac5c33346539ec0972298957d"
|
||||||
|
integrity sha512-WtF2mIWJKnyAaZledC48NfynFckEseCPG8tybwFgisFxHvLPKrArlLISJOi2cjIY20DkMAkSkdlEljXifJi0ZA==
|
||||||
|
dependencies:
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
|
||||||
"@stripe/stripe-js@^1.29.0":
|
"@stripe/stripe-js@^1.29.0":
|
||||||
version "1.29.0"
|
version "1.29.0"
|
||||||
resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.29.0.tgz#f41e46aee711d1eabcb3bbc77376016a250ec962"
|
resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.29.0.tgz#f41e46aee711d1eabcb3bbc77376016a250ec962"
|
||||||
@@ -4402,6 +4409,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
|
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
|
||||||
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
|
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
|
||||||
|
|
||||||
|
"@types/stripe@^8.0.417":
|
||||||
|
version "8.0.417"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/stripe/-/stripe-8.0.417.tgz#b651677a9fc33be8ce8fd5bceadd7ca077214244"
|
||||||
|
integrity sha512-PTuqskh9YKNENnOHGVJBm4sM0zE8B1jZw1JIskuGAPkMB+OH236QeN8scclhYGPA4nG6zTtPXgwpXdp+HPDTVw==
|
||||||
|
dependencies:
|
||||||
|
stripe "*"
|
||||||
|
|
||||||
"@types/tinycolor2@^1.4.3":
|
"@types/tinycolor2@^1.4.3":
|
||||||
version "1.4.3"
|
version "1.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.3.tgz#ed4a0901f954b126e6a914b4839c77462d56e706"
|
resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.3.tgz#ed4a0901f954b126e6a914b4839c77462d56e706"
|
||||||
@@ -13898,7 +13912,7 @@ strip-json-comments@~2.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||||
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
||||||
|
|
||||||
stripe@^9.1.0:
|
stripe@*, stripe@^9.1.0:
|
||||||
version "9.1.0"
|
version "9.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/stripe/-/stripe-9.1.0.tgz#9e0ff10eef1febe215e5794ac13f0111609bd750"
|
resolved "https://registry.yarnpkg.com/stripe/-/stripe-9.1.0.tgz#9e0ff10eef1febe215e5794ac13f0111609bd750"
|
||||||
integrity sha512-Kpa+DoB5GCV360a6fvIrWokLhj3SXRadWAxR6duN2mIgrIkvLK0tVDd8b2OT2/QtJ6WX9EI3vLvaErzG76d+tg==
|
integrity sha512-Kpa+DoB5GCV360a6fvIrWokLhj3SXRadWAxR6duN2mIgrIkvLK0tVDd8b2OT2/QtJ6WX9EI3vLvaErzG76d+tg==
|
||||||
|
|||||||
Reference in New Issue
Block a user