@@ -32,11 +32,17 @@ import { trpc } from '@/lib/trpc'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId'
|
||||
|
||||
export const PublishButton = (props: ButtonProps) => {
|
||||
type Props = ButtonProps & {
|
||||
isMoreMenuDisabled?: boolean
|
||||
}
|
||||
export const PublishButton = ({
|
||||
isMoreMenuDisabled = false,
|
||||
...props
|
||||
}: Props) => {
|
||||
const t = useI18n()
|
||||
const warningTextColor = useColorModeValue('red.300', 'red.600')
|
||||
const { workspace } = useWorkspace()
|
||||
const { push, query } = useRouter()
|
||||
const { push, query, pathname } = useRouter()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const {
|
||||
isPublished,
|
||||
@@ -66,7 +72,8 @@ export const PublishButton = (props: ButtonProps) => {
|
||||
refetchPublishedTypebot({
|
||||
typebotId: typebot?.id as string,
|
||||
})
|
||||
if (!publishedTypebot) push(`/typebots/${query.typebotId}/share`)
|
||||
if (!publishedTypebot && !pathname.endsWith('share'))
|
||||
push(`/typebots/${query.typebotId}/share`)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -153,7 +160,9 @@ export const PublishButton = (props: ButtonProps) => {
|
||||
isLoading={isPublishing || isUnpublishing}
|
||||
isDisabled={isPublished || isSavingLoading}
|
||||
onClick={handlePublishClick}
|
||||
borderRightRadius={publishedTypebot ? 0 : undefined}
|
||||
borderRightRadius={
|
||||
publishedTypebot && !isMoreMenuDisabled ? 0 : undefined
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{isPublished
|
||||
@@ -164,7 +173,7 @@ export const PublishButton = (props: ButtonProps) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{publishedTypebot && (
|
||||
{!isMoreMenuDisabled && publishedTypebot && (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
|
||||
@@ -68,7 +68,7 @@ export const SharePage = () => {
|
||||
|
||||
const checkIfPublicIdIsValid = async (publicId: string) => {
|
||||
const isLongerThanAllowed = publicId.length >= 4
|
||||
if (!isLongerThanAllowed && isCloudProdInstance) {
|
||||
if (!isLongerThanAllowed && isCloudProdInstance()) {
|
||||
showToast({
|
||||
description: 'Should be longer than 4 characters',
|
||||
})
|
||||
|
||||
@@ -39,6 +39,10 @@ import { FlutterFlowLogo } from './logos/FlutterFlowLogo'
|
||||
import { FlutterFlowModal } from './modals/FlutterFlowModal'
|
||||
import { NextjsLogo } from './logos/NextjsLogo'
|
||||
import { NextjsModal } from './modals/Nextjs/NextjsModal'
|
||||
import { WhatsAppLogo } from '@/components/logos/WhatsAppLogo'
|
||||
import { WhatsAppModal } from './modals/WhatsAppModal/WhatsAppModal'
|
||||
import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider'
|
||||
import { getFeatureFlags } from '@/features/telemetry/posthog'
|
||||
|
||||
export type ModalProps = {
|
||||
publicId: string
|
||||
@@ -79,6 +83,19 @@ export const EmbedButton = ({
|
||||
}
|
||||
|
||||
export const integrationsList = [
|
||||
(props: Pick<ModalProps, 'publicId' | 'isPublished'>) => {
|
||||
if (getFeatureFlags().includes('whatsApp'))
|
||||
return (
|
||||
<ParentModalProvider>
|
||||
<EmbedButton
|
||||
logo={<WhatsAppLogo height={100} width="70px" />}
|
||||
label="WhatsApp"
|
||||
Modal={WhatsAppModal}
|
||||
{...props}
|
||||
/>
|
||||
</ParentModalProvider>
|
||||
)
|
||||
},
|
||||
(props: Pick<ModalProps, 'publicId' | 'isPublished'>) => (
|
||||
<EmbedButton
|
||||
logo={<WordpressLogo height={100} width="70px" />}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { HStack, Text } from '@chakra-ui/react'
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { ComparisonOperators } from '@typebot.io/schemas'
|
||||
import { TableListItemProps } from '@/components/TableList'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { WhatsAppComparison } from '@typebot.io/schemas/features/whatsapp'
|
||||
|
||||
export const WhatsAppComparisonItem = ({
|
||||
item,
|
||||
onItemChange,
|
||||
}: TableListItemProps<WhatsAppComparison>) => {
|
||||
const handleSelectComparisonOperator = (
|
||||
comparisonOperator: ComparisonOperators
|
||||
) => {
|
||||
if (comparisonOperator === item.comparisonOperator) return
|
||||
onItemChange({ ...item, comparisonOperator })
|
||||
}
|
||||
const handleChangeValue = (value: string) => {
|
||||
if (value === item.value) return
|
||||
onItemChange({ ...item, value })
|
||||
}
|
||||
|
||||
return (
|
||||
<HStack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<Text flexShrink={0}>User message</Text>
|
||||
<DropdownList
|
||||
currentItem={item.comparisonOperator}
|
||||
onItemSelect={handleSelectComparisonOperator}
|
||||
items={Object.values(ComparisonOperators)}
|
||||
placeholder="Select an operator"
|
||||
size="sm"
|
||||
flexShrink={0}
|
||||
/>
|
||||
{item.comparisonOperator !== ComparisonOperators.IS_SET &&
|
||||
item.comparisonOperator !== ComparisonOperators.IS_EMPTY && (
|
||||
<TextInput
|
||||
defaultValue={item.value ?? ''}
|
||||
onChange={handleChangeValue}
|
||||
placeholder={parseValuePlaceholder(item.comparisonOperator)}
|
||||
withVariableButton={false}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const parseValuePlaceholder = (
|
||||
operator: ComparisonOperators | undefined
|
||||
): string => {
|
||||
switch (operator) {
|
||||
case ComparisonOperators.NOT_EQUAL:
|
||||
case ComparisonOperators.EQUAL:
|
||||
case ComparisonOperators.CONTAINS:
|
||||
case ComparisonOperators.STARTS_WITH:
|
||||
case ComparisonOperators.ENDS_WITH:
|
||||
case ComparisonOperators.NOT_CONTAINS:
|
||||
case undefined:
|
||||
return 'Type a value...'
|
||||
case ComparisonOperators.LESS:
|
||||
case ComparisonOperators.GREATER:
|
||||
return 'Type a number...'
|
||||
case ComparisonOperators.IS_SET:
|
||||
case ComparisonOperators.IS_EMPTY:
|
||||
return ''
|
||||
case ComparisonOperators.MATCHES_REGEX:
|
||||
case ComparisonOperators.NOT_MATCH_REGEX:
|
||||
return '^[0-9]+$'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
import { CopyButton } from '@/components/CopyButton'
|
||||
import { TextLink } from '@/components/TextLink'
|
||||
import { ChevronLeftIcon, ExternalLinkIcon } from '@/components/icons'
|
||||
import { TextInput } from '@/components/inputs/TextInput'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { trpc, trpcVanilla } from '@/lib/trpc'
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
Stack,
|
||||
ModalFooter,
|
||||
Stepper,
|
||||
useSteps,
|
||||
Step,
|
||||
StepIndicator,
|
||||
Box,
|
||||
StepIcon,
|
||||
StepNumber,
|
||||
StepSeparator,
|
||||
StepStatus,
|
||||
StepTitle,
|
||||
UnorderedList,
|
||||
ListItem,
|
||||
Text,
|
||||
Image,
|
||||
Button,
|
||||
HStack,
|
||||
IconButton,
|
||||
Heading,
|
||||
OrderedList,
|
||||
Link,
|
||||
Code,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
} from '@chakra-ui/react'
|
||||
import { env } from '@typebot.io/env'
|
||||
import { getViewerUrl, isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
const steps = [
|
||||
{ title: 'Requirements' },
|
||||
{ title: 'User Token' },
|
||||
{ title: 'Phone Number' },
|
||||
{ title: 'Webhook' },
|
||||
]
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onNewCredentials: (id: string) => void
|
||||
}
|
||||
|
||||
export const WhatsAppCredentialsModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onNewCredentials,
|
||||
}: Props) => {
|
||||
const { workspace } = useWorkspace()
|
||||
const { showToast } = useToast()
|
||||
const { activeStep, goToNext, goToPrevious, setActiveStep } = useSteps({
|
||||
index: 0,
|
||||
count: steps.length,
|
||||
})
|
||||
const [systemUserAccessToken, setSystemUserAccessToken] = useState('')
|
||||
const [phoneNumberId, setPhoneNumberId] = useState('')
|
||||
const [phoneNumberName, setPhoneNumberName] = useState('')
|
||||
const [verificationToken, setVerificationToken] = useState('')
|
||||
const [isVerifying, setIsVerifying] = useState(false)
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
|
||||
const {
|
||||
credentials: {
|
||||
listCredentials: { refetch: refetchCredentials },
|
||||
},
|
||||
} = trpc.useContext()
|
||||
|
||||
const { mutate } = trpc.credentials.createCredentials.useMutation({
|
||||
onMutate: () => setIsCreating(true),
|
||||
onSettled: () => setIsCreating(false),
|
||||
onError: (err) => {
|
||||
showToast({
|
||||
description: err.message,
|
||||
status: 'error',
|
||||
})
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
refetchCredentials()
|
||||
onNewCredentials(data.credentialsId)
|
||||
onClose()
|
||||
resetForm()
|
||||
},
|
||||
})
|
||||
|
||||
const { data: tokenInfoData } = trpc.whatsApp.getSystemTokenInfo.useQuery(
|
||||
{
|
||||
token: systemUserAccessToken,
|
||||
},
|
||||
{ enabled: isNotEmpty(systemUserAccessToken) }
|
||||
)
|
||||
|
||||
const resetForm = () => {
|
||||
setActiveStep(0)
|
||||
setSystemUserAccessToken('')
|
||||
setPhoneNumberId('')
|
||||
}
|
||||
|
||||
const createMetaCredentials = async () => {
|
||||
if (!workspace) return
|
||||
mutate({
|
||||
credentials: {
|
||||
type: 'whatsApp',
|
||||
workspaceId: workspace.id,
|
||||
name: phoneNumberName,
|
||||
data: {
|
||||
systemUserAccessToken,
|
||||
phoneNumberId,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const isTokenValid = async () => {
|
||||
setIsVerifying(true)
|
||||
try {
|
||||
const { expiresAt, scopes } =
|
||||
await trpcVanilla.whatsApp.getSystemTokenInfo.query({
|
||||
token: systemUserAccessToken,
|
||||
})
|
||||
if (expiresAt !== 0) {
|
||||
showToast({
|
||||
description:
|
||||
'Token expiration was not set to *never*. Create the token again with the correct expiration.',
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (
|
||||
['whatsapp_business_management', 'whatsapp_business_messaging'].find(
|
||||
(scope) => !scopes.includes(scope)
|
||||
)
|
||||
) {
|
||||
showToast({
|
||||
description: 'Token does not have all the necessary scopes',
|
||||
})
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
setIsVerifying(false)
|
||||
showToast({
|
||||
description: 'Could not get system info',
|
||||
})
|
||||
return false
|
||||
}
|
||||
setIsVerifying(false)
|
||||
return true
|
||||
}
|
||||
|
||||
const isPhoneNumberAvailable = async () => {
|
||||
setIsVerifying(true)
|
||||
try {
|
||||
const { name } = await trpcVanilla.whatsApp.getPhoneNumber.query({
|
||||
systemToken: systemUserAccessToken,
|
||||
phoneNumberId,
|
||||
})
|
||||
setPhoneNumberName(name)
|
||||
try {
|
||||
const { message } =
|
||||
await trpcVanilla.whatsApp.verifyIfPhoneNumberAvailable.query({
|
||||
phoneNumberDisplayName: name,
|
||||
})
|
||||
|
||||
if (message === 'taken') {
|
||||
setIsVerifying(false)
|
||||
showToast({
|
||||
description: 'Phone number is already registered on Typebot',
|
||||
})
|
||||
return false
|
||||
}
|
||||
const { verificationToken } =
|
||||
await trpcVanilla.whatsApp.generateVerificationToken.mutate()
|
||||
setVerificationToken(verificationToken)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setIsVerifying(false)
|
||||
showToast({
|
||||
description: 'Could not verify if phone number is available',
|
||||
})
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setIsVerifying(false)
|
||||
showToast({
|
||||
description: 'Could not get phone number info',
|
||||
})
|
||||
return false
|
||||
}
|
||||
setIsVerifying(false)
|
||||
return true
|
||||
}
|
||||
|
||||
const goToNextStep = async () => {
|
||||
if (activeStep === steps.length - 1) return createMetaCredentials()
|
||||
if (activeStep === 1 && !(await isTokenValid())) return
|
||||
if (activeStep === 2 && !(await isPhoneNumberAvailable())) return
|
||||
|
||||
goToNext()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="3xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<HStack h="40px">
|
||||
{activeStep > 0 && (
|
||||
<IconButton
|
||||
icon={<ChevronLeftIcon />}
|
||||
aria-label={'Go back'}
|
||||
variant="ghost"
|
||||
onClick={goToPrevious}
|
||||
/>
|
||||
)}
|
||||
<Heading size="md">Add a WhatsApp phone number</Heading>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody as={Stack} spacing="10">
|
||||
<Stepper index={activeStep} size="sm" pt="4">
|
||||
{steps.map((step, index) => (
|
||||
<Step key={index}>
|
||||
<StepIndicator>
|
||||
<StepStatus
|
||||
complete={<StepIcon />}
|
||||
incomplete={<StepNumber />}
|
||||
active={<StepNumber />}
|
||||
/>
|
||||
</StepIndicator>
|
||||
|
||||
<Box flexShrink="0">
|
||||
<StepTitle>{step.title}</StepTitle>
|
||||
</Box>
|
||||
|
||||
<StepSeparator />
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
{activeStep === 0 && <Requirements />}
|
||||
{activeStep === 1 && (
|
||||
<SystemUserToken
|
||||
initialToken={systemUserAccessToken}
|
||||
setToken={setSystemUserAccessToken}
|
||||
/>
|
||||
)}
|
||||
{activeStep === 2 && (
|
||||
<PhoneNumber
|
||||
appId={tokenInfoData?.appId}
|
||||
initialPhoneNumberId={phoneNumberId}
|
||||
setPhoneNumberId={setPhoneNumberId}
|
||||
/>
|
||||
)}
|
||||
{activeStep === 3 && (
|
||||
<Webhook
|
||||
appId={tokenInfoData?.appId}
|
||||
verificationToken={verificationToken}
|
||||
phoneNumberId={phoneNumberId}
|
||||
/>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onClick={goToNextStep}
|
||||
colorScheme="blue"
|
||||
isDisabled={
|
||||
(activeStep === 1 && isEmpty(systemUserAccessToken)) ||
|
||||
(activeStep === 2 && isEmpty(phoneNumberId))
|
||||
}
|
||||
isLoading={isVerifying || isCreating}
|
||||
>
|
||||
{activeStep === steps.length - 1 ? 'Submit' : 'Continue'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const Requirements = () => (
|
||||
<Stack spacing={4}>
|
||||
<Text>
|
||||
Make sure you have{' '}
|
||||
<TextLink
|
||||
href="https://developers.facebook.com/docs/whatsapp/cloud-api/get-started#set-up-developer-assets"
|
||||
isExternal
|
||||
>
|
||||
created a WhatsApp Business Account
|
||||
</TextLink>
|
||||
. You should be able to get to this page:
|
||||
</Text>
|
||||
<Image
|
||||
src="/images/whatsapp-quickstart-page.png"
|
||||
alt="WhatsApp quickstart page"
|
||||
rounded="md"
|
||||
/>
|
||||
<Text>
|
||||
You can find your Meta apps here:{' '}
|
||||
<TextLink href="https://developers.facebook.com/apps" isExternal>
|
||||
https://developers.facebook.com/apps
|
||||
</TextLink>
|
||||
.
|
||||
</Text>
|
||||
</Stack>
|
||||
)
|
||||
|
||||
const SystemUserToken = ({
|
||||
initialToken,
|
||||
setToken,
|
||||
}: {
|
||||
initialToken: string
|
||||
setToken: (id: string) => void
|
||||
}) => (
|
||||
<OrderedList spacing={4}>
|
||||
<ListItem>
|
||||
Go to your{' '}
|
||||
<Button
|
||||
as={Link}
|
||||
href="https://business.facebook.com/settings/system-users"
|
||||
isExternal
|
||||
rightIcon={<ExternalLinkIcon />}
|
||||
size="sm"
|
||||
>
|
||||
System users page
|
||||
</Button>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Create a new user by clicking on <Code>Add</Code>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Fill it with any name and give it the <Code>Admin</Code> role
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Stack>
|
||||
<Text>
|
||||
Click on <Code>Add assets</Code>. Under <Code>Apps</Code>, look for
|
||||
your previously created app, select it and check{' '}
|
||||
<Code>Manage app</Code>
|
||||
</Text>
|
||||
<Image
|
||||
src="/images/meta-system-user-assets.png"
|
||||
alt="Meta system user assets"
|
||||
rounded="md"
|
||||
/>
|
||||
</Stack>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Stack spacing={4}>
|
||||
<Text>
|
||||
Now, click on <Code>Generate new token</Code>. Select your app.
|
||||
</Text>
|
||||
<UnorderedList spacing={4}>
|
||||
<ListItem>
|
||||
Token expiration: <Code>Never</Code>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Available Permissions: <Code>whatsapp_business_messaging</Code>,{' '}
|
||||
<Code>whatsapp_business_management</Code>{' '}
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</Stack>
|
||||
</ListItem>
|
||||
<ListItem>Copy and paste the generated token:</ListItem>
|
||||
<TextInput
|
||||
isRequired
|
||||
label="System User Token"
|
||||
defaultValue={initialToken}
|
||||
onChange={(val) => setToken(val.trim())}
|
||||
withVariableButton={false}
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
</OrderedList>
|
||||
)
|
||||
|
||||
const PhoneNumber = ({
|
||||
appId,
|
||||
initialPhoneNumberId,
|
||||
setPhoneNumberId,
|
||||
}: {
|
||||
appId?: string
|
||||
initialPhoneNumberId: string
|
||||
setPhoneNumberId: (id: string) => void
|
||||
}) => (
|
||||
<OrderedList spacing={4}>
|
||||
<ListItem>
|
||||
<HStack>
|
||||
<Text>
|
||||
Go to your{' '}
|
||||
<TextLink
|
||||
href={`https://developers.facebook.com/apps/${appId}/whatsapp-business/wa-dev-console`}
|
||||
isExternal
|
||||
>
|
||||
WhatsApp Dev Console
|
||||
</TextLink>
|
||||
</Text>
|
||||
</HStack>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Add your phone number by clicking on the <Code>Add phone number</Code>{' '}
|
||||
button.
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Stack>
|
||||
<Text>
|
||||
Select a phone number and paste the associated{' '}
|
||||
<Code>Phone number ID</Code> and{' '}
|
||||
<Code>WhatsApp Business Account ID</Code>:
|
||||
</Text>
|
||||
<HStack>
|
||||
<TextInput
|
||||
label="Phone number ID"
|
||||
defaultValue={initialPhoneNumberId}
|
||||
withVariableButton={false}
|
||||
debounceTimeout={0}
|
||||
isRequired
|
||||
onChange={setPhoneNumberId}
|
||||
/>
|
||||
</HStack>
|
||||
<Image
|
||||
src="/images/whatsapp-phone-selection.png"
|
||||
alt="WA phone selection"
|
||||
/>
|
||||
</Stack>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
)
|
||||
|
||||
const Webhook = ({
|
||||
appId,
|
||||
verificationToken,
|
||||
phoneNumberId,
|
||||
}: {
|
||||
appId?: string
|
||||
verificationToken: string
|
||||
phoneNumberId: string
|
||||
}) => {
|
||||
const { workspace } = useWorkspace()
|
||||
const webhookUrl = `${
|
||||
env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? getViewerUrl()
|
||||
}/api/v1/workspaces/${
|
||||
workspace?.id
|
||||
}/whatsapp/phoneNumbers/${phoneNumberId}/webhook`
|
||||
|
||||
return (
|
||||
<Stack spacing={6}>
|
||||
<Text>
|
||||
In your{' '}
|
||||
<TextLink
|
||||
href={`https://developers.facebook.com/apps/${appId}/whatsapp-business/wa-settings`}
|
||||
isExternal
|
||||
>
|
||||
WhatsApp Settings page
|
||||
</TextLink>
|
||||
, click on the Edit button and insert the following values:
|
||||
</Text>
|
||||
<UnorderedList spacing={6}>
|
||||
<ListItem>
|
||||
<HStack>
|
||||
<Text flexShrink={0}>Callback URL:</Text>
|
||||
<InputGroup size="sm">
|
||||
<Input type={'text'} defaultValue={webhookUrl} />
|
||||
<InputRightElement width="60px">
|
||||
<CopyButton size="sm" textToCopy={webhookUrl} />
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</HStack>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<HStack>
|
||||
<Text flexShrink={0}>Verify Token:</Text>
|
||||
<InputGroup size="sm">
|
||||
<Input type={'text'} defaultValue={verificationToken} />
|
||||
<InputRightElement width="60px">
|
||||
<CopyButton size="sm" textToCopy={verificationToken} />
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</HStack>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<HStack>
|
||||
<Text flexShrink={0}>
|
||||
Webhook fields: check <Code>messages</Code>
|
||||
</Text>
|
||||
</HStack>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
Heading,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Stack,
|
||||
Text,
|
||||
OrderedList,
|
||||
ListItem,
|
||||
HStack,
|
||||
useDisclosure,
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Flex,
|
||||
} from '@chakra-ui/react'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { CredentialsDropdown } from '@/features/credentials/components/CredentialsDropdown'
|
||||
import { ModalProps } from '../../EmbedButton'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { WhatsAppCredentialsModal } from './WhatsAppCredentialsModal'
|
||||
import { TextLink } from '@/components/TextLink'
|
||||
import { PublishButton } from '../../../PublishButton'
|
||||
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
||||
import { isDefined } from '@typebot.io/lib/utils'
|
||||
import { TableList } from '@/components/TableList'
|
||||
import { Comparison, LogicalOperator } from '@typebot.io/schemas'
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { WhatsAppComparisonItem } from './WhatsAppComparisonItem'
|
||||
import { AlertInfo } from '@/components/AlertInfo'
|
||||
|
||||
export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
|
||||
const { typebot, updateTypebot, isPublished } = useTypebot()
|
||||
const { ref } = useParentModal()
|
||||
const { workspace } = useWorkspace()
|
||||
const {
|
||||
isOpen: isCredentialsModalOpen,
|
||||
onOpen,
|
||||
onClose: onCredentialsModalClose,
|
||||
} = useDisclosure()
|
||||
|
||||
const whatsAppSettings = typebot?.settings.whatsApp
|
||||
|
||||
const { data: phoneNumberData } = trpc.whatsApp.getPhoneNumber.useQuery(
|
||||
{
|
||||
credentialsId: whatsAppSettings?.credentialsId as string,
|
||||
},
|
||||
{
|
||||
enabled: !!whatsAppSettings?.credentialsId,
|
||||
}
|
||||
)
|
||||
|
||||
const toggleEnableWhatsApp = (isChecked: boolean) => {
|
||||
if (!phoneNumberData?.id) return
|
||||
updateTypebot({
|
||||
updates: { whatsAppPhoneNumberId: isChecked ? phoneNumberData.id : null },
|
||||
save: true,
|
||||
})
|
||||
}
|
||||
|
||||
const updateCredentialsId = (credentialsId: string | undefined) => {
|
||||
if (!typebot) return
|
||||
updateTypebot({
|
||||
updates: {
|
||||
settings: {
|
||||
...typebot.settings,
|
||||
whatsApp: {
|
||||
...typebot.settings.whatsApp,
|
||||
credentialsId,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const updateStartConditionComparisons = (comparisons: Comparison[]) => {
|
||||
if (!typebot) return
|
||||
updateTypebot({
|
||||
updates: {
|
||||
settings: {
|
||||
...typebot.settings,
|
||||
whatsApp: {
|
||||
...typebot.settings.whatsApp,
|
||||
startCondition: {
|
||||
logicalOperator:
|
||||
typebot.settings.whatsApp?.startCondition?.logicalOperator ??
|
||||
LogicalOperator.AND,
|
||||
comparisons,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const updateStartConditionLogicalOperator = (
|
||||
logicalOperator: LogicalOperator
|
||||
) => {
|
||||
if (!typebot) return
|
||||
updateTypebot({
|
||||
updates: {
|
||||
settings: {
|
||||
...typebot.settings,
|
||||
whatsApp: {
|
||||
...typebot.settings.whatsApp,
|
||||
startCondition: {
|
||||
comparisons:
|
||||
typebot.settings.whatsApp?.startCondition?.comparisons ?? [],
|
||||
logicalOperator,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent ref={ref}>
|
||||
<ModalHeader>
|
||||
<Heading size="md">WhatsApp</Heading>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody as={Stack} spacing="6">
|
||||
{!isPublished && phoneNumberData?.id && (
|
||||
<AlertInfo>You have modifications that can be published.</AlertInfo>
|
||||
)}
|
||||
<OrderedList spacing={4} pl="4">
|
||||
<ListItem>
|
||||
<HStack>
|
||||
<Text>Select a phone number:</Text>
|
||||
{workspace && (
|
||||
<>
|
||||
<WhatsAppCredentialsModal
|
||||
isOpen={isCredentialsModalOpen}
|
||||
onClose={onCredentialsModalClose}
|
||||
onNewCredentials={updateCredentialsId}
|
||||
/>
|
||||
<CredentialsDropdown
|
||||
type="whatsApp"
|
||||
workspaceId={workspace.id}
|
||||
currentCredentialsId={whatsAppSettings?.credentialsId}
|
||||
onCredentialsSelect={updateCredentialsId}
|
||||
onCreateNewClick={onOpen}
|
||||
credentialsName="WA phone number"
|
||||
size="sm"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</ListItem>
|
||||
{typebot?.settings.whatsApp?.credentialsId && (
|
||||
<>
|
||||
<ListItem>
|
||||
<Accordion allowToggle>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Start flow only if
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel as={Stack} spacing="4" pt="4">
|
||||
<TableList<Comparison>
|
||||
initialItems={
|
||||
whatsAppSettings?.startCondition?.comparisons ?? []
|
||||
}
|
||||
onItemsChange={updateStartConditionComparisons}
|
||||
Item={WhatsAppComparisonItem}
|
||||
ComponentBetweenItems={() => (
|
||||
<Flex justify="center">
|
||||
<DropdownList
|
||||
currentItem={
|
||||
whatsAppSettings?.startCondition
|
||||
?.logicalOperator
|
||||
}
|
||||
onItemSelect={
|
||||
updateStartConditionLogicalOperator
|
||||
}
|
||||
items={Object.values(LogicalOperator)}
|
||||
size="sm"
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
addLabel="Add a comparison"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<HStack>
|
||||
<Text>Publish your bot:</Text>
|
||||
<PublishButton size="sm" isMoreMenuDisabled />
|
||||
</HStack>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<SwitchWithLabel
|
||||
label="Enable WhatsApp integration"
|
||||
initialValue={
|
||||
isDefined(typebot?.whatsAppPhoneNumberId) ? true : false
|
||||
}
|
||||
onCheckChange={toggleEnableWhatsApp}
|
||||
justifyContent="flex-start"
|
||||
/>
|
||||
</ListItem>
|
||||
{phoneNumberData?.id && (
|
||||
<ListItem>
|
||||
<TextLink
|
||||
href={`https://wa.me/${phoneNumberData.name}?text=Start`}
|
||||
isExternal
|
||||
>
|
||||
Try it out
|
||||
</TextLink>
|
||||
</ListItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</OrderedList>
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ const parseWordpressShortcode = ({
|
||||
publicId: string
|
||||
}) => {
|
||||
return `[typebot typebot="${publicId}"${
|
||||
isCloudProdInstance ? '' : ` host="${getViewerUrl()}"`
|
||||
isCloudProdInstance() ? '' : ` host="${getViewerUrl()}"`
|
||||
}${width ? ` width="${width}"` : ''}${height ? ` height="${height}"` : ''}]
|
||||
`
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export const parseReactBotProps = ({ typebot, apiHost }: BotProps) => {
|
||||
return `${typebotLine} ${apiHostLine}`
|
||||
}
|
||||
|
||||
export const typebotImportCode = isCloudProdInstance
|
||||
export const typebotImportCode = isCloudProdInstance()
|
||||
? `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.1/dist/web.js'`
|
||||
: `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@${packageJson.version}/dist/web.js'`
|
||||
|
||||
@@ -64,6 +64,6 @@ export const parseApiHost = (
|
||||
export const parseApiHostValue = (
|
||||
customDomain: Typebot['customDomain'] | undefined
|
||||
) => {
|
||||
if (isCloudProdInstance) return
|
||||
if (isCloudProdInstance()) return
|
||||
return parseApiHost(customDomain)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user