2
0

Auto continue bot on whatsApp if starting block is input (#849)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
### Summary by CodeRabbit

**New Features:**
- Added WhatsApp integration feature to the Pro plan.

**Refactor:**
- Introduced the ability to exclude specific plans from being displayed
in the Change Plan Modal.
- Renamed the function `isProPlan` to `hasProPerks`, enhancing code
readability and maintainability.
- Updated the `EmbedButton` component to handle a new `lockTagPlan`
property and use the `modal` function instead of the `Modal` component.

**Chore:**
- Removed the `whatsAppPhoneNumberId` field from the `Typebot` model
across various files, simplifying the data structure of the model.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Baptiste Arnaud
2023-09-25 17:20:42 +02:00
committed by GitHub
parent 459fc4debc
commit b81fcf0167
30 changed files with 224 additions and 140 deletions

View File

@@ -8,19 +8,23 @@ import {
useDisclosure, useDisclosure,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import React from 'react' import React from 'react'
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal' import {
ChangePlanModal,
ChangePlanModalProps,
} from '@/features/billing/components/ChangePlanModal'
import { useI18n } from '@/locales' import { useI18n } from '@/locales'
type Props = { type Props = {
contentLabel: React.ReactNode contentLabel: React.ReactNode
buttonLabel?: string buttonLabel?: string
type?: string } & AlertProps &
} & AlertProps Pick<ChangePlanModalProps, 'type' | 'excludedPlans'>
export const UnlockPlanAlertInfo = ({ export const UnlockPlanAlertInfo = ({
contentLabel, contentLabel,
buttonLabel, buttonLabel,
type, type,
excludedPlans,
...props ...props
}: Props) => { }: Props) => {
const t = useI18n() const t = useI18n()
@@ -45,7 +49,12 @@ export const UnlockPlanAlertInfo = ({
> >
{buttonLabel ?? t('billing.upgradeAlert.buttonDefaultLabel')} {buttonLabel ?? t('billing.upgradeAlert.buttonDefaultLabel')}
</Button> </Button>
<ChangePlanModal isOpen={isOpen} onClose={onClose} type={type} /> <ChangePlanModal
isOpen={isOpen}
onClose={onClose}
type={type}
excludedPlans={excludedPlans}
/>
</Alert> </Alert>
) )
} }

View File

@@ -82,6 +82,7 @@ export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
onClose={onClose} onClose={onClose}
isOpen={isOpen} isOpen={isOpen}
type={t('billing.limitMessage.analytics')} type={t('billing.limitMessage.analytics')}
excludedPlans={['STARTER']}
/> />
<StatsCards stats={stats} pos="absolute" /> <StatsCards stats={stats} pos="absolute" />
</Flex> </Flex>

View File

@@ -16,9 +16,10 @@ import { StripeClimateLogo } from './StripeClimateLogo'
type Props = { type Props = {
workspace: Workspace workspace: Workspace
excludedPlans?: ('STARTER' | 'PRO')[]
} }
export const ChangePlanForm = ({ workspace }: Props) => { export const ChangePlanForm = ({ workspace, excludedPlans }: Props) => {
const scopedT = useScopedI18n('billing') const scopedT = useScopedI18n('billing')
const { user } = useUser() const { user } = useUser()
@@ -133,6 +134,7 @@ export const ChangePlanForm = ({ workspace }: Props) => {
</HStack> </HStack>
</HStack> </HStack>
<HStack alignItems="stretch" spacing="4" w="full"> <HStack alignItems="stretch" spacing="4" w="full">
{excludedPlans?.includes('STARTER') ? null : (
<StarterPlanPricingCard <StarterPlanPricingCard
workspace={workspace} workspace={workspace}
currentSubscription={{ isYearly: data.subscription?.isYearly }} currentSubscription={{ isYearly: data.subscription?.isYearly }}
@@ -143,7 +145,9 @@ export const ChangePlanForm = ({ workspace }: Props) => {
isLoading={isUpdatingSubscription} isLoading={isUpdatingSubscription}
currency={data.subscription?.currency} currency={data.subscription?.currency}
/> />
)}
{excludedPlans?.includes('PRO') ? null : (
<ProPlanPricingCard <ProPlanPricingCard
workspace={workspace} workspace={workspace}
currentSubscription={{ isYearly: data.subscription?.isYearly }} currentSubscription={{ isYearly: data.subscription?.isYearly }}
@@ -154,6 +158,7 @@ export const ChangePlanForm = ({ workspace }: Props) => {
isLoading={isUpdatingSubscription} isLoading={isUpdatingSubscription}
currency={data.subscription?.currency} currency={data.subscription?.currency}
/> />
)}
</HStack> </HStack>
</Stack> </Stack>
)} )}

View File

@@ -13,9 +13,10 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { ChangePlanForm } from './ChangePlanForm' import { ChangePlanForm } from './ChangePlanForm'
type ChangePlanModalProps = { export type ChangePlanModalProps = {
type?: string type?: string
isOpen: boolean isOpen: boolean
excludedPlans?: ('STARTER' | 'PRO')[]
onClose: () => void onClose: () => void
} }
@@ -23,11 +24,16 @@ export const ChangePlanModal = ({
onClose, onClose,
isOpen, isOpen,
type, type,
excludedPlans,
}: ChangePlanModalProps) => { }: ChangePlanModalProps) => {
const t = useI18n() const t = useI18n()
const { workspace } = useWorkspace() const { workspace } = useWorkspace()
return ( return (
<Modal isOpen={isOpen} onClose={onClose} size="2xl"> <Modal
isOpen={isOpen}
onClose={onClose}
size={excludedPlans ? 'lg' : '2xl'}
>
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent>
<ModalBody as={Stack} spacing="6" pt="10"> <ModalBody as={Stack} spacing="6" pt="10">
@@ -36,7 +42,12 @@ export const ChangePlanModal = ({
{t('billing.upgradeLimitLabel', { type: type })} {t('billing.upgradeLimitLabel', { type: type })}
</AlertInfo> </AlertInfo>
)} )}
{workspace && <ChangePlanForm workspace={workspace} />} {workspace && (
<ChangePlanForm
workspace={workspace}
excludedPlans={excludedPlans}
/>
)}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>

View File

@@ -200,6 +200,7 @@ export const ProPlanPricingCard = ({
</Text> </Text>
<MoreInfoTooltip>{scopedT('chatsTooltip')}</MoreInfoTooltip> <MoreInfoTooltip>{scopedT('chatsTooltip')}</MoreInfoTooltip>
</HStack>, </HStack>,
scopedT('pro.whatsAppIntegration'),
scopedT('pro.customDomains'), scopedT('pro.customDomains'),
scopedT('pro.analytics'), scopedT('pro.analytics'),
]} ]}

