2
0

feat(editor): Payment input

This commit is contained in:
Baptiste Arnaud
2022-05-24 14:25:15 -07:00
parent 91ea637a08
commit 3a6ca3dbde
35 changed files with 1516 additions and 52 deletions

View File

@ -5,6 +5,7 @@ import {
ChatIcon,
CheckSquareIcon,
CodeIcon,
CreditCardIcon,
EditIcon,
EmailIcon,
ExternalLinkIcon,
@ -62,6 +63,8 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
return <PhoneIcon color="orange.500" {...props} />
case InputStepType.CHOICE:
return <CheckSquareIcon color="orange.500" {...props} />
case InputStepType.PAYMENT:
return <CreditCardIcon color="orange.500" {...props} />
case LogicStepType.SET_VARIABLE:
return <EditIcon color="purple.500" {...props} />
case LogicStepType.CONDITION:

View File

@ -37,6 +37,8 @@ export const StepTypeLabel = ({ type }: Props) => {
return <Text>Phone</Text>
case InputStepType.CHOICE:
return <Text>Button</Text>
case InputStepType.PAYMENT:
return <Text>Payment</Text>
case LogicStepType.SET_VARIABLE:
return <Text>Set variable</Text>
case LogicStepType.CONDITION:

View File

@ -7,15 +7,13 @@ import {
Stack,
Image,
PopoverContent,
Tooltip,
HStack,
Text,
Flex,
} from '@chakra-ui/react'
import { ImageUploadContent } from 'components/shared/ImageUploadContent'
import { Input, Textarea } from 'components/shared/Textbox'
import { CodeEditor } from 'components/shared/CodeEditor'
import { HelpCircleIcon } from 'assets/icons'
import { MoreInfoTooltip } from 'components/shared/MoreInfoTooltip'
type Props = {
typebotName: string
@ -113,17 +111,10 @@ export const MetadataForm = ({
<Stack>
<HStack as={FormLabel} mb="0" htmlFor="head">
<Text>Custom head code:</Text>
<Tooltip
label={
'Will be pasted at the bottom of the header section, just above the closing head tag. Only `meta` and `script` tags are allowed.'
}
placement="top"
hasArrow
>
<Flex cursor="pointer">
<HelpCircleIcon />
</Flex>
</Tooltip>
<MoreInfoTooltip>
Will be pasted at the bottom of the header section, just above the
closing head tag. Only `meta` and `script` tags are allowed.
</MoreInfoTooltip>
</HStack>
<CodeEditor
id="head"

View File

@ -31,6 +31,7 @@ import { CodeSettings } from './bodies/CodeSettings'
import { ConditionSettingsBody } from './bodies/ConditionSettingsBody'
import { GoogleAnalyticsSettings } from './bodies/GoogleAnalyticsSettings'
import { GoogleSheetsSettingsBody } from './bodies/GoogleSheetsSettingsBody'
import { PaymentSettings } from './bodies/PaymentSettings'
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
import { RedirectSettings } from './bodies/RedirectSettings'
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: {
return (
<SetVariableSettings

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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',
},
]

View File

@ -0,0 +1 @@
export { PaymentSettings } from './PaymentSettings'

View File

@ -20,6 +20,7 @@ import {
} from './contents'
import { ConfigureContent } from './contents/ConfigureContent'
import { ImageBubbleContent } from './contents/ImageBubbleContent'
import { PaymentInputContent } from './contents/PaymentInputContent'
import { PlaceholderContent } from './contents/PlaceholderContent'
import { SendEmailContent } from './contents/SendEmailContent'
import { TypebotLinkContent } from './contents/TypebotLinkContent'
@ -68,6 +69,9 @@ export const StepNodeContent = ({ step, indices }: Props) => {
case InputStepType.CHOICE: {
return <ItemNodesList step={step} indices={indices} />
}
case InputStepType.PAYMENT: {
return <PaymentInputContent step={step} />
}
case LogicStepType.SET_VARIABLE: {
return <SetVariableContent step={step} />
}

View File

@ -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>
)
}

View 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>
)
}

View File

@ -44,7 +44,7 @@ export const SmartNumberInput = ({
return (
<NumberInput onChange={handleValueChange} value={currentValue} {...props}>
<NumberInputField />
<NumberInputField placeholder={props.placeholder} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />

View File

@ -1,13 +1,6 @@
import {
chakra,
FormLabel,
HStack,
Switch,
SwitchProps,
Tooltip,
} from '@chakra-ui/react'
import { HelpCircleIcon } from 'assets/icons'
import { FormLabel, HStack, Switch, SwitchProps } from '@chakra-ui/react'
import React, { useState } from 'react'
import { MoreInfoTooltip } from './MoreInfoTooltip'
type SwitchWithLabelProps = {
id: string
@ -38,11 +31,7 @@ export const SwitchWithLabel = ({
{label}
</FormLabel>
{moreInfoContent && (
<Tooltip label={moreInfoContent}>
<chakra.span cursor="pointer">
<HelpCircleIcon />
</chakra.span>
</Tooltip>
<MoreInfoTooltip>{moreInfoContent}</MoreInfoTooltip>
)}
</HStack>
<Switch

View File

@ -65,6 +65,7 @@
"nprogress": "^0.2.0",
"papaparse": "^5.3.2",
"prettier": "^2.6.2",
"qs": "^6.10.3",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-draggable": "^4.4.5",
@ -73,15 +74,13 @@
"slate-history": "^0.66.0",
"slate-hyperscript": "^0.77.0",
"slate-react": "^0.79.0",
"stripe": "^9.1.0",
"styled-components": "^5.3.5",
"svg-round-corners": "^0.3.0",
"swr": "^1.3.0",
"tinycolor2": "^1.4.2",
"typebot-js": "*",
"use-debounce": "^8.0.1",
"utils": "*",
"qs": "^6.10.3"
"utils": "*"
},
"devDependencies": {
"@playwright/test": "^1.22.0",
@ -96,6 +95,7 @@
"@types/nprogress": "^0.2.0",
"@types/papaparse": "^5.3.2",
"@types/prettier": "^2.6.1",
"@types/qs": "^6.9.7",
"@types/react": "^18.0.9",
"@types/react-table": "^7.7.12",
"@types/tinycolor2": "^1.4.3",
@ -104,7 +104,6 @@
"eslint": "<8.0.0",
"eslint-config-next": "12.1.6",
"msw": "^0.39.2",
"typescript": "^4.6.4",
"@types/qs": "^6.9.7"
"typescript": "^4.6.4"
}
}

View File

@ -5,4 +5,7 @@ SMTP_HOST=smtp.ethereal.email
SMTP_PORT=587
SMTP_SECURE=true
SMTP_USERNAME=tobin.tillman65@ethereal.email
SMTP_PASSWORD=Ty9BcwCBrK6w8AG2hx
SMTP_PASSWORD=Ty9BcwCBrK6w8AG2hx
STRIPE_TEST_PUBLIC_KEY=
STRIPE_TEST_SECRET_KEY=

View File

@ -5,3 +5,8 @@ export const deleteButtonInConfirmDialog = (page: Page) =>
export const typebotViewer = (page: Page) =>
page.frameLocator('#typebot-iframe')
export const stripePaymentForm = (page: Page) =>
page
.frameLocator('#typebot-iframe')
.frameLocator('[title="Secure payment input frame"]')

View 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()
})
})

View File

@ -1,5 +1,5 @@
import { User } from 'db'
import { loadStripe } from '@stripe/stripe-js'
import { loadStripe } from '@stripe/stripe-js/pure'
import { sendRequest } from 'utils'
type Props = {

View File

@ -37,6 +37,7 @@ import {
defaultEmbedBubbleContent,
ChoiceInputStep,
ConditionStep,
defaultPaymentInputOptions,
} from 'models'
import { Typebot } from 'models'
import useSWR from 'swr'
@ -320,6 +321,8 @@ const parseDefaultStepOptions = (type: StepWithOptionsType): StepOptions => {
return defaultUrlInputOptions
case InputStepType.CHOICE:
return defaultChoiceInputOptions
case InputStepType.PAYMENT:
return defaultPaymentInputOptions
case LogicStepType.SET_VARIABLE:
return defaultSetVariablesOptions
case LogicStepType.REDIRECT: