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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,9 +5,16 @@ import { isNotDefined } from '@typebot.io/lib'
import { ChangePlanModal } from './ChangePlanModal'
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 { isOpen, onOpen, onClose } = useDisclosure()
const { workspace } = useWorkspace()
@ -23,6 +30,7 @@ export const UpgradeButton = ({ limitReachedType, ...props }: Props) => {
isOpen={isOpen}
onClose={onClose}
type={limitReachedType}
excludedPlans={excludedPlans}
/>
</Button>
)

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import {
import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden'
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
import { Prisma } from '@typebot.io/prisma'
import { hasProPerks } from '@/features/billing/helpers/hasProPerks'
export const updateTypebot = authenticatedProcedure
.meta({
@ -30,7 +31,6 @@ export const updateTypebot = authenticatedProcedure
typebotSchema._def.schema
.pick({
isClosed: true,
whatsAppPhoneNumberId: true,
whatsAppCredentialsId: true,
})
.partial()
@ -70,7 +70,6 @@ export const updateTypebot = authenticatedProcedure
plan: true,
},
},
whatsAppPhoneNumberId: 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({
where: {
id: existingTypebot.id,
@ -151,7 +160,6 @@ export const updateTypebot = authenticatedProcedure
customDomain:
typebot.customDomain === null ? null : typebot.customDomain,
isClosed: typebot.isClosed,
whatsAppPhoneNumberId: typebot.whatsAppPhoneNumberId ?? 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.everythingFromStarter': 'Alles in Starter',
'billing.pricingCard.pro.includedSeats': '5 Plätze inklusive',
'billing.pricingCard.pro.whatsAppIntegration': 'WhatsApp-Integration',
'billing.pricingCard.pro.customDomains': 'Eigene Domains',
'billing.pricingCard.pro.analytics': 'Detaillierte Analysen',
'billing.usage.heading': 'Nutzung',

View File

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

View File

@ -151,6 +151,7 @@ export default {
'billing.pricingCard.pro.everythingFromStarter':
"Tout ce qu'il y a dans Starter",
'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.analytics': 'Analyses approfondies',
'billing.usage.heading': 'Utilisation',

View File

@ -154,6 +154,7 @@ export default {
'Para agências e startups em crescimento.',
'billing.pricingCard.pro.everythingFromStarter': 'Tudo em Starter',
'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.analytics': 'Análises aprofundadas',
'billing.usage.heading': 'Uso',

View File

@ -155,6 +155,7 @@ export default {
'Para agências e startups em crescimento.',
'billing.pricingCard.pro.everythingFromStarter': 'Tudo em Starter',
'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.analytics': 'Análises aprofundadas',
'billing.usage.heading': 'Uso',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,11 +13,14 @@ import {
WhatsAppIncomingMessage,
defaultSessionExpiryTimeout,
} 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 { getNextGroup } from '../getNextGroup'
import { continueBotFlow } from '../continueBotFlow'
import { upsertResult } from '../queries/upsertResult'
type Props = {
message: WhatsAppIncomingMessage
incomingMessage?: string
sessionId: string
workspaceId?: string
credentials: WhatsAppCredentials['data'] & Pick<WhatsAppCredentials, 'id'>
@ -25,7 +28,7 @@ type Props = {
}
export const startWhatsAppSession = async ({
message,
incomingMessage,
workspaceId,
credentials,
contact,
@ -63,20 +66,41 @@ export const startWhatsAppSession = async ({
(publicTypebot) =>
publicTypebot.settings.whatsApp?.startCondition &&
messageMatchStartCondition(
getIncomingMessageText(message),
incomingMessage ?? '',
publicTypebot.settings.whatsApp?.startCondition
)
) ?? botsWithWhatsAppEnabled[0]
if (isNotDefined(publicTypebot)) return
const session = await startSession({
let session = await startSession({
startParams: {
typebot: publicTypebot.typebot.publicId as string,
},
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 =
publicTypebot.settings.whatsApp?.sessionExpiryTimeout ??
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,
isClosed: false,
resultsTablePreferences: null,
whatsAppPhoneNumberId: null,
whatsAppCredentialsId: null,
variables: [{ id: 'var1', name: 'var1' }],
...partialTypebot,

View File

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

View File

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