View File

@@ -5,9 +5,16 @@ import { isNotDefined } from '@typebot.io/lib'
import { ChangePlanModal } from './ChangePlanModal' import { ChangePlanModal } from './ChangePlanModal'
import { useI18n } from '@/locales' import { useI18n } from '@/locales'
type Props = { limitReachedType?: string } & ButtonProps type Props = {
limitReachedType?: string
excludedPlans?: ('STARTER' | 'PRO')[]
} & ButtonProps
export const UpgradeButton = ({ limitReachedType, ...props }: Props) => { export const UpgradeButton = ({
limitReachedType,
excludedPlans,
...props
}: Props) => {
const t = useI18n() const t = useI18n()
const { isOpen, onOpen, onClose } = useDisclosure() const { isOpen, onOpen, onClose } = useDisclosure()
const { workspace } = useWorkspace() const { workspace } = useWorkspace()
@@ -23,6 +30,7 @@ export const UpgradeButton = ({ limitReachedType, ...props }: Props) => {
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
type={limitReachedType} type={limitReachedType}
excludedPlans={excludedPlans}
/> />
</Button> </Button>
) )

View File

@@ -1,7 +1,7 @@
import { isDefined } from '@typebot.io/lib' import { isDefined } from '@typebot.io/lib'
import { Workspace, Plan } from '@typebot.io/prisma' import { Workspace, Plan } from '@typebot.io/prisma'
export const isProPlan = (workspace?: Pick<Workspace, 'plan'>) => export const hasProPerks = (workspace?: Pick<Workspace, 'plan'>) =>
isDefined(workspace) && isDefined(workspace) &&
(workspace.plan === Plan.PRO || (workspace.plan === Plan.PRO ||
workspace.plan === Plan.LIFETIME || workspace.plan === Plan.LIFETIME ||

View File

@@ -11,7 +11,7 @@ import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { useEndpoints } from '../../providers/EndpointsProvider' import { useEndpoints } from '../../providers/EndpointsProvider'
import { useGroupsCoordinates } from '../../providers/GroupsCoordinateProvider' import { useGroupsCoordinates } from '../../providers/GroupsCoordinateProvider'
import { isProPlan } from '@/features/billing/helpers/isProPlan' import { hasProPerks } from '@/features/billing/helpers/hasProPerks'
import { computeDropOffPath } from '../../helpers/computeDropOffPath' import { computeDropOffPath } from '../../helpers/computeDropOffPath'
import { computeSourceCoordinates } from '../../helpers/computeSourceCoordinates' import { computeSourceCoordinates } from '../../helpers/computeSourceCoordinates'
import { TotalAnswersInBlock } from '@typebot.io/schemas/features/analytics' import { TotalAnswersInBlock } from '@typebot.io/schemas/features/analytics'
@@ -64,7 +64,7 @@ export const DropOffEdge = ({
[blockId, totalAnswersInBlocks] [blockId, totalAnswersInBlocks]
) )
const isWorkspaceProPlan = isProPlan(workspace) const isWorkspaceProPlan = hasProPerks(workspace)
const { totalDroppedUser, dropOffRate } = useMemo(() => { const { totalDroppedUser, dropOffRate } = useMemo(() => {
if (!publishedTypebot || currentBlock?.total === undefined) if (!publishedTypebot || currentBlock?.total === undefined)

View File

@@ -20,7 +20,7 @@ import { integrationsList } from './embeds/EmbedButton'
import { useTypebot } from '@/features/editor/providers/TypebotProvider' import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { LockTag } from '@/features/billing/components/LockTag' import { LockTag } from '@/features/billing/components/LockTag'
import { UpgradeButton } from '@/features/billing/components/UpgradeButton' import { UpgradeButton } from '@/features/billing/components/UpgradeButton'
import { isProPlan } from '@/features/billing/helpers/isProPlan' import { hasProPerks } from '@/features/billing/helpers/hasProPerks'
import { CustomDomainsDropdown } from '@/features/customDomains/components/CustomDomainsDropdown' import { CustomDomainsDropdown } from '@/features/customDomains/components/CustomDomainsDropdown'
import { TypebotHeader } from '@/features/editor/components/TypebotHeader' import { TypebotHeader } from '@/features/editor/components/TypebotHeader'
import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId' import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId'
@@ -130,7 +130,7 @@ export const SharePage = () => {
{isNotDefined(typebot?.customDomain) && {isNotDefined(typebot?.customDomain) &&
env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME ? ( env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME ? (
<> <>
{isProPlan(workspace) ? ( {hasProPerks(workspace) ? (
<CustomDomainsDropdown <CustomDomainsDropdown
onCustomDomainSelect={handleCustomDomainChange} onCustomDomainSelect={handleCustomDomainChange}
/> />
@@ -138,6 +138,7 @@ export const SharePage = () => {
<UpgradeButton <UpgradeButton
colorScheme="gray" colorScheme="gray"
limitReachedType={t('billing.limitMessage.customDomain')} limitReachedType={t('billing.limitMessage.customDomain')}
excludedPlans={[Plan.STARTER]}
> >
<Text mr="2">Add my domain</Text>{' '} <Text mr="2">Add my domain</Text>{' '}
<LockTag plan={Plan.PRO} /> <LockTag plan={Plan.PRO} />

View File

@@ -16,7 +16,6 @@ import {
NotionLogo, NotionLogo,
WebflowLogo, WebflowLogo,
IframeLogo, IframeLogo,
OtherLogo,
} from './logos' } from './logos'
import React from 'react' import React from 'react'
import { import {
@@ -30,7 +29,6 @@ import {
IframeModal, IframeModal,
WixModal, WixModal,
} from './modals' } from './modals'
import { OtherModal } from './modals/OtherModal'
import { ScriptModal } from './modals/Script/ScriptModal' import { ScriptModal } from './modals/Script/ScriptModal'
import { CodeIcon } from '@/components/icons' import { CodeIcon } from '@/components/icons'
import { ApiModal } from './modals/ApiModal' import { ApiModal } from './modals/ApiModal'
@@ -43,6 +41,11 @@ import { WhatsAppLogo } from '@/components/logos/WhatsAppLogo'
import { WhatsAppModal } from './modals/WhatsAppModal/WhatsAppModal' import { WhatsAppModal } from './modals/WhatsAppModal/WhatsAppModal'
import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider' import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider'
import { isWhatsAppAvailable } from '@/features/telemetry/posthog' import { isWhatsAppAvailable } from '@/features/telemetry/posthog'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { hasProPerks } from '@/features/billing/helpers/hasProPerks'
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
import { LockTag } from '@/features/billing/components/LockTag'
import { Plan } from '@typebot.io/prisma'
export type ModalProps = { export type ModalProps = {
publicId: string publicId: string
@@ -54,13 +57,15 @@ export type ModalProps = {
type EmbedButtonProps = Pick<ModalProps, 'publicId' | 'isPublished'> & { type EmbedButtonProps = Pick<ModalProps, 'publicId' | 'isPublished'> & {
logo: JSX.Element logo: JSX.Element
label: string label: string
Modal: (props: ModalProps) => JSX.Element lockTagPlan?: Plan
modal: (modalProps: { onClose: () => void; isOpen: boolean }) => JSX.Element
} }
export const EmbedButton = ({ export const EmbedButton = ({
logo, logo,
label, label,
Modal, modal,
lockTagPlan,
...modalProps ...modalProps
}: EmbedButtonProps) => { }: EmbedButtonProps) => {
const { isOpen, onOpen, onClose } = useDisclosure() const { isOpen, onOpen, onClose } = useDisclosure()
@@ -75,22 +80,44 @@ export const EmbedButton = ({
> >
<VStack> <VStack>
{logo} {logo}
<Text>{label}</Text> <Text>
{label}
{lockTagPlan && (
<>
{' '}
<LockTag plan={lockTagPlan} />
</>
)}
</Text>
</VStack> </VStack>
<Modal isOpen={isOpen} onClose={onClose} {...modalProps} /> {modal({ isOpen, onClose, ...modalProps })}
</WrapItem> </WrapItem>
) )
} }
export const integrationsList = [ export const integrationsList = [
(props: Pick<ModalProps, 'publicId' | 'isPublished'>) => { (props: Pick<ModalProps, 'publicId' | 'isPublished'>) => {
const { workspace } = useWorkspace()
if (isWhatsAppAvailable()) if (isWhatsAppAvailable())
return ( return (
<ParentModalProvider> <ParentModalProvider>
<EmbedButton <EmbedButton
logo={<WhatsAppLogo height={100} width="70px" />} logo={<WhatsAppLogo height={100} width="70px" />}
label="WhatsApp" label="WhatsApp"
Modal={WhatsAppModal} lockTagPlan={hasProPerks(workspace) ? undefined : 'PRO'}
modal={({ onClose, isOpen }) =>
hasProPerks(workspace) ? (
<WhatsAppModal isOpen={isOpen} onClose={onClose} {...props} />
) : (
<ChangePlanModal
isOpen={isOpen}
onClose={onClose}
excludedPlans={['STARTER']}
type="deploy on WhatsApp"
/>
)
}
{...props} {...props}
/> />
</ParentModalProvider> </ParentModalProvider>
@@ -100,7 +127,9 @@ export const integrationsList = [
<EmbedButton <EmbedButton
logo={<WordpressLogo height={100} width="70px" />} logo={<WordpressLogo height={100} width="70px" />}
label="Wordpress" label="Wordpress"
Modal={WordpressModal} modal={({ onClose, isOpen }) => (
<WordpressModal isOpen={isOpen} onClose={onClose} {...props} />
)}
{...props} {...props}
/> />
), ),
@@ -108,7 +137,7 @@ export const integrationsList = [
<EmbedButton <EmbedButton
logo={<ShopifyLogo height={100} width="65px" />} logo={<ShopifyLogo height={100} width="65px" />}
label="Shopify" label="Shopify"
Modal={ShopifyModal} modal={(modalProps) => <ShopifyModal {...modalProps} {...props} />}
{...props} {...props}
/> />
), ),
@@ -116,7 +145,7 @@ export const integrationsList = [
<EmbedButton <EmbedButton
logo={<WixLogo height={100} width="90px" />} logo={<WixLogo height={100} width="90px" />}
label="Wix" label="Wix"
Modal={WixModal} modal={(modalProps) => <WixModal {...modalProps} {...props} />}
{...props} {...props}
/> />
), ),
@@ -124,7 +153,7 @@ export const integrationsList = [
<EmbedButton <EmbedButton
logo={<GtmLogo height={100} width="70px" />} logo={<GtmLogo height={100} width="70px" />}
label="Google Tag Manager" label="Google Tag Manager"
Modal={GtmModal} modal={(modalProps) => <GtmModal {...modalProps} {...props} />}
{...props} {...props}
/> />
), ),
@@ -132,7 +161,7 @@ export const integrationsList = [
<EmbedButton <EmbedButton
logo={<JavascriptLogo height={100} width="70px" />} logo={<JavascriptLogo height={100} width="70px" />}
label="HTML & Javascript" label="HTML & Javascript"
Modal={JavascriptModal} modal={(modalProps) => <JavascriptModal {...modalProps} {...props} />}
{...props} {...props}
/> />
), ),
@@ -140,7 +169,7 @@ export const integrationsList = [
<EmbedButton <EmbedButton
logo={<ReactLogo height={100} width="70px" />} logo={<ReactLogo height={100} width="70px" />}
label="React" label="React"
Modal={ReactModal} modal={(modalProps) => <ReactModal {...modalProps} {...props} />}
{...props} {...props}
/> />
), ),
@@ -148,7 +177,7 @@ export const integrationsList = [
<EmbedButton <EmbedButton
logo={<NextjsLogo height={100} width="70px" />} logo={<NextjsLogo height={100} width="70px" />}
label="Nextjs" label="Nextjs"
Modal={NextjsModal} modal={(modalProps) => <NextjsModal {...modalProps} {...props} />}
{...props} {...props}
/> />
), ),
@@ -156,7 +185,7 @@ export const integrationsList = [
<EmbedButton <EmbedButton
logo={<CodeIcon height={100} width="60px" />} logo={<CodeIcon height={100} width="60px" />}
label="API" label="API"
Modal={ApiModal} modal={(modalProps) => <ApiModal {...modalProps} {...props} />}
{...props} {...props}
/> />
), ),
@@ -164,7 +193,7 @@ export const integrationsList = [
<EmbedButton <EmbedButton
logo={<NotionLogo height={100} width="60px" />} logo={<NotionLogo height={100} width="60px" />}
label="Notion" label="Notion"
Modal={NotionModal} modal={(modalProps) => <NotionModal {...modalProps} {...props} />}
{...props} {...props}
/> />
), ),
@@ -172,7 +201,7 @@ export const integrationsList = [
<EmbedButton <EmbedButton
logo={<WebflowLogo height={100} width="70px" />} logo={<WebflowLogo height={100} width="70px" />}
label="Webflow" label="Webflow"
Modal={WebflowModal} modal={(modalProps) => <WebflowModal {...modalProps} {...props} />}
{...props} {...props}
/> />
), ),
@@ -180,7 +209,7 @@ export const integrationsList = [
<EmbedButton <EmbedButton
logo={<FlutterFlowLogo height={100} width="60px" />} logo={<FlutterFlowLogo height={100} width="60px" />}
label="FlutterFlow" label="FlutterFlow"
Modal={FlutterFlowModal} modal={(modalProps) => <FlutterFlowModal {...modalProps} {...props} />}
{...props} {...props}
/> />
), ),
@@ -194,7 +223,7 @@ export const integrationsList = [
/> />
} }
label="Script" label="Script"
Modal={ScriptModal} modal={(modalProps) => <ScriptModal {...modalProps} {...props} />}
{...props} {...props}
/> />
), ),
@@ -202,15 +231,7 @@ export const integrationsList = [
<EmbedButton <EmbedButton
logo={<IframeLogo height={100} width="70px" />} logo={<IframeLogo height={100} width="70px" />}
label="Iframe" label="Iframe"
Modal={IframeModal} modal={(modalProps) => <IframeModal {...modalProps} {...props} />}
{...props}
/>
),
(props: Pick<ModalProps, 'publicId' | 'isPublished'>) => (
<EmbedButton
logo={<OtherLogo height={100} width="70px" />}
label="Other"
Modal={OtherModal}
{...props} {...props}
/> />
), ),

View File

@@ -1,25 +0,0 @@
import React, { useState } from 'react'
import { isDefined } from '@udecode/plate-common'
import { EmbedModal } from '../EmbedModal'
import { JavascriptInstructions } from './Javascript/instructions/JavascriptInstructions'
import { ModalProps } from '../EmbedButton'
export const OtherModal = ({ isOpen, onClose, isPublished }: ModalProps) => {
const [selectedEmbedType, setSelectedEmbedType] = useState<
'standard' | 'popup' | 'bubble' | undefined
>()
return (
<EmbedModal
titlePrefix="Other"
isOpen={isOpen}
onClose={onClose}
isPublished={isPublished}
onSelectEmbedType={setSelectedEmbedType}
selectedEmbedType={selectedEmbedType}
>
{isDefined(selectedEmbedType) && (
<JavascriptInstructions type={selectedEmbedType} />
)}
</EmbedModal>
)
}

View File

@@ -23,6 +23,5 @@ export const convertPublicTypebotToTypebot = (
isClosed: existingTypebot.isClosed, isClosed: existingTypebot.isClosed,
resultsTablePreferences: existingTypebot.resultsTablePreferences, resultsTablePreferences: existingTypebot.resultsTablePreferences,
selectedThemeTemplateId: existingTypebot.selectedThemeTemplateId, selectedThemeTemplateId: existingTypebot.selectedThemeTemplateId,
whatsAppPhoneNumberId: existingTypebot.whatsAppPhoneNumberId,
whatsAppCredentialsId: existingTypebot.whatsAppCredentialsId, whatsAppCredentialsId: existingTypebot.whatsAppCredentialsId,
}) })

View File

@@ -12,6 +12,7 @@ import {
import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden' import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden'
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance' import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
import { Prisma } from '@typebot.io/prisma' import { Prisma } from '@typebot.io/prisma'
import { hasProPerks } from '@/features/billing/helpers/hasProPerks'
export const updateTypebot = authenticatedProcedure export const updateTypebot = authenticatedProcedure
.meta({ .meta({
@@ -30,7 +31,6 @@ export const updateTypebot = authenticatedProcedure
typebotSchema._def.schema typebotSchema._def.schema
.pick({ .pick({
isClosed: true, isClosed: true,
whatsAppPhoneNumberId: true,
whatsAppCredentialsId: true, whatsAppCredentialsId: true,
}) })
.partial() .partial()
@@ -70,7 +70,6 @@ export const updateTypebot = authenticatedProcedure
plan: true, plan: true,
}, },
}, },
whatsAppPhoneNumberId: true,
updatedAt: true, updatedAt: true,
}, },
}) })
@@ -119,6 +118,16 @@ export const updateTypebot = authenticatedProcedure
}) })
} }
if (
typebot.whatsAppCredentialsId &&
!hasProPerks(existingTypebot.workspace)
) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'WhatsApp is only available for Pro workspaces',
})
}
const newTypebot = await prisma.typebot.update({ const newTypebot = await prisma.typebot.update({
where: { where: {
id: existingTypebot.id, id: existingTypebot.id,
@@ -151,7 +160,6 @@ export const updateTypebot = authenticatedProcedure
customDomain: customDomain:
typebot.customDomain === null ? null : typebot.customDomain, typebot.customDomain === null ? null : typebot.customDomain,
isClosed: typebot.isClosed, isClosed: typebot.isClosed,
whatsAppPhoneNumberId: typebot.whatsAppPhoneNumberId ?? undefined,
whatsAppCredentialsId: typebot.whatsAppCredentialsId ?? undefined, whatsAppCredentialsId: typebot.whatsAppCredentialsId ?? undefined,
}, },
}) })

View File

@@ -152,6 +152,7 @@ export default {
'billing.pricingCard.pro.description': 'Für Agenturen & wachsende Start-ups.', 'billing.pricingCard.pro.description': 'Für Agenturen & wachsende Start-ups.',
'billing.pricingCard.pro.everythingFromStarter': 'Alles in Starter', 'billing.pricingCard.pro.everythingFromStarter': 'Alles in Starter',
'billing.pricingCard.pro.includedSeats': '5 Plätze inklusive', 'billing.pricingCard.pro.includedSeats': '5 Plätze inklusive',
'billing.pricingCard.pro.whatsAppIntegration': 'WhatsApp-Integration',
'billing.pricingCard.pro.customDomains': 'Eigene Domains', 'billing.pricingCard.pro.customDomains': 'Eigene Domains',
'billing.pricingCard.pro.analytics': 'Detaillierte Analysen', 'billing.pricingCard.pro.analytics': 'Detaillierte Analysen',
'billing.usage.heading': 'Nutzung', 'billing.usage.heading': 'Nutzung',

View File

@@ -147,6 +147,7 @@ export default {
'billing.pricingCard.pro.description': 'For agencies & growing startups.', 'billing.pricingCard.pro.description': 'For agencies & growing startups.',
'billing.pricingCard.pro.everythingFromStarter': 'Everything in Starter', 'billing.pricingCard.pro.everythingFromStarter': 'Everything in Starter',
'billing.pricingCard.pro.includedSeats': '5 seats included', 'billing.pricingCard.pro.includedSeats': '5 seats included',
'billing.pricingCard.pro.whatsAppIntegration': 'WhatsApp integration',
'billing.pricingCard.pro.customDomains': 'Custom domains', 'billing.pricingCard.pro.customDomains': 'Custom domains',
'billing.pricingCard.pro.analytics': 'In-depth analytics', 'billing.pricingCard.pro.analytics': 'In-depth analytics',
'billing.usage.heading': 'Usage', 'billing.usage.heading': 'Usage',

View File

@@ -151,6 +151,7 @@ export default {
'billing.pricingCard.pro.everythingFromStarter': 'billing.pricingCard.pro.everythingFromStarter':
"Tout ce qu'il y a dans Starter", "Tout ce qu'il y a dans Starter",
'billing.pricingCard.pro.includedSeats': '5 collègues inclus', 'billing.pricingCard.pro.includedSeats': '5 collègues inclus',
'billing.pricingCard.pro.whatsAppIntegration': 'Intégration WhatsApp',
'billing.pricingCard.pro.customDomains': 'Domaines personnalisés', 'billing.pricingCard.pro.customDomains': 'Domaines personnalisés',
'billing.pricingCard.pro.analytics': 'Analyses approfondies', 'billing.pricingCard.pro.analytics': 'Analyses approfondies',
'billing.usage.heading': 'Utilisation', 'billing.usage.heading': 'Utilisation',

View File

@@ -154,6 +154,7 @@ export default {
'Para agências e startups em crescimento.', 'Para agências e startups em crescimento.',
'billing.pricingCard.pro.everythingFromStarter': 'Tudo em Starter', 'billing.pricingCard.pro.everythingFromStarter': 'Tudo em Starter',
'billing.pricingCard.pro.includedSeats': '5 assentos incluídos', 'billing.pricingCard.pro.includedSeats': '5 assentos incluídos',
'billing.pricingCard.pro.whatsAppIntegration': 'Integração do WhatsApp',
'billing.pricingCard.pro.customDomains': 'Domínios personalizados', 'billing.pricingCard.pro.customDomains': 'Domínios personalizados',
'billing.pricingCard.pro.analytics': 'Análises aprofundadas', 'billing.pricingCard.pro.analytics': 'Análises aprofundadas',
'billing.usage.heading': 'Uso', 'billing.usage.heading': 'Uso',

View File

@@ -155,6 +155,7 @@ export default {
'Para agências e startups em crescimento.', 'Para agências e startups em crescimento.',
'billing.pricingCard.pro.everythingFromStarter': 'Tudo em Starter', 'billing.pricingCard.pro.everythingFromStarter': 'Tudo em Starter',
'billing.pricingCard.pro.includedSeats': '5 lugares incluídos', 'billing.pricingCard.pro.includedSeats': '5 lugares incluídos',
'billing.pricingCard.pro.whatsAppIntegration': 'Integração do WhatsApp',
'billing.pricingCard.pro.customDomains': 'Domínios personalizados', 'billing.pricingCard.pro.customDomains': 'Domínios personalizados',
'billing.pricingCard.pro.analytics': 'Análises aprofundadas', 'billing.pricingCard.pro.analytics': 'Análises aprofundadas',
'billing.usage.heading': 'Uso', 'billing.usage.heading': 'Uso',

View File

@@ -191,6 +191,7 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
await prisma.typebot.updateMany({ await prisma.typebot.updateMany({
where: { id: typebot.id }, where: { id: typebot.id },
data: { data: {
whatsAppCredentialsId: null,
settings: { settings: {
...settings, ...settings,
general: { general: {

View File

@@ -8687,6 +8687,12 @@
"comparisons" "comparisons"
], ],
"additionalProperties": false "additionalProperties": false
},
"sessionExpiryTimeout": {
"type": "number",
"maximum": 48,
"minimum": 0.01,
"description": "Expiration delay in hours after latest interaction"
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -12799,6 +12805,12 @@
"comparisons" "comparisons"
], ],
"additionalProperties": false "additionalProperties": false
},
"sessionExpiryTimeout": {
"type": "number",
"maximum": 48,
"minimum": 0.01,
"description": "Expiration delay in hours after latest interaction"
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -12874,10 +12886,6 @@
"isClosed": { "isClosed": {
"type": "boolean" "type": "boolean"
}, },
"whatsAppPhoneNumberId": {
"type": "string",
"nullable": true
},
"whatsAppCredentialsId": { "whatsAppCredentialsId": {
"type": "string", "type": "string",
"nullable": true "nullable": true
@@ -12903,7 +12911,6 @@
"resultsTablePreferences", "resultsTablePreferences",
"isArchived", "isArchived",
"isClosed", "isClosed",
"whatsAppPhoneNumberId",
"whatsAppCredentialsId" "whatsAppCredentialsId"
], ],
"additionalProperties": false "additionalProperties": false
@@ -16878,6 +16885,12 @@
"comparisons" "comparisons"
], ],
"additionalProperties": false "additionalProperties": false
},
"sessionExpiryTimeout": {
"type": "number",
"maximum": 48,
"minimum": 0.01,
"description": "Expiration delay in hours after latest interaction"
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -17020,10 +17033,6 @@
"isClosed": { "isClosed": {
"type": "boolean" "type": "boolean"
}, },
"whatsAppPhoneNumberId": {
"type": "string",
"nullable": true
},
"whatsAppCredentialsId": { "whatsAppCredentialsId": {
"type": "string", "type": "string",
"nullable": true "nullable": true
@@ -21014,6 +21023,12 @@
"comparisons" "comparisons"
], ],
"additionalProperties": false "additionalProperties": false
},
"sessionExpiryTimeout": {
"type": "number",
"maximum": 48,
"minimum": 0.01,
"description": "Expiration delay in hours after latest interaction"
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -21089,10 +21104,6 @@
"isClosed": { "isClosed": {
"type": "boolean" "type": "boolean"
}, },
"whatsAppPhoneNumberId": {
"type": "string",
"nullable": true
},
"whatsAppCredentialsId": { "whatsAppCredentialsId": {
"type": "string", "type": "string",
"nullable": true "nullable": true
@@ -21118,7 +21129,6 @@
"resultsTablePreferences", "resultsTablePreferences",
"isArchived", "isArchived",
"isClosed", "isClosed",
"whatsAppPhoneNumberId",
"whatsAppCredentialsId" "whatsAppCredentialsId"
], ],
"additionalProperties": false "additionalProperties": false
@@ -25117,6 +25127,12 @@
"comparisons" "comparisons"
], ],
"additionalProperties": false "additionalProperties": false
},
"sessionExpiryTimeout": {
"type": "number",
"maximum": 48,
"minimum": 0.01,
"description": "Expiration delay in hours after latest interaction"
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -25192,10 +25208,6 @@
"isClosed": { "isClosed": {
"type": "boolean" "type": "boolean"
}, },
"whatsAppPhoneNumberId": {
"type": "string",
"nullable": true
},
"whatsAppCredentialsId": { "whatsAppCredentialsId": {
"type": "string", "type": "string",
"nullable": true "nullable": true
@@ -25221,7 +25233,6 @@
"resultsTablePreferences", "resultsTablePreferences",
"isArchived", "isArchived",
"isClosed", "isClosed",
"whatsAppPhoneNumberId",
"whatsAppCredentialsId" "whatsAppCredentialsId"
], ],
"additionalProperties": false "additionalProperties": false
@@ -29279,6 +29290,12 @@
"comparisons" "comparisons"
], ],
"additionalProperties": false "additionalProperties": false
},
"sessionExpiryTimeout": {
"type": "number",
"maximum": 48,
"minimum": 0.01,
"description": "Expiration delay in hours after latest interaction"
} }
}, },
"additionalProperties": false "additionalProperties": false

View File

@@ -3815,6 +3815,12 @@
"comparisons" "comparisons"
], ],
"additionalProperties": false "additionalProperties": false
},
"sessionExpiryTimeout": {
"type": "number",
"maximum": 48,
"minimum": 0.01,
"description": "Expiration delay in hours after latest interaction"
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -6226,6 +6232,12 @@
"comparisons" "comparisons"
], ],
"additionalProperties": false "additionalProperties": false
},
"sessionExpiryTimeout": {
"type": "number",
"maximum": 48,
"minimum": 0.01,
"description": "Expiration delay in hours after latest interaction"
} }
}, },
"additionalProperties": false "additionalProperties": false

View File

@@ -85,6 +85,7 @@ export const ProPlanCard = ({ isYearly }: Props) => {
</chakra.span> </chakra.span>
</Tooltip> </Tooltip>
</HStack>, </HStack>,
'WhatsApp integration',
'Custom domains', 'Custom domains',
'In-depth analytics', 'In-depth analytics',
], ],

View File

@@ -20,7 +20,6 @@ import { validateUrl } from './blocks/inputs/url/validateUrl'
import { resumeChatCompletion } from './blocks/integrations/openai/resumeChatCompletion' import { resumeChatCompletion } from './blocks/integrations/openai/resumeChatCompletion'
import { resumeWebhookExecution } from './blocks/integrations/webhook/resumeWebhookExecution' import { resumeWebhookExecution } from './blocks/integrations/webhook/resumeWebhookExecution'
import { upsertAnswer } from './queries/upsertAnswer' import { upsertAnswer } from './queries/upsertAnswer'
import { startBotFlow } from './startBotFlow'
import { parseButtonsReply } from './blocks/inputs/buttons/parseButtonsReply' import { parseButtonsReply } from './blocks/inputs/buttons/parseButtonsReply'
import { ParsedReply } from './types' import { ParsedReply } from './types'
import { validateNumber } from './blocks/inputs/number/validateNumber' import { validateNumber } from './blocks/inputs/number/validateNumber'

View File

@@ -75,7 +75,7 @@ export const resumeWhatsAppFlow = async ({
? await continueBotFlow(sessionState)(messageContent) ? await continueBotFlow(sessionState)(messageContent)
: workspaceId : workspaceId
? await startWhatsAppSession({ ? await startWhatsAppSession({
message: receivedMessage, incomingMessage: messageContent,
sessionId, sessionId,
workspaceId, workspaceId,
credentials: { ...credentials, id: credentialsId as string }, credentials: { ...credentials, id: credentialsId as string },

View File

@@ -13,11 +13,14 @@ import {
WhatsAppIncomingMessage, WhatsAppIncomingMessage,
defaultSessionExpiryTimeout, defaultSessionExpiryTimeout,
} from '@typebot.io/schemas/features/whatsapp' } from '@typebot.io/schemas/features/whatsapp'
import { isNotDefined } from '@typebot.io/lib/utils' import { isInputBlock, isNotDefined } from '@typebot.io/lib/utils'
import { startSession } from '../startSession' import { startSession } from '../startSession'
import { getNextGroup } from '../getNextGroup'
import { continueBotFlow } from '../continueBotFlow'
import { upsertResult } from '../queries/upsertResult'
type Props = { type Props = {
message: WhatsAppIncomingMessage incomingMessage?: string
sessionId: string sessionId: string
workspaceId?: string workspaceId?: string
credentials: WhatsAppCredentials['data'] & Pick<WhatsAppCredentials, 'id'> credentials: WhatsAppCredentials['data'] & Pick<WhatsAppCredentials, 'id'>
@@ -25,7 +28,7 @@ type Props = {
} }
export const startWhatsAppSession = async ({ export const startWhatsAppSession = async ({
message, incomingMessage,
workspaceId, workspaceId,
credentials, credentials,
contact, contact,
@@ -63,20 +66,41 @@ export const startWhatsAppSession = async ({
(publicTypebot) => (publicTypebot) =>
publicTypebot.settings.whatsApp?.startCondition && publicTypebot.settings.whatsApp?.startCondition &&
messageMatchStartCondition( messageMatchStartCondition(
getIncomingMessageText(message), incomingMessage ?? '',
publicTypebot.settings.whatsApp?.startCondition publicTypebot.settings.whatsApp?.startCondition
) )
) ?? botsWithWhatsAppEnabled[0] ) ?? botsWithWhatsAppEnabled[0]
if (isNotDefined(publicTypebot)) return if (isNotDefined(publicTypebot)) return
const session = await startSession({ let session = await startSession({
startParams: { startParams: {
typebot: publicTypebot.typebot.publicId as string, typebot: publicTypebot.typebot.publicId as string,
}, },
userId: undefined, userId: undefined,
}) })
// If first block is an input block, we can directly continue the bot flow
const firstEdgeId =
session.newSessionState.typebotsQueue[0].typebot.groups[0].blocks[0]
.outgoingEdgeId
const nextGroup = await getNextGroup(session.newSessionState)(firstEdgeId)
const firstBlock = nextGroup.group?.blocks.at(0)
if (firstBlock && isInputBlock(firstBlock)) {
const resultId = session.newSessionState.typebotsQueue[0].resultId
if (resultId)
await upsertResult({
hasStarted: true,
isCompleted: false,
resultId,
typebot: session.newSessionState.typebotsQueue[0].typebot,
})
session = await continueBotFlow({
...session.newSessionState,
currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id },
})(incomingMessage)
}
const sessionExpiryTimeoutHours = const sessionExpiryTimeoutHours =
publicTypebot.settings.whatsApp?.sessionExpiryTimeout ?? publicTypebot.settings.whatsApp?.sessionExpiryTimeout ??
defaultSessionExpiryTimeout defaultSessionExpiryTimeout
@@ -166,21 +190,3 @@ const matchComparison = (
} }
} }
} }
const getIncomingMessageText = (message: WhatsAppIncomingMessage): string => {
switch (message.type) {
case 'text':
return message.text.body
case 'button':
return message.button.text
case 'interactive': {
return message.interactive.button_reply.title
}
case 'video':
case 'document':
case 'audio':
case 'image': {
return ''
}
}
}

View File

@@ -31,7 +31,6 @@ export const parseTestTypebot = (
isArchived: false, isArchived: false,
isClosed: false, isClosed: false,
resultsTablePreferences: null, resultsTablePreferences: null,
whatsAppPhoneNumberId: null,
whatsAppCredentialsId: null, whatsAppCredentialsId: null,
variables: [{ id: 'var1', name: 'var1' }], variables: [{ id: 'var1', name: 'var1' }],
...partialTypebot, ...partialTypebot,

View File

@@ -198,7 +198,6 @@ model Typebot {
webhooks Webhook[] webhooks Webhook[]
isArchived Boolean @default(false) isArchived Boolean @default(false)
isClosed Boolean @default(false) isClosed Boolean @default(false)
whatsAppPhoneNumberId String?
whatsAppCredentialsId String? whatsAppCredentialsId String?
@@index([workspaceId]) @@index([workspaceId])

View File

@@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `whatsAppPhoneNumberId` on the `Typebot` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Typebot" DROP COLUMN "whatsAppPhoneNumberId";

View File

@@ -182,7 +182,6 @@ model Typebot {
webhooks Webhook[] webhooks Webhook[]
isArchived Boolean @default(false) isArchived Boolean @default(false)
isClosed Boolean @default(false) isClosed Boolean @default(false)
whatsAppPhoneNumberId String?
whatsAppCredentialsId String? whatsAppCredentialsId String?
@@index([workspaceId]) @@index([workspaceId])

View File

@@ -56,7 +56,6 @@ export const typebotSchema = z.preprocess(
resultsTablePreferences: resultsTablePreferencesSchema.nullable(), resultsTablePreferences: resultsTablePreferencesSchema.nullable(),
isArchived: z.boolean(), isArchived: z.boolean(),
isClosed: z.boolean(), isClosed: z.boolean(),
whatsAppPhoneNumberId: z.string().nullable(),
whatsAppCredentialsId: z.string().nullable(), whatsAppCredentialsId: z.string().nullable(),
}) satisfies z.ZodType<TypebotPrisma, z.ZodTypeDef, unknown> }) satisfies z.ZodType<TypebotPrisma, z.ZodTypeDef, unknown>
) )