From 21ad061f7bcc001be884c3db033c97f7baab718d Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Mon, 18 Sep 2023 10:59:56 +0200 Subject: [PATCH 001/233] :children_crossing: (typebotLink) Make sure variables from child bots are merged if necessary --- .../src/features/chat/helpers/getNextGroup.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/viewer/src/features/chat/helpers/getNextGroup.ts b/apps/viewer/src/features/chat/helpers/getNextGroup.ts index 245088d1be..b2c81eb16c 100644 --- a/apps/viewer/src/features/chat/helpers/getNextGroup.ts +++ b/apps/viewer/src/features/chat/helpers/getNextGroup.ts @@ -1,6 +1,7 @@ -import { byId } from '@typebot.io/lib' -import { Group, SessionState } from '@typebot.io/schemas' +import { byId, isNotDefined } from '@typebot.io/lib' +import { Group, SessionState, VariableWithValue } from '@typebot.io/schemas' import { upsertResult } from '../queries/upsertResult' +import { isDefined } from '@udecode/plate-common' export type NextGroup = { group?: Group @@ -31,15 +32,25 @@ export const getNextGroup = typebot: isMergingWithParent ? { ...state.typebotsQueue[1].typebot, - variables: state.typebotsQueue[1].typebot.variables.map( - (variable) => ({ + variables: state.typebotsQueue[1].typebot.variables + .map((variable) => ({ ...variable, value: state.typebotsQueue[0].answers.find( (answer) => answer.key === variable.name )?.value ?? variable.value, - }) - ), + })) + .concat( + state.typebotsQueue[0].typebot.variables.filter( + (variable) => + isDefined(variable.value) && + isNotDefined( + state.typebotsQueue[1].typebot.variables.find( + (v) => v.name === variable.name + ) + ) + ) as VariableWithValue[] + ), } : state.typebotsQueue[1].typebot, answers: isMergingWithParent From 322c48cddcbf348ab44850499e849d124b9adfed Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Mon, 18 Sep 2023 17:16:30 +0200 Subject: [PATCH 002/233] :zap: (customDomain) Add configuration modal for domain verification Closes #742 --- apps/builder/src/components/icons.tsx | 8 + .../customDomains/api/deleteCustomDomain.ts | 2 +- .../src/features/customDomains/api/router.ts | 2 + .../customDomains/api/verifyCustomDomain.ts | 131 ++++++++++++ ...nModal.tsx => CreateCustomDomainModal.tsx} | 6 +- .../components/CustomDomainConfigModal.tsx | 187 ++++++++++++++++++ .../components/CustomDomainsDropdown.tsx | 4 +- .../components/DomainStatusIcon.tsx | 43 ++++ .../src/features/customDomains/types.ts | 27 +++ .../features/publish/components/SharePage.tsx | 11 +- packages/schemas/features/customDomains.ts | 34 ++++ 11 files changed, 447 insertions(+), 8 deletions(-) create mode 100644 apps/builder/src/features/customDomains/api/verifyCustomDomain.ts rename apps/builder/src/features/customDomains/components/{CustomDomainModal.tsx => CreateCustomDomainModal.tsx} (98%) create mode 100644 apps/builder/src/features/customDomains/components/CustomDomainConfigModal.tsx create mode 100644 apps/builder/src/features/customDomains/components/DomainStatusIcon.tsx create mode 100644 apps/builder/src/features/customDomains/types.ts diff --git a/apps/builder/src/components/icons.tsx b/apps/builder/src/components/icons.tsx index 844f6afef5..87b4ce2103 100644 --- a/apps/builder/src/components/icons.tsx +++ b/apps/builder/src/components/icons.tsx @@ -635,3 +635,11 @@ export const ChevronLastIcon = (props: IconProps) => ( ) + +export const XCircleIcon = (props: IconProps) => ( + + + + + +) diff --git a/apps/builder/src/features/customDomains/api/deleteCustomDomain.ts b/apps/builder/src/features/customDomains/api/deleteCustomDomain.ts index a84d143cbb..6dc651407d 100644 --- a/apps/builder/src/features/customDomains/api/deleteCustomDomain.ts +++ b/apps/builder/src/features/customDomains/api/deleteCustomDomain.ts @@ -10,7 +10,7 @@ export const deleteCustomDomain = authenticatedProcedure .meta({ openapi: { method: 'DELETE', - path: '/custom-domains', + path: '/custom-domains/{name}', protect: true, summary: 'Delete custom domain', tags: ['Custom domains'], diff --git a/apps/builder/src/features/customDomains/api/router.ts b/apps/builder/src/features/customDomains/api/router.ts index c0133187e0..04acf0a469 100644 --- a/apps/builder/src/features/customDomains/api/router.ts +++ b/apps/builder/src/features/customDomains/api/router.ts @@ -2,9 +2,11 @@ import { router } from '@/helpers/server/trpc' import { createCustomDomain } from './createCustomDomain' import { deleteCustomDomain } from './deleteCustomDomain' import { listCustomDomains } from './listCustomDomains' +import { verifyCustomDomain } from './verifyCustomDomain' export const customDomainsRouter = router({ createCustomDomain, deleteCustomDomain, listCustomDomains, + verifyCustomDomain, }) diff --git a/apps/builder/src/features/customDomains/api/verifyCustomDomain.ts b/apps/builder/src/features/customDomains/api/verifyCustomDomain.ts new file mode 100644 index 0000000000..91e32d4351 --- /dev/null +++ b/apps/builder/src/features/customDomains/api/verifyCustomDomain.ts @@ -0,0 +1,131 @@ +import { authenticatedProcedure } from '@/helpers/server/trpc' +import { z } from 'zod' +import { DomainConfigResponse, DomainVerificationResponse } from '../types' +import { + DomainResponse, + DomainVerificationStatus, + domainResponseSchema, + domainVerificationStatusSchema, +} from '@typebot.io/schemas/features/customDomains' +import prisma from '@/lib/prisma' +import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden' +import { TRPCError } from '@trpc/server' +import { env } from '@typebot.io/env' + +export const verifyCustomDomain = authenticatedProcedure + .meta({ + openapi: { + method: 'GET', + path: '/custom-domains/{name}/verify', + protect: true, + summary: 'Verify domain config', + tags: ['Custom domains'], + }, + }) + .input( + z.object({ + workspaceId: z.string(), + name: z.string(), + }) + ) + .output( + z.object({ + status: domainVerificationStatusSchema, + domainJson: domainResponseSchema, + }) + ) + .query(async ({ input: { workspaceId, name }, ctx: { user } }) => { + const workspace = await prisma.workspace.findFirst({ + where: { id: workspaceId }, + select: { + members: { + select: { + userId: true, + role: true, + }, + }, + }, + }) + + if (!workspace || isWriteWorkspaceForbidden(workspace, user)) + throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' }) + + let status: DomainVerificationStatus = 'Valid Configuration' + + const [domainJson, configJson] = await Promise.all([ + getDomainResponse(name), + getConfigResponse(name), + ]) + + if (domainJson?.error?.code === 'not_found') { + status = 'Domain Not Found' + } else if (domainJson.error) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: domainJson.error.message, + }) + } else if (!domainJson.verified) { + status = 'Pending Verification' + const verificationJson = await verifyDomain(name) + + if (verificationJson && verificationJson.verified) { + status = 'Valid Configuration' + } + } else if (configJson.misconfigured) { + status = 'Invalid Configuration' + } else { + status = 'Valid Configuration' + } + + return { + status, + domainJson, + } + }) + +const getDomainResponse = async ( + domain: string +): Promise => { + return await fetch( + `https://api.vercel.com/v9/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${domain}?teamId=${env.VERCEL_TEAM_ID}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${env.VERCEL_TOKEN}`, + 'Content-Type': 'application/json', + }, + } + ).then((res) => { + return res.json() + }) +} + +const getConfigResponse = async ( + domain: string +): Promise => { + return await fetch( + `https://api.vercel.com/v6/domains/${domain}/config?teamId=${env.VERCEL_TEAM_ID}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${env.VERCEL_TOKEN}`, + 'Content-Type': 'application/json', + }, + } + ).then((res) => res.json()) +} + +const verifyDomain = async ( + domain: string +): Promise => { + return await fetch( + `https://api.vercel.com/v9/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${domain}/verify?teamId=${env.VERCEL_TEAM_ID}`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.VERCEL_TOKEN}`, + 'Content-Type': 'application/json', + }, + } + ).then((res) => res.json()) +} diff --git a/apps/builder/src/features/customDomains/components/CustomDomainModal.tsx b/apps/builder/src/features/customDomains/components/CreateCustomDomainModal.tsx similarity index 98% rename from apps/builder/src/features/customDomains/components/CustomDomainModal.tsx rename to apps/builder/src/features/customDomains/components/CreateCustomDomainModal.tsx index 7ee52a800f..d43edf37fb 100644 --- a/apps/builder/src/features/customDomains/components/CustomDomainModal.tsx +++ b/apps/builder/src/features/customDomains/components/CreateCustomDomainModal.tsx @@ -22,7 +22,7 @@ import { trpc } from '@/lib/trpc' const hostnameRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/ -type CustomDomainModalProps = { +type Props = { workspaceId: string isOpen: boolean onClose: () => void @@ -30,13 +30,13 @@ type CustomDomainModalProps = { onNewDomain: (customDomain: string) => void } -export const CustomDomainModal = ({ +export const CreateCustomDomainModal = ({ workspaceId, isOpen, onClose, onNewDomain, domain = '', -}: CustomDomainModalProps) => { +}: Props) => { const inputRef = useRef(null) const [isLoading, setIsLoading] = useState(false) const [inputValue, setInputValue] = useState(domain) diff --git a/apps/builder/src/features/customDomains/components/CustomDomainConfigModal.tsx b/apps/builder/src/features/customDomains/components/CustomDomainConfigModal.tsx new file mode 100644 index 0000000000..c0d3514c06 --- /dev/null +++ b/apps/builder/src/features/customDomains/components/CustomDomainConfigModal.tsx @@ -0,0 +1,187 @@ +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + HStack, + ModalFooter, + Button, + Text, + Box, + Code, + Stack, + Alert, + AlertIcon, +} from '@chakra-ui/react' +import { XCircleIcon } from '@/components/icons' +import { trpc } from '@/lib/trpc' + +type Props = { + workspaceId: string + isOpen: boolean + domain: string + onClose: () => void +} + +export const CustomDomainConfigModal = ({ + workspaceId, + isOpen, + onClose, + domain, +}: Props) => { + const { data, error } = trpc.customDomains.verifyCustomDomain.useQuery({ + name: domain, + workspaceId, + }) + + const { domainJson, status } = data ?? {} + + if (!status || status === 'Valid Configuration' || !domainJson) return null + + if ('error' in domainJson) return null + + const subdomain = getSubdomain(domainJson.name, domainJson.apexName) + + const recordType = subdomain ? 'CNAME' : 'A' + + const txtVerification = + (status === 'Pending Verification' && + domainJson.verification?.find((x) => x.type === 'TXT')) || + null + + return ( + + + + + + + + {status} + + + + + + {txtVerification ? ( + + + Please set the following TXT record on{' '} + + {domainJson.apexName} + {' '} + to prove ownership of{' '} + + {domainJson.name} + + : + + + + Type + + {txtVerification.type} + + + + Name + + {txtVerification.domain.slice( + 0, + txtVerification.domain.length - + domainJson.apexName.length - + 1 + )} + + + + Value + + + {txtVerification.value} + + + + + + + + If you are using this domain for another site, setting this + TXT record will transfer domain ownership away from that site + and break it. Please exercise caution when setting this + record. + + + + ) : status === 'Unknown Error' ? ( + + {error?.message} + + ) : ( + + + To configure your{' '} + {recordType === 'A' ? 'apex domain' : 'subdomain'} ( + + {recordType === 'A' ? domainJson.apexName : domainJson.name} + + ), set the following {recordType} record on your DNS provider to + continue: + + + + Type + + {recordType} + + + + Name + + {recordType === 'A' ? '@' : subdomain ?? 'www'} + + + + Value + + {recordType === 'A' + ? '76.76.21.21' + : `cname.vercel-dns.com`} + + + + TTL + + 86400 + + + + + + + Note: for TTL, if 86400 is not available, set the + highest value possible. Also, domain propagation can take up + to an hour. + + + + )} + + + + + + + ) +} + +const getSubdomain = (name: string, apexName: string) => { + if (name === apexName) return null + return name.slice(0, name.length - apexName.length - 1) +} diff --git a/apps/builder/src/features/customDomains/components/CustomDomainsDropdown.tsx b/apps/builder/src/features/customDomains/components/CustomDomainsDropdown.tsx index 640856e962..0f1c0f372f 100644 --- a/apps/builder/src/features/customDomains/components/CustomDomainsDropdown.tsx +++ b/apps/builder/src/features/customDomains/components/CustomDomainsDropdown.tsx @@ -12,7 +12,7 @@ import { } from '@chakra-ui/react' import { ChevronLeftIcon, PlusIcon, TrashIcon } from '@/components/icons' import React, { useState } from 'react' -import { CustomDomainModal } from './CustomDomainModal' +import { CreateCustomDomainModal } from './CreateCustomDomainModal' import { useWorkspace } from '@/features/workspace/WorkspaceProvider' import { useToast } from '@/hooks/useToast' import { trpc } from '@/lib/trpc' @@ -83,7 +83,7 @@ export const CustomDomainsDropdown = ({ return ( {workspace?.id && ( - { + showToast({ description: err.message }) + }, + } + ) + + if (isLoading || data?.status === 'Valid Configuration') return null + + return ( + <> + + + + + + + + ) +} diff --git a/apps/builder/src/features/customDomains/types.ts b/apps/builder/src/features/customDomains/types.ts new file mode 100644 index 0000000000..f5a25cc0cb --- /dev/null +++ b/apps/builder/src/features/customDomains/types.ts @@ -0,0 +1,27 @@ +// Copied from https://github.com/vercel/platforms/blob/main/lib/types.ts + +// From https://vercel.com/docs/rest-api/endpoints#get-a-domain-s-configuration +export interface DomainConfigResponse { + configuredBy?: ('CNAME' | 'A' | 'http') | null + acceptedChallenges?: ('dns-01' | 'http-01')[] + misconfigured: boolean +} + +// From https://vercel.com/docs/rest-api/endpoints#verify-project-domain +export interface DomainVerificationResponse { + name: string + apexName: string + projectId: string + redirect?: string | null + redirectStatusCode?: (307 | 301 | 302 | 308) | null + gitBranch?: string | null + updatedAt?: number + createdAt?: number + verified: boolean + verification?: { + type: string + domain: string + value: string + reason: string + }[] +} diff --git a/apps/builder/src/features/publish/components/SharePage.tsx b/apps/builder/src/features/publish/components/SharePage.tsx index 839d6c33a6..c28821ca5d 100644 --- a/apps/builder/src/features/publish/components/SharePage.tsx +++ b/apps/builder/src/features/publish/components/SharePage.tsx @@ -1,4 +1,4 @@ -import { CloseIcon } from '@/components/icons' +import { TrashIcon } from '@/components/icons' import { Seo } from '@/components/Seo' import { useWorkspace } from '@/features/workspace/WorkspaceProvider' import { useToast } from '@/hooks/useToast' @@ -26,6 +26,7 @@ import { TypebotHeader } from '@/features/editor/components/TypebotHeader' import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId' import { useI18n } from '@/locales' import { env } from '@typebot.io/env' +import DomainStatusIcon from '@/features/customDomains/components/DomainStatusIcon' export const SharePage = () => { const t = useI18n() @@ -113,11 +114,17 @@ export const SharePage = () => { onPathnameChange={handlePathnameChange} /> } + icon={} aria-label="Remove custom URL" size="xs" onClick={() => handleCustomDomainChange(null)} /> + {workspace?.id && ( + + )} )} {isNotDefined(typebot?.customDomain) && diff --git a/packages/schemas/features/customDomains.ts b/packages/schemas/features/customDomains.ts index ab5118f941..f891132007 100644 --- a/packages/schemas/features/customDomains.ts +++ b/packages/schemas/features/customDomains.ts @@ -1,6 +1,40 @@ import { CustomDomain as CustomDomainInDb } from '@typebot.io/prisma' import { z } from 'zod' +export const domainVerificationStatusSchema = z.enum([ + 'Valid Configuration', + 'Invalid Configuration', + 'Domain Not Found', + 'Pending Verification', + 'Unknown Error', +]) +export type DomainVerificationStatus = z.infer< + typeof domainVerificationStatusSchema +> + +export const domainResponseSchema = z.object({ + name: z.string(), + apexName: z.string(), + projectId: z.string(), + redirect: z.string().nullable(), + redirectStatusCode: z.number().nullable(), + gitBranch: z.string().nullable(), + updatedAt: z.number().nullable(), + createdAt: z.number().nullable(), + verified: z.boolean(), + verification: z + .array( + z.object({ + type: z.string(), + domain: z.string(), + value: z.string(), + reason: z.string(), + }) + ) + .optional(), +}) +export type DomainResponse = z.infer + const domainNameRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/ export const customDomainSchema = z.object({ From 6548752b1bd29fbe3acab7efcaf4733fd80708e8 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Mon, 18 Sep 2023 17:26:05 +0200 Subject: [PATCH 003/233] :bug: Fix bubble icon file upload --- .../BubbleSettings/ButtonThemeSettings.tsx | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/builder/src/features/publish/components/embeds/settings/BubbleSettings/ButtonThemeSettings.tsx b/apps/builder/src/features/publish/components/embeds/settings/BubbleSettings/ButtonThemeSettings.tsx index aaba53c99d..d8ce13b222 100644 --- a/apps/builder/src/features/publish/components/embeds/settings/BubbleSettings/ButtonThemeSettings.tsx +++ b/apps/builder/src/features/publish/components/embeds/settings/BubbleSettings/ButtonThemeSettings.tsx @@ -1,6 +1,8 @@ import { ColorPicker } from '@/components/ColorPicker' import { ImageUploadContent } from '@/components/ImageUploadContent' import { ChevronDownIcon } from '@/components/icons' +import { useTypebot } from '@/features/editor/providers/TypebotProvider' +import { useWorkspace } from '@/features/workspace/WorkspaceProvider' import { Button, Heading, @@ -24,6 +26,8 @@ type Props = { } export const ButtonThemeSettings = ({ buttonTheme, onChange }: Props) => { + const { workspace } = useWorkspace() + const { typebot } = useTypebot() const updateBackgroundColor = (backgroundColor: string) => { onChange({ ...buttonTheme, @@ -76,13 +80,19 @@ export const ButtonThemeSettings = ({ buttonTheme, onChange }: Props) => { - { - updateCustomIconSrc(url) - onClose() - }} - uploadFileProps={undefined} - /> + {workspace?.id && typebot?.id && ( + { + updateCustomIconSrc(url) + onClose() + }} + uploadFileProps={{ + workspaceId: workspace.id, + typebotId: typebot.id, + fileName: 'bubble-icon', + }} + /> + )} )} From 61c46bcb465ff1a0fd769b02e20c981037a4dfaa Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Mon, 18 Sep 2023 18:34:53 +0200 Subject: [PATCH 004/233] :children_crossing: (results) Use header id as table accessor to allow duplicate names --- .../table/ExportAllResultsModal.tsx | 45 ++++++++++--------- .../results/components/table/ResultsTable.tsx | 3 +- .../components/table/SelectionToolbar.tsx | 38 +++++++--------- .../helpers/convertResultsToTableData.tsx | 18 +++----- .../features/results/helpers/parseAccessor.ts | 1 - .../results/helpers/parseHeaderCells.tsx | 3 +- 6 files changed, 50 insertions(+), 58 deletions(-) delete mode 100644 apps/builder/src/features/results/helpers/parseAccessor.ts diff --git a/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx b/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx index bb3fe90c82..443a71f4fb 100644 --- a/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx +++ b/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx @@ -22,8 +22,7 @@ import { parseResultHeader } from '@typebot.io/lib/results' import { useResults } from '../../ResultsProvider' import { parseColumnOrder } from '../../helpers/parseColumnsOrder' import { convertResultsToTableData } from '../../helpers/convertResultsToTableData' -import { parseAccessor } from '../../helpers/parseAccessor' -import { isDefined } from '@typebot.io/lib' +import { byId, isDefined } from '@typebot.io/lib' type Props = { isOpen: boolean @@ -90,40 +89,35 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => { const dataToUnparse = convertResultsToTableData(results, resultHeader) - const fields = parseColumnOrder( + const headerIds = parseColumnOrder( typebot?.resultsTablePreferences?.columnsOrder, resultHeader - ).reduce((currentHeaderLabels, columnId) => { + ).reduce((currentHeaderIds, columnId) => { if ( typebot?.resultsTablePreferences?.columnsVisibility[columnId] === false ) - return currentHeaderLabels + return currentHeaderIds const columnLabel = resultHeader.find( (headerCell) => headerCell.id === columnId - )?.label - if (!columnLabel) return currentHeaderLabels - return [...currentHeaderLabels, columnLabel] + )?.id + if (!columnLabel) return currentHeaderIds + return [...currentHeaderIds, columnLabel] }, []) const data = dataToUnparse.map<{ [key: string]: string }>((data) => { const newObject: { [key: string]: string } = {} - fields?.forEach((field) => { - newObject[field] = data[parseAccessor(field)]?.plainText + headerIds?.forEach((headerId) => { + const headerLabel = resultHeader.find(byId(headerId))?.label + if (!headerLabel) return + const newKey = parseUniqueKey(headerLabel, Object.keys(newObject)) + newObject[newKey] = data[headerId]?.plainText }) return newObject }) - const csvData = new Blob( - [ - unparse({ - data, - fields, - }), - ], - { - type: 'text/csv;charset=utf-8;', - } - ) + const csvData = new Blob([unparse(data)], { + type: 'text/csv;charset=utf-8;', + }) const fileName = `typebot-export_${new Date() .toLocaleDateString() .replaceAll('/', '-')}` @@ -166,3 +160,12 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => { ) } + +export const parseUniqueKey = ( + key: string, + existingKeys: string[], + count = 0 +): string => { + if (!existingKeys.includes(key)) return key + return parseUniqueKey(`${key} (${count + 1})`, existingKeys, count + 1) +} diff --git a/apps/builder/src/features/results/components/table/ResultsTable.tsx b/apps/builder/src/features/results/components/table/ResultsTable.tsx index 30e1aed725..dc2a43e4d5 100644 --- a/apps/builder/src/features/results/components/table/ResultsTable.tsx +++ b/apps/builder/src/features/results/components/table/ResultsTable.tsx @@ -26,7 +26,6 @@ import { CellValueType, TableData } from '../../types' import { IndeterminateCheckbox } from './IndeterminateCheckbox' import { colors } from '@/lib/theme' import { parseColumnOrder } from '../../helpers/parseColumnsOrder' -import { parseAccessor } from '../../helpers/parseAccessor' import { HeaderIcon } from '../HeaderIcon' type ResultsTableProps = { @@ -136,7 +135,7 @@ export const ResultsTable = ({ }, ...resultHeader.map>((header) => ({ id: header.id, - accessorKey: parseAccessor(header.label), + accessorKey: header.id, size: 200, header: () => ( diff --git a/apps/builder/src/features/results/components/table/SelectionToolbar.tsx b/apps/builder/src/features/results/components/table/SelectionToolbar.tsx index fdeaca2425..19153cbe26 100644 --- a/apps/builder/src/features/results/components/table/SelectionToolbar.tsx +++ b/apps/builder/src/features/results/components/table/SelectionToolbar.tsx @@ -15,7 +15,8 @@ import { useToast } from '@/hooks/useToast' import { useResults } from '../../ResultsProvider' import { trpc } from '@/lib/trpc' import { parseColumnOrder } from '../../helpers/parseColumnsOrder' -import { parseAccessor } from '../../helpers/parseAccessor' +import { byId } from '@typebot.io/lib/utils' +import { parseUniqueKey } from './ExportAllResultsModal' type Props = { selectedResultsId: string[] @@ -69,21 +70,21 @@ export const SelectionToolbar = ({ selectedResultsId.includes(data.id.plainText) ) - const fields = parseColumnOrder( + const headerIds = parseColumnOrder( typebot?.resultsTablePreferences?.columnsOrder, resultHeader ) - .reduce((currentHeaderLabels, columnId) => { + .reduce((currentHeaderIds, columnId) => { if ( typebot?.resultsTablePreferences?.columnsVisibility[columnId] === false ) - return currentHeaderLabels + return currentHeaderIds const columnLabel = resultHeader.find( (headerCell) => headerCell.id === columnId - )?.label - if (!columnLabel) return currentHeaderLabels - return [...currentHeaderLabels, columnLabel] + )?.id + if (!columnLabel) return currentHeaderIds + return [...currentHeaderIds, columnLabel] }, []) .concat( typebot?.resultsTablePreferences?.columnsOrder @@ -94,29 +95,24 @@ export const SelectionToolbar = ({ headerCell.id ) ) - .map((headerCell) => headerCell.label) + .map((headerCell) => headerCell.id) : [] ) const data = dataToUnparse.map<{ [key: string]: string }>((data) => { const newObject: { [key: string]: string } = {} - fields?.forEach((field) => { - newObject[field] = data[parseAccessor(field)]?.plainText + headerIds?.forEach((headerId) => { + const headerLabel = resultHeader.find(byId(headerId))?.label + if (!headerLabel) return + const newKey = parseUniqueKey(headerLabel, Object.keys(newObject)) + newObject[newKey] = data[headerId]?.plainText }) return newObject }) - const csvData = new Blob( - [ - unparse({ - data, - fields, - }), - ], - { - type: 'text/csv;charset=utf-8;', - } - ) + const csvData = new Blob([unparse(data)], { + type: 'text/csv;charset=utf-8;', + }) const fileName = `typebot-export_${new Date() .toLocaleDateString() .replaceAll('/', '-')}` diff --git a/apps/builder/src/features/results/helpers/convertResultsToTableData.tsx b/apps/builder/src/features/results/helpers/convertResultsToTableData.tsx index f1c14d61dd..5d8b5cb9de 100644 --- a/apps/builder/src/features/results/helpers/convertResultsToTableData.tsx +++ b/apps/builder/src/features/results/helpers/convertResultsToTableData.tsx @@ -10,7 +10,6 @@ import { import { FileLinks } from '../components/FileLinks' import { TableData } from '../types' import { convertDateToReadable } from './convertDateToReadable' -import { parseAccessor } from './parseAccessor' export const convertResultsToTableData = ( results: ResultWithAnswers[] | undefined, @@ -18,7 +17,7 @@ export const convertResultsToTableData = ( ): TableData[] => (results ?? []).map((result) => ({ id: { plainText: result.id }, - 'Submitted at': { + date: { plainText: convertDateToReadable(result.createdAt), }, ...[...result.answers, ...result.variables].reduce<{ @@ -40,22 +39,19 @@ export const convertResultsToTableData = ( const content = variableValue ?? answer.content return { ...tableData, - [parseAccessor(header.label)]: parseCellContent( - content, - header.blockType - ), + [header.id]: parseCellContent(content, header.blockType), } } const variable = answerOrVariable satisfies VariableWithValue if (variable.value === null) return tableData - const key = headerCells.find((headerCell) => + const headerId = headerCells.find((headerCell) => headerCell.variableIds?.includes(variable.id) - )?.label - if (!key) return tableData - if (isDefined(tableData[key])) return tableData + )?.id + if (!headerId) return tableData + if (isDefined(tableData[headerId])) return tableData return { ...tableData, - [parseAccessor(key)]: parseCellContent(variable.value), + [headerId]: parseCellContent(variable.value), } }, {}), })) diff --git a/apps/builder/src/features/results/helpers/parseAccessor.ts b/apps/builder/src/features/results/helpers/parseAccessor.ts deleted file mode 100644 index 2e42419051..0000000000 --- a/apps/builder/src/features/results/helpers/parseAccessor.ts +++ /dev/null @@ -1 +0,0 @@ -export const parseAccessor = (label: string) => label.replaceAll('.', '') diff --git a/apps/builder/src/features/results/helpers/parseHeaderCells.tsx b/apps/builder/src/features/results/helpers/parseHeaderCells.tsx index 98c8eb23cc..1211c45c8c 100644 --- a/apps/builder/src/features/results/helpers/parseHeaderCells.tsx +++ b/apps/builder/src/features/results/helpers/parseHeaderCells.tsx @@ -2,7 +2,6 @@ import { HStack, Text } from '@chakra-ui/react' import { ResultHeaderCell } from '@typebot.io/schemas' import { HeaderIcon } from '../components/HeaderIcon' import { HeaderCell } from '../types' -import { parseAccessor } from './parseAccessor' export const parseHeaderCells = ( resultHeader: ResultHeaderCell[] @@ -14,5 +13,5 @@ export const parseHeaderCells = ( {header.label} ), - accessor: parseAccessor(header.label), + accessor: header.id, })) From 69ef41b5347c08049a88a321eaf86657f62f9490 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Mon, 18 Sep 2023 18:43:30 +0200 Subject: [PATCH 005/233] :bug: (payment) Fix postalCode camel case issue Closes #822 --- packages/embeds/js/package.json | 2 +- .../blocks/inputs/payment/components/StripePaymentForm.tsx | 7 ++++--- packages/embeds/nextjs/package.json | 2 +- packages/embeds/react/package.json | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/embeds/js/package.json b/packages/embeds/js/package.json index 48a47d343c..021a7edb43 100644 --- a/packages/embeds/js/package.json +++ b/packages/embeds/js/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/js", - "version": "0.1.26", + "version": "0.1.27", "description": "Javascript library to display typebots on your website", "type": "module", "main": "dist/index.js", diff --git a/packages/embeds/js/src/features/blocks/inputs/payment/components/StripePaymentForm.tsx b/packages/embeds/js/src/features/blocks/inputs/payment/components/StripePaymentForm.tsx index 671baf9d21..caa86e87c6 100644 --- a/packages/embeds/js/src/features/blocks/inputs/payment/components/StripePaymentForm.tsx +++ b/packages/embeds/js/src/features/blocks/inputs/payment/components/StripePaymentForm.tsx @@ -59,6 +59,8 @@ export const StripePaymentForm = (props: Props) => { sessionId: props.context.sessionId, typebot: props.context.typebot, }) + const { postalCode, ...address } = + props.options.additionalInformation?.address ?? {} const { error, paymentIntent } = await stripe.confirmPayment({ elements, confirmParams: { @@ -69,9 +71,8 @@ export const StripePaymentForm = (props: Props) => { email: props.options.additionalInformation?.email, phone: props.options.additionalInformation?.phoneNumber, address: { - ...props.options.additionalInformation?.address, - postal_code: - props.options.additionalInformation?.address?.postalCode, + ...address, + postal_code: postalCode, }, }, }, diff --git a/packages/embeds/nextjs/package.json b/packages/embeds/nextjs/package.json index 0973f898ec..df251d1dd5 100644 --- a/packages/embeds/nextjs/package.json +++ b/packages/embeds/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/nextjs", - "version": "0.1.26", + "version": "0.1.27", "description": "Convenient library to display typebots on your Next.js website", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/embeds/react/package.json b/packages/embeds/react/package.json index c26cfaf852..d6156f122e 100644 --- a/packages/embeds/react/package.json +++ b/packages/embeds/react/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/react", - "version": "0.1.26", + "version": "0.1.27", "description": "Convenient library to display typebots on your React app", "main": "dist/index.js", "types": "dist/index.d.ts", From 2ce63f5d06515f7808db0bd9abfb0bf9aeb48cbd Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 19 Sep 2023 11:47:41 +0200 Subject: [PATCH 006/233] :bug: (results) Fix result modal content display --- apps/builder/src/features/results/components/ResultModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/builder/src/features/results/components/ResultModal.tsx b/apps/builder/src/features/results/components/ResultModal.tsx index ade0e04931..ee6b07b66b 100644 --- a/apps/builder/src/features/results/components/ResultModal.tsx +++ b/apps/builder/src/features/results/components/ResultModal.tsx @@ -36,14 +36,14 @@ export const ResultModal = ({ resultId, onClose }: Props) => { {resultHeader.map((header) => - result && result[header.label] ? ( + result && result[header.id] ? ( {header.label} - {getHeaderValue(result[header.label])} + {getHeaderValue(result[header.id])} ) : null From f626c9867cb324b8546e8fca40fe8065ff36b5c4 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 19 Sep 2023 11:53:18 +0200 Subject: [PATCH 007/233] :zap: (whatsapp) Improve WhatsApp preview management Closes #800 --- apps/builder/package.json | 3 +- .../preview/api/sendWhatsAppInitialMessage.ts | 48 -- .../WhatsAppPreviewInstructions.tsx | 2 +- .../typebot/helpers/isReadTypebotForbidden.ts | 2 +- .../whatsapp}/receiveMessagePreview.ts | 8 +- apps/builder/src/features/whatsapp/router.ts | 6 + .../whatsapp}/startWhatsAppPreview.ts | 61 +- .../whatsapp}/subscribePreviewWebhook.ts | 3 +- .../helpers/server/routers/v1/trpcRouter.ts | 2 - apps/builder/tsconfig.json | 2 +- apps/docs/docs/self-hosting/configuration.md | 52 +- apps/docs/openapi/builder/_spec_.json | 679 +++++++++++++++++- apps/docs/openapi/chat/_spec_.json | 512 ------------- apps/viewer/package.json | 2 +- .../logic/setVariable/executeSetVariable.ts | 2 +- .../features/whatsApp/api/receiveMessage.ts | 5 +- .../src/features/whatsApp/api/router.ts | 6 - .../features/whatsApp/api/subscribeWebhook.ts | 3 +- .../whatsApp/helpers/resumeWhatsAppFlow.ts | 2 +- packages/env/env.ts | 2 + packages/lib/package.json | 9 +- packages/lib/tsconfig.json | 5 +- .../convertInputToWhatsAppMessage.ts | 2 +- .../convertMessageToWhatsAppMessage.ts | 2 +- .../convertRichTextToWhatsAppText.ts | 0 .../lib/whatsApp}/sendChatReplyToWhatsApp.ts | 4 +- .../lib/whatsApp}/sendWhatsAppMessage.ts | 0 packages/schemas/features/whatsapp.ts | 7 +- pnpm-lock.yaml | 12 + 29 files changed, 796 insertions(+), 647 deletions(-) delete mode 100644 apps/builder/src/features/preview/api/sendWhatsAppInitialMessage.ts rename apps/{viewer/src/features/whatsApp/api => builder/src/features/whatsapp}/receiveMessagePreview.ts (85%) rename apps/{viewer/src/features/whatsApp/api => builder/src/features/whatsapp}/startWhatsAppPreview.ts (64%) rename apps/{viewer/src/features/whatsApp/api => builder/src/features/whatsapp}/subscribePreviewWebhook.ts (92%) rename {apps/viewer/src/features/whatsApp/helpers => packages/lib/whatsApp}/convertInputToWhatsAppMessage.ts (98%) rename {apps/viewer/src/features/whatsApp/helpers => packages/lib/whatsApp}/convertMessageToWhatsAppMessage.ts (98%) rename {apps/viewer/src/features/whatsApp/helpers => packages/lib/whatsApp}/convertRichTextToWhatsAppText.ts (100%) rename {apps/viewer/src/features/whatsApp/helpers => packages/lib/whatsApp}/sendChatReplyToWhatsApp.ts (97%) rename {apps/viewer/src/features/whatsApp/helpers => packages/lib/whatsApp}/sendWhatsAppMessage.ts (100%) diff --git a/apps/builder/package.json b/apps/builder/package.json index 41d495f7b1..e3c0fdcf7a 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -93,7 +93,8 @@ "tinycolor2": "1.6.0", "trpc-openapi": "1.2.0", "unsplash-js": "^7.0.18", - "use-debounce": "9.0.4" + "use-debounce": "9.0.4", + "@typebot.io/viewer": "workspace:*" }, "devDependencies": { "@chakra-ui/styled-system": "2.9.1", diff --git a/apps/builder/src/features/preview/api/sendWhatsAppInitialMessage.ts b/apps/builder/src/features/preview/api/sendWhatsAppInitialMessage.ts deleted file mode 100644 index 360eab9d03..0000000000 --- a/apps/builder/src/features/preview/api/sendWhatsAppInitialMessage.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { authenticatedProcedure } from '@/helpers/server/trpc' -import { z } from 'zod' -import got, { HTTPError } from 'got' -import { getViewerUrl } from '@typebot.io/lib/getViewerUrl' -import prisma from '@/lib/prisma' -import { TRPCError } from '@trpc/server' - -export const sendWhatsAppInitialMessage = authenticatedProcedure - .input( - z.object({ - to: z.string(), - typebotId: z.string(), - startGroupId: z.string().optional(), - }) - ) - .mutation( - async ({ input: { to, typebotId, startGroupId }, ctx: { user } }) => { - const apiToken = await prisma.apiToken.findFirst({ - where: { ownerId: user.id }, - select: { - token: true, - }, - }) - if (!apiToken) - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Api Token not found', - }) - try { - await got.post({ - method: 'POST', - url: `${getViewerUrl()}/api/v1/typebots/${typebotId}/whatsapp/start-preview`, - headers: { - Authorization: `Bearer ${apiToken.token}`, - }, - json: { to, isPreview: true, startGroupId }, - }) - } catch (error) { - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Request to viewer failed', - cause: error instanceof HTTPError ? error.response.body : error, - }) - } - - return { message: 'success' } - } - ) diff --git a/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx b/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx index b3ffd1dc02..ca376dd1b3 100644 --- a/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx +++ b/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx @@ -32,7 +32,7 @@ export const WhatsAppPreviewInstructions = (props: StackProps) => { const [hasMessageBeenSent, setHasMessageBeenSent] = useState(false) const { showToast } = useToast() - const { mutate } = trpc.sendWhatsAppInitialMessage.useMutation({ + const { mutate } = trpc.whatsApp.startWhatsAppPreview.useMutation({ onMutate: () => setIsSendingMessage(true), onSettled: () => setIsSendingMessage(false), onError: (error) => showToast({ description: error.message }), diff --git a/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts b/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts index c4719cc859..7192e404c2 100644 --- a/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts +++ b/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts @@ -5,7 +5,7 @@ import { Typebot } from '@typebot.io/schemas' export const isReadTypebotForbidden = async ( typebot: Pick & { - collaborators: Pick[] + collaborators: Pick[] }, user: Pick ) => { diff --git a/apps/viewer/src/features/whatsApp/api/receiveMessagePreview.ts b/apps/builder/src/features/whatsapp/receiveMessagePreview.ts similarity index 85% rename from apps/viewer/src/features/whatsApp/api/receiveMessagePreview.ts rename to apps/builder/src/features/whatsapp/receiveMessagePreview.ts index 7f42b8e558..1141e505f9 100644 --- a/apps/viewer/src/features/whatsApp/api/receiveMessagePreview.ts +++ b/apps/builder/src/features/whatsapp/receiveMessagePreview.ts @@ -1,7 +1,7 @@ import { publicProcedure } from '@/helpers/server/trpc' import { whatsAppWebhookRequestBodySchema } from '@typebot.io/schemas/features/whatsapp' import { z } from 'zod' -import { resumeWhatsAppFlow } from '../helpers/resumeWhatsAppFlow' +import { resumeWhatsAppFlow } from '@typebot.io/viewer/src/features/whatsApp/helpers/resumeWhatsAppFlow' import { isNotDefined } from '@typebot.io/lib' import { TRPCError } from '@trpc/server' import { env } from '@typebot.io/env' @@ -11,7 +11,8 @@ export const receiveMessagePreview = publicProcedure openapi: { method: 'POST', path: '/whatsapp/preview/webhook', - summary: 'WhatsApp', + summary: 'Message webhook', + tags: ['WhatsApp'], }, }) .input(whatsAppWebhookRequestBodySchema) @@ -30,8 +31,7 @@ export const receiveMessagePreview = publicProcedure if (isNotDefined(receivedMessage)) return { message: 'No message found' } const contactName = entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? '' - const contactPhoneNumber = - entry.at(0)?.changes.at(0)?.value?.metadata.display_phone_number ?? '' + const contactPhoneNumber = '+' + receivedMessage.from return resumeWhatsAppFlow({ receivedMessage, sessionId: `wa-${receivedMessage.from}-preview`, diff --git a/apps/builder/src/features/whatsapp/router.ts b/apps/builder/src/features/whatsapp/router.ts index e9c0114a57..6f4dd298cf 100644 --- a/apps/builder/src/features/whatsapp/router.ts +++ b/apps/builder/src/features/whatsapp/router.ts @@ -3,10 +3,16 @@ import { getPhoneNumber } from './getPhoneNumber' import { getSystemTokenInfo } from './getSystemTokenInfo' import { verifyIfPhoneNumberAvailable } from './verifyIfPhoneNumberAvailable' import { generateVerificationToken } from './generateVerificationToken' +import { startWhatsAppPreview } from './startWhatsAppPreview' +import { subscribePreviewWebhook } from './subscribePreviewWebhook' +import { receiveMessagePreview } from './receiveMessagePreview' export const whatsAppRouter = router({ getPhoneNumber, getSystemTokenInfo, verifyIfPhoneNumberAvailable, generateVerificationToken, + startWhatsAppPreview, + subscribePreviewWebhook, + receiveMessagePreview, }) diff --git a/apps/viewer/src/features/whatsApp/api/startWhatsAppPreview.ts b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts similarity index 64% rename from apps/viewer/src/features/whatsApp/api/startWhatsAppPreview.ts rename to apps/builder/src/features/whatsapp/startWhatsAppPreview.ts index a5e3186ced..02ea9f0d6f 100644 --- a/apps/viewer/src/features/whatsApp/api/startWhatsAppPreview.ts +++ b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts @@ -1,21 +1,24 @@ -import { publicProcedure } from '@/helpers/server/trpc' +import { authenticatedProcedure } from '@/helpers/server/trpc' import { z } from 'zod' import { TRPCError } from '@trpc/server' -import { sendWhatsAppMessage } from '../helpers/sendWhatsAppMessage' -import { startSession } from '@/features/chat/helpers/startSession' -import { restartSession } from '@/features/chat/queries/restartSession' +import { sendWhatsAppMessage } from '@typebot.io/lib/whatsApp/sendWhatsAppMessage' +import { startSession } from '@typebot.io/viewer/src/features/chat/helpers/startSession' import { env } from '@typebot.io/env' import { HTTPError } from 'got' import prisma from '@/lib/prisma' -import { sendChatReplyToWhatsApp } from '../helpers/sendChatReplyToWhatsApp' -import { saveStateToDatabase } from '@/features/chat/helpers/saveStateToDatabase' +import { sendChatReplyToWhatsApp } from '@typebot.io/lib/whatsApp/sendChatReplyToWhatsApp' +import { saveStateToDatabase } from '@typebot.io/viewer/src/features/chat/helpers/saveStateToDatabase' +import { restartSession } from '@typebot.io/viewer/src/features/chat/queries/restartSession' +import { isReadTypebotForbidden } from '../typebot/helpers/isReadTypebotForbidden' +import { SessionState } from '@typebot.io/schemas' -export const startWhatsAppPreview = publicProcedure +export const startWhatsAppPreview = authenticatedProcedure .meta({ openapi: { method: 'POST', path: '/typebots/{typebotId}/whatsapp/start-preview', - summary: 'Start WhatsApp Preview', + summary: 'Start preview', + tags: ['WhatsApp'], protect: true, }, }) @@ -38,20 +41,35 @@ export const startWhatsAppPreview = publicProcedure async ({ input: { to, typebotId, startGroupId }, ctx: { user } }) => { if ( !env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID || - !env.META_SYSTEM_USER_TOKEN + !env.META_SYSTEM_USER_TOKEN || + !env.WHATSAPP_PREVIEW_TEMPLATE_NAME ) throw new TRPCError({ code: 'BAD_REQUEST', message: - 'Missing WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID and/or META_SYSTEM_USER_TOKEN env variables', - }) - if (!user) - throw new TRPCError({ - code: 'UNAUTHORIZED', - message: - 'You need to authenticate your request in order to start a preview', + 'Missing WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID or META_SYSTEM_USER_TOKEN or WHATSAPP_PREVIEW_TEMPLATE_NAME env variables', }) + const existingTypebot = await prisma.typebot.findFirst({ + where: { + id: typebotId, + }, + select: { + id: true, + workspaceId: true, + collaborators: { + select: { + userId: true, + }, + }, + }, + }) + if ( + !existingTypebot?.id || + (await isReadTypebotForbidden(existingTypebot, user)) + ) + throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' }) + const sessionId = `wa-${to}-preview` const existingSession = await prisma.chatSession.findFirst({ @@ -60,6 +78,7 @@ export const startWhatsAppPreview = publicProcedure }, select: { updatedAt: true, + state: true, }, }) @@ -105,7 +124,11 @@ export const startWhatsAppPreview = publicProcedure }) } else { await restartSession({ - state: newSessionState, + state: { + ...newSessionState, + whatsApp: (existingSession?.state as SessionState | undefined) + ?.whatsApp, + }, id: `wa-${to}-preview`, }) try { @@ -115,9 +138,9 @@ export const startWhatsAppPreview = publicProcedure type: 'template', template: { language: { - code: 'en', + code: env.WHATSAPP_PREVIEW_TEMPLATE_LANG, }, - name: 'preview_initial_message', + name: env.WHATSAPP_PREVIEW_TEMPLATE_NAME, }, }, credentials: { diff --git a/apps/viewer/src/features/whatsApp/api/subscribePreviewWebhook.ts b/apps/builder/src/features/whatsapp/subscribePreviewWebhook.ts similarity index 92% rename from apps/viewer/src/features/whatsApp/api/subscribePreviewWebhook.ts rename to apps/builder/src/features/whatsapp/subscribePreviewWebhook.ts index 18c6fe7618..b08d49af70 100644 --- a/apps/viewer/src/features/whatsApp/api/subscribePreviewWebhook.ts +++ b/apps/builder/src/features/whatsapp/subscribePreviewWebhook.ts @@ -8,7 +8,8 @@ export const subscribePreviewWebhook = publicProcedure openapi: { method: 'GET', path: '/whatsapp/preview/webhook', - summary: 'WhatsApp', + summary: 'Subscribe webhook', + tags: ['WhatsApp'], }, }) .input( diff --git a/apps/builder/src/helpers/server/routers/v1/trpcRouter.ts b/apps/builder/src/helpers/server/routers/v1/trpcRouter.ts index f7cb74838e..0d5a597efc 100644 --- a/apps/builder/src/helpers/server/routers/v1/trpcRouter.ts +++ b/apps/builder/src/helpers/server/routers/v1/trpcRouter.ts @@ -3,7 +3,6 @@ import { webhookRouter } from '@/features/blocks/integrations/webhook/api/router import { getLinkedTypebots } from '@/features/blocks/logic/typebotLink/api/getLinkedTypebots' import { credentialsRouter } from '@/features/credentials/api/router' import { getAppVersionProcedure } from '@/features/dashboard/api/getAppVersionProcedure' -import { sendWhatsAppInitialMessage } from '@/features/preview/api/sendWhatsAppInitialMessage' import { resultsRouter } from '@/features/results/api/router' import { processTelemetryEvent } from '@/features/telemetry/api/processTelemetryEvent' import { themeRouter } from '@/features/theme/api/router' @@ -23,7 +22,6 @@ export const trpcRouter = router({ processTelemetryEvent, getLinkedTypebots, analytics: analyticsRouter, - sendWhatsAppInitialMessage, workspace: workspaceRouter, typebot: typebotRouter, webhook: webhookRouter, diff --git a/apps/builder/tsconfig.json b/apps/builder/tsconfig.json index 84e4ae1b48..5ec9b33e29 100644 --- a/apps/builder/tsconfig.json +++ b/apps/builder/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@/*": ["src/*"] + "@/*": ["src/*", "../viewer/src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] diff --git a/apps/docs/docs/self-hosting/configuration.md b/apps/docs/docs/self-hosting/configuration.md index b0aae723fb..4bfed6133f 100644 --- a/apps/docs/docs/self-hosting/configuration.md +++ b/apps/docs/docs/self-hosting/configuration.md @@ -200,22 +200,54 @@ In order to be able to test your bot on WhatsApp from the Preview drawer, you ne

Requirements

-1. Make sure you have [created a WhatsApp Business Account](https://developers.facebook.com/docs/whatsapp/cloud-api/get-started#set-up-developer-assets). -2. Go to your [System users page](https://business.facebook.com/settings/system-users) and create a new system user that has access to the related. +### Create a Facebook Business account + +1. Head over to https://business.facebook.com and log in +2. Create a new business account on the left side bar + +:::note +It is possible that Meta directly restricts your newly created Business account. In that case, make sure to verify your identity to proceed. +::: + +### Create a Meta app + +1. Head over to https://developers.facebook.com/apps +2. Click on Create App +3. Give it any name and select `Business` type +4. Select your newly created Business Account +5. On the app page, set up the `WhatsApp` product + +### Get the System User token + +1. Go to your [System users page](https://business.facebook.com/settings/system-users) and create a new system user that has access to the related. - Token expiration: `Never` - Available Permissions: `whatsapp_business_messaging`, `whatsapp_business_management` -3. The generated token will be used as `META_SYSTEM_USER_TOKEN` in your viewer configuration. -4. Click on `Add assets`. Under `Apps`, look for your app, select it and check `Manage app` -5. Go to your WhatsApp Dev Console +2. The generated token will be used as `META_SYSTEM_USER_TOKEN` in your viewer configuration. +3. Click on `Add assets`. Under `Apps`, look for your app, select it and check `Manage app` + +### Get the phone number ID + +1. Go to your WhatsApp Dev Console WhatsApp dev console -6. Add your phone number by clicking on the `Add phone number` button. -7. Select the newly created phone number in the `From` dropdown list. This will be used as `WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID` in your viewer configuration. -8. Head over to `Quickstart > Configuration`. Edit the webhook URL to `$NEXT_PUBLIC_VIEWER_URL/api/v1/whatsapp/preview/webhook`. Set the Verify token to `$ENCRYPTION_SECRET` and click on `Verify and save`. -9. Add the `messages` webhook field. +2. Add your phone number by clicking on the `Add phone number` button. +3. Select the newly created phone number in the `From` dropdown list and you will see right below the associated `Phone number ID` This will be used as `WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID` in your viewer configuration. + +### Set up the webhook + +1. Head over to `Quickstart > Configuration`. Edit the webhook URL to `$NEXTAUTH_URL/api/v1/whatsapp/preview/webhook`. Set the Verify token to `$ENCRYPTION_SECRET` and click on `Verify and save`. +2. Add the `messages` webhook field. + +### Set up the message template + +1. Head over to `Messaging > Message Templates` and click on `Create Template` +2. Select the `Utility` category +3. Give it a name that corresponds to your `WHATSAPP_PREVIEW_TEMPLATE_NAME` configuration. +4. Select the language that corresponds to your `WHATSAPP_PREVIEW_TEMPLATE_LANG` configuration. +5. You can format it as you'd like. The user will just have to send a message to start the preview.

@@ -223,6 +255,8 @@ In order to be able to test your bot on WhatsApp from the Preview drawer, you ne | ------------------------------------- | ------- | ------------------------------------------------------- | | META_SYSTEM_USER_TOKEN | | The system user token used to send WhatsApp messages | | WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID | | The phone number ID from which the message will be sent | +| WHATSAPP_PREVIEW_TEMPLATE_NAME | | The preview start template message name | +| WHATSAPP_PREVIEW_TEMPLATE_LANG | en | The preview start template message name | ## Others diff --git a/apps/docs/openapi/builder/_spec_.json b/apps/docs/openapi/builder/_spec_.json index 77279272eb..c216a1364d 100644 --- a/apps/docs/openapi/builder/_spec_.json +++ b/apps/docs/openapi/builder/_spec_.json @@ -32433,6 +32433,71 @@ } } }, + "get": { + "operationId": "customDomains-listCustomDomains", + "summary": "List custom domains", + "tags": [ + "Custom domains" + ], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [ + { + "name": "workspaceId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "customDomains": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "name", + "createdAt" + ], + "additionalProperties": false + } + } + }, + "required": [ + "customDomains" + ], + "additionalProperties": false + } + } + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/custom-domains/{name}": { "delete": { "operationId": "customDomains-deleteCustomDomain", "summary": "Delete custom domain", @@ -32455,7 +32520,7 @@ }, { "name": "name", - "in": "query", + "in": "path", "required": true, "schema": { "type": "string" @@ -32489,10 +32554,12 @@ "$ref": "#/components/responses/error" } } - }, + } + }, + "/custom-domains/{name}/verify": { "get": { - "operationId": "customDomains-listCustomDomains", - "summary": "List custom domains", + "operationId": "customDomains-verifyCustomDomain", + "summary": "Verify domain config", "tags": [ "Custom domains" ], @@ -32509,6 +32576,14 @@ "schema": { "type": "string" } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } } ], "responses": { @@ -32519,29 +32594,96 @@ "schema": { "type": "object", "properties": { - "customDomains": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } + "status": { + "type": "string", + "enum": [ + "Valid Configuration", + "Invalid Configuration", + "Domain Not Found", + "Pending Verification", + "Unknown Error" + ] + }, + "domainJson": { + "type": "object", + "properties": { + "name": { + "type": "string" }, - "required": [ - "name", - "createdAt" - ], - "additionalProperties": false - } + "apexName": { + "type": "string" + }, + "projectId": { + "type": "string" + }, + "redirect": { + "type": "string", + "nullable": true + }, + "redirectStatusCode": { + "type": "number", + "nullable": true + }, + "gitBranch": { + "type": "string", + "nullable": true + }, + "updatedAt": { + "type": "number", + "nullable": true + }, + "createdAt": { + "type": "number", + "nullable": true + }, + "verified": { + "type": "boolean" + }, + "verification": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "value": { + "type": "string" + }, + "reason": { + "type": "string" + } + }, + "required": [ + "type", + "domain", + "value", + "reason" + ], + "additionalProperties": false + } + } + }, + "required": [ + "name", + "apexName", + "projectId", + "redirect", + "redirectStatusCode", + "gitBranch", + "updatedAt", + "createdAt", + "verified" + ], + "additionalProperties": false } }, "required": [ - "customDomains" + "status", + "domainJson" ], "additionalProperties": false } @@ -32768,6 +32910,497 @@ } } }, + "/typebots/{typebotId}/whatsapp/start-preview": { + "post": { + "operationId": "whatsApp-startWhatsAppPreview", + "summary": "Start preview", + "tags": [ + "WhatsApp" + ], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "to": { + "type": "string", + "minLength": 1 + }, + "startGroupId": { + "type": "string" + } + }, + "required": [ + "to" + ], + "additionalProperties": false + } + } + } + }, + "parameters": [ + { + "name": "typebotId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/whatsapp/preview/webhook": { + "get": { + "operationId": "whatsApp-subscribePreviewWebhook", + "summary": "Subscribe webhook", + "tags": [ + "WhatsApp" + ], + "parameters": [ + { + "name": "hub.challenge", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "hub.verify_token", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "number" + } + } + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + }, + "post": { + "operationId": "whatsApp-receiveMessagePreview", + "summary": "Message webhook", + "tags": [ + "WhatsApp" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "entry": { + "type": "array", + "items": { + "type": "object", + "properties": { + "changes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "type": "object", + "properties": { + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "profile": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + }, + "required": [ + "profile" + ], + "additionalProperties": false + } + }, + "messages": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "text": { + "type": "object", + "properties": { + "body": { + "type": "string" + } + }, + "required": [ + "body" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "text", + "timestamp" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "button" + ] + }, + "button": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "payload": { + "type": "string" + } + }, + "required": [ + "text", + "payload" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "button", + "timestamp" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "interactive" + ] + }, + "interactive": { + "type": "object", + "properties": { + "button_reply": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "id", + "title" + ], + "additionalProperties": false + } + }, + "required": [ + "button_reply" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "interactive", + "timestamp" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "image" + ] + }, + "image": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "image", + "timestamp" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "video" + ] + }, + "video": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "video", + "timestamp" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "audio" + ] + }, + "audio": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "audio", + "timestamp" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "document" + ] + }, + "document": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "document", + "timestamp" + ], + "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + } + }, + "required": [ + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "changes" + ], + "additionalProperties": false + } + } + }, + "required": [ + "entry" + ], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, "/openai/models": { "get": { "operationId": "openAI-listModels", diff --git a/apps/docs/openapi/chat/_spec_.json b/apps/docs/openapi/chat/_spec_.json index e8e9e671d2..5adbd7f675 100644 --- a/apps/docs/openapi/chat/_spec_.json +++ b/apps/docs/openapi/chat/_spec_.json @@ -6496,435 +6496,6 @@ } } }, - "/whatsapp/preview/webhook": { - "get": { - "operationId": "whatsAppRouter-subscribePreviewWebhook", - "summary": "WhatsApp", - "parameters": [ - { - "name": "hub.challenge", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "hub.verify_token", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": { - "schema": { - "type": "number" - } - } - } - }, - "default": { - "$ref": "#/components/responses/error" - } - } - }, - "post": { - "operationId": "whatsAppRouter-receiveMessagePreview", - "summary": "WhatsApp", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "entry": { - "type": "array", - "items": { - "type": "object", - "properties": { - "changes": { - "type": "array", - "items": { - "type": "object", - "properties": { - "value": { - "type": "object", - "properties": { - "contacts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "profile": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "name" - ], - "additionalProperties": false - } - }, - "required": [ - "profile" - ], - "additionalProperties": false - } - }, - "metadata": { - "type": "object", - "properties": { - "display_phone_number": { - "type": "string" - } - }, - "required": [ - "display_phone_number" - ], - "additionalProperties": false - }, - "messages": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "from": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "text" - ] - }, - "text": { - "type": "object", - "properties": { - "body": { - "type": "string" - } - }, - "required": [ - "body" - ], - "additionalProperties": false - }, - "timestamp": { - "type": "string" - } - }, - "required": [ - "from", - "type", - "text", - "timestamp" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "from": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "button" - ] - }, - "button": { - "type": "object", - "properties": { - "text": { - "type": "string" - }, - "payload": { - "type": "string" - } - }, - "required": [ - "text", - "payload" - ], - "additionalProperties": false - }, - "timestamp": { - "type": "string" - } - }, - "required": [ - "from", - "type", - "button", - "timestamp" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "from": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "interactive" - ] - }, - "interactive": { - "type": "object", - "properties": { - "button_reply": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - } - }, - "required": [ - "id", - "title" - ], - "additionalProperties": false - } - }, - "required": [ - "button_reply" - ], - "additionalProperties": false - }, - "timestamp": { - "type": "string" - } - }, - "required": [ - "from", - "type", - "interactive", - "timestamp" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "from": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "image" - ] - }, - "image": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ], - "additionalProperties": false - }, - "timestamp": { - "type": "string" - } - }, - "required": [ - "from", - "type", - "image", - "timestamp" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "from": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "video" - ] - }, - "video": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ], - "additionalProperties": false - }, - "timestamp": { - "type": "string" - } - }, - "required": [ - "from", - "type", - "video", - "timestamp" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "from": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "audio" - ] - }, - "audio": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ], - "additionalProperties": false - }, - "timestamp": { - "type": "string" - } - }, - "required": [ - "from", - "type", - "audio", - "timestamp" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "from": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "document" - ] - }, - "document": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ], - "additionalProperties": false - }, - "timestamp": { - "type": "string" - } - }, - "required": [ - "from", - "type", - "document", - "timestamp" - ], - "additionalProperties": false - } - ] - } - } - }, - "required": [ - "metadata" - ], - "additionalProperties": false - } - }, - "required": [ - "value" - ], - "additionalProperties": false - } - } - }, - "required": [ - "changes" - ], - "additionalProperties": false - } - } - }, - "required": [ - "entry" - ], - "additionalProperties": false - } - } - } - }, - "parameters": [], - "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ], - "additionalProperties": false - } - } - } - }, - "default": { - "$ref": "#/components/responses/error" - } - } - } - }, "/workspaces/{workspaceId}/whatsapp/phoneNumbers/{phoneNumberId}/webhook": { "get": { "operationId": "whatsAppRouter-subscribeWebhook", @@ -7031,18 +6602,6 @@ "additionalProperties": false } }, - "metadata": { - "type": "object", - "properties": { - "display_phone_number": { - "type": "string" - } - }, - "required": [ - "display_phone_number" - ], - "additionalProperties": false - }, "messages": { "type": "array", "items": { @@ -7320,9 +6879,6 @@ } } }, - "required": [ - "metadata" - ], "additionalProperties": false } }, @@ -7391,74 +6947,6 @@ } } } - }, - "/typebots/{typebotId}/whatsapp/start-preview": { - "post": { - "operationId": "whatsAppRouter-startWhatsAppPreview", - "summary": "Start WhatsApp Preview", - "security": [ - { - "Authorization": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "to": { - "type": "string", - "minLength": 1 - }, - "startGroupId": { - "type": "string" - } - }, - "required": [ - "to" - ], - "additionalProperties": false - } - } - } - }, - "parameters": [ - { - "name": "typebotId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ], - "additionalProperties": false - } - } - } - }, - "default": { - "$ref": "#/components/responses/error" - } - } - } } }, "components": { diff --git a/apps/viewer/package.json b/apps/viewer/package.json index 80b7cc028b..4838840ca5 100644 --- a/apps/viewer/package.json +++ b/apps/viewer/package.json @@ -1,5 +1,5 @@ { - "name": "viewer", + "name": "@typebot.io/viewer", "license": "AGPL-3.0-or-later", "version": "0.1.0", "scripts": { diff --git a/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts b/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts index d6295b9c64..1462e31859 100644 --- a/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts +++ b/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts @@ -79,7 +79,7 @@ const getExpressionToEvaluate = case 'Contact name': return state.whatsApp?.contact.name ?? '' case 'Phone number': - return state.whatsApp?.contact.phoneNumber ?? '' + return `"${state.whatsApp?.contact.phoneNumber}"` ?? '' case 'Now': case 'Today': return 'new Date().toISOString()' diff --git a/apps/viewer/src/features/whatsApp/api/receiveMessage.ts b/apps/viewer/src/features/whatsApp/api/receiveMessage.ts index 85bd640863..ff0907e0a9 100644 --- a/apps/viewer/src/features/whatsApp/api/receiveMessage.ts +++ b/apps/viewer/src/features/whatsApp/api/receiveMessage.ts @@ -9,7 +9,8 @@ export const receiveMessage = publicProcedure openapi: { method: 'POST', path: '/workspaces/{workspaceId}/whatsapp/phoneNumbers/{phoneNumberId}/webhook', - summary: 'Receive WhatsApp Message', + summary: 'Message webhook', + tags: ['WhatsApp'], }, }) .input( @@ -28,7 +29,7 @@ export const receiveMessage = publicProcedure const contactName = entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? '' const contactPhoneNumber = - entry.at(0)?.changes.at(0)?.value?.metadata.display_phone_number ?? '' + entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? '' return resumeWhatsAppFlow({ receivedMessage, sessionId: `wa-${phoneNumberId}-${receivedMessage.from}`, diff --git a/apps/viewer/src/features/whatsApp/api/router.ts b/apps/viewer/src/features/whatsApp/api/router.ts index 579ace950a..b9cd890eee 100644 --- a/apps/viewer/src/features/whatsApp/api/router.ts +++ b/apps/viewer/src/features/whatsApp/api/router.ts @@ -1,14 +1,8 @@ import { router } from '@/helpers/server/trpc' -import { receiveMessagePreview } from './receiveMessagePreview' -import { startWhatsAppPreview } from './startWhatsAppPreview' -import { subscribePreviewWebhook } from './subscribePreviewWebhook' import { subscribeWebhook } from './subscribeWebhook' import { receiveMessage } from './receiveMessage' export const whatsAppRouter = router({ - subscribePreviewWebhook, subscribeWebhook, - receiveMessagePreview, receiveMessage, - startWhatsAppPreview, }) diff --git a/apps/viewer/src/features/whatsApp/api/subscribeWebhook.ts b/apps/viewer/src/features/whatsApp/api/subscribeWebhook.ts index 46b9da9840..b0e64b3990 100644 --- a/apps/viewer/src/features/whatsApp/api/subscribeWebhook.ts +++ b/apps/viewer/src/features/whatsApp/api/subscribeWebhook.ts @@ -8,7 +8,8 @@ export const subscribeWebhook = publicProcedure openapi: { method: 'GET', path: '/workspaces/{workspaceId}/whatsapp/phoneNumbers/{phoneNumberId}/webhook', - summary: 'Subscribe WhatsApp webhook', + summary: 'Subscribe webhook', + tags: ['WhatsApp'], protect: true, }, }) diff --git a/apps/viewer/src/features/whatsApp/helpers/resumeWhatsAppFlow.ts b/apps/viewer/src/features/whatsApp/helpers/resumeWhatsAppFlow.ts index 32fac73805..fcb14a8242 100644 --- a/apps/viewer/src/features/whatsApp/helpers/resumeWhatsAppFlow.ts +++ b/apps/viewer/src/features/whatsApp/helpers/resumeWhatsAppFlow.ts @@ -11,7 +11,7 @@ import prisma from '@/lib/prisma' import { decrypt } from '@typebot.io/lib/api' import { downloadMedia } from './downloadMedia' import { env } from '@typebot.io/env' -import { sendChatReplyToWhatsApp } from './sendChatReplyToWhatsApp' +import { sendChatReplyToWhatsApp } from '@typebot.io/lib/whatsApp/sendChatReplyToWhatsApp' export const resumeWhatsAppFlow = async ({ receivedMessage, diff --git a/packages/env/env.ts b/packages/env/env.ts index bc460ef75d..6f6abfdc96 100644 --- a/packages/env/env.ts +++ b/packages/env/env.ts @@ -230,6 +230,8 @@ const whatsAppEnv = { server: { META_SYSTEM_USER_TOKEN: z.string().min(1).optional(), WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID: z.string().min(1).optional(), + WHATSAPP_PREVIEW_TEMPLATE_NAME: z.string().min(1).optional(), + WHATSAPP_PREVIEW_TEMPLATE_LANG: z.string().min(1).optional().default('en'), }, } diff --git a/packages/lib/package.json b/packages/lib/package.json index 55479b0104..d27e833b51 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -8,21 +8,24 @@ "devDependencies": { "@paralleldrive/cuid2": "2.2.1", "@playwright/test": "1.36.0", + "@typebot.io/env": "workspace:*", "@typebot.io/prisma": "workspace:*", "@typebot.io/schemas": "workspace:*", "@typebot.io/tsconfig": "workspace:*", "@types/nodemailer": "6.4.8", "next": "13.4.3", "nodemailer": "6.9.3", - "typescript": "5.1.6", - "@typebot.io/env": "workspace:*" + "typescript": "5.1.6" }, "peerDependencies": { "next": "13.0.0", "nodemailer": "6.7.8" }, "dependencies": { + "@sentry/nextjs": "7.66.0", + "@udecode/plate-common": "^21.1.5", "got": "12.6.0", - "minio": "7.1.3" + "minio": "7.1.3", + "remark-slate": "^1.8.6" } } diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index 57c1b52376..ce77240db1 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "@typebot.io/tsconfig/base.json", "include": ["**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules"], + "compilerOptions": { + "target": "ES2021" + } } diff --git a/apps/viewer/src/features/whatsApp/helpers/convertInputToWhatsAppMessage.ts b/packages/lib/whatsApp/convertInputToWhatsAppMessage.ts similarity index 98% rename from apps/viewer/src/features/whatsApp/helpers/convertInputToWhatsAppMessage.ts rename to packages/lib/whatsApp/convertInputToWhatsAppMessage.ts index be670dba8a..bf17774d7c 100644 --- a/apps/viewer/src/features/whatsApp/helpers/convertInputToWhatsAppMessage.ts +++ b/packages/lib/whatsApp/convertInputToWhatsAppMessage.ts @@ -1,4 +1,3 @@ -import { isDefined, isEmpty } from '@typebot.io/lib' import { BubbleBlockType, ButtonItem, @@ -7,6 +6,7 @@ import { } from '@typebot.io/schemas' import { WhatsAppSendingMessage } from '@typebot.io/schemas/features/whatsapp' import { convertRichTextToWhatsAppText } from './convertRichTextToWhatsAppText' +import { isDefined, isEmpty } from '../utils' export const convertInputToWhatsAppMessages = ( input: NonNullable, diff --git a/apps/viewer/src/features/whatsApp/helpers/convertMessageToWhatsAppMessage.ts b/packages/lib/whatsApp/convertMessageToWhatsAppMessage.ts similarity index 98% rename from apps/viewer/src/features/whatsApp/helpers/convertMessageToWhatsAppMessage.ts rename to packages/lib/whatsApp/convertMessageToWhatsAppMessage.ts index f4f9d89fb4..e2ec2cf6ff 100644 --- a/apps/viewer/src/features/whatsApp/helpers/convertMessageToWhatsAppMessage.ts +++ b/packages/lib/whatsApp/convertMessageToWhatsAppMessage.ts @@ -5,7 +5,7 @@ import { } from '@typebot.io/schemas' import { WhatsAppSendingMessage } from '@typebot.io/schemas/features/whatsapp' import { convertRichTextToWhatsAppText } from './convertRichTextToWhatsAppText' -import { isSvgSrc } from '@typebot.io/lib' +import { isSvgSrc } from '../utils' const mp4HttpsUrlRegex = /^https:\/\/.*\.mp4$/ diff --git a/apps/viewer/src/features/whatsApp/helpers/convertRichTextToWhatsAppText.ts b/packages/lib/whatsApp/convertRichTextToWhatsAppText.ts similarity index 100% rename from apps/viewer/src/features/whatsApp/helpers/convertRichTextToWhatsAppText.ts rename to packages/lib/whatsApp/convertRichTextToWhatsAppText.ts diff --git a/apps/viewer/src/features/whatsApp/helpers/sendChatReplyToWhatsApp.ts b/packages/lib/whatsApp/sendChatReplyToWhatsApp.ts similarity index 97% rename from apps/viewer/src/features/whatsApp/helpers/sendChatReplyToWhatsApp.ts rename to packages/lib/whatsApp/sendChatReplyToWhatsApp.ts index 51b499ba60..b8d8cb5845 100644 --- a/apps/viewer/src/features/whatsApp/helpers/sendChatReplyToWhatsApp.ts +++ b/packages/lib/whatsApp/sendChatReplyToWhatsApp.ts @@ -11,10 +11,10 @@ import { import { convertMessageToWhatsAppMessage } from './convertMessageToWhatsAppMessage' import { sendWhatsAppMessage } from './sendWhatsAppMessage' import { captureException } from '@sentry/nextjs' -import { isNotDefined } from '@typebot.io/lib/utils' import { HTTPError } from 'got' -import { computeTypingDuration } from '@typebot.io/lib/computeTypingDuration' import { convertInputToWhatsAppMessages } from './convertInputToWhatsAppMessage' +import { isNotDefined } from '../utils' +import { computeTypingDuration } from '../computeTypingDuration' // Media can take some time to be delivered. This make sure we don't send a message before the media is delivered. const messageAfterMediaTimeout = 5000 diff --git a/apps/viewer/src/features/whatsApp/helpers/sendWhatsAppMessage.ts b/packages/lib/whatsApp/sendWhatsAppMessage.ts similarity index 100% rename from apps/viewer/src/features/whatsApp/helpers/sendWhatsAppMessage.ts rename to packages/lib/whatsApp/sendWhatsAppMessage.ts diff --git a/packages/schemas/features/whatsapp.ts b/packages/schemas/features/whatsapp.ts index 77b3ecd5b9..48860637a6 100644 --- a/packages/schemas/features/whatsapp.ts +++ b/packages/schemas/features/whatsapp.ts @@ -36,9 +36,9 @@ const actionSchema = z.object({ }) const templateSchema = z.object({ - name: z.literal('preview_initial_message'), + name: z.string(), language: z.object({ - code: z.literal('en'), + code: z.string(), }), }) @@ -151,9 +151,6 @@ export const whatsAppWebhookRequestBodySchema = z.object({ }) ) .optional(), - metadata: z.object({ - display_phone_number: z.string(), - }), messages: z.array(incomingMessageSchema).optional(), }), }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fedfd7abd..398f374803 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: '@typebot.io/nextjs': specifier: workspace:* version: link:../../packages/embeds/nextjs + '@typebot.io/viewer': + specifier: workspace:* + version: link:../viewer '@udecode/plate-basic-marks': specifier: 21.1.5 version: 21.1.5(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-react@0.94.2)(slate@0.94.1) @@ -1118,12 +1121,21 @@ importers: packages/lib: dependencies: + '@sentry/nextjs': + specifier: 7.66.0 + version: 7.66.0(next@13.4.3)(react@18.2.0) + '@udecode/plate-common': + specifier: ^21.1.5 + version: 21.1.5(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-react@0.94.2)(slate@0.94.1) got: specifier: 12.6.0 version: 12.6.0 minio: specifier: 7.1.3 version: 7.1.3 + remark-slate: + specifier: ^1.8.6 + version: 1.8.6 devDependencies: '@paralleldrive/cuid2': specifier: 2.2.1 From bb13c2bd61870a8da18bcf1a00dc85d79ec1f9c9 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 19 Sep 2023 15:42:33 +0200 Subject: [PATCH 008/233] :passport_control: (fileUpload) Improve file upload size limit enforcement Closes #799, closes #797 --- .../ImageUploadContent/UploadButton.tsx | 10 ++- .../billing/components/UsageProgressBars.tsx | 69 +------------------ .../components/FileInputSettings.tsx | 17 +---- .../results/components/UsageAlertBanners.tsx | 40 +---------- .../features/upload/api/generateUploadUrl.ts | 10 +-- .../src/pages/api/storage/upload-url.ts | 12 +++- .../docs/editor/blocks/inputs/file-upload.mdx | 4 +- apps/docs/docs/self-hosting/configuration.md | 25 +++---- .../fileUpload/api/deprecated/getUploadUrl.ts | 9 ++- .../fileUpload/api/generateUploadUrl.ts | 26 +++++-- .../inputs/fileUpload/fileUpload.spec.ts | 21 ------ .../src/features/chat/queries/upsertAnswer.ts | 25 +------ .../inputs/fileUpload/helpers/uploadFiles.ts | 12 +++- .../fileUpload/components/FileUploadForm.tsx | 14 ++-- .../inputs/fileUpload/helpers/uploadFiles.ts | 10 ++- packages/env/env.ts | 4 ++ .../lib/s3/generatePresignedPostPolicy.ts | 40 +++++++++++ packages/lib/s3/generatePresignedUrl.ts | 32 --------- .../schemas/features/blocks/inputs/file.ts | 2 +- 19 files changed, 143 insertions(+), 239 deletions(-) create mode 100644 packages/lib/s3/generatePresignedPostPolicy.ts delete mode 100644 packages/lib/s3/generatePresignedUrl.ts diff --git a/apps/builder/src/components/ImageUploadContent/UploadButton.tsx b/apps/builder/src/components/ImageUploadContent/UploadButton.tsx index 9eb63ad6c8..f73e00369e 100644 --- a/apps/builder/src/components/ImageUploadContent/UploadButton.tsx +++ b/apps/builder/src/components/ImageUploadContent/UploadButton.tsx @@ -26,9 +26,15 @@ export const UploadButton = ({ setIsUploading(false) }, onSuccess: async (data) => { + if (!file) return + const formData = new FormData() + Object.entries(data.formData).forEach(([key, value]) => { + formData.append(key, value) + }) + formData.append('file', file) const upload = await fetch(data.presignedUrl, { - method: 'PUT', - body: file, + method: 'POST', + body: formData, }) if (!upload.ok) { diff --git a/apps/builder/src/features/billing/components/UsageProgressBars.tsx b/apps/builder/src/features/billing/components/UsageProgressBars.tsx index 30b5c9fdc6..5388885c00 100644 --- a/apps/builder/src/features/billing/components/UsageProgressBars.tsx +++ b/apps/builder/src/features/billing/components/UsageProgressBars.tsx @@ -9,12 +9,11 @@ import { Tooltip, } from '@chakra-ui/react' import { AlertIcon } from '@/components/icons' -import { Plan, Workspace } from '@typebot.io/prisma' +import { Workspace } from '@typebot.io/prisma' import React from 'react' import { parseNumberWithCommas } from '@typebot.io/lib' -import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing' +import { getChatsLimit } from '@typebot.io/lib/pricing' import { defaultQueryOptions, trpc } from '@/lib/trpc' -import { storageToReadable } from '../helpers/storageToReadable' import { useScopedI18n } from '@/locales' type Props = { @@ -30,19 +29,12 @@ export const UsageProgressBars = ({ workspace }: Props) => { defaultQueryOptions ) const totalChatsUsed = data?.totalChatsUsed ?? 0 - const totalStorageUsed = data?.totalStorageUsed ?? 0 const workspaceChatsLimit = getChatsLimit(workspace) - const workspaceStorageLimit = getStorageLimit(workspace) - const workspaceStorageLimitGigabites = - workspaceStorageLimit * 1024 * 1024 * 1024 const chatsPercentage = Math.round( (totalChatsUsed / workspaceChatsLimit) * 100 ) - const storagePercentage = Math.round( - (totalStorageUsed / workspaceStorageLimitGigabites) * 100 - ) return ( @@ -103,63 +95,6 @@ export const UsageProgressBars = ({ workspace }: Props) => { colorScheme={totalChatsUsed >= workspaceChatsLimit ? 'red' : 'blue'} /> - {workspace.plan !== Plan.FREE && ( - - - - - {scopedT('storage.heading')} - - {storagePercentage >= 80 && ( - - {scopedT('storage.alert.soonReach')} -
-
- {scopedT('storage.alert.updatePlan')} - - } - > - - - -
- )} -
- - - {storageToReadable(totalStorageUsed)} - - - /{' '} - {workspaceStorageLimit === -1 - ? scopedT('unlimited') - : `${workspaceStorageLimit} GB`} - - -
- = workspaceStorageLimitGigabites - ? 'red' - : 'blue' - } - rounded="full" - hasStripe - isIndeterminate={isLoading} - /> -
- )} ) } diff --git a/apps/builder/src/features/blocks/inputs/fileUpload/components/FileInputSettings.tsx b/apps/builder/src/features/blocks/inputs/fileUpload/components/FileInputSettings.tsx index d68fa464c7..49e336c1de 100644 --- a/apps/builder/src/features/blocks/inputs/fileUpload/components/FileInputSettings.tsx +++ b/apps/builder/src/features/blocks/inputs/fileUpload/components/FileInputSettings.tsx @@ -1,8 +1,8 @@ -import { FormLabel, HStack, Stack, Text } from '@chakra-ui/react' +import { FormLabel, Stack } from '@chakra-ui/react' import { CodeEditor } from '@/components/inputs/CodeEditor' import { FileInputOptions, Variable } from '@typebot.io/schemas' import React from 'react' -import { TextInput, NumberInput } from '@/components/inputs' +import { TextInput } from '@/components/inputs' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput' @@ -24,9 +24,6 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => { const handleVariableChange = (variable?: Variable) => onOptionsChange({ ...options, variableId: variable?.id }) - const handleSizeLimitChange = (sizeLimit?: number) => - onOptionsChange({ ...options, sizeLimit }) - const handleRequiredChange = (isRequired: boolean) => onOptionsChange({ ...options, isRequired }) @@ -48,16 +45,6 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => { initialValue={options.isMultipleAllowed} onCheckChange={handleMultipleFilesChange} /> - - - MB - - Placeholder: { workspace?.plan, ]) - const storageLimitPercentage = useMemo(() => { - if (!usageData?.totalStorageUsed || !workspace?.plan) return 0 - return Math.round( - (usageData.totalStorageUsed / - 1024 / - 1024 / - 1024 / - getStorageLimit({ - additionalStorageIndex: workspace.additionalStorageIndex, - plan: workspace.plan, - customStorageLimit: workspace.customStorageLimit, - })) * - 100 - ) - }, [ - usageData?.totalStorageUsed, - workspace?.additionalStorageIndex, - workspace?.customStorageLimit, - workspace?.plan, - ]) - return ( <> {chatsLimitPercentage > ALERT_CHATS_PERCENT_THRESHOLD && ( @@ -74,22 +52,6 @@ export const UsageAlertBanners = ({ workspace }: Props) => { /> )} - {storageLimitPercentage > ALERT_STORAGE_PERCENT_THRESHOLD && ( - - - Your workspace collected{' '} - {storageLimitPercentage}% of your total storage - allowed. Upgrade your plan or delete some existing results to - continue collecting files from your user beyond this limit. - - } - buttonLabel="Upgrade" - /> - - )} ) } diff --git a/apps/builder/src/features/upload/api/generateUploadUrl.ts b/apps/builder/src/features/upload/api/generateUploadUrl.ts index c03d40a462..f568340df3 100644 --- a/apps/builder/src/features/upload/api/generateUploadUrl.ts +++ b/apps/builder/src/features/upload/api/generateUploadUrl.ts @@ -2,7 +2,7 @@ import { authenticatedProcedure } from '@/helpers/server/trpc' import { z } from 'zod' import { env } from '@typebot.io/env' import { TRPCError } from '@trpc/server' -import { generatePresignedUrl } from '@typebot.io/lib/s3/generatePresignedUrl' +import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy' import prisma from '@/lib/prisma' import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden' import { isWriteTypebotForbidden } from '@/features/typebot/helpers/isWriteTypebotForbidden' @@ -54,6 +54,7 @@ export const generateUploadUrl = authenticatedProcedure .output( z.object({ presignedUrl: z.string(), + formData: z.record(z.string(), z.any()), fileUrl: z.string(), }) ) @@ -76,16 +77,17 @@ export const generateUploadUrl = authenticatedProcedure uploadProps: filePathProps, }) - const presignedUrl = await generatePresignedUrl({ + const presignedPostPolicy = await generatePresignedPostPolicy({ fileType, filePath, }) return { - presignedUrl, + presignedUrl: presignedPostPolicy.postURL, + formData: presignedPostPolicy.formData, fileUrl: env.S3_PUBLIC_CUSTOM_DOMAIN ? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}` - : presignedUrl.split('?')[0], + : `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`, } }) diff --git a/apps/builder/src/pages/api/storage/upload-url.ts b/apps/builder/src/pages/api/storage/upload-url.ts index a6cce8897c..49fbe854cc 100644 --- a/apps/builder/src/pages/api/storage/upload-url.ts +++ b/apps/builder/src/pages/api/storage/upload-url.ts @@ -5,7 +5,7 @@ import { methodNotAllowed, notAuthenticated, } from '@typebot.io/lib/api' -import { generatePresignedUrl } from '@typebot.io/lib/s3/generatePresignedUrl' +import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy' import { env } from '@typebot.io/env' const handler = async ( @@ -25,9 +25,15 @@ const handler = async ( const filePath = req.query.filePath as string | undefined const fileType = req.query.fileType as string | undefined if (!filePath || !fileType) return badRequest(res) - const presignedUrl = await generatePresignedUrl({ fileType, filePath }) + const presignedPostPolicy = await generatePresignedPostPolicy({ + fileType, + filePath, + }) - return res.status(200).send({ presignedUrl }) + return res.status(200).send({ + presignedUrl: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`, + formData: presignedPostPolicy.formData, + }) } return methodNotAllowed(res) } diff --git a/apps/docs/docs/editor/blocks/inputs/file-upload.mdx b/apps/docs/docs/editor/blocks/inputs/file-upload.mdx index 2f1df61067..3730255b56 100644 --- a/apps/docs/docs/editor/blocks/inputs/file-upload.mdx +++ b/apps/docs/docs/editor/blocks/inputs/file-upload.mdx @@ -29,4 +29,6 @@ The File upload input block allows you to collect files from your user. The placeholder accepts [HTML](https://en.wikipedia.org/wiki/HTML). -Note that there is a 10MB fixed limit per file. +## Size limit + +There is a 10MB fixed limit per uploaded file. If you want your respondents to upload larger files, you should ask them to upload their files to a cloud storage service (e.g. Google Drive, Dropbox, etc.) and share the link with you. diff --git a/apps/docs/docs/self-hosting/configuration.md b/apps/docs/docs/self-hosting/configuration.md index 4bfed6133f..819b2fcb0a 100644 --- a/apps/docs/docs/self-hosting/configuration.md +++ b/apps/docs/docs/self-hosting/configuration.md @@ -11,18 +11,19 @@ Parameters marked with are required. ## General -| Parameter | Default | Description | -| --------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| DATABASE_URL | | The database URL | -| ENCRYPTION_SECRET | | A 256-bit key used to encrypt sensitive data. It is strongly recommended to [generate](https://www.allkeysgenerator.com/Random/Security-Encryption-Key-Generator.aspx) a new one. The secret should be the same between builder and viewer. | -| NEXTAUTH_URL | | The builder base URL. Should be the publicly accessible URL (i.e. `https://typebot.domain.com`) | -| NEXT_PUBLIC_VIEWER_URL | | The viewer base URL. Should be the publicly accessible URL (i.e. `https://bot.domain.com`) | -| ADMIN_EMAIL | | The email that will get an `UNLIMITED` plan on user creation. The associated user will be able to bypass database rules. | -| NEXTAUTH_URL_INTERNAL | | The internal builder base URL. You have to set it only when `NEXTAUTH_URL` can't be reached by your builder container / server. For a docker deployment, you should set it to `http://localhost:3000`. | -| DEFAULT_WORKSPACE_PLAN | FREE | Default workspace plan on user creation or when a user creates a new workspace. Possible values are `FREE`, `STARTER`, `PRO`, `LIFETIME`, `UNLIMITED`. The default plan for admin user is `UNLIMITED` | -| DISABLE_SIGNUP | false | Disable new user sign ups. Invited users are still able to sign up. | -| NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID | | Typebot ID used for the onboarding. Onboarding page is skipped if not provided. | -| DEBUG | false | If enabled, the server will print valuable logs to debug config issues. | +| Parameter | Default | Description | +| ------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| DATABASE_URL | | The database URL | +| ENCRYPTION_SECRET | | A 256-bit key used to encrypt sensitive data. It is strongly recommended to [generate](https://www.allkeysgenerator.com/Random/Security-Encryption-Key-Generator.aspx) a new one. The secret should be the same between builder and viewer. | +| NEXTAUTH_URL | | The builder base URL. Should be the publicly accessible URL (i.e. `https://typebot.domain.com`) | +| NEXT_PUBLIC_VIEWER_URL | | The viewer base URL. Should be the publicly accessible URL (i.e. `https://bot.domain.com`) | +| ADMIN_EMAIL | | The email that will get an `UNLIMITED` plan on user creation. The associated user will be able to bypass database rules. | +| NEXTAUTH_URL_INTERNAL | | The internal builder base URL. You have to set it only when `NEXTAUTH_URL` can't be reached by your builder container / server. For a docker deployment, you should set it to `http://localhost:3000`. | +| DEFAULT_WORKSPACE_PLAN | FREE | Default workspace plan on user creation or when a user creates a new workspace. Possible values are `FREE`, `STARTER`, `PRO`, `LIFETIME`, `UNLIMITED`. The default plan for admin user is `UNLIMITED` | +| DISABLE_SIGNUP | false | Disable new user sign ups. Invited users are still able to sign up. | +| NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID | | Typebot ID used for the onboarding. Onboarding page is skipped if not provided. | +| DEBUG | false | If enabled, the server will print valuable logs to debug config issues. | +| NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE | | Limits the size of each file that can be uploaded in the bots (i.e. Set `10` to limit the file upload to 10MB) | ## Email (Auth, notifications) diff --git a/apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts b/apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts index 0599975157..b9b11582ca 100644 --- a/apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts +++ b/apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts @@ -10,7 +10,7 @@ import { } from '@typebot.io/schemas' import { byId, isDefined } from '@typebot.io/lib' import { z } from 'zod' -import { generatePresignedUrl } from '@typebot.io/lib/s3/generatePresignedUrl' +import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy' import { env } from '@typebot.io/env' export const getUploadUrl = publicProcedure @@ -34,6 +34,7 @@ export const getUploadUrl = publicProcedure .output( z.object({ presignedUrl: z.string(), + formData: z.record(z.string(), z.any()), hasReachedStorageLimit: z.boolean(), }) ) @@ -61,13 +62,15 @@ export const getUploadUrl = publicProcedure message: 'File upload block not found', }) - const presignedUrl = await generatePresignedUrl({ + const presignedPostPolicy = await generatePresignedPostPolicy({ fileType, filePath, + maxFileSize: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE, }) return { - presignedUrl, + presignedUrl: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`, + formData: presignedPostPolicy.formData, hasReachedStorageLimit: false, } }) diff --git a/apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts b/apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts index 1c66a0bb61..175f679392 100644 --- a/apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts +++ b/apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts @@ -2,8 +2,9 @@ import { publicProcedure } from '@/helpers/server/trpc' import prisma from '@/lib/prisma' import { TRPCError } from '@trpc/server' import { z } from 'zod' -import { generatePresignedUrl } from '@typebot.io/lib/s3/generatePresignedUrl' +import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy' import { env } from '@typebot.io/env' +import { InputBlockType, publicTypebotSchema } from '@typebot.io/schemas' export const generateUploadUrl = publicProcedure .meta({ @@ -28,6 +29,7 @@ export const generateUploadUrl = publicProcedure .output( z.object({ presignedUrl: z.string(), + formData: z.record(z.string(), z.any()), fileUrl: z.string(), }) ) @@ -44,6 +46,7 @@ export const generateUploadUrl = publicProcedure typebotId: filePathProps.typebotId, }, select: { + groups: true, typebot: { select: { workspaceId: true, @@ -62,15 +65,30 @@ export const generateUploadUrl = publicProcedure const filePath = `public/workspaces/${workspaceId}/typebots/${filePathProps.typebotId}/results/${filePathProps.resultId}/${filePathProps.fileName}` - const presignedUrl = await generatePresignedUrl({ + const fileUploadBlock = publicTypebotSchema._def.schema.shape.groups + .parse(publicTypebot.groups) + .flatMap((group) => group.blocks) + .find((block) => block.id === filePathProps.blockId) + + if (fileUploadBlock?.type !== InputBlockType.FILE) + throw new TRPCError({ + code: 'BAD_REQUEST', + message: "Can't find file upload block", + }) + + const presignedPostPolicy = await generatePresignedPostPolicy({ fileType, filePath, + maxFileSize: + fileUploadBlock.options.sizeLimit ?? + env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE, }) return { - presignedUrl, + presignedUrl: presignedPostPolicy.postURL, + formData: presignedPostPolicy.formData, fileUrl: env.S3_PUBLIC_CUSTOM_DOMAIN ? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}` - : presignedUrl.split('?')[0], + : `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`, } }) diff --git a/apps/viewer/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts b/apps/viewer/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts index c8db4ad584..0eb4d8a1bd 100644 --- a/apps/viewer/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts +++ b/apps/viewer/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts @@ -93,25 +93,4 @@ test.describe('Storage limit is reached', () => { fakeStorage: THREE_GIGABYTES, }) }) - - test("shouldn't upload anything if limit has been reached", async ({ - page, - }) => { - await page.goto(`/${typebotId}-public`) - await page - .locator(`input[type="file"]`) - .setInputFiles([ - getTestAsset('typebots/api.json'), - getTestAsset('typebots/fileUpload.json'), - getTestAsset('typebots/hugeGroup.json'), - ]) - await expect(page.locator(`text="3"`)).toBeVisible() - await page.locator('text="Upload 3 files"').click() - await expect(page.locator(`text="3 files uploaded"`)).toBeVisible() - await page.evaluate(() => - window.localStorage.setItem('workspaceId', 'starterWorkspace') - ) - await page.goto(`${env.NEXTAUTH_URL}/typebots/${typebotId}/results`) - await expect(page.locator('text="150%"')).toBeVisible() - }) }) diff --git a/apps/viewer/src/features/chat/queries/upsertAnswer.ts b/apps/viewer/src/features/chat/queries/upsertAnswer.ts index 76a5c8ba33..49a359a938 100644 --- a/apps/viewer/src/features/chat/queries/upsertAnswer.ts +++ b/apps/viewer/src/features/chat/queries/upsertAnswer.ts @@ -1,8 +1,6 @@ import prisma from '@/lib/prisma' -import { isNotDefined } from '@typebot.io/lib' import { Prisma } from '@typebot.io/prisma' -import { InputBlock, InputBlockType, SessionState } from '@typebot.io/schemas' -import got from 'got' +import { InputBlock, SessionState } from '@typebot.io/schemas' type Props = { answer: Omit @@ -11,12 +9,9 @@ type Props = { itemId?: string state: SessionState } -export const upsertAnswer = async ({ answer, reply, block, state }: Props) => { +export const upsertAnswer = async ({ answer, block, state }: Props) => { const resultId = state.typebotsQueue[0].resultId if (!resultId) return - if (reply.includes('http') && block.type === InputBlockType.FILE) { - answer.storageUsed = await computeStorageUsed(reply) - } const where = { resultId, blockId: block.id, @@ -33,7 +28,6 @@ export const upsertAnswer = async ({ answer, reply, block, state }: Props) => { where, data: { content: answer.content, - storageUsed: answer.storageUsed, itemId: answer.itemId, }, }) @@ -41,18 +35,3 @@ export const upsertAnswer = async ({ answer, reply, block, state }: Props) => { data: [{ ...answer, resultId }], }) } - -const computeStorageUsed = async (reply: string) => { - let storageUsed = 0 - const fileUrls = reply.split(', ') - const hasReachedStorageLimit = fileUrls[0] === null - if (!hasReachedStorageLimit) { - for (const url of fileUrls) { - const { headers } = await got(url) - const size = headers['content-length'] - if (isNotDefined(size)) continue - storageUsed += parseInt(size, 10) - } - } - return storageUsed -} diff --git a/packages/deprecated/bot-engine/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts b/packages/deprecated/bot-engine/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts index f793aa6d9e..682f5cff35 100644 --- a/packages/deprecated/bot-engine/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts +++ b/packages/deprecated/bot-engine/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts @@ -23,6 +23,7 @@ export const uploadFiles = async ({ i += 1 const { data } = await sendRequest<{ presignedUrl: string + formData: Record hasReachedStorageLimit: boolean }>( `${basePath}/storage/upload-url?filePath=${encodeURIComponent( @@ -35,9 +36,14 @@ export const uploadFiles = async ({ const url = data.presignedUrl if (data.hasReachedStorageLimit) urls.push(null) else { - const upload = await fetch(url, { - method: 'PUT', - body: file, + const formData = new FormData() + Object.entries(data.formData).forEach(([key, value]) => { + formData.append(key, value) + }) + formData.append('file', file) + const upload = await fetch(data.presignedUrl, { + method: 'POST', + body: formData, }) if (!upload.ok) continue diff --git a/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx b/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx index 455848d760..3bcd29e056 100644 --- a/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx +++ b/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx @@ -7,6 +7,7 @@ import { Button } from '@/components/Button' import { Spinner } from '@/components/Spinner' import { uploadFiles } from '../helpers/uploadFiles' import { guessApiHost } from '@/utils/guessApiHost' +import { getRuntimeVariable } from '@typebot.io/env/getRuntimeVariable' type Props = { context: BotContext @@ -25,15 +26,14 @@ export const FileUploadForm = (props: Props) => { const onNewFiles = (files: FileList) => { setErrorMessage(undefined) const newFiles = Array.from(files) + const sizeLimit = + props.block.options.sizeLimit ?? + getRuntimeVariable('NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE') if ( - newFiles.some( - (file) => - file.size > (props.block.options.sizeLimit ?? 10) * 1024 * 1024 - ) + sizeLimit && + newFiles.some((file) => file.size > sizeLimit * 1024 * 1024) ) - return setErrorMessage( - `A file is larger than ${props.block.options.sizeLimit ?? 10}MB` - ) + return setErrorMessage(`A file is larger than ${sizeLimit}MB`) if (!props.block.options.isMultipleAllowed && files) return startSingleFileUpload(newFiles[0]) setSelectedFiles([...selectedFiles(), ...newFiles]) diff --git a/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts b/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts index c0e5af889a..2792739c64 100644 --- a/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts +++ b/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts @@ -28,6 +28,7 @@ export const uploadFiles = async ({ i += 1 const { data } = await sendRequest<{ presignedUrl: string + formData: Record fileUrl: string }>({ method: 'POST', @@ -40,9 +41,14 @@ export const uploadFiles = async ({ if (!data?.presignedUrl) continue else { + const formData = new FormData() + Object.entries(data.formData).forEach(([key, value]) => { + formData.append(key, value) + }) + formData.append('file', file) const upload = await fetch(data.presignedUrl, { - method: 'PUT', - body: file, + method: 'POST', + body: formData, }) if (!upload.ok) continue diff --git a/packages/env/env.ts b/packages/env/env.ts index 6f6abfdc96..072db6d20c 100644 --- a/packages/env/env.ts +++ b/packages/env/env.ts @@ -35,6 +35,7 @@ const baseEnv = { .transform((string) => string.split(',')), NEXT_PUBLIC_VIEWER_INTERNAL_URL: z.string().url().optional(), NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID: z.string().min(1).optional(), + NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE: z.coerce.number().optional(), }, runtimeEnv: { NEXT_PUBLIC_E2E_TEST: getRuntimeVariable('NEXT_PUBLIC_E2E_TEST'), @@ -45,6 +46,9 @@ const baseEnv = { NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID: getRuntimeVariable( 'NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID' ), + NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE: getRuntimeVariable( + 'NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE' + ), }, } const githubEnv = { diff --git a/packages/lib/s3/generatePresignedPostPolicy.ts b/packages/lib/s3/generatePresignedPostPolicy.ts new file mode 100644 index 0000000000..e514fefc34 --- /dev/null +++ b/packages/lib/s3/generatePresignedPostPolicy.ts @@ -0,0 +1,40 @@ +import { env } from '@typebot.io/env' +import { Client, PostPolicyResult } from 'minio' + +type Props = { + filePath: string + fileType?: string + maxFileSize?: number +} + +const tenMinutes = 10 * 60 + +export const generatePresignedPostPolicy = async ({ + filePath, + fileType, + maxFileSize, +}: Props): Promise => { + if (!env.S3_ENDPOINT || !env.S3_ACCESS_KEY || !env.S3_SECRET_KEY) + throw new Error( + 'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY' + ) + + const minioClient = new Client({ + endPoint: env.S3_ENDPOINT, + port: env.S3_PORT, + useSSL: env.S3_SSL, + accessKey: env.S3_ACCESS_KEY, + secretKey: env.S3_SECRET_KEY, + region: env.S3_REGION, + }) + + const postPolicy = minioClient.newPostPolicy() + if (maxFileSize) + postPolicy.setContentLengthRange(0, maxFileSize * 1024 * 1024) + postPolicy.setKey(filePath) + postPolicy.setBucket(env.S3_BUCKET) + postPolicy.setExpires(new Date(Date.now() + tenMinutes)) + if (fileType) postPolicy.setContentType(fileType) + + return minioClient.presignedPostPolicy(postPolicy) +} diff --git a/packages/lib/s3/generatePresignedUrl.ts b/packages/lib/s3/generatePresignedUrl.ts deleted file mode 100644 index 2003cf8057..0000000000 --- a/packages/lib/s3/generatePresignedUrl.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { env } from '@typebot.io/env' -import { Client } from 'minio' - -type GeneratePresignedUrlProps = { - filePath: string - fileType?: string -} - -const tenMinutes = 10 * 60 - -export const generatePresignedUrl = async ({ - filePath, - fileType, -}: GeneratePresignedUrlProps): Promise => { - if (!env.S3_ENDPOINT || !env.S3_ACCESS_KEY || !env.S3_SECRET_KEY) - throw new Error( - 'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY' - ) - - const minioClient = new Client({ - endPoint: env.S3_ENDPOINT, - port: env.S3_PORT, - useSSL: env.S3_SSL, - accessKey: env.S3_ACCESS_KEY, - secretKey: env.S3_SECRET_KEY, - region: env.S3_REGION, - }) - - return minioClient.presignedUrl('PUT', env.S3_BUCKET, filePath, tenMinutes, { - 'Content-Type': fileType, - }) -} diff --git a/packages/schemas/features/blocks/inputs/file.ts b/packages/schemas/features/blocks/inputs/file.ts index 46a455834d..5fd624670b 100644 --- a/packages/schemas/features/blocks/inputs/file.ts +++ b/packages/schemas/features/blocks/inputs/file.ts @@ -12,7 +12,7 @@ export const fileInputOptionsSchema = optionBaseSchema.merge( clear: z.string().optional(), skip: z.string().optional(), }), - sizeLimit: z.number().optional(), + sizeLimit: z.number().optional().describe('Deprecated'), }) ) From 797685aa9d3de68afedc7515aaaeb6ef972d6b7c Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Wed, 20 Sep 2023 10:48:24 +0200 Subject: [PATCH 009/233] :pencil: Change googleSheets date system var name --- apps/docs/docs/editor/blocks/integrations/google-sheets.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/docs/editor/blocks/integrations/google-sheets.mdx b/apps/docs/docs/editor/blocks/integrations/google-sheets.mdx index 74ca67d4c9..2555d12d09 100644 --- a/apps/docs/docs/editor/blocks/integrations/google-sheets.mdx +++ b/apps/docs/docs/editor/blocks/integrations/google-sheets.mdx @@ -16,7 +16,7 @@ In order to properly work, your spreadsheet must have its first row as a header ## How to add the submission date to my row? -For this, you will need to set a new variable with the value "Today" before the Google Sheets block. Then you can simply use this variable in the Google Sheets block. +For this, you will need to set a new variable with the value "Now" before the Google Sheets block. Then you can simply use this variable in the Google Sheets block. ## Advanced From 7d57e8dd065c01b90c4eb03ccfdbc3b73da85c23 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Wed, 20 Sep 2023 15:26:52 +0200 Subject: [PATCH 010/233] :recycle: Export bot-engine code into its own package --- .gitignore | 1 + apps/builder/package.json | 6 +- .../analytics/api/getTotalAnswersInBlocks.ts | 2 +- .../auth/helpers/getAuthenticatedUser.ts | 2 +- .../billing/api/createCheckoutSession.ts | 2 +- .../api/createCustomCheckoutSession.ts | 2 +- .../billing/api/getBillingPortalUrl.ts | 2 +- .../features/billing/api/getSubscription.ts | 2 +- .../src/features/billing/api/getUsage.ts | 2 +- .../src/features/billing/api/listInvoices.ts | 2 +- .../billing/api/updateSubscription.ts | 2 +- .../src/features/billing/billing.spec.ts | 15 -- .../inputs/fileUpload/fileUpload.spec.ts | 1 - .../integrations/openai/api/listModels.ts | 2 +- .../webhook/api/getResultExample.ts | 2 +- .../webhook/api/listWebhookBlocks.ts | 2 +- .../webhook/api/subscribeWebhook.ts | 2 +- .../webhook/api/unsubscribeWebhook.ts | 2 +- .../zemanticAi/api/listProjects.ts | 2 +- .../typebotLink/api/getLinkedTypebots.ts | 2 +- .../helpers/fetchLinkedTypebots.ts | 2 +- .../collaboration/api/getCollaborators.ts | 2 +- .../collaboration/collaboration.spec.ts | 2 +- .../credentials/api/createCredentials.ts | 2 +- .../credentials/api/deleteCredentials.ts | 2 +- .../credentials/api/listCredentials.ts | 2 +- .../customDomains/api/createCustomDomain.ts | 2 +- .../customDomains/api/deleteCustomDomain.ts | 2 +- .../customDomains/api/listCustomDomains.ts | 2 +- .../customDomains/api/verifyCustomDomain.ts | 2 +- .../src/features/results/api/deleteResults.ts | 2 +- .../src/features/results/api/getResult.ts | 2 +- .../src/features/results/api/getResultLogs.ts | 2 +- .../src/features/results/api/getResults.ts | 2 +- .../features/theme/api/deleteThemeTemplate.ts | 2 +- .../features/theme/api/listThemeTemplates.ts | 2 +- .../features/theme/api/saveThemeTemplate.ts | 2 +- .../src/features/typebot/api/createTypebot.ts | 2 +- .../src/features/typebot/api/deleteTypebot.ts | 2 +- .../typebot/api/getPublishedTypebot.ts | 2 +- .../src/features/typebot/api/getTypebot.ts | 2 +- .../src/features/typebot/api/listTypebots.ts | 2 +- .../features/typebot/api/publishTypebot.ts | 2 +- .../features/typebot/api/unpublishTypebot.ts | 2 +- .../src/features/typebot/api/updateTypebot.ts | 2 +- .../typebot/helpers/isReadTypebotForbidden.ts | 2 +- .../helpers/isWriteTypebotForbidden.ts | 2 +- .../features/typebot/helpers/sanitizers.ts | 2 +- .../features/upload/api/generateUploadUrl.ts | 2 +- .../whatsapp/generateVerificationToken.ts | 2 +- .../src/features/whatsapp/getPhoneNumber.ts | 2 +- .../features/whatsapp/getSystemTokenInfo.ts | 2 +- .../whatsapp/receiveMessagePreview.ts | 2 +- .../features/whatsapp/startWhatsAppPreview.ts | 12 +- .../whatsapp/verifyIfPhoneNumberAvailable.ts | 2 +- .../features/workspace/api/createWorkspace.ts | 2 +- .../features/workspace/api/deleteWorkspace.ts | 2 +- .../features/workspace/api/getWorkspace.ts | 2 +- .../api/listInvitationsInWorkspace.ts | 2 +- .../workspace/api/listMembersInWorkspace.ts | 2 +- .../features/workspace/api/listWorkspaces.ts | 2 +- .../features/workspace/api/updateWorkspace.ts | 2 +- apps/builder/src/helpers/databaseRules.ts | 2 +- apps/builder/src/lib/googleSheets.ts | 2 +- apps/builder/src/lib/prisma.ts | 22 -- .../src/pages/api/auth/[...nextauth].ts | 2 +- apps/builder/src/pages/api/credentials.ts | 2 +- .../pages/api/credentials/[credentialsId].ts | 2 +- .../api/credentials/google-sheets/callback.ts | 2 +- .../src/pages/api/customDomains/[domain].ts | 2 +- apps/builder/src/pages/api/folders.ts | 2 +- apps/builder/src/pages/api/folders/[id].ts | 2 +- .../src/pages/api/publicIdAvailable.ts | 2 +- apps/builder/src/pages/api/publicTypebots.ts | 2 +- .../src/pages/api/publicTypebots/[id].ts | 2 +- apps/builder/src/pages/api/stripe/webhook.ts | 2 +- .../typebots/[typebotId]/analytics/stats.ts | 2 +- .../pages/api/typebots/[typebotId]/blocks.ts | 2 +- .../api/typebots/[typebotId]/collaborators.ts | 2 +- .../[typebotId]/collaborators/[userId].ts | 2 +- .../api/typebots/[typebotId]/invitations.ts | 2 +- .../[typebotId]/invitations/[email].ts | 2 +- apps/builder/src/pages/api/users/[userId].ts | 2 +- .../pages/api/users/[userId]/api-tokens.ts | 2 +- .../users/[userId]/api-tokens/[tokenId].ts | 2 +- .../workspaces/[workspaceId]/invitations.ts | 2 +- .../[workspaceId]/invitations/[id].ts | 2 +- .../api/workspaces/[workspaceId]/members.ts | 2 +- .../workspaces/[workspaceId]/members/[id].ts | 2 +- apps/builder/tsconfig.json | 2 +- apps/docs/openapi/builder/_spec_.json | 26 +- apps/docs/openapi/chat/_spec_.json | 26 +- apps/viewer/package.json | 12 +- .../helpers/getAuthenticatedGoogleDoc.ts | 24 -- .../logic/typebotLink/fetchLinkedTypebots.ts | 34 --- .../src/features/chat/api/sendMessage.ts | 12 +- .../chat/api/updateTypebotInSession.ts | 4 +- .../fileUpload/api/deprecated/getUploadUrl.ts | 2 +- .../fileUpload/api/generateUploadUrl.ts | 2 +- .../api/receiveMessage.ts | 2 +- .../{whatsApp => whatsapp}/api/router.ts | 0 .../api/subscribeWebhook.ts | 2 +- apps/viewer/src/helpers/api/dbRules.ts | 58 ----- apps/viewer/src/helpers/api/isVercel.ts | 3 - apps/viewer/src/helpers/authenticateUser.ts | 2 +- apps/viewer/src/helpers/server/context.ts | 2 +- .../src/helpers/server/routers/v1/_app.ts | 6 +- apps/viewer/src/lib/google-sheets.ts | 2 +- apps/viewer/src/pages/[[...publicId]].tsx | 2 +- .../[spreadsheetId]/sheets/[sheetId].ts | 4 +- .../pages/api/integrations/openai/streamer.ts | 2 +- .../stripe/createPaymentIntent.ts | 4 +- .../pages/api/publicTypebots/[typebotId].ts | 2 +- apps/viewer/src/pages/api/typebots.ts | 2 +- .../blocks/[blockId]/executeWebhook.ts | 18 +- .../[typebotId]/integrations/email.tsx | 6 +- .../pages/api/typebots/[typebotId]/results.ts | 2 +- .../[typebotId]/results/[resultId].ts | 2 +- .../[typebotId]/results/[resultId]/answers.ts | 2 +- .../api/typebots/[typebotId]/webhookBlocks.ts | 2 +- .../api/typebots/[typebotId]/webhookSteps.ts | 2 +- apps/viewer/src/pages/old/[[...publicId]].tsx | 2 +- .../src/{features/chat => test}/chat.spec.ts | 2 +- .../chatwoot => test}/chatwoot.spec.ts | 0 .../fileUpload => test}/fileUpload.spec.ts | 0 .../results => test}/results.spec.ts | 0 .../sendEmail => test}/sendEmail.spec.ts | 2 +- .../settings => test}/settings.spec.ts | 0 .../typebotLink => test}/typebotLink.spec.ts | 0 .../variables => test}/variables.spec.ts | 0 .../webhook => test}/webhook.spec.ts | 0 apps/viewer/src/trpc/generateOpenApi.ts | 15 ++ .../bot-engine}/addEdgeToTypebot.ts | 0 .../inputs/buttons/filterChoiceItems.ts | 0 ...injectVariableValuesInButtonsInputBlock.ts | 8 +- .../inputs/buttons/parseButtonsReply.ts | 2 +- .../blocks/inputs/date/parseDateInput.ts | 6 +- .../blocks/inputs/date/parseDateReply.ts | 2 +- .../blocks/inputs/email/validateEmail.ts | 0 .../blocks/inputs/number/validateNumber.ts | 0 .../computePaymentInputRuntimeOptions.ts | 4 +- .../blocks/inputs/phone/formatPhoneNumber.ts | 0 .../pictureChoice/filterPictureChoiceItems.ts | 0 ...njectVariableValuesInPictureChoiceBlock.ts | 2 +- .../pictureChoice/parsePictureChoicesReply.ts | 2 +- .../inputs/rating/validateRatingReply.ts | 0 .../blocks/inputs/url/validateUrl.ts | 0 .../chatwoot/executeChatwootBlock.ts | 8 +- .../executeGoogleAnalyticsBlock.ts | 4 +- .../googleSheets/executeGoogleSheetBlock.ts | 2 +- .../integrations/googleSheets/getRow.ts | 8 +- .../helpers/getAuthenticatedGoogleDoc.ts | 75 ++++++ .../googleSheets/helpers/matchFilter.ts | 0 .../googleSheets/helpers/parseCellValues.ts | 2 +- .../integrations/googleSheets/insertRow.ts | 2 +- .../integrations/googleSheets/updateRow.ts | 4 +- .../openai/createChatCompletionOpenAI.ts | 14 +- .../executeChatCompletionOpenAIRequest.ts | 0 .../integrations/openai/executeOpenAIBlock.ts | 2 +- .../openai/getChatCompletionStream.ts | 2 +- .../openai/parseChatCompletionMessages.ts | 4 +- .../openai/resumeChatCompletion.ts | 4 +- .../integrations/pixel/executePixelBlock.ts | 4 +- .../integrations/sendEmail/constants.ts | 0 .../sendEmail/executeSendEmailBlock.tsx | 9 +- .../webhook/executeWebhookBlock.ts | 6 +- .../integrations/webhook/parseSampleResult.ts | 0 .../webhook/resumeWebhookExecution.ts | 8 +- .../zemanticAi/executeZemanticAiBlock.ts | 10 +- .../blocks/logic/abTest/executeAbTest.ts | 2 +- .../logic/condition/executeCondition.ts | 4 +- .../logic/condition/executeConditionBlock.ts | 2 +- .../blocks/logic/jump/executeJumpBlock.ts | 7 +- .../blocks/logic/redirect/executeRedirect.ts | 4 +- .../blocks/logic/script/executeScript.ts | 8 +- .../logic/setVariable/executeSetVariable.ts | 10 +- .../logic/typebotLink/executeTypebotLink.ts | 11 +- .../logic/typebotLink/fetchLinkedTypebots.ts | 45 ++++ .../getPreviouslyLinkedTypebots.ts | 0 .../blocks/logic/wait/executeWait.ts | 4 +- .../computeTypingDuration.ts | 0 .../bot-engine}/continueBotFlow.ts | 32 +-- .../bot-engine}/executeGroup.ts | 12 +- .../bot-engine}/executeIntegration.ts | 18 +- .../bot-engine}/executeLogic.ts | 18 +- .../bot-engine}/getNextGroup.ts | 5 +- .../bot-engine}/getPrefilledValue.ts | 0 .../logs/helpers/formatLogDetails.ts | 0 .../bot-engine}/logs/saveErrorLog.ts | 0 .../bot-engine}/logs/saveLog.ts | 2 +- .../bot-engine}/logs/saveSuccessLog.ts | 0 packages/bot-engine/package.json | 38 +++ .../bot-engine}/parseDynamicTheme.ts | 2 +- .../queries/createResultIfNotExist.ts | 2 +- .../bot-engine}/queries/createSession.ts | 2 +- .../bot-engine}/queries/deleteSession.ts | 2 +- .../bot-engine}/queries/findPublicTypebot.ts | 2 +- .../bot-engine}/queries/findResult.ts | 2 +- .../bot-engine}/queries/findTypebot.ts | 2 +- .../bot-engine}/queries/getSession.ts | 2 +- .../bot-engine}/queries/restartSession.ts | 2 +- .../bot-engine}/queries/saveLogs.ts | 2 +- .../bot-engine}/queries/updateSession.ts | 2 +- .../bot-engine}/queries/upsertAnswer.ts | 2 +- .../bot-engine}/queries/upsertResult.ts | 2 +- .../bot-engine}/saveStateToDatabase.ts | 12 +- .../bot-engine}/startBotFlow.ts | 0 .../bot-engine}/startSession.ts | 16 +- packages/bot-engine/tsconfig.json | 9 + .../chat => packages/bot-engine}/types.ts | 0 .../variables/deepParseVariables.ts | 0 .../variables/extractVariablesFromText.ts | 0 .../variables/findUniqueVariableValue.ts | 0 .../bot-engine}/variables/hasVariable.ts | 0 .../injectVariablesFromExistingResult.ts | 0 .../variables/parseGuessedTypeFromString.ts | 0 .../variables/parseGuessedValueType.ts | 0 .../variables/parseVariableNumber.ts | 0 .../bot-engine}/variables/parseVariables.ts | 4 +- .../bot-engine}/variables/prefillVariables.ts | 2 +- .../variables/transformVariablesToList.ts | 2 +- .../variables/updateVariablesInSession.ts | 4 +- .../convertInputToWhatsAppMessage.ts | 2 +- .../convertMessageToWhatsAppMessage.ts | 2 +- .../convertRichTextToWhatsAppText.ts | 0 .../bot-engine/whatsapp}/downloadMedia.ts | 0 .../whatsapp}/resumeWhatsAppFlow.ts | 14 +- .../whatsapp}/sendChatReplyToWhatsApp.ts | 2 +- .../whatsapp}/sendWhatsAppMessage.ts | 0 .../whatsapp}/startWhatsAppSession.ts | 6 +- packages/emails/src/index.ts | 1 + packages/embeds/js/package.json | 3 +- .../textBubble/components/TextBubble.tsx | 2 +- packages/embeds/js/tsconfig.json | 1 + .../api => packages/lib}/isPlanetScale.ts | 0 packages/lib/package.json | 1 + {apps/viewer/src => packages}/lib/prisma.ts | 0 packages/lib/tsconfig.json | 2 +- packages/prisma/tsconfig.json | 5 +- packages/tsconfig/base.json | 7 +- packages/tsconfig/nextjs.json | 3 - pnpm-lock.yaml | 225 ++++++++---------- 242 files changed, 644 insertions(+), 638 deletions(-) delete mode 100644 apps/builder/src/lib/prisma.ts delete mode 100644 apps/viewer/src/features/blocks/integrations/googleSheets/helpers/getAuthenticatedGoogleDoc.ts delete mode 100644 apps/viewer/src/features/blocks/logic/typebotLink/fetchLinkedTypebots.ts rename apps/viewer/src/features/{blocks/inputs => }/fileUpload/api/deprecated/getUploadUrl.ts (98%) rename apps/viewer/src/features/{blocks/inputs => }/fileUpload/api/generateUploadUrl.ts (98%) rename apps/viewer/src/features/{whatsApp => whatsapp}/api/receiveMessage.ts (93%) rename apps/viewer/src/features/{whatsApp => whatsapp}/api/router.ts (100%) rename apps/viewer/src/features/{whatsApp => whatsapp}/api/subscribeWebhook.ts (96%) delete mode 100644 apps/viewer/src/helpers/api/dbRules.ts delete mode 100644 apps/viewer/src/helpers/api/isVercel.ts rename apps/viewer/src/{features/chat => test}/chat.spec.ts (99%) rename apps/viewer/src/{features/blocks/integrations/chatwoot => test}/chatwoot.spec.ts (100%) rename apps/viewer/src/{features/blocks/inputs/fileUpload => test}/fileUpload.spec.ts (100%) rename apps/viewer/src/{features/results => test}/results.spec.ts (100%) rename apps/viewer/src/{features/blocks/integrations/sendEmail => test}/sendEmail.spec.ts (94%) rename apps/viewer/src/{features/settings => test}/settings.spec.ts (100%) rename apps/viewer/src/{features/blocks/logic/typebotLink => test}/typebotLink.spec.ts (100%) rename apps/viewer/src/{features/variables => test}/variables.spec.ts (100%) rename apps/viewer/src/{features/blocks/integrations/webhook => test}/webhook.spec.ts (100%) create mode 100644 apps/viewer/src/trpc/generateOpenApi.ts rename {apps/viewer/src/features/chat/helpers => packages/bot-engine}/addEdgeToTypebot.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/inputs/buttons/filterChoiceItems.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts (82%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/inputs/buttons/parseButtonsReply.ts (98%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/inputs/date/parseDateInput.ts (84%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/inputs/date/parseDateReply.ts (96%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/inputs/email/validateEmail.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/inputs/number/validateNumber.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/inputs/payment/computePaymentInputRuntimeOptions.ts (96%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/inputs/phone/formatPhoneNumber.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/inputs/pictureChoice/filterPictureChoiceItems.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts (96%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/inputs/pictureChoice/parsePictureChoicesReply.ts (98%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/inputs/rating/validateRatingReply.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/inputs/url/validateUrl.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/chatwoot/executeChatwootBlock.ts (91%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock.ts (81%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/googleSheets/executeGoogleSheetBlock.ts (93%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/googleSheets/getRow.ts (90%) create mode 100644 packages/bot-engine/blocks/integrations/googleSheets/helpers/getAuthenticatedGoogleDoc.ts rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/googleSheets/helpers/matchFilter.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/googleSheets/helpers/parseCellValues.ts (84%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/googleSheets/insertRow.ts (94%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/googleSheets/updateRow.ts (93%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/openai/createChatCompletionOpenAI.ts (91%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/openai/executeChatCompletionOpenAIRequest.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/openai/executeOpenAIBlock.ts (90%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/openai/getChatCompletionStream.ts (96%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/openai/parseChatCompletionMessages.ts (95%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/openai/resumeChatCompletion.ts (90%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/pixel/executePixelBlock.ts (83%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/sendEmail/constants.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/sendEmail/executeSendEmailBlock.tsx (96%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/webhook/executeWebhookBlock.ts (97%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/webhook/parseSampleResult.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/webhook/resumeWebhookExecution.ts (87%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts (90%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/logic/abTest/executeAbTest.ts (89%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/logic/condition/executeCondition.ts (97%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/logic/condition/executeConditionBlock.ts (89%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/logic/jump/executeJumpBlock.ts (85%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/logic/redirect/executeRedirect.ts (82%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/logic/script/executeScript.ts (77%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/logic/setVariable/executeSetVariable.ts (91%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/logic/typebotLink/executeTypebotLink.ts (95%) create mode 100644 packages/bot-engine/blocks/logic/typebotLink/fetchLinkedTypebots.ts rename {apps/viewer/src/features => packages/bot-engine}/blocks/logic/typebotLink/getPreviouslyLinkedTypebots.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/blocks/logic/wait/executeWait.ts (86%) rename packages/{lib => bot-engine}/computeTypingDuration.ts (100%) rename {apps/viewer/src/features/chat/helpers => packages/bot-engine}/continueBotFlow.ts (89%) rename {apps/viewer/src/features/chat/helpers => packages/bot-engine}/executeGroup.ts (92%) rename {apps/viewer/src/features/chat/helpers => packages/bot-engine}/executeIntegration.ts (56%) rename {apps/viewer/src/features/chat/helpers => packages/bot-engine}/executeLogic.ts (55%) rename {apps/viewer/src/features/chat/helpers => packages/bot-engine}/getNextGroup.ts (95%) rename {apps/viewer/src/features/chat/helpers => packages/bot-engine}/getPrefilledValue.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/logs/helpers/formatLogDetails.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/logs/saveErrorLog.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/logs/saveLog.ts (91%) rename {apps/viewer/src/features => packages/bot-engine}/logs/saveSuccessLog.ts (100%) create mode 100644 packages/bot-engine/package.json rename {apps/viewer/src/features/chat/helpers => packages/bot-engine}/parseDynamicTheme.ts (87%) rename {apps/viewer/src/features/chat => packages/bot-engine}/queries/createResultIfNotExist.ts (94%) rename {apps/viewer/src/features/chat => packages/bot-engine}/queries/createSession.ts (84%) rename {apps/viewer/src/features/chat => packages/bot-engine}/queries/deleteSession.ts (72%) rename {apps/viewer/src/features/chat => packages/bot-engine}/queries/findPublicTypebot.ts (94%) rename {apps/viewer/src/features/chat => packages/bot-engine}/queries/findResult.ts (92%) rename {apps/viewer/src/features/chat => packages/bot-engine}/queries/findTypebot.ts (89%) rename {apps/viewer/src/features/chat => packages/bot-engine}/queries/getSession.ts (90%) rename {apps/viewer/src/features/chat => packages/bot-engine}/queries/restartSession.ts (89%) rename {apps/viewer/src/features/chat => packages/bot-engine}/queries/saveLogs.ts (77%) rename {apps/viewer/src/features/chat => packages/bot-engine}/queries/updateSession.ts (85%) rename {apps/viewer/src/features/chat => packages/bot-engine}/queries/upsertAnswer.ts (95%) rename {apps/viewer/src/features/chat => packages/bot-engine}/queries/upsertResult.ts (95%) rename {apps/viewer/src/features/chat/helpers => packages/bot-engine}/saveStateToDatabase.ts (79%) rename {apps/viewer/src/features/chat/helpers => packages/bot-engine}/startBotFlow.ts (100%) rename {apps/viewer/src/features/chat/helpers => packages/bot-engine}/startSession.ts (96%) create mode 100644 packages/bot-engine/tsconfig.json rename {apps/viewer/src/features/chat => packages/bot-engine}/types.ts (100%) rename apps/viewer/src/features/variables/deepParseVariable.ts => packages/bot-engine/variables/deepParseVariables.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/variables/extractVariablesFromText.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/variables/findUniqueVariableValue.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/variables/hasVariable.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/variables/injectVariablesFromExistingResult.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/variables/parseGuessedTypeFromString.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/variables/parseGuessedValueType.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/variables/parseVariableNumber.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/variables/parseVariables.ts (97%) rename {apps/viewer/src/features => packages/bot-engine}/variables/prefillVariables.ts (100%) rename {apps/viewer/src/features => packages/bot-engine}/variables/transformVariablesToList.ts (92%) rename apps/viewer/src/features/variables/updateVariables.ts => packages/bot-engine/variables/updateVariablesInSession.ts (96%) rename packages/{lib/whatsApp => bot-engine/whatsapp}/convertInputToWhatsAppMessage.ts (98%) rename packages/{lib/whatsApp => bot-engine/whatsapp}/convertMessageToWhatsAppMessage.ts (97%) rename packages/{lib/whatsApp => bot-engine/whatsapp}/convertRichTextToWhatsAppText.ts (100%) rename {apps/viewer/src/features/whatsApp/helpers => packages/bot-engine/whatsapp}/downloadMedia.ts (100%) rename {apps/viewer/src/features/whatsApp/helpers => packages/bot-engine/whatsapp}/resumeWhatsAppFlow.ts (93%) rename packages/{lib/whatsApp => bot-engine/whatsapp}/sendChatReplyToWhatsApp.ts (98%) rename packages/{lib/whatsApp => bot-engine/whatsapp}/sendWhatsAppMessage.ts (100%) rename {apps/viewer/src/features/whatsApp/helpers => packages/bot-engine/whatsapp}/startWhatsAppSession.ts (97%) rename {apps/viewer/src/helpers/api => packages/lib}/isPlanetScale.ts (100%) rename {apps/viewer/src => packages}/lib/prisma.ts (100%) diff --git a/.gitignore b/.gitignore index e169eaef57..cb66c1c86d 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ dump.sql dump.tar __env.js +__ENV.js typebotsToFix.json **/scripts/logs diff --git a/apps/builder/package.json b/apps/builder/package.json index e3c0fdcf7a..865584da9e 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -27,8 +27,6 @@ "@googleapis/drive": "8.0.0", "@paralleldrive/cuid2": "2.2.1", "@sentry/nextjs": "7.66.0", - "@stripe/stripe-js": "1.54.1", - "@t3-oss/env-nextjs": "^0.6.0", "@tanstack/react-query": "^4.29.19", "@tanstack/react-table": "8.9.3", "@trpc/client": "10.34.0", @@ -42,7 +40,6 @@ "@udecode/plate-common": "^21.1.5", "@udecode/plate-core": "21.1.5", "@udecode/plate-link": "21.2.0", - "@udecode/plate-serializer-html": "21.1.5", "@udecode/plate-ui-link": "21.2.0", "@udecode/plate-ui-toolbar": "21.1.5", "@uiw/codemirror-extensions-langs": "^4.21.7", @@ -85,7 +82,6 @@ "sharp": "^0.32.4", "slate": "0.94.1", "slate-history": "0.93.0", - "slate-hyperscript": "0.77.0", "slate-react": "0.94.2", "stripe": "12.13.0", "svg-round-corners": "0.4.1", @@ -94,7 +90,7 @@ "trpc-openapi": "1.2.0", "unsplash-js": "^7.0.18", "use-debounce": "9.0.4", - "@typebot.io/viewer": "workspace:*" + "@typebot.io/bot-engine": "workspace:*" }, "devDependencies": { "@chakra-ui/styled-system": "2.9.1", diff --git a/apps/builder/src/features/analytics/api/getTotalAnswersInBlocks.ts b/apps/builder/src/features/analytics/api/getTotalAnswersInBlocks.ts index 5b3df8c70b..837336d01c 100644 --- a/apps/builder/src/features/analytics/api/getTotalAnswersInBlocks.ts +++ b/apps/builder/src/features/analytics/api/getTotalAnswersInBlocks.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { PublicTypebot } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/auth/helpers/getAuthenticatedUser.ts b/apps/builder/src/features/auth/helpers/getAuthenticatedUser.ts index 99e48c7043..593d0456a8 100644 --- a/apps/builder/src/features/auth/helpers/getAuthenticatedUser.ts +++ b/apps/builder/src/features/auth/helpers/getAuthenticatedUser.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authOptions } from '@/pages/api/auth/[...nextauth]' import { setUser } from '@sentry/nextjs' import { User } from '@typebot.io/prisma' diff --git a/apps/builder/src/features/billing/api/createCheckoutSession.ts b/apps/builder/src/features/billing/api/createCheckoutSession.ts index de94a44aa7..7b8f9958dc 100644 --- a/apps/builder/src/features/billing/api/createCheckoutSession.ts +++ b/apps/builder/src/features/billing/api/createCheckoutSession.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { Plan } from '@typebot.io/prisma' diff --git a/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts b/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts index 4498709774..ffbf03b224 100644 --- a/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts +++ b/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { Plan } from '@typebot.io/prisma' diff --git a/apps/builder/src/features/billing/api/getBillingPortalUrl.ts b/apps/builder/src/features/billing/api/getBillingPortalUrl.ts index b79b7a4d57..50b7a1d0c2 100644 --- a/apps/builder/src/features/billing/api/getBillingPortalUrl.ts +++ b/apps/builder/src/features/billing/api/getBillingPortalUrl.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import Stripe from 'stripe' diff --git a/apps/builder/src/features/billing/api/getSubscription.ts b/apps/builder/src/features/billing/api/getSubscription.ts index 41ad02d7a1..36707c608c 100644 --- a/apps/builder/src/features/billing/api/getSubscription.ts +++ b/apps/builder/src/features/billing/api/getSubscription.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import Stripe from 'stripe' diff --git a/apps/builder/src/features/billing/api/getUsage.ts b/apps/builder/src/features/billing/api/getUsage.ts index d62bf370f6..0877e35af2 100644 --- a/apps/builder/src/features/billing/api/getUsage.ts +++ b/apps/builder/src/features/billing/api/getUsage.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { z } from 'zod' diff --git a/apps/builder/src/features/billing/api/listInvoices.ts b/apps/builder/src/features/billing/api/listInvoices.ts index 8e9ac082bd..40f072b2f5 100644 --- a/apps/builder/src/features/billing/api/listInvoices.ts +++ b/apps/builder/src/features/billing/api/listInvoices.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import Stripe from 'stripe' diff --git a/apps/builder/src/features/billing/api/updateSubscription.ts b/apps/builder/src/features/billing/api/updateSubscription.ts index 3f8dc575a8..26abfad57b 100644 --- a/apps/builder/src/features/billing/api/updateSubscription.ts +++ b/apps/builder/src/features/billing/api/updateSubscription.ts @@ -1,5 +1,5 @@ import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEvent' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { Plan } from '@typebot.io/prisma' diff --git a/apps/builder/src/features/billing/billing.spec.ts b/apps/builder/src/features/billing/billing.spec.ts index 107ef01f97..e04fb1a0eb 100644 --- a/apps/builder/src/features/billing/billing.spec.ts +++ b/apps/builder/src/features/billing/billing.spec.ts @@ -51,7 +51,6 @@ test('should display valid usage', async ({ page }) => { await page.click('text=Settings & Members') await page.click('text=Billing & Usage') await expect(page.locator('text="/ 10,000"')).toBeVisible() - await expect(page.locator('text="/ 10 GB"')).toBeVisible() await page.getByText('Members', { exact: true }).click() await expect( page.getByRole('heading', { name: 'Members (1/5)' }) @@ -63,7 +62,6 @@ test('should display valid usage', async ({ page }) => { await page.click('text=Settings & Members') await page.click('text=Billing & Usage') await expect(page.locator('text="/ 100,000"')).toBeVisible() - await expect(page.locator('text="/ 50 GB"')).toBeVisible() await expect(page.getByText('Upgrade to Starter')).toBeHidden() await expect(page.getByText('Upgrade to Pro')).toBeHidden() await expect(page.getByText('Need custom limits?')).toBeHidden() @@ -78,7 +76,6 @@ test('should display valid usage', async ({ page }) => { await page.click('text=Settings & Members') await page.click('text=Billing & Usage') await expect(page.locator('text="/ 200"')).toBeVisible() - await expect(page.locator('text="Storage"')).toBeHidden() await page.getByText('Members', { exact: true }).click() await expect( page.getByRole('heading', { name: 'Members (1/1)' }) @@ -95,17 +92,11 @@ test('should display valid usage', async ({ page }) => { await page.click('text=Settings & Members') await page.click('text=Billing & Usage') await expect(page.locator('text="/ 2,000"')).toBeVisible() - await expect(page.locator('text="/ 2 GB"')).toBeVisible() await expect(page.locator('text="10" >> nth=0')).toBeVisible() await expect(page.locator('[role="progressbar"] >> nth=0')).toHaveAttribute( 'aria-valuenow', '1' ) - await expect(page.locator('text="1.07 GB"')).toBeVisible() - await expect(page.locator('[role="progressbar"] >> nth=1')).toHaveAttribute( - 'aria-valuenow', - '54' - ) await injectFakeResults({ typebotId: usageTypebotId, @@ -116,10 +107,7 @@ test('should display valid usage', async ({ page }) => { await page.click('text="Billing & Usage"') await expect(page.locator('text="/ 2,000"')).toBeVisible() await expect(page.locator('text="1,100"')).toBeVisible() - await expect(page.locator('text="/ 2 GB"')).toBeVisible() - await expect(page.locator('text="2.25 GB"')).toBeVisible() await expect(page.locator('[aria-valuenow="55"]')).toBeVisible() - await expect(page.locator('[aria-valuenow="112"]')).toBeVisible() }) test('plan changes should work', async ({ page }) => { @@ -160,9 +148,7 @@ test('plan changes should work', async ({ page }) => { await page.click('text=Settings & Members') await page.click('text=Billing & Usage') await expect(page.locator('text="/ 2,000"')).toBeVisible() - await expect(page.locator('text="/ 2 GB"')).toBeVisible() await expect(page.getByText('/ 2,000')).toBeVisible() - await expect(page.getByText('/ 2 GB')).toBeVisible() await page.click('button >> text="2,000"') await page.click('button >> text="3,500"') await page.click('button >> text="2"') @@ -178,7 +164,6 @@ test('plan changes should work', async ({ page }) => { await page.click('text="Billing & Usage"') await expect(page.locator('text="$73"')).toBeVisible() await expect(page.locator('text="/ 3,500"')).toBeVisible() - await expect(page.locator('text="/ 4 GB"')).toBeVisible() await expect(page.getByRole('button', { name: '3,500' })).toBeVisible() await expect(page.getByRole('button', { name: '4' })).toBeVisible() diff --git a/apps/builder/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts b/apps/builder/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts index 27a34b5cec..7132248197 100644 --- a/apps/builder/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts +++ b/apps/builder/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts @@ -36,7 +36,6 @@ test('options should work', async ({ page }) => { await page.fill('[value="Upload"]', 'Go') await page.fill('[value="Clear"]', 'Reset') await page.fill('[value="Skip"]', 'Pass') - await page.fill('input[value="10"]', '20') await page.click('text="Restart"') await expect(page.locator(`text="Pass"`)).toBeVisible() await expect(page.locator(`text="Upload now!!"`)).toBeVisible() diff --git a/apps/builder/src/features/blocks/integrations/openai/api/listModels.ts b/apps/builder/src/features/blocks/integrations/openai/api/listModels.ts index 9a707d5250..9a4a6dc903 100644 --- a/apps/builder/src/features/blocks/integrations/openai/api/listModels.ts +++ b/apps/builder/src/features/blocks/integrations/openai/api/listModels.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { z } from 'zod' diff --git a/apps/builder/src/features/blocks/integrations/webhook/api/getResultExample.ts b/apps/builder/src/features/blocks/integrations/webhook/api/getResultExample.ts index 4496f0197d..54cf29b3d6 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/api/getResultExample.ts +++ b/apps/builder/src/features/blocks/integrations/webhook/api/getResultExample.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { canReadTypebots } from '@/helpers/databaseRules' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' diff --git a/apps/builder/src/features/blocks/integrations/webhook/api/listWebhookBlocks.ts b/apps/builder/src/features/blocks/integrations/webhook/api/listWebhookBlocks.ts index ea63628d0c..15e8264dee 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/api/listWebhookBlocks.ts +++ b/apps/builder/src/features/blocks/integrations/webhook/api/listWebhookBlocks.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { canReadTypebots } from '@/helpers/databaseRules' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' diff --git a/apps/builder/src/features/blocks/integrations/webhook/api/subscribeWebhook.ts b/apps/builder/src/features/blocks/integrations/webhook/api/subscribeWebhook.ts index cdffab3611..a3cb233889 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/api/subscribeWebhook.ts +++ b/apps/builder/src/features/blocks/integrations/webhook/api/subscribeWebhook.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { canWriteTypebots } from '@/helpers/databaseRules' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' diff --git a/apps/builder/src/features/blocks/integrations/webhook/api/unsubscribeWebhook.ts b/apps/builder/src/features/blocks/integrations/webhook/api/unsubscribeWebhook.ts index 2f4b4a2083..3829eb5ff7 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/api/unsubscribeWebhook.ts +++ b/apps/builder/src/features/blocks/integrations/webhook/api/unsubscribeWebhook.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { canWriteTypebots } from '@/helpers/databaseRules' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' diff --git a/apps/builder/src/features/blocks/integrations/zemanticAi/api/listProjects.ts b/apps/builder/src/features/blocks/integrations/zemanticAi/api/listProjects.ts index a8c2b33d1e..2852acdeb2 100644 --- a/apps/builder/src/features/blocks/integrations/zemanticAi/api/listProjects.ts +++ b/apps/builder/src/features/blocks/integrations/zemanticAi/api/listProjects.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { z } from 'zod' diff --git a/apps/builder/src/features/blocks/logic/typebotLink/api/getLinkedTypebots.ts b/apps/builder/src/features/blocks/logic/typebotLink/api/getLinkedTypebots.ts index 9bdfdfa181..d0483ea4c3 100644 --- a/apps/builder/src/features/blocks/logic/typebotLink/api/getLinkedTypebots.ts +++ b/apps/builder/src/features/blocks/logic/typebotLink/api/getLinkedTypebots.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { LogicBlockType, typebotSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/blocks/logic/typebotLink/helpers/fetchLinkedTypebots.ts b/apps/builder/src/features/blocks/logic/typebotLink/helpers/fetchLinkedTypebots.ts index 93d393fae4..7c3e94f493 100644 --- a/apps/builder/src/features/blocks/logic/typebotLink/helpers/fetchLinkedTypebots.ts +++ b/apps/builder/src/features/blocks/logic/typebotLink/helpers/fetchLinkedTypebots.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { canReadTypebots } from '@/helpers/databaseRules' import { User } from '@typebot.io/prisma' import { LogicBlockType, PublicTypebot, Typebot } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/collaboration/api/getCollaborators.ts b/apps/builder/src/features/collaboration/api/getCollaborators.ts index 9436e57e23..2a79544317 100644 --- a/apps/builder/src/features/collaboration/api/getCollaborators.ts +++ b/apps/builder/src/features/collaboration/api/getCollaborators.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { z } from 'zod' diff --git a/apps/builder/src/features/collaboration/collaboration.spec.ts b/apps/builder/src/features/collaboration/collaboration.spec.ts index cb8c130a3a..e5ef1d2cbb 100644 --- a/apps/builder/src/features/collaboration/collaboration.spec.ts +++ b/apps/builder/src/features/collaboration/collaboration.spec.ts @@ -1,7 +1,7 @@ import test, { expect } from '@playwright/test' import { createId } from '@paralleldrive/cuid2' import { CollaborationType, Plan, WorkspaceRole } from '@typebot.io/prisma' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { InputBlockType, defaultTextInputOptions } from '@typebot.io/schemas' import { createTypebots, diff --git a/apps/builder/src/features/credentials/api/createCredentials.ts b/apps/builder/src/features/credentials/api/createCredentials.ts index b24e5a2189..62047d980d 100644 --- a/apps/builder/src/features/credentials/api/createCredentials.ts +++ b/apps/builder/src/features/credentials/api/createCredentials.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { stripeCredentialsSchema } from '@typebot.io/schemas/features/blocks/inputs/payment/schemas' diff --git a/apps/builder/src/features/credentials/api/deleteCredentials.ts b/apps/builder/src/features/credentials/api/deleteCredentials.ts index 14bd14ba35..f002906bb5 100644 --- a/apps/builder/src/features/credentials/api/deleteCredentials.ts +++ b/apps/builder/src/features/credentials/api/deleteCredentials.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { z } from 'zod' diff --git a/apps/builder/src/features/credentials/api/listCredentials.ts b/apps/builder/src/features/credentials/api/listCredentials.ts index aa2a4c9389..e60ff8d64f 100644 --- a/apps/builder/src/features/credentials/api/listCredentials.ts +++ b/apps/builder/src/features/credentials/api/listCredentials.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { stripeCredentialsSchema } from '@typebot.io/schemas/features/blocks/inputs/payment/schemas' diff --git a/apps/builder/src/features/customDomains/api/createCustomDomain.ts b/apps/builder/src/features/customDomains/api/createCustomDomain.ts index 7f9d929355..9a1449ab7e 100644 --- a/apps/builder/src/features/customDomains/api/createCustomDomain.ts +++ b/apps/builder/src/features/customDomains/api/createCustomDomain.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { z } from 'zod' diff --git a/apps/builder/src/features/customDomains/api/deleteCustomDomain.ts b/apps/builder/src/features/customDomains/api/deleteCustomDomain.ts index 6dc651407d..6326ce221b 100644 --- a/apps/builder/src/features/customDomains/api/deleteCustomDomain.ts +++ b/apps/builder/src/features/customDomains/api/deleteCustomDomain.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { z } from 'zod' diff --git a/apps/builder/src/features/customDomains/api/listCustomDomains.ts b/apps/builder/src/features/customDomains/api/listCustomDomains.ts index 94a25feaa7..78774678c0 100644 --- a/apps/builder/src/features/customDomains/api/listCustomDomains.ts +++ b/apps/builder/src/features/customDomains/api/listCustomDomains.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { z } from 'zod' diff --git a/apps/builder/src/features/customDomains/api/verifyCustomDomain.ts b/apps/builder/src/features/customDomains/api/verifyCustomDomain.ts index 91e32d4351..432ac241ce 100644 --- a/apps/builder/src/features/customDomains/api/verifyCustomDomain.ts +++ b/apps/builder/src/features/customDomains/api/verifyCustomDomain.ts @@ -7,7 +7,7 @@ import { domainResponseSchema, domainVerificationStatusSchema, } from '@typebot.io/schemas/features/customDomains' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden' import { TRPCError } from '@trpc/server' import { env } from '@typebot.io/env' diff --git a/apps/builder/src/features/results/api/deleteResults.ts b/apps/builder/src/features/results/api/deleteResults.ts index 71ed74228b..e99367d5ed 100644 --- a/apps/builder/src/features/results/api/deleteResults.ts +++ b/apps/builder/src/features/results/api/deleteResults.ts @@ -3,7 +3,7 @@ import { TRPCError } from '@trpc/server' import { Group } from '@typebot.io/schemas' import { z } from 'zod' import { archiveResults } from '@typebot.io/lib/api/helpers/archiveResults' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { isWriteTypebotForbidden } from '@/features/typebot/helpers/isWriteTypebotForbidden' export const deleteResults = authenticatedProcedure diff --git a/apps/builder/src/features/results/api/getResult.ts b/apps/builder/src/features/results/api/getResult.ts index 0648c862d5..f6e333eac1 100644 --- a/apps/builder/src/features/results/api/getResult.ts +++ b/apps/builder/src/features/results/api/getResult.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { ResultWithAnswers, resultWithAnswersSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/results/api/getResultLogs.ts b/apps/builder/src/features/results/api/getResultLogs.ts index 2e19615c8a..2e8a6b6de2 100644 --- a/apps/builder/src/features/results/api/getResultLogs.ts +++ b/apps/builder/src/features/results/api/getResultLogs.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { logSchema } from '@typebot.io/schemas' import { z } from 'zod' diff --git a/apps/builder/src/features/results/api/getResults.ts b/apps/builder/src/features/results/api/getResults.ts index 5327413641..7abf267991 100644 --- a/apps/builder/src/features/results/api/getResults.ts +++ b/apps/builder/src/features/results/api/getResults.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { ResultWithAnswers, resultWithAnswersSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/theme/api/deleteThemeTemplate.ts b/apps/builder/src/features/theme/api/deleteThemeTemplate.ts index d541389ccf..bb8fd9f103 100644 --- a/apps/builder/src/features/theme/api/deleteThemeTemplate.ts +++ b/apps/builder/src/features/theme/api/deleteThemeTemplate.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { ThemeTemplate, themeTemplateSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/theme/api/listThemeTemplates.ts b/apps/builder/src/features/theme/api/listThemeTemplates.ts index b2e183af94..605d971194 100644 --- a/apps/builder/src/features/theme/api/listThemeTemplates.ts +++ b/apps/builder/src/features/theme/api/listThemeTemplates.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { ThemeTemplate, themeTemplateSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/theme/api/saveThemeTemplate.ts b/apps/builder/src/features/theme/api/saveThemeTemplate.ts index 10d0126a23..4f8f25c751 100644 --- a/apps/builder/src/features/theme/api/saveThemeTemplate.ts +++ b/apps/builder/src/features/theme/api/saveThemeTemplate.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { ThemeTemplate, themeTemplateSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/typebot/api/createTypebot.ts b/apps/builder/src/features/typebot/api/createTypebot.ts index 544b06e3c5..ecdab70255 100644 --- a/apps/builder/src/features/typebot/api/createTypebot.ts +++ b/apps/builder/src/features/typebot/api/createTypebot.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { Plan, WorkspaceRole } from '@typebot.io/prisma' diff --git a/apps/builder/src/features/typebot/api/deleteTypebot.ts b/apps/builder/src/features/typebot/api/deleteTypebot.ts index ae688e68d8..14220ebbb2 100644 --- a/apps/builder/src/features/typebot/api/deleteTypebot.ts +++ b/apps/builder/src/features/typebot/api/deleteTypebot.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { Group } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/typebot/api/getPublishedTypebot.ts b/apps/builder/src/features/typebot/api/getPublishedTypebot.ts index dd5249d086..6f7fc5be6b 100644 --- a/apps/builder/src/features/typebot/api/getPublishedTypebot.ts +++ b/apps/builder/src/features/typebot/api/getPublishedTypebot.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { publicTypebotSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/typebot/api/getTypebot.ts b/apps/builder/src/features/typebot/api/getTypebot.ts index 517e07c0fc..34fe434242 100644 --- a/apps/builder/src/features/typebot/api/getTypebot.ts +++ b/apps/builder/src/features/typebot/api/getTypebot.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { Typebot, typebotSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/typebot/api/listTypebots.ts b/apps/builder/src/features/typebot/api/listTypebots.ts index 31ce74db44..79dcc845e5 100644 --- a/apps/builder/src/features/typebot/api/listTypebots.ts +++ b/apps/builder/src/features/typebot/api/listTypebots.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { WorkspaceRole } from '@typebot.io/prisma' diff --git a/apps/builder/src/features/typebot/api/publishTypebot.ts b/apps/builder/src/features/typebot/api/publishTypebot.ts index 912e6375e3..cb853f449c 100644 --- a/apps/builder/src/features/typebot/api/publishTypebot.ts +++ b/apps/builder/src/features/typebot/api/publishTypebot.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { InputBlockType, typebotSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/typebot/api/unpublishTypebot.ts b/apps/builder/src/features/typebot/api/unpublishTypebot.ts index d8e3f6b6bb..d3c91ae7b8 100644 --- a/apps/builder/src/features/typebot/api/unpublishTypebot.ts +++ b/apps/builder/src/features/typebot/api/unpublishTypebot.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { z } from 'zod' diff --git a/apps/builder/src/features/typebot/api/updateTypebot.ts b/apps/builder/src/features/typebot/api/updateTypebot.ts index bf93a1f049..8c3c35634c 100644 --- a/apps/builder/src/features/typebot/api/updateTypebot.ts +++ b/apps/builder/src/features/typebot/api/updateTypebot.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { typebotCreateSchema, typebotSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts b/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts index 7192e404c2..0a40628542 100644 --- a/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts +++ b/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { env } from '@typebot.io/env' import { CollaboratorsOnTypebots, User } from '@typebot.io/prisma' import { Typebot } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/typebot/helpers/isWriteTypebotForbidden.ts b/apps/builder/src/features/typebot/helpers/isWriteTypebotForbidden.ts index d70a1160b5..e6a4b72ea0 100644 --- a/apps/builder/src/features/typebot/helpers/isWriteTypebotForbidden.ts +++ b/apps/builder/src/features/typebot/helpers/isWriteTypebotForbidden.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { CollaborationType, CollaboratorsOnTypebots, diff --git a/apps/builder/src/features/typebot/helpers/sanitizers.ts b/apps/builder/src/features/typebot/helpers/sanitizers.ts index da4613d5ec..3e3b3c0007 100644 --- a/apps/builder/src/features/typebot/helpers/sanitizers.ts +++ b/apps/builder/src/features/typebot/helpers/sanitizers.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { Plan } from '@typebot.io/prisma' import { Block, diff --git a/apps/builder/src/features/upload/api/generateUploadUrl.ts b/apps/builder/src/features/upload/api/generateUploadUrl.ts index f568340df3..f9c98633b0 100644 --- a/apps/builder/src/features/upload/api/generateUploadUrl.ts +++ b/apps/builder/src/features/upload/api/generateUploadUrl.ts @@ -3,7 +3,7 @@ import { z } from 'zod' import { env } from '@typebot.io/env' import { TRPCError } from '@trpc/server' import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden' import { isWriteTypebotForbidden } from '@/features/typebot/helpers/isWriteTypebotForbidden' diff --git a/apps/builder/src/features/whatsapp/generateVerificationToken.ts b/apps/builder/src/features/whatsapp/generateVerificationToken.ts index ce0b295496..ca53b371b8 100644 --- a/apps/builder/src/features/whatsapp/generateVerificationToken.ts +++ b/apps/builder/src/features/whatsapp/generateVerificationToken.ts @@ -1,6 +1,6 @@ import { authenticatedProcedure } from '@/helpers/server/trpc' import { z } from 'zod' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { createId } from '@paralleldrive/cuid2' export const generateVerificationToken = authenticatedProcedure diff --git a/apps/builder/src/features/whatsapp/getPhoneNumber.ts b/apps/builder/src/features/whatsapp/getPhoneNumber.ts index 2fa5d5f7de..b54d46a036 100644 --- a/apps/builder/src/features/whatsapp/getPhoneNumber.ts +++ b/apps/builder/src/features/whatsapp/getPhoneNumber.ts @@ -1,7 +1,7 @@ import { authenticatedProcedure } from '@/helpers/server/trpc' import { z } from 'zod' import got from 'got' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { decrypt } from '@typebot.io/lib/api' import { TRPCError } from '@trpc/server' import { WhatsAppCredentials } from '@typebot.io/schemas/features/whatsapp' diff --git a/apps/builder/src/features/whatsapp/getSystemTokenInfo.ts b/apps/builder/src/features/whatsapp/getSystemTokenInfo.ts index f848905c5f..b6bccc19cb 100644 --- a/apps/builder/src/features/whatsapp/getSystemTokenInfo.ts +++ b/apps/builder/src/features/whatsapp/getSystemTokenInfo.ts @@ -3,7 +3,7 @@ import { z } from 'zod' import got from 'got' import { TRPCError } from '@trpc/server' import { WhatsAppCredentials } from '@typebot.io/schemas/features/whatsapp' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { decrypt } from '@typebot.io/lib/api/encryption' const inputSchema = z.object({ diff --git a/apps/builder/src/features/whatsapp/receiveMessagePreview.ts b/apps/builder/src/features/whatsapp/receiveMessagePreview.ts index 1141e505f9..0d3585913b 100644 --- a/apps/builder/src/features/whatsapp/receiveMessagePreview.ts +++ b/apps/builder/src/features/whatsapp/receiveMessagePreview.ts @@ -1,7 +1,7 @@ import { publicProcedure } from '@/helpers/server/trpc' import { whatsAppWebhookRequestBodySchema } from '@typebot.io/schemas/features/whatsapp' import { z } from 'zod' -import { resumeWhatsAppFlow } from '@typebot.io/viewer/src/features/whatsApp/helpers/resumeWhatsAppFlow' +import { resumeWhatsAppFlow } from '@typebot.io/bot-engine/whatsapp/resumeWhatsAppFlow' import { isNotDefined } from '@typebot.io/lib' import { TRPCError } from '@trpc/server' import { env } from '@typebot.io/env' diff --git a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts index 02ea9f0d6f..ebf78fc870 100644 --- a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts +++ b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts @@ -1,14 +1,14 @@ import { authenticatedProcedure } from '@/helpers/server/trpc' import { z } from 'zod' import { TRPCError } from '@trpc/server' -import { sendWhatsAppMessage } from '@typebot.io/lib/whatsApp/sendWhatsAppMessage' -import { startSession } from '@typebot.io/viewer/src/features/chat/helpers/startSession' +import { startSession } from '@typebot.io/bot-engine/startSession' import { env } from '@typebot.io/env' import { HTTPError } from 'got' -import prisma from '@/lib/prisma' -import { sendChatReplyToWhatsApp } from '@typebot.io/lib/whatsApp/sendChatReplyToWhatsApp' -import { saveStateToDatabase } from '@typebot.io/viewer/src/features/chat/helpers/saveStateToDatabase' -import { restartSession } from '@typebot.io/viewer/src/features/chat/queries/restartSession' +import prisma from '@typebot.io/lib/prisma' +import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase' +import { restartSession } from '@typebot.io/bot-engine/queries/restartSession' +import { sendChatReplyToWhatsApp } from '@typebot.io/bot-engine/whatsapp/sendChatReplyToWhatsApp' +import { sendWhatsAppMessage } from '@typebot.io/bot-engine/whatsapp/sendWhatsAppMessage' import { isReadTypebotForbidden } from '../typebot/helpers/isReadTypebotForbidden' import { SessionState } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/whatsapp/verifyIfPhoneNumberAvailable.ts b/apps/builder/src/features/whatsapp/verifyIfPhoneNumberAvailable.ts index 0b14d6b6b5..9c32186c02 100644 --- a/apps/builder/src/features/whatsapp/verifyIfPhoneNumberAvailable.ts +++ b/apps/builder/src/features/whatsapp/verifyIfPhoneNumberAvailable.ts @@ -1,6 +1,6 @@ import { authenticatedProcedure } from '@/helpers/server/trpc' import { z } from 'zod' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' export const verifyIfPhoneNumberAvailable = authenticatedProcedure .meta({ diff --git a/apps/builder/src/features/workspace/api/createWorkspace.ts b/apps/builder/src/features/workspace/api/createWorkspace.ts index 4d1061b838..c029d1bfb7 100644 --- a/apps/builder/src/features/workspace/api/createWorkspace.ts +++ b/apps/builder/src/features/workspace/api/createWorkspace.ts @@ -1,5 +1,5 @@ import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEvent' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { Workspace, workspaceSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/workspace/api/deleteWorkspace.ts b/apps/builder/src/features/workspace/api/deleteWorkspace.ts index 18f475051d..ad456df5a4 100644 --- a/apps/builder/src/features/workspace/api/deleteWorkspace.ts +++ b/apps/builder/src/features/workspace/api/deleteWorkspace.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { z } from 'zod' import { isAdminWriteWorkspaceForbidden } from '../helpers/isAdminWriteWorkspaceForbidden' diff --git a/apps/builder/src/features/workspace/api/getWorkspace.ts b/apps/builder/src/features/workspace/api/getWorkspace.ts index ee8114afcd..0cf700abad 100644 --- a/apps/builder/src/features/workspace/api/getWorkspace.ts +++ b/apps/builder/src/features/workspace/api/getWorkspace.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { workspaceSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/workspace/api/listInvitationsInWorkspace.ts b/apps/builder/src/features/workspace/api/listInvitationsInWorkspace.ts index b4ff42488a..751934bf19 100644 --- a/apps/builder/src/features/workspace/api/listInvitationsInWorkspace.ts +++ b/apps/builder/src/features/workspace/api/listInvitationsInWorkspace.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { workspaceInvitationSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/workspace/api/listMembersInWorkspace.ts b/apps/builder/src/features/workspace/api/listMembersInWorkspace.ts index 6800c1b34a..048cf91ea5 100644 --- a/apps/builder/src/features/workspace/api/listMembersInWorkspace.ts +++ b/apps/builder/src/features/workspace/api/listMembersInWorkspace.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { workspaceMemberSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/workspace/api/listWorkspaces.ts b/apps/builder/src/features/workspace/api/listWorkspaces.ts index 213e285046..6e3326bdc7 100644 --- a/apps/builder/src/features/workspace/api/listWorkspaces.ts +++ b/apps/builder/src/features/workspace/api/listWorkspaces.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { workspaceSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/features/workspace/api/updateWorkspace.ts b/apps/builder/src/features/workspace/api/updateWorkspace.ts index 11b26a832d..0a3889802f 100644 --- a/apps/builder/src/features/workspace/api/updateWorkspace.ts +++ b/apps/builder/src/features/workspace/api/updateWorkspace.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { workspaceSchema } from '@typebot.io/schemas' diff --git a/apps/builder/src/helpers/databaseRules.ts b/apps/builder/src/helpers/databaseRules.ts index 11e5f327c2..6a4aba67d9 100644 --- a/apps/builder/src/helpers/databaseRules.ts +++ b/apps/builder/src/helpers/databaseRules.ts @@ -5,7 +5,7 @@ import { User, WorkspaceRole, } from '@typebot.io/prisma' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiResponse } from 'next' import { forbidden } from '@typebot.io/lib/api' import { env } from '@typebot.io/env' diff --git a/apps/builder/src/lib/googleSheets.ts b/apps/builder/src/lib/googleSheets.ts index 580ccce580..694c0d6f1e 100644 --- a/apps/builder/src/lib/googleSheets.ts +++ b/apps/builder/src/lib/googleSheets.ts @@ -3,8 +3,8 @@ import { OAuth2Client, Credentials } from 'google-auth-library' import { GoogleSheetsCredentials } from '@typebot.io/schemas' import { isDefined } from '@typebot.io/lib' import { decrypt, encrypt } from '@typebot.io/lib/api' -import prisma from './prisma' import { env } from '@typebot.io/env' +import prisma from '@typebot.io/lib/prisma' export const oauth2Client = new OAuth2Client( env.GOOGLE_CLIENT_ID, diff --git a/apps/builder/src/lib/prisma.ts b/apps/builder/src/lib/prisma.ts deleted file mode 100644 index d91fcca1e5..0000000000 --- a/apps/builder/src/lib/prisma.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Instantiates a single instance PrismaClient and save it on the global object. - * @link https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices - */ -import { env } from '@typebot.io/env' -import { PrismaClient } from '@typebot.io/prisma' - -const prismaGlobal = global as typeof global & { - prisma?: PrismaClient -} - -const prisma: PrismaClient = - prismaGlobal.prisma || - new PrismaClient({ - log: env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'], - }) - -if (env.NODE_ENV !== 'production') { - prismaGlobal.prisma = prisma -} - -export default prisma diff --git a/apps/builder/src/pages/api/auth/[...nextauth].ts b/apps/builder/src/pages/api/auth/[...nextauth].ts index c5618df119..696fd5bfcb 100644 --- a/apps/builder/src/pages/api/auth/[...nextauth].ts +++ b/apps/builder/src/pages/api/auth/[...nextauth].ts @@ -5,7 +5,7 @@ import GitlabProvider from 'next-auth/providers/gitlab' import GoogleProvider from 'next-auth/providers/google' import FacebookProvider from 'next-auth/providers/facebook' import AzureADProvider from 'next-auth/providers/azure-ad' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { Provider } from 'next-auth/providers' import { NextApiRequest, NextApiResponse } from 'next' import { customAdapter } from '../../../features/auth/api/customAdapter' diff --git a/apps/builder/src/pages/api/credentials.ts b/apps/builder/src/pages/api/credentials.ts index 84187ad90a..48ecd6b300 100644 --- a/apps/builder/src/pages/api/credentials.ts +++ b/apps/builder/src/pages/api/credentials.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { Credentials } from '@typebot.io/schemas' import { NextApiRequest, NextApiResponse } from 'next' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' diff --git a/apps/builder/src/pages/api/credentials/[credentialsId].ts b/apps/builder/src/pages/api/credentials/[credentialsId].ts index e738e40623..0458db4265 100644 --- a/apps/builder/src/pages/api/credentials/[credentialsId].ts +++ b/apps/builder/src/pages/api/credentials/[credentialsId].ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { badRequest, diff --git a/apps/builder/src/pages/api/credentials/google-sheets/callback.ts b/apps/builder/src/pages/api/credentials/google-sheets/callback.ts index b3d56ea4b4..40e9a57163 100644 --- a/apps/builder/src/pages/api/credentials/google-sheets/callback.ts +++ b/apps/builder/src/pages/api/credentials/google-sheets/callback.ts @@ -1,6 +1,6 @@ import { NextApiRequest, NextApiResponse } from 'next' import { Prisma } from '@typebot.io/prisma' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { googleSheetsScopes } from './consent-url' import { stringify } from 'querystring' import { badRequest, encrypt, notAuthenticated } from '@typebot.io/lib/api' diff --git a/apps/builder/src/pages/api/customDomains/[domain].ts b/apps/builder/src/pages/api/customDomains/[domain].ts index c6480596de..faa5c353a4 100644 --- a/apps/builder/src/pages/api/customDomains/[domain].ts +++ b/apps/builder/src/pages/api/customDomains/[domain].ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { badRequest, diff --git a/apps/builder/src/pages/api/folders.ts b/apps/builder/src/pages/api/folders.ts index c9191c8ab8..ed81fd86df 100644 --- a/apps/builder/src/pages/api/folders.ts +++ b/apps/builder/src/pages/api/folders.ts @@ -1,5 +1,5 @@ import { DashboardFolder, WorkspaceRole } from '@typebot.io/prisma' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { badRequest, diff --git a/apps/builder/src/pages/api/folders/[id].ts b/apps/builder/src/pages/api/folders/[id].ts index 692f73ac85..d3c9fe85e0 100644 --- a/apps/builder/src/pages/api/folders/[id].ts +++ b/apps/builder/src/pages/api/folders/[id].ts @@ -1,5 +1,5 @@ import { DashboardFolder } from '@typebot.io/prisma' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' diff --git a/apps/builder/src/pages/api/publicIdAvailable.ts b/apps/builder/src/pages/api/publicIdAvailable.ts index b437df48b1..2df82f721a 100644 --- a/apps/builder/src/pages/api/publicIdAvailable.ts +++ b/apps/builder/src/pages/api/publicIdAvailable.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' diff --git a/apps/builder/src/pages/api/publicTypebots.ts b/apps/builder/src/pages/api/publicTypebots.ts index c51d9c3f2c..ad7ba7bfa1 100644 --- a/apps/builder/src/pages/api/publicTypebots.ts +++ b/apps/builder/src/pages/api/publicTypebots.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { InputBlockType, PublicTypebot } from '@typebot.io/schemas' import { NextApiRequest, NextApiResponse } from 'next' import { canPublishFileInput } from '@/helpers/databaseRules' diff --git a/apps/builder/src/pages/api/publicTypebots/[id].ts b/apps/builder/src/pages/api/publicTypebots/[id].ts index f925238f80..8e291cfbaf 100644 --- a/apps/builder/src/pages/api/publicTypebots/[id].ts +++ b/apps/builder/src/pages/api/publicTypebots/[id].ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { InputBlockType, PublicTypebot } from '@typebot.io/schemas' import { NextApiRequest, NextApiResponse } from 'next' import { canPublishFileInput, canWriteTypebots } from '@/helpers/databaseRules' diff --git a/apps/builder/src/pages/api/stripe/webhook.ts b/apps/builder/src/pages/api/stripe/webhook.ts index 390afa8576..e279694271 100644 --- a/apps/builder/src/pages/api/stripe/webhook.ts +++ b/apps/builder/src/pages/api/stripe/webhook.ts @@ -3,7 +3,7 @@ import { methodNotAllowed } from '@typebot.io/lib/api' import Stripe from 'stripe' import Cors from 'micro-cors' import { buffer } from 'micro' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { Plan, WorkspaceRole } from '@typebot.io/prisma' import { RequestHandler } from 'next/dist/server/next' import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEvent' diff --git a/apps/builder/src/pages/api/typebots/[typebotId]/analytics/stats.ts b/apps/builder/src/pages/api/typebots/[typebotId]/analytics/stats.ts index 246e778223..a367337d96 100644 --- a/apps/builder/src/pages/api/typebots/[typebotId]/analytics/stats.ts +++ b/apps/builder/src/pages/api/typebots/[typebotId]/analytics/stats.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { Stats } from '@typebot.io/schemas' import { NextApiRequest, NextApiResponse } from 'next' import { canReadTypebots } from '@/helpers/databaseRules' diff --git a/apps/builder/src/pages/api/typebots/[typebotId]/blocks.ts b/apps/builder/src/pages/api/typebots/[typebotId]/blocks.ts index 5a1c19fef5..c928b9d318 100644 --- a/apps/builder/src/pages/api/typebots/[typebotId]/blocks.ts +++ b/apps/builder/src/pages/api/typebots/[typebotId]/blocks.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { canReadTypebots } from '@/helpers/databaseRules' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' diff --git a/apps/builder/src/pages/api/typebots/[typebotId]/collaborators.ts b/apps/builder/src/pages/api/typebots/[typebotId]/collaborators.ts index 370afd465e..6c2da316fe 100644 --- a/apps/builder/src/pages/api/typebots/[typebotId]/collaborators.ts +++ b/apps/builder/src/pages/api/typebots/[typebotId]/collaborators.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { canReadTypebots } from '@/helpers/databaseRules' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' diff --git a/apps/builder/src/pages/api/typebots/[typebotId]/collaborators/[userId].ts b/apps/builder/src/pages/api/typebots/[typebotId]/collaborators/[userId].ts index e213cce101..97fa4e0d3d 100644 --- a/apps/builder/src/pages/api/typebots/[typebotId]/collaborators/[userId].ts +++ b/apps/builder/src/pages/api/typebots/[typebotId]/collaborators/[userId].ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { canEditGuests } from '@/helpers/databaseRules' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' diff --git a/apps/builder/src/pages/api/typebots/[typebotId]/invitations.ts b/apps/builder/src/pages/api/typebots/[typebotId]/invitations.ts index c74ea9b4fd..f7d614171c 100644 --- a/apps/builder/src/pages/api/typebots/[typebotId]/invitations.ts +++ b/apps/builder/src/pages/api/typebots/[typebotId]/invitations.ts @@ -1,5 +1,5 @@ import { CollaborationType, WorkspaceRole } from '@typebot.io/prisma' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { canReadTypebots, diff --git a/apps/builder/src/pages/api/typebots/[typebotId]/invitations/[email].ts b/apps/builder/src/pages/api/typebots/[typebotId]/invitations/[email].ts index c3f5d23225..3388186b23 100644 --- a/apps/builder/src/pages/api/typebots/[typebotId]/invitations/[email].ts +++ b/apps/builder/src/pages/api/typebots/[typebotId]/invitations/[email].ts @@ -1,5 +1,5 @@ import { Invitation } from '@typebot.io/prisma' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { canEditGuests } from '@/helpers/databaseRules' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' diff --git a/apps/builder/src/pages/api/users/[userId].ts b/apps/builder/src/pages/api/users/[userId].ts index d5b2c4a940..2de569a650 100644 --- a/apps/builder/src/pages/api/users/[userId].ts +++ b/apps/builder/src/pages/api/users/[userId].ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api' diff --git a/apps/builder/src/pages/api/users/[userId]/api-tokens.ts b/apps/builder/src/pages/api/users/[userId]/api-tokens.ts index 532a9842f4..3f3357cd3b 100644 --- a/apps/builder/src/pages/api/users/[userId]/api-tokens.ts +++ b/apps/builder/src/pages/api/users/[userId]/api-tokens.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' import { generateId } from '@typebot.io/lib' diff --git a/apps/builder/src/pages/api/users/[userId]/api-tokens/[tokenId].ts b/apps/builder/src/pages/api/users/[userId]/api-tokens/[tokenId].ts index 8ab544ee8f..078234cd47 100644 --- a/apps/builder/src/pages/api/users/[userId]/api-tokens/[tokenId].ts +++ b/apps/builder/src/pages/api/users/[userId]/api-tokens/[tokenId].ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api' diff --git a/apps/builder/src/pages/api/workspaces/[workspaceId]/invitations.ts b/apps/builder/src/pages/api/workspaces/[workspaceId]/invitations.ts index 4ffc058b66..79b9d80e46 100644 --- a/apps/builder/src/pages/api/workspaces/[workspaceId]/invitations.ts +++ b/apps/builder/src/pages/api/workspaces/[workspaceId]/invitations.ts @@ -1,5 +1,5 @@ import { WorkspaceInvitation, WorkspaceRole } from '@typebot.io/prisma' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { forbidden, diff --git a/apps/builder/src/pages/api/workspaces/[workspaceId]/invitations/[id].ts b/apps/builder/src/pages/api/workspaces/[workspaceId]/invitations/[id].ts index 244e46df2a..0a945e4ed5 100644 --- a/apps/builder/src/pages/api/workspaces/[workspaceId]/invitations/[id].ts +++ b/apps/builder/src/pages/api/workspaces/[workspaceId]/invitations/[id].ts @@ -1,5 +1,5 @@ import { WorkspaceInvitation, WorkspaceRole } from '@typebot.io/prisma' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api' diff --git a/apps/builder/src/pages/api/workspaces/[workspaceId]/members.ts b/apps/builder/src/pages/api/workspaces/[workspaceId]/members.ts index 7645094d89..3eb541af7e 100644 --- a/apps/builder/src/pages/api/workspaces/[workspaceId]/members.ts +++ b/apps/builder/src/pages/api/workspaces/[workspaceId]/members.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' import { diff --git a/apps/builder/src/pages/api/workspaces/[workspaceId]/members/[id].ts b/apps/builder/src/pages/api/workspaces/[workspaceId]/members/[id].ts index eb56b0ed62..9fde77c192 100644 --- a/apps/builder/src/pages/api/workspaces/[workspaceId]/members/[id].ts +++ b/apps/builder/src/pages/api/workspaces/[workspaceId]/members/[id].ts @@ -1,5 +1,5 @@ import { MemberInWorkspace, WorkspaceRole } from '@typebot.io/prisma' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api' diff --git a/apps/builder/tsconfig.json b/apps/builder/tsconfig.json index 5ec9b33e29..84e4ae1b48 100644 --- a/apps/builder/tsconfig.json +++ b/apps/builder/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@/*": ["src/*", "../viewer/src/*"] + "@/*": ["src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] diff --git a/apps/docs/openapi/builder/_spec_.json b/apps/docs/openapi/builder/_spec_.json index c216a1364d..f8c4fa53e3 100644 --- a/apps/docs/openapi/builder/_spec_.json +++ b/apps/docs/openapi/builder/_spec_.json @@ -1565,7 +1565,8 @@ "additionalProperties": false }, "sizeLimit": { - "type": "number" + "type": "number", + "description": "Deprecated" } }, "required": [ @@ -5952,7 +5953,8 @@ "additionalProperties": false }, "sizeLimit": { - "type": "number" + "type": "number", + "description": "Deprecated" } }, "required": [ @@ -9974,7 +9976,8 @@ "additionalProperties": false }, "sizeLimit": { - "type": "number" + "type": "number", + "description": "Deprecated" } }, "required": [ @@ -14136,7 +14139,8 @@ "additionalProperties": false }, "sizeLimit": { - "type": "number" + "type": "number", + "description": "Deprecated" } }, "required": [ @@ -18178,7 +18182,8 @@ "additionalProperties": false }, "sizeLimit": { - "type": "number" + "type": "number", + "description": "Deprecated" } }, "required": [ @@ -22275,7 +22280,8 @@ "additionalProperties": false }, "sizeLimit": { - "type": "number" + "type": "number", + "description": "Deprecated" } }, "required": [ @@ -26435,7 +26441,8 @@ "additionalProperties": false }, "sizeLimit": { - "type": "number" + "type": "number", + "description": "Deprecated" } }, "required": [ @@ -33600,12 +33607,17 @@ "presignedUrl": { "type": "string" }, + "formData": { + "type": "object", + "additionalProperties": {} + }, "fileUrl": { "type": "string" } }, "required": [ "presignedUrl", + "formData", "fileUrl" ], "additionalProperties": false diff --git a/apps/docs/openapi/chat/_spec_.json b/apps/docs/openapi/chat/_spec_.json index 5adbd7f675..cc90077c23 100644 --- a/apps/docs/openapi/chat/_spec_.json +++ b/apps/docs/openapi/chat/_spec_.json @@ -1148,7 +1148,8 @@ "additionalProperties": false }, "sizeLimit": { - "type": "number" + "type": "number", + "description": "Deprecated" } }, "required": [ @@ -5075,7 +5076,8 @@ "additionalProperties": false }, "sizeLimit": { - "type": "number" + "type": "number", + "description": "Deprecated" } }, "required": [ @@ -6348,12 +6350,17 @@ "presignedUrl": { "type": "string" }, + "formData": { + "type": "object", + "additionalProperties": {} + }, "hasReachedStorageLimit": { "type": "boolean" } }, "required": [ "presignedUrl", + "formData", "hasReachedStorageLimit" ], "additionalProperties": false @@ -6428,12 +6435,17 @@ "presignedUrl": { "type": "string" }, + "formData": { + "type": "object", + "additionalProperties": {} + }, "fileUrl": { "type": "string" } }, "required": [ "presignedUrl", + "formData", "fileUrl" ], "additionalProperties": false @@ -6499,7 +6511,10 @@ "/workspaces/{workspaceId}/whatsapp/phoneNumbers/{phoneNumberId}/webhook": { "get": { "operationId": "whatsAppRouter-subscribeWebhook", - "summary": "Subscribe WhatsApp webhook", + "summary": "Subscribe webhook", + "tags": [ + "WhatsApp" + ], "security": [ { "Authorization": [] @@ -6557,7 +6572,10 @@ }, "post": { "operationId": "whatsAppRouter-receiveMessage", - "summary": "Receive WhatsApp Message", + "summary": "Message webhook", + "tags": [ + "WhatsApp" + ], "requestBody": { "required": true, "content": { diff --git a/apps/viewer/package.json b/apps/viewer/package.json index 4838840ca5..8ef0aa53d2 100644 --- a/apps/viewer/package.json +++ b/apps/viewer/package.json @@ -16,27 +16,21 @@ "@trpc/server": "10.34.0", "@typebot.io/nextjs": "workspace:*", "@typebot.io/prisma": "workspace:*", - "@udecode/plate-common": "^21.1.5", "ai": "2.1.32", "bot-engine": "workspace:*", - "chrono-node": "2.6.6", "cors": "2.8.5", - "date-fns": "^2.30.0", - "eventsource-parser": "^1.0.0", "google-spreadsheet": "4.0.2", "got": "12.6.0", - "libphonenumber-js": "1.10.37", "next": "13.4.3", "nextjs-cors": "2.1.2", - "node-html-parser": "^6.1.5", "nodemailer": "6.9.3", "openai-edge": "1.2.2", "qs": "6.11.2", "react": "18.2.0", "react-dom": "18.2.0", - "remark-slate": "^1.8.6", "stripe": "12.13.0", - "trpc-openapi": "1.2.0" + "trpc-openapi": "1.2.0", + "@typebot.io/bot-engine": "workspace:*" }, "devDependencies": { "@faire/mjml-react": "3.3.0", @@ -53,13 +47,11 @@ "@types/papaparse": "5.3.7", "@types/qs": "6.9.7", "@types/react": "18.2.15", - "@types/sanitize-html": "2.9.0", "dotenv-cli": "^7.2.1", "eslint": "8.44.0", "eslint-config-custom": "workspace:*", "google-auth-library": "8.9.0", "next-runtime-env": "^1.6.2", - "node-fetch": "3.3.1", "papaparse": "5.4.1", "superjson": "1.12.4", "typescript": "5.1.6", diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/helpers/getAuthenticatedGoogleDoc.ts b/apps/viewer/src/features/blocks/integrations/googleSheets/helpers/getAuthenticatedGoogleDoc.ts deleted file mode 100644 index b5eaea94fd..0000000000 --- a/apps/viewer/src/features/blocks/integrations/googleSheets/helpers/getAuthenticatedGoogleDoc.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getAuthenticatedGoogleClient } from '@/lib/google-sheets' -import { TRPCError } from '@trpc/server' -import { GoogleSpreadsheet } from 'google-spreadsheet' - -export const getAuthenticatedGoogleDoc = async ({ - credentialsId, - spreadsheetId, -}: { - credentialsId?: string - spreadsheetId?: string -}) => { - if (!credentialsId || !spreadsheetId) - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'Missing credentialsId or spreadsheetId', - }) - const auth = await getAuthenticatedGoogleClient(credentialsId) - if (!auth) - throw new TRPCError({ - code: 'NOT_FOUND', - message: "Couldn't find credentials in database", - }) - return new GoogleSpreadsheet(spreadsheetId, auth) -} diff --git a/apps/viewer/src/features/blocks/logic/typebotLink/fetchLinkedTypebots.ts b/apps/viewer/src/features/blocks/logic/typebotLink/fetchLinkedTypebots.ts deleted file mode 100644 index 96b060f3ef..0000000000 --- a/apps/viewer/src/features/blocks/logic/typebotLink/fetchLinkedTypebots.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { canReadTypebots } from '@/helpers/api/dbRules' -import prisma from '@/lib/prisma' -import { User } from '@typebot.io/prisma' -import { PublicTypebot, Typebot } from '@typebot.io/schemas' - -type Props = { - isPreview?: boolean - typebotIds: string[] - user?: User -} - -export const fetchLinkedTypebots = async ({ - user, - isPreview, - typebotIds, -}: Props) => { - const linkedTypebots = ( - isPreview - ? await prisma.typebot.findMany({ - where: user - ? { - AND: [ - { id: { in: typebotIds } }, - canReadTypebots(typebotIds, user as User), - ], - } - : { id: { in: typebotIds } }, - }) - : await prisma.publicTypebot.findMany({ - where: { id: { in: typebotIds } }, - }) - ) as (Typebot | PublicTypebot)[] - return linkedTypebots -} diff --git a/apps/viewer/src/features/chat/api/sendMessage.ts b/apps/viewer/src/features/chat/api/sendMessage.ts index e79f00780a..375ad04a4f 100644 --- a/apps/viewer/src/features/chat/api/sendMessage.ts +++ b/apps/viewer/src/features/chat/api/sendMessage.ts @@ -1,15 +1,15 @@ import { publicProcedure } from '@/helpers/server/trpc' -import { saveStateToDatabase } from '../helpers/saveStateToDatabase' -import { getSession } from '../queries/getSession' -import { continueBotFlow } from '../helpers/continueBotFlow' -import { parseDynamicTheme } from '../helpers/parseDynamicTheme' -import { startSession } from '../helpers/startSession' -import { restartSession } from '../queries/restartSession' import { chatReplySchema, sendMessageInputSchema, } from '@typebot.io/schemas/features/chat/schema' import { TRPCError } from '@trpc/server' +import { getSession } from '@typebot.io/bot-engine/queries/getSession' +import { startSession } from '@typebot.io/bot-engine/startSession' +import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase' +import { restartSession } from '@typebot.io/bot-engine/queries/restartSession' +import { continueBotFlow } from '@typebot.io/bot-engine/continueBotFlow' +import { parseDynamicTheme } from '@typebot.io/bot-engine/parseDynamicTheme' export const sendMessage = publicProcedure .meta({ diff --git a/apps/viewer/src/features/chat/api/updateTypebotInSession.ts b/apps/viewer/src/features/chat/api/updateTypebotInSession.ts index f199f4ce58..5986cc2621 100644 --- a/apps/viewer/src/features/chat/api/updateTypebotInSession.ts +++ b/apps/viewer/src/features/chat/api/updateTypebotInSession.ts @@ -1,14 +1,14 @@ import { publicProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { z } from 'zod' -import { getSession } from '../queries/getSession' -import prisma from '@/lib/prisma' +import { getSession } from '@typebot.io/bot-engine/queries/getSession' import { PublicTypebot, SessionState, Typebot, Variable, } from '@typebot.io/schemas' +import prisma from '@typebot.io/lib/prisma' export const updateTypebotInSession = publicProcedure .meta({ diff --git a/apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts b/apps/viewer/src/features/fileUpload/api/deprecated/getUploadUrl.ts similarity index 98% rename from apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts rename to apps/viewer/src/features/fileUpload/api/deprecated/getUploadUrl.ts index b9b11582ca..9de92b7c7a 100644 --- a/apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts +++ b/apps/viewer/src/features/fileUpload/api/deprecated/getUploadUrl.ts @@ -1,5 +1,4 @@ import { publicProcedure } from '@/helpers/server/trpc' -import prisma from '@/lib/prisma' import { TRPCError } from '@trpc/server' import { FileInputBlock, @@ -12,6 +11,7 @@ import { byId, isDefined } from '@typebot.io/lib' import { z } from 'zod' import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy' import { env } from '@typebot.io/env' +import prisma from '@typebot.io/lib/prisma' export const getUploadUrl = publicProcedure .meta({ diff --git a/apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts b/apps/viewer/src/features/fileUpload/api/generateUploadUrl.ts similarity index 98% rename from apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts rename to apps/viewer/src/features/fileUpload/api/generateUploadUrl.ts index 175f679392..d60d2a5213 100644 --- a/apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts +++ b/apps/viewer/src/features/fileUpload/api/generateUploadUrl.ts @@ -1,10 +1,10 @@ import { publicProcedure } from '@/helpers/server/trpc' -import prisma from '@/lib/prisma' import { TRPCError } from '@trpc/server' import { z } from 'zod' import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy' import { env } from '@typebot.io/env' import { InputBlockType, publicTypebotSchema } from '@typebot.io/schemas' +import prisma from '@typebot.io/lib/prisma' export const generateUploadUrl = publicProcedure .meta({ diff --git a/apps/viewer/src/features/whatsApp/api/receiveMessage.ts b/apps/viewer/src/features/whatsapp/api/receiveMessage.ts similarity index 93% rename from apps/viewer/src/features/whatsApp/api/receiveMessage.ts rename to apps/viewer/src/features/whatsapp/api/receiveMessage.ts index ff0907e0a9..7f6f99c19f 100644 --- a/apps/viewer/src/features/whatsApp/api/receiveMessage.ts +++ b/apps/viewer/src/features/whatsapp/api/receiveMessage.ts @@ -1,8 +1,8 @@ import { publicProcedure } from '@/helpers/server/trpc' import { whatsAppWebhookRequestBodySchema } from '@typebot.io/schemas/features/whatsapp' -import { resumeWhatsAppFlow } from '../helpers/resumeWhatsAppFlow' import { z } from 'zod' import { isNotDefined } from '@typebot.io/lib' +import { resumeWhatsAppFlow } from '@typebot.io/bot-engine/whatsapp/resumeWhatsAppFlow' export const receiveMessage = publicProcedure .meta({ diff --git a/apps/viewer/src/features/whatsApp/api/router.ts b/apps/viewer/src/features/whatsapp/api/router.ts similarity index 100% rename from apps/viewer/src/features/whatsApp/api/router.ts rename to apps/viewer/src/features/whatsapp/api/router.ts diff --git a/apps/viewer/src/features/whatsApp/api/subscribeWebhook.ts b/apps/viewer/src/features/whatsapp/api/subscribeWebhook.ts similarity index 96% rename from apps/viewer/src/features/whatsApp/api/subscribeWebhook.ts rename to apps/viewer/src/features/whatsapp/api/subscribeWebhook.ts index b0e64b3990..cf0f2a77d3 100644 --- a/apps/viewer/src/features/whatsApp/api/subscribeWebhook.ts +++ b/apps/viewer/src/features/whatsapp/api/subscribeWebhook.ts @@ -1,5 +1,5 @@ import { publicProcedure } from '@/helpers/server/trpc' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { TRPCError } from '@trpc/server' import { z } from 'zod' diff --git a/apps/viewer/src/helpers/api/dbRules.ts b/apps/viewer/src/helpers/api/dbRules.ts deleted file mode 100644 index 4a006834f2..0000000000 --- a/apps/viewer/src/helpers/api/dbRules.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - CollaborationType, - Prisma, - User, - WorkspaceRole, -} from '@typebot.io/prisma' -import { env } from '@typebot.io/env' - -const parseWhereFilter = ( - typebotIds: string[] | string, - user: User, - type: 'read' | 'write' -): Prisma.TypebotWhereInput => ({ - OR: [ - { - id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds }, - collaborators: { - some: { - userId: user.id, - type: type === 'write' ? CollaborationType.WRITE : undefined, - }, - }, - }, - { - id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds }, - workspace: - (type === 'read' && user.email === env.ADMIN_EMAIL) || - env.NEXT_PUBLIC_E2E_TEST - ? undefined - : { - members: { - some: { userId: user.id, role: { not: WorkspaceRole.GUEST } }, - }, - }, - }, - ], -}) - -export const canReadTypebot = (typebotId: string, user: User) => - parseWhereFilter(typebotId, user, 'read') - -export const canWriteTypebot = (typebotId: string, user: User) => - parseWhereFilter(typebotId, user, 'write') - -export const canReadTypebots = (typebotIds: string[], user: User) => - parseWhereFilter(typebotIds, user, 'read') - -export const canWriteTypebots = (typebotIds: string[], user: User) => - parseWhereFilter(typebotIds, user, 'write') - -export const canEditGuests = (user: User, typebotId: string) => ({ - id: typebotId, - workspace: { - members: { - some: { userId: user.id, role: { not: WorkspaceRole.GUEST } }, - }, - }, -}) diff --git a/apps/viewer/src/helpers/api/isVercel.ts b/apps/viewer/src/helpers/api/isVercel.ts deleted file mode 100644 index 64a882ff96..0000000000 --- a/apps/viewer/src/helpers/api/isVercel.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { env } from '@typebot.io/env' - -export const isVercel = () => env.NEXT_PUBLIC_VERCEL_ENV diff --git a/apps/viewer/src/helpers/authenticateUser.ts b/apps/viewer/src/helpers/authenticateUser.ts index a029b2e93f..3716be806f 100644 --- a/apps/viewer/src/helpers/authenticateUser.ts +++ b/apps/viewer/src/helpers/authenticateUser.ts @@ -1,6 +1,6 @@ import { User } from '@typebot.io/prisma' import { NextApiRequest } from 'next' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' export const authenticateUser = async ( req: NextApiRequest diff --git a/apps/viewer/src/helpers/server/context.ts b/apps/viewer/src/helpers/server/context.ts index 2b5adc59f0..e0a21ac1d0 100644 --- a/apps/viewer/src/helpers/server/context.ts +++ b/apps/viewer/src/helpers/server/context.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { inferAsyncReturnType } from '@trpc/server' import * as trpcNext from '@trpc/server/adapters/next' import { User } from '@typebot.io/prisma' diff --git a/apps/viewer/src/helpers/server/routers/v1/_app.ts b/apps/viewer/src/helpers/server/routers/v1/_app.ts index df4d56d896..e1912aaefd 100644 --- a/apps/viewer/src/helpers/server/routers/v1/_app.ts +++ b/apps/viewer/src/helpers/server/routers/v1/_app.ts @@ -1,9 +1,9 @@ -import { getUploadUrl } from '@/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl' -import { generateUploadUrl } from '@/features/blocks/inputs/fileUpload/api/generateUploadUrl' import { sendMessage } from '@/features/chat/api/sendMessage' -import { whatsAppRouter } from '@/features/whatsApp/api/router' +import { whatsAppRouter } from '@/features/whatsapp/api/router' import { router } from '../../trpc' import { updateTypebotInSession } from '@/features/chat/api/updateTypebotInSession' +import { getUploadUrl } from '@/features/fileUpload/api/deprecated/getUploadUrl' +import { generateUploadUrl } from '@/features/fileUpload/api/generateUploadUrl' export const appRouter = router({ sendMessage, diff --git a/apps/viewer/src/lib/google-sheets.ts b/apps/viewer/src/lib/google-sheets.ts index 0a573e7b1b..3dd807650d 100644 --- a/apps/viewer/src/lib/google-sheets.ts +++ b/apps/viewer/src/lib/google-sheets.ts @@ -3,8 +3,8 @@ import { OAuth2Client, Credentials } from 'google-auth-library' import { GoogleSheetsCredentials } from '@typebot.io/schemas' import { isDefined } from '@typebot.io/lib' import { decrypt, encrypt } from '@typebot.io/lib/api' -import prisma from './prisma' import { env } from '@typebot.io/env' +import prisma from '@typebot.io/lib/prisma' export const getAuthenticatedGoogleClient = async ( credentialsId: string diff --git a/apps/viewer/src/pages/[[...publicId]].tsx b/apps/viewer/src/pages/[[...publicId]].tsx index 2f6f2ae786..6cab44d98b 100644 --- a/apps/viewer/src/pages/[[...publicId]].tsx +++ b/apps/viewer/src/pages/[[...publicId]].tsx @@ -3,10 +3,10 @@ import { ErrorPage } from '@/components/ErrorPage' import { NotFoundPage } from '@/components/NotFoundPage' import { GetServerSideProps, GetServerSidePropsContext } from 'next' import { isNotDefined } from '@typebot.io/lib' -import prisma from '../lib/prisma' import { TypebotPageProps, TypebotPageV2 } from '@/components/TypebotPageV2' import { TypebotPageV3, TypebotV3PageProps } from '@/components/TypebotPageV3' import { env } from '@typebot.io/env' +import prisma from '@typebot.io/lib/prisma' // Browsers that doesn't support ES modules and/or web components const incompatibleBrowsers = [ diff --git a/apps/viewer/src/pages/api/integrations/google-sheets/spreadsheets/[spreadsheetId]/sheets/[sheetId].ts b/apps/viewer/src/pages/api/integrations/google-sheets/spreadsheets/[spreadsheetId]/sheets/[sheetId].ts index de1fba4804..c2e3f9db01 100644 --- a/apps/viewer/src/pages/api/integrations/google-sheets/spreadsheets/[spreadsheetId]/sheets/[sheetId].ts +++ b/apps/viewer/src/pages/api/integrations/google-sheets/spreadsheets/[spreadsheetId]/sheets/[sheetId].ts @@ -17,8 +17,8 @@ import { } from '@typebot.io/schemas' import Cors from 'cors' import { getAuthenticatedGoogleClient } from '@/lib/google-sheets' -import { saveErrorLog } from '@/features/logs/saveErrorLog' -import { saveSuccessLog } from '@/features/logs/saveSuccessLog' +import { saveErrorLog } from '@typebot.io/bot-engine/logs/saveErrorLog' +import { saveSuccessLog } from '@typebot.io/bot-engine/logs/saveSuccessLog' const cors = initMiddleware(Cors()) diff --git a/apps/viewer/src/pages/api/integrations/openai/streamer.ts b/apps/viewer/src/pages/api/integrations/openai/streamer.ts index 41bd46fd57..ad74f53298 100644 --- a/apps/viewer/src/pages/api/integrations/openai/streamer.ts +++ b/apps/viewer/src/pages/api/integrations/openai/streamer.ts @@ -1,9 +1,9 @@ -import { getChatCompletionStream } from '@/features/blocks/integrations/openai/getChatCompletionStream' import { connect } from '@planetscale/database' import { env } from '@typebot.io/env' import { IntegrationBlockType, SessionState } from '@typebot.io/schemas' import { StreamingTextResponse } from 'ai' import { ChatCompletionRequestMessage } from 'openai-edge' +import { getChatCompletionStream } from '@typebot.io/bot-engine/blocks/integrations/openai/getChatCompletionStream' export const config = { runtime: 'edge', diff --git a/apps/viewer/src/pages/api/integrations/stripe/createPaymentIntent.ts b/apps/viewer/src/pages/api/integrations/stripe/createPaymentIntent.ts index 1641557194..615549754c 100644 --- a/apps/viewer/src/pages/api/integrations/stripe/createPaymentIntent.ts +++ b/apps/viewer/src/pages/api/integrations/stripe/createPaymentIntent.ts @@ -14,8 +14,8 @@ import { StripeCredentials, Variable, } from '@typebot.io/schemas' -import prisma from '@/lib/prisma' -import { parseVariables } from '@/features/variables/parseVariables' +import prisma from '@typebot.io/lib/prisma' +import { parseVariables } from '@typebot.io/bot-engine/variables/parseVariables' const cors = initMiddleware(Cors()) diff --git a/apps/viewer/src/pages/api/publicTypebots/[typebotId].ts b/apps/viewer/src/pages/api/publicTypebots/[typebotId].ts index c82075e071..460f239539 100644 --- a/apps/viewer/src/pages/api/publicTypebots/[typebotId].ts +++ b/apps/viewer/src/pages/api/publicTypebots/[typebotId].ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import Cors from 'cors' import { initMiddleware, methodNotAllowed, notFound } from '@typebot.io/lib/api' diff --git a/apps/viewer/src/pages/api/typebots.ts b/apps/viewer/src/pages/api/typebots.ts index 9da5e5ac3c..de1d01ae57 100644 --- a/apps/viewer/src/pages/api/typebots.ts +++ b/apps/viewer/src/pages/api/typebots.ts @@ -1,5 +1,5 @@ import { authenticateUser } from '@/helpers/authenticateUser' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { methodNotAllowed } from '@typebot.io/lib/api' diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts index 2087298d0b..6b7b328948 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts @@ -17,14 +17,14 @@ import { parseAnswers } from '@typebot.io/lib/results' import { initMiddleware, methodNotAllowed, notFound } from '@typebot.io/lib/api' import { stringify } from 'qs' import Cors from 'cors' -import prisma from '@/lib/prisma' -import { parseVariables } from '@/features/variables/parseVariables' -import { parseSampleResult } from '@/features/blocks/integrations/webhook/parseSampleResult' -import { fetchLinkedTypebots } from '@/features/blocks/logic/typebotLink/fetchLinkedTypebots' -import { getPreviouslyLinkedTypebots } from '@/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots' -import { saveErrorLog } from '@/features/logs/saveErrorLog' -import { saveSuccessLog } from '@/features/logs/saveSuccessLog' +import prisma from '@typebot.io/lib/prisma' import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums' +import { fetchLinkedTypebots } from '@typebot.io/bot-engine/blocks/logic/typebotLink/fetchLinkedTypebots' +import { getPreviouslyLinkedTypebots } from '@typebot.io/bot-engine/blocks/logic/typebotLink/getPreviouslyLinkedTypebots' +import { parseVariables } from '@typebot.io/bot-engine/variables/parseVariables' +import { saveErrorLog } from '@typebot.io/bot-engine/logs/saveErrorLog' +import { saveSuccessLog } from '@typebot.io/bot-engine/logs/saveSuccessLog' +import { parseSampleResult } from '@typebot.io/bot-engine/blocks/integrations/webhook/parseSampleResult' const cors = initMiddleware(Cors()) @@ -128,10 +128,10 @@ export const executeWebhook = convertKeyValueTableToObject(webhook.queryParams, variables) ) const contentType = headers ? headers['Content-Type'] : undefined - const linkedTypebotsParents = await fetchLinkedTypebots({ + const linkedTypebotsParents = (await fetchLinkedTypebots({ isPreview: !('typebotId' in typebot), typebotIds: parentTypebotIds, - }) + })) as (Typebot | PublicTypebot)[] const linkedTypebotsChildren = await getPreviouslyLinkedTypebots({ isPreview: !('typebotId' in typebot), typebots: [typebot], diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx b/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx index 4c8d28d3af..0db54d9534 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx @@ -14,10 +14,10 @@ import Cors from 'cors' import Mail from 'nodemailer/lib/mailer' import { DefaultBotNotificationEmail } from '@typebot.io/emails' import { render } from '@faire/mjml-react/utils/render' -import prisma from '@/lib/prisma' -import { saveErrorLog } from '@/features/logs/saveErrorLog' -import { saveSuccessLog } from '@/features/logs/saveSuccessLog' +import prisma from '@typebot.io/lib/prisma' import { env } from '@typebot.io/env' +import { saveErrorLog } from '@typebot.io/bot-engine/logs/saveErrorLog' +import { saveSuccessLog } from '@typebot.io/bot-engine/logs/saveSuccessLog' const cors = initMiddleware(Cors()) diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/results.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/results.ts index d9fd0a2997..7404914468 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/results.ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/results.ts @@ -1,5 +1,5 @@ import { authenticateUser } from '@/helpers/authenticateUser' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { ResultWithAnswers } from '@typebot.io/schemas' import { NextApiRequest, NextApiResponse } from 'next' import { methodNotAllowed } from '@typebot.io/lib/api' diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/results/[resultId].ts b/apps/viewer/src/pages/api/typebots/[typebotId]/results/[resultId].ts index 750aa4508e..1fd70e8832 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/results/[resultId].ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/results/[resultId].ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { Result } from '@typebot.io/schemas' import { NextApiRequest, NextApiResponse } from 'next' import { methodNotAllowed } from '@typebot.io/lib/api' diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/results/[resultId]/answers.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/results/[resultId]/answers.ts index 9763cd5bb1..4affeeb316 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/results/[resultId]/answers.ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/results/[resultId]/answers.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { Answer } from '@typebot.io/prisma' import { got } from 'got' import { NextApiRequest, NextApiResponse } from 'next' diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/webhookBlocks.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/webhookBlocks.ts index 7831407ef9..9ea9d21ce3 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/webhookBlocks.ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/webhookBlocks.ts @@ -1,5 +1,5 @@ import { authenticateUser } from '@/helpers/authenticateUser' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { Group, WebhookBlock } from '@typebot.io/schemas' import { NextApiRequest, NextApiResponse } from 'next' import { byId, isWebhookBlock, parseGroupTitle } from '@typebot.io/lib' diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/webhookSteps.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/webhookSteps.ts index 3858c1fb4c..ad2734330a 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/webhookSteps.ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/webhookSteps.ts @@ -1,5 +1,5 @@ import { authenticateUser } from '@/helpers/authenticateUser' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { Group, WebhookBlock } from '@typebot.io/schemas' import { NextApiRequest, NextApiResponse } from 'next' import { diff --git a/apps/viewer/src/pages/old/[[...publicId]].tsx b/apps/viewer/src/pages/old/[[...publicId]].tsx index 083f94456e..629b4c8ded 100644 --- a/apps/viewer/src/pages/old/[[...publicId]].tsx +++ b/apps/viewer/src/pages/old/[[...publicId]].tsx @@ -3,8 +3,8 @@ import { ErrorPage } from '@/components/ErrorPage' import { NotFoundPage } from '@/components/NotFoundPage' import { GetServerSideProps, GetServerSidePropsContext } from 'next' import { isDefined, isNotDefined, omit } from '@typebot.io/lib' -import prisma from '../../lib/prisma' import { TypebotPageProps, TypebotPageV2 } from '@/components/TypebotPageV2' +import prisma from '@typebot.io/lib/prisma' export const getServerSideProps: GetServerSideProps = async ( context: GetServerSidePropsContext diff --git a/apps/viewer/src/features/chat/chat.spec.ts b/apps/viewer/src/test/chat.spec.ts similarity index 99% rename from apps/viewer/src/features/chat/chat.spec.ts rename to apps/viewer/src/test/chat.spec.ts index 99f034c7eb..eee71307db 100644 --- a/apps/viewer/src/features/chat/chat.spec.ts +++ b/apps/viewer/src/test/chat.spec.ts @@ -1,7 +1,7 @@ import { getTestAsset } from '@/test/utils/playwright' import test, { expect } from '@playwright/test' import { createId } from '@paralleldrive/cuid2' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { SendMessageInput } from '@typebot.io/schemas' import { createWebhook, diff --git a/apps/viewer/src/features/blocks/integrations/chatwoot/chatwoot.spec.ts b/apps/viewer/src/test/chatwoot.spec.ts similarity index 100% rename from apps/viewer/src/features/blocks/integrations/chatwoot/chatwoot.spec.ts rename to apps/viewer/src/test/chatwoot.spec.ts diff --git a/apps/viewer/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts b/apps/viewer/src/test/fileUpload.spec.ts similarity index 100% rename from apps/viewer/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts rename to apps/viewer/src/test/fileUpload.spec.ts diff --git a/apps/viewer/src/features/results/results.spec.ts b/apps/viewer/src/test/results.spec.ts similarity index 100% rename from apps/viewer/src/features/results/results.spec.ts rename to apps/viewer/src/test/results.spec.ts diff --git a/apps/viewer/src/features/blocks/integrations/sendEmail/sendEmail.spec.ts b/apps/viewer/src/test/sendEmail.spec.ts similarity index 94% rename from apps/viewer/src/features/blocks/integrations/sendEmail/sendEmail.spec.ts rename to apps/viewer/src/test/sendEmail.spec.ts index 0105897f76..f9e8dd16e4 100644 --- a/apps/viewer/src/features/blocks/integrations/sendEmail/sendEmail.spec.ts +++ b/apps/viewer/src/test/sendEmail.spec.ts @@ -1,10 +1,10 @@ import test, { expect } from '@playwright/test' -import { createSmtpCredentials } from '../../../../test/utils/databaseActions' import { createId } from '@paralleldrive/cuid2' import { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions' import { getTestAsset } from '@/test/utils/playwright' import { SmtpCredentials } from '@typebot.io/schemas' import { env } from '@typebot.io/env' +import { createSmtpCredentials } from './utils/databaseActions' export const mockSmtpCredentials: SmtpCredentials['data'] = { from: { diff --git a/apps/viewer/src/features/settings/settings.spec.ts b/apps/viewer/src/test/settings.spec.ts similarity index 100% rename from apps/viewer/src/features/settings/settings.spec.ts rename to apps/viewer/src/test/settings.spec.ts diff --git a/apps/viewer/src/features/blocks/logic/typebotLink/typebotLink.spec.ts b/apps/viewer/src/test/typebotLink.spec.ts similarity index 100% rename from apps/viewer/src/features/blocks/logic/typebotLink/typebotLink.spec.ts rename to apps/viewer/src/test/typebotLink.spec.ts diff --git a/apps/viewer/src/features/variables/variables.spec.ts b/apps/viewer/src/test/variables.spec.ts similarity index 100% rename from apps/viewer/src/features/variables/variables.spec.ts rename to apps/viewer/src/test/variables.spec.ts diff --git a/apps/viewer/src/features/blocks/integrations/webhook/webhook.spec.ts b/apps/viewer/src/test/webhook.spec.ts similarity index 100% rename from apps/viewer/src/features/blocks/integrations/webhook/webhook.spec.ts rename to apps/viewer/src/test/webhook.spec.ts diff --git a/apps/viewer/src/trpc/generateOpenApi.ts b/apps/viewer/src/trpc/generateOpenApi.ts new file mode 100644 index 0000000000..88cee9df04 --- /dev/null +++ b/apps/viewer/src/trpc/generateOpenApi.ts @@ -0,0 +1,15 @@ +import { generateOpenApiDocument } from 'trpc-openapi' +import { writeFileSync } from 'fs' +import { appRouter } from '@/helpers/server/routers/v1/_app' + +const openApiDocument = generateOpenApiDocument(appRouter, { + title: 'Chat API', + version: '1.0.0', + baseUrl: 'https://typebot.io/api/v1', + docsUrl: 'https://docs.typebot.io/api', +}) + +writeFileSync( + './openapi/chat/_spec_.json', + JSON.stringify(openApiDocument, null, 2) +) diff --git a/apps/viewer/src/features/chat/helpers/addEdgeToTypebot.ts b/packages/bot-engine/addEdgeToTypebot.ts similarity index 100% rename from apps/viewer/src/features/chat/helpers/addEdgeToTypebot.ts rename to packages/bot-engine/addEdgeToTypebot.ts diff --git a/apps/viewer/src/features/blocks/inputs/buttons/filterChoiceItems.ts b/packages/bot-engine/blocks/inputs/buttons/filterChoiceItems.ts similarity index 100% rename from apps/viewer/src/features/blocks/inputs/buttons/filterChoiceItems.ts rename to packages/bot-engine/blocks/inputs/buttons/filterChoiceItems.ts diff --git a/apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts b/packages/bot-engine/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts similarity index 82% rename from apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts rename to packages/bot-engine/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts index 313fe5278b..cc7cc69cfb 100644 --- a/apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts +++ b/packages/bot-engine/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts @@ -5,10 +5,10 @@ import { ItemType, } from '@typebot.io/schemas' import { isDefined } from '@typebot.io/lib' -import { deepParseVariables } from '@/features/variables/deepParseVariable' -import { transformStringVariablesToList } from '@/features/variables/transformVariablesToList' -import { updateVariables } from '@/features/variables/updateVariables' import { filterChoiceItems } from './filterChoiceItems' +import { deepParseVariables } from '../../../variables/deepParseVariables' +import { transformStringVariablesToList } from '../../../variables/transformVariablesToList' +import { updateVariablesInSession } from '../../../variables/updateVariablesInSession' export const injectVariableValuesInButtonsInputBlock = (state: SessionState) => @@ -43,7 +43,7 @@ const getVariableValue = const [transformedVariable] = transformStringVariablesToList(variables)([ variable.id, ]) - updateVariables(state)([transformedVariable]) + updateVariablesInSession(state)([transformedVariable]) return transformedVariable.value as string[] } return variable.value diff --git a/apps/viewer/src/features/blocks/inputs/buttons/parseButtonsReply.ts b/packages/bot-engine/blocks/inputs/buttons/parseButtonsReply.ts similarity index 98% rename from apps/viewer/src/features/blocks/inputs/buttons/parseButtonsReply.ts rename to packages/bot-engine/blocks/inputs/buttons/parseButtonsReply.ts index 9d2ce50886..9624c12407 100644 --- a/apps/viewer/src/features/blocks/inputs/buttons/parseButtonsReply.ts +++ b/packages/bot-engine/blocks/inputs/buttons/parseButtonsReply.ts @@ -1,6 +1,6 @@ import { ChoiceInputBlock, SessionState } from '@typebot.io/schemas' import { injectVariableValuesInButtonsInputBlock } from './injectVariableValuesInButtonsInputBlock' -import { ParsedReply } from '@/features/chat/types' +import { ParsedReply } from '../../../types' export const parseButtonsReply = (state: SessionState) => diff --git a/apps/viewer/src/features/blocks/inputs/date/parseDateInput.ts b/packages/bot-engine/blocks/inputs/date/parseDateInput.ts similarity index 84% rename from apps/viewer/src/features/blocks/inputs/date/parseDateInput.ts rename to packages/bot-engine/blocks/inputs/date/parseDateInput.ts index d276da72e1..861d80ecd4 100644 --- a/apps/viewer/src/features/blocks/inputs/date/parseDateInput.ts +++ b/packages/bot-engine/blocks/inputs/date/parseDateInput.ts @@ -1,12 +1,12 @@ -import { getPrefilledInputValue } from '@/features/chat/helpers/getPrefilledValue' -import { deepParseVariables } from '@/features/variables/deepParseVariable' -import { parseVariables } from '@/features/variables/parseVariables' +import { getPrefilledInputValue } from '../../../getPrefilledValue' import { DateInputBlock, DateInputOptions, SessionState, Variable, } from '@typebot.io/schemas' +import { deepParseVariables } from '../../../variables/deepParseVariables' +import { parseVariables } from '../../../variables/parseVariables' export const parseDateInput = (state: SessionState) => (block: DateInputBlock) => { diff --git a/apps/viewer/src/features/blocks/inputs/date/parseDateReply.ts b/packages/bot-engine/blocks/inputs/date/parseDateReply.ts similarity index 96% rename from apps/viewer/src/features/blocks/inputs/date/parseDateReply.ts rename to packages/bot-engine/blocks/inputs/date/parseDateReply.ts index e669775f98..cc620093f8 100644 --- a/apps/viewer/src/features/blocks/inputs/date/parseDateReply.ts +++ b/packages/bot-engine/blocks/inputs/date/parseDateReply.ts @@ -1,4 +1,4 @@ -import { ParsedReply } from '@/features/chat/types' +import { ParsedReply } from '../../../types' import { DateInputBlock } from '@typebot.io/schemas' import { parse as chronoParse } from 'chrono-node' import { format } from 'date-fns' diff --git a/apps/viewer/src/features/blocks/inputs/email/validateEmail.ts b/packages/bot-engine/blocks/inputs/email/validateEmail.ts similarity index 100% rename from apps/viewer/src/features/blocks/inputs/email/validateEmail.ts rename to packages/bot-engine/blocks/inputs/email/validateEmail.ts diff --git a/apps/viewer/src/features/blocks/inputs/number/validateNumber.ts b/packages/bot-engine/blocks/inputs/number/validateNumber.ts similarity index 100% rename from apps/viewer/src/features/blocks/inputs/number/validateNumber.ts rename to packages/bot-engine/blocks/inputs/number/validateNumber.ts diff --git a/apps/viewer/src/features/blocks/inputs/payment/computePaymentInputRuntimeOptions.ts b/packages/bot-engine/blocks/inputs/payment/computePaymentInputRuntimeOptions.ts similarity index 96% rename from apps/viewer/src/features/blocks/inputs/payment/computePaymentInputRuntimeOptions.ts rename to packages/bot-engine/blocks/inputs/payment/computePaymentInputRuntimeOptions.ts index 20549d27e0..d9033fc19f 100644 --- a/apps/viewer/src/features/blocks/inputs/payment/computePaymentInputRuntimeOptions.ts +++ b/packages/bot-engine/blocks/inputs/payment/computePaymentInputRuntimeOptions.ts @@ -1,5 +1,3 @@ -import { parseVariables } from '@/features/variables/parseVariables' -import prisma from '@/lib/prisma' import { TRPCError } from '@trpc/server' import { PaymentInputOptions, @@ -9,6 +7,8 @@ import { } from '@typebot.io/schemas' import Stripe from 'stripe' import { decrypt } from '@typebot.io/lib/api/encryption' +import { parseVariables } from '../../../variables/parseVariables' +import prisma from '@typebot.io/lib/prisma' export const computePaymentInputRuntimeOptions = (state: SessionState) => (options: PaymentInputOptions) => diff --git a/apps/viewer/src/features/blocks/inputs/phone/formatPhoneNumber.ts b/packages/bot-engine/blocks/inputs/phone/formatPhoneNumber.ts similarity index 100% rename from apps/viewer/src/features/blocks/inputs/phone/formatPhoneNumber.ts rename to packages/bot-engine/blocks/inputs/phone/formatPhoneNumber.ts diff --git a/apps/viewer/src/features/blocks/inputs/pictureChoice/filterPictureChoiceItems.ts b/packages/bot-engine/blocks/inputs/pictureChoice/filterPictureChoiceItems.ts similarity index 100% rename from apps/viewer/src/features/blocks/inputs/pictureChoice/filterPictureChoiceItems.ts rename to packages/bot-engine/blocks/inputs/pictureChoice/filterPictureChoiceItems.ts diff --git a/apps/viewer/src/features/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts b/packages/bot-engine/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts similarity index 96% rename from apps/viewer/src/features/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts rename to packages/bot-engine/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts index d5fd257ce3..8d29496eaf 100644 --- a/apps/viewer/src/features/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts +++ b/packages/bot-engine/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts @@ -5,8 +5,8 @@ import { Variable, } from '@typebot.io/schemas' import { isDefined } from '@typebot.io/lib' -import { deepParseVariables } from '@/features/variables/deepParseVariable' import { filterPictureChoiceItems } from './filterPictureChoiceItems' +import { deepParseVariables } from '../../../variables/deepParseVariables' export const injectVariableValuesInPictureChoiceBlock = (variables: Variable[]) => diff --git a/apps/viewer/src/features/blocks/inputs/pictureChoice/parsePictureChoicesReply.ts b/packages/bot-engine/blocks/inputs/pictureChoice/parsePictureChoicesReply.ts similarity index 98% rename from apps/viewer/src/features/blocks/inputs/pictureChoice/parsePictureChoicesReply.ts rename to packages/bot-engine/blocks/inputs/pictureChoice/parsePictureChoicesReply.ts index 956edcc350..f98a4ff0e6 100644 --- a/apps/viewer/src/features/blocks/inputs/pictureChoice/parsePictureChoicesReply.ts +++ b/packages/bot-engine/blocks/inputs/pictureChoice/parsePictureChoicesReply.ts @@ -1,5 +1,5 @@ import { PictureChoiceBlock, SessionState } from '@typebot.io/schemas' -import { ParsedReply } from '@/features/chat/types' +import { ParsedReply } from '../../../types' import { injectVariableValuesInPictureChoiceBlock } from './injectVariableValuesInPictureChoiceBlock' export const parsePictureChoicesReply = diff --git a/apps/viewer/src/features/blocks/inputs/rating/validateRatingReply.ts b/packages/bot-engine/blocks/inputs/rating/validateRatingReply.ts similarity index 100% rename from apps/viewer/src/features/blocks/inputs/rating/validateRatingReply.ts rename to packages/bot-engine/blocks/inputs/rating/validateRatingReply.ts diff --git a/apps/viewer/src/features/blocks/inputs/url/validateUrl.ts b/packages/bot-engine/blocks/inputs/url/validateUrl.ts similarity index 100% rename from apps/viewer/src/features/blocks/inputs/url/validateUrl.ts rename to packages/bot-engine/blocks/inputs/url/validateUrl.ts diff --git a/apps/viewer/src/features/blocks/integrations/chatwoot/executeChatwootBlock.ts b/packages/bot-engine/blocks/integrations/chatwoot/executeChatwootBlock.ts similarity index 91% rename from apps/viewer/src/features/blocks/integrations/chatwoot/executeChatwootBlock.ts rename to packages/bot-engine/blocks/integrations/chatwoot/executeChatwootBlock.ts index 0e167ba45a..a5a87c22fe 100644 --- a/apps/viewer/src/features/blocks/integrations/chatwoot/executeChatwootBlock.ts +++ b/packages/bot-engine/blocks/integrations/chatwoot/executeChatwootBlock.ts @@ -1,7 +1,4 @@ -import { ExecuteIntegrationResponse } from '@/features/chat/types' -import { extractVariablesFromText } from '@/features/variables/extractVariablesFromText' -import { parseGuessedValueType } from '@/features/variables/parseGuessedValueType' -import { parseVariables } from '@/features/variables/parseVariables' +import { ExecuteIntegrationResponse } from '../../../types' import { env } from '@typebot.io/env' import { isDefined } from '@typebot.io/lib' import { @@ -9,6 +6,9 @@ import { ChatwootOptions, SessionState, } from '@typebot.io/schemas' +import { extractVariablesFromText } from '../../../variables/extractVariablesFromText' +import { parseGuessedValueType } from '../../../variables/parseGuessedValueType' +import { parseVariables } from '../../../variables/parseVariables' const parseSetUserCode = (user: ChatwootOptions['user'], resultId: string) => user?.email || user?.id diff --git a/apps/viewer/src/features/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock.ts b/packages/bot-engine/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock.ts similarity index 81% rename from apps/viewer/src/features/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock.ts rename to packages/bot-engine/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock.ts index 9ccc87abd0..33b6383a7b 100644 --- a/apps/viewer/src/features/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock.ts +++ b/packages/bot-engine/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock.ts @@ -1,6 +1,6 @@ -import { ExecuteIntegrationResponse } from '@/features/chat/types' -import { deepParseVariables } from '@/features/variables/deepParseVariable' +import { ExecuteIntegrationResponse } from '../../../types' import { GoogleAnalyticsBlock, SessionState } from '@typebot.io/schemas' +import { deepParseVariables } from '../../../variables/deepParseVariables' export const executeGoogleAnalyticsBlock = ( state: SessionState, diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/executeGoogleSheetBlock.ts b/packages/bot-engine/blocks/integrations/googleSheets/executeGoogleSheetBlock.ts similarity index 93% rename from apps/viewer/src/features/blocks/integrations/googleSheets/executeGoogleSheetBlock.ts rename to packages/bot-engine/blocks/integrations/googleSheets/executeGoogleSheetBlock.ts index aa9989e1d7..cbef8e2a29 100644 --- a/apps/viewer/src/features/blocks/integrations/googleSheets/executeGoogleSheetBlock.ts +++ b/packages/bot-engine/blocks/integrations/googleSheets/executeGoogleSheetBlock.ts @@ -6,7 +6,7 @@ import { import { insertRow } from './insertRow' import { updateRow } from './updateRow' import { getRow } from './getRow' -import { ExecuteIntegrationResponse } from '@/features/chat/types' +import { ExecuteIntegrationResponse } from '../../../types' export const executeGoogleSheetBlock = async ( state: SessionState, diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts b/packages/bot-engine/blocks/integrations/googleSheets/getRow.ts similarity index 90% rename from apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts rename to packages/bot-engine/blocks/integrations/googleSheets/getRow.ts index 82b67ea128..b693256cdd 100644 --- a/apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts +++ b/packages/bot-engine/blocks/integrations/googleSheets/getRow.ts @@ -6,10 +6,10 @@ import { } from '@typebot.io/schemas' import { isNotEmpty, byId } from '@typebot.io/lib' import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc' -import { ExecuteIntegrationResponse } from '@/features/chat/types' -import { updateVariables } from '@/features/variables/updateVariables' -import { deepParseVariables } from '@/features/variables/deepParseVariable' +import { ExecuteIntegrationResponse } from '../../../types' import { matchFilter } from './helpers/matchFilter' +import { deepParseVariables } from '../../../variables/deepParseVariables' +import { updateVariablesInSession } from '../../../variables/updateVariablesInSession' export const getRow = async ( state: SessionState, @@ -75,7 +75,7 @@ export const getRow = async ( }, [] ) - const newSessionState = updateVariables(state)(newVariables) + const newSessionState = updateVariablesInSession(state)(newVariables) return { outgoingEdgeId, newSessionState, diff --git a/packages/bot-engine/blocks/integrations/googleSheets/helpers/getAuthenticatedGoogleDoc.ts b/packages/bot-engine/blocks/integrations/googleSheets/helpers/getAuthenticatedGoogleDoc.ts new file mode 100644 index 0000000000..5602831685 --- /dev/null +++ b/packages/bot-engine/blocks/integrations/googleSheets/helpers/getAuthenticatedGoogleDoc.ts @@ -0,0 +1,75 @@ +import { TRPCError } from '@trpc/server' +import { env } from '@typebot.io/env' +import { decrypt, encrypt } from '@typebot.io/lib/api/encryption' +import { isDefined } from '@typebot.io/lib/utils' +import { GoogleSheetsCredentials } from '@typebot.io/schemas/features/blocks/integrations/googleSheets/schemas' +import { Credentials as CredentialsFromDb } from '@typebot.io/prisma' +import { GoogleSpreadsheet } from 'google-spreadsheet' +import { OAuth2Client, Credentials } from 'google-auth-library' +import prisma from '@typebot.io/lib/prisma' + +export const getAuthenticatedGoogleDoc = async ({ + credentialsId, + spreadsheetId, +}: { + credentialsId?: string + spreadsheetId?: string +}) => { + if (!credentialsId || !spreadsheetId) + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Missing credentialsId or spreadsheetId', + }) + const auth = await getAuthenticatedGoogleClient(credentialsId) + if (!auth) + throw new TRPCError({ + code: 'NOT_FOUND', + message: "Couldn't find credentials in database", + }) + return new GoogleSpreadsheet(spreadsheetId, auth) +} + +const getAuthenticatedGoogleClient = async ( + credentialsId: string +): Promise => { + const credentials = (await prisma.credentials.findFirst({ + where: { id: credentialsId }, + })) as CredentialsFromDb | undefined + if (!credentials) return + const data = (await decrypt( + credentials.data, + credentials.iv + )) as GoogleSheetsCredentials['data'] + + const oauth2Client = new OAuth2Client( + env.GOOGLE_CLIENT_ID, + env.GOOGLE_CLIENT_SECRET, + `${env.NEXTAUTH_URL}/api/credentials/google-sheets/callback` + ) + oauth2Client.setCredentials(data) + oauth2Client.on('tokens', updateTokens(credentialsId, data)) + return oauth2Client +} + +const updateTokens = + ( + credentialsId: string, + existingCredentials: GoogleSheetsCredentials['data'] + ) => + async (credentials: Credentials) => { + if ( + isDefined(existingCredentials.id_token) && + credentials.id_token !== existingCredentials.id_token + ) + return + const newCredentials: GoogleSheetsCredentials['data'] = { + ...existingCredentials, + expiry_date: credentials.expiry_date, + access_token: credentials.access_token, + } + const { encryptedData, iv } = await encrypt(newCredentials) + await prisma.credentials.updateMany({ + where: { id: credentialsId }, + data: { data: encryptedData, iv }, + }) + } diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/helpers/matchFilter.ts b/packages/bot-engine/blocks/integrations/googleSheets/helpers/matchFilter.ts similarity index 100% rename from apps/viewer/src/features/blocks/integrations/googleSheets/helpers/matchFilter.ts rename to packages/bot-engine/blocks/integrations/googleSheets/helpers/matchFilter.ts diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/helpers/parseCellValues.ts b/packages/bot-engine/blocks/integrations/googleSheets/helpers/parseCellValues.ts similarity index 84% rename from apps/viewer/src/features/blocks/integrations/googleSheets/helpers/parseCellValues.ts rename to packages/bot-engine/blocks/integrations/googleSheets/helpers/parseCellValues.ts index f5bc604503..e93ca3a751 100644 --- a/apps/viewer/src/features/blocks/integrations/googleSheets/helpers/parseCellValues.ts +++ b/packages/bot-engine/blocks/integrations/googleSheets/helpers/parseCellValues.ts @@ -1,5 +1,5 @@ -import { parseVariables } from '@/features/variables/parseVariables' import { Variable, Cell } from '@typebot.io/schemas' +import { parseVariables } from '../../../../variables/parseVariables' export const parseCellValues = (variables: Variable[]) => diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/insertRow.ts b/packages/bot-engine/blocks/integrations/googleSheets/insertRow.ts similarity index 94% rename from apps/viewer/src/features/blocks/integrations/googleSheets/insertRow.ts rename to packages/bot-engine/blocks/integrations/googleSheets/insertRow.ts index bbdff72169..59f16c1487 100644 --- a/apps/viewer/src/features/blocks/integrations/googleSheets/insertRow.ts +++ b/packages/bot-engine/blocks/integrations/googleSheets/insertRow.ts @@ -5,7 +5,7 @@ import { } from '@typebot.io/schemas' import { parseCellValues } from './helpers/parseCellValues' import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc' -import { ExecuteIntegrationResponse } from '@/features/chat/types' +import { ExecuteIntegrationResponse } from '../../../types' export const insertRow = async ( state: SessionState, diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts b/packages/bot-engine/blocks/integrations/googleSheets/updateRow.ts similarity index 93% rename from apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts rename to packages/bot-engine/blocks/integrations/googleSheets/updateRow.ts index 6b5f16052f..f2094ecc5f 100644 --- a/apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts +++ b/packages/bot-engine/blocks/integrations/googleSheets/updateRow.ts @@ -5,9 +5,9 @@ import { } from '@typebot.io/schemas' import { parseCellValues } from './helpers/parseCellValues' import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc' -import { deepParseVariables } from '@/features/variables/deepParseVariable' -import { ExecuteIntegrationResponse } from '@/features/chat/types' +import { ExecuteIntegrationResponse } from '../../../types' import { matchFilter } from './helpers/matchFilter' +import { deepParseVariables } from '../../../variables/deepParseVariables' export const updateRow = async ( state: SessionState, diff --git a/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts b/packages/bot-engine/blocks/integrations/openai/createChatCompletionOpenAI.ts similarity index 91% rename from apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts rename to packages/bot-engine/blocks/integrations/openai/createChatCompletionOpenAI.ts index c6c4a71d57..01366c1df9 100644 --- a/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts +++ b/packages/bot-engine/blocks/integrations/openai/createChatCompletionOpenAI.ts @@ -1,5 +1,3 @@ -import { ExecuteIntegrationResponse } from '@/features/chat/types' -import prisma from '@/lib/prisma' import { Block, BubbleBlockType, @@ -13,12 +11,14 @@ import { } from '@typebot.io/schemas/features/blocks/integrations/openai' import { byId, isEmpty } from '@typebot.io/lib' import { decrypt, isCredentialsV2 } from '@typebot.io/lib/api/encryption' -import { updateVariables } from '@/features/variables/updateVariables' -import { parseVariableNumber } from '@/features/variables/parseVariableNumber' import { resumeChatCompletion } from './resumeChatCompletion' import { parseChatCompletionMessages } from './parseChatCompletionMessages' import { executeChatCompletionOpenAIRequest } from './executeChatCompletionOpenAIRequest' -import { isPlaneteScale } from '@/helpers/api/isPlanetScale' +import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale' +import prisma from '@typebot.io/lib/prisma' +import { ExecuteIntegrationResponse } from '../../../types' +import { parseVariableNumber } from '../../../variables/parseVariableNumber' +import { updateVariablesInSession } from '../../../variables/updateVariablesInSession' export const createChatCompletionOpenAI = async ( state: SessionState, @@ -63,7 +63,9 @@ export const createChatCompletionOpenAI = async ( typebot.variables )(options.messages) if (variablesTransformedToList.length > 0) - newSessionState = updateVariables(state)(variablesTransformedToList) + newSessionState = updateVariablesInSession(state)( + variablesTransformedToList + ) const temperature = parseVariableNumber(typebot.variables)( options.advancedSettings?.temperature diff --git a/apps/viewer/src/features/blocks/integrations/openai/executeChatCompletionOpenAIRequest.ts b/packages/bot-engine/blocks/integrations/openai/executeChatCompletionOpenAIRequest.ts similarity index 100% rename from apps/viewer/src/features/blocks/integrations/openai/executeChatCompletionOpenAIRequest.ts rename to packages/bot-engine/blocks/integrations/openai/executeChatCompletionOpenAIRequest.ts diff --git a/apps/viewer/src/features/blocks/integrations/openai/executeOpenAIBlock.ts b/packages/bot-engine/blocks/integrations/openai/executeOpenAIBlock.ts similarity index 90% rename from apps/viewer/src/features/blocks/integrations/openai/executeOpenAIBlock.ts rename to packages/bot-engine/blocks/integrations/openai/executeOpenAIBlock.ts index 9bab97818b..ac9a7805db 100644 --- a/apps/viewer/src/features/blocks/integrations/openai/executeOpenAIBlock.ts +++ b/packages/bot-engine/blocks/integrations/openai/executeOpenAIBlock.ts @@ -1,7 +1,7 @@ -import { ExecuteIntegrationResponse } from '@/features/chat/types' import { SessionState } from '@typebot.io/schemas' import { OpenAIBlock } from '@typebot.io/schemas/features/blocks/integrations/openai' import { createChatCompletionOpenAI } from './createChatCompletionOpenAI' +import { ExecuteIntegrationResponse } from '../../../types' export const executeOpenAIBlock = async ( state: SessionState, diff --git a/apps/viewer/src/features/blocks/integrations/openai/getChatCompletionStream.ts b/packages/bot-engine/blocks/integrations/openai/getChatCompletionStream.ts similarity index 96% rename from apps/viewer/src/features/blocks/integrations/openai/getChatCompletionStream.ts rename to packages/bot-engine/blocks/integrations/openai/getChatCompletionStream.ts index b64f039b9e..f1c61620a7 100644 --- a/apps/viewer/src/features/blocks/integrations/openai/getChatCompletionStream.ts +++ b/packages/bot-engine/blocks/integrations/openai/getChatCompletionStream.ts @@ -1,4 +1,3 @@ -import { parseVariableNumber } from '@/features/variables/parseVariableNumber' import { Connection } from '@planetscale/database' import { decrypt } from '@typebot.io/lib/api/encryption' import { isNotEmpty } from '@typebot.io/lib/utils' @@ -13,6 +12,7 @@ import { Configuration, OpenAIApi, } from 'openai-edge' +import { parseVariableNumber } from '../../../variables/parseVariableNumber' export const getChatCompletionStream = (conn: Connection) => diff --git a/apps/viewer/src/features/blocks/integrations/openai/parseChatCompletionMessages.ts b/packages/bot-engine/blocks/integrations/openai/parseChatCompletionMessages.ts similarity index 95% rename from apps/viewer/src/features/blocks/integrations/openai/parseChatCompletionMessages.ts rename to packages/bot-engine/blocks/integrations/openai/parseChatCompletionMessages.ts index d427d233eb..07e8069e45 100644 --- a/apps/viewer/src/features/blocks/integrations/openai/parseChatCompletionMessages.ts +++ b/packages/bot-engine/blocks/integrations/openai/parseChatCompletionMessages.ts @@ -1,9 +1,9 @@ -import { parseVariables } from '@/features/variables/parseVariables' -import { transformStringVariablesToList } from '@/features/variables/transformVariablesToList' import { byId, isNotEmpty } from '@typebot.io/lib' import { Variable, VariableWithValue } from '@typebot.io/schemas' import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai' import type { ChatCompletionRequestMessage } from 'openai-edge' +import { parseVariables } from '../../../variables/parseVariables' +import { transformStringVariablesToList } from '../../../variables/transformVariablesToList' export const parseChatCompletionMessages = (variables: Variable[]) => diff --git a/apps/viewer/src/features/blocks/integrations/openai/resumeChatCompletion.ts b/packages/bot-engine/blocks/integrations/openai/resumeChatCompletion.ts similarity index 90% rename from apps/viewer/src/features/blocks/integrations/openai/resumeChatCompletion.ts rename to packages/bot-engine/blocks/integrations/openai/resumeChatCompletion.ts index 79eb990383..dbf17b8c9f 100644 --- a/apps/viewer/src/features/blocks/integrations/openai/resumeChatCompletion.ts +++ b/packages/bot-engine/blocks/integrations/openai/resumeChatCompletion.ts @@ -1,8 +1,8 @@ -import { updateVariables } from '@/features/variables/updateVariables' import { byId, isDefined } from '@typebot.io/lib' import { ChatReply, SessionState } from '@typebot.io/schemas' import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai' import { VariableWithUnknowValue } from '@typebot.io/schemas/features/typebot/variable' +import { updateVariablesInSession } from '../../../variables/updateVariablesInSession' export const resumeChatCompletion = ( @@ -42,7 +42,7 @@ export const resumeChatCompletion = return newVariables }, []) if (newVariables.length > 0) - newSessionState = updateVariables(newSessionState)(newVariables) + newSessionState = updateVariablesInSession(newSessionState)(newVariables) return { outgoingEdgeId, newSessionState, diff --git a/apps/viewer/src/features/blocks/integrations/pixel/executePixelBlock.ts b/packages/bot-engine/blocks/integrations/pixel/executePixelBlock.ts similarity index 83% rename from apps/viewer/src/features/blocks/integrations/pixel/executePixelBlock.ts rename to packages/bot-engine/blocks/integrations/pixel/executePixelBlock.ts index 68730c0fa6..d877b88ee0 100644 --- a/apps/viewer/src/features/blocks/integrations/pixel/executePixelBlock.ts +++ b/packages/bot-engine/blocks/integrations/pixel/executePixelBlock.ts @@ -1,6 +1,6 @@ -import { ExecuteIntegrationResponse } from '@/features/chat/types' -import { deepParseVariables } from '@/features/variables/deepParseVariable' import { PixelBlock, SessionState } from '@typebot.io/schemas' +import { ExecuteIntegrationResponse } from '../../../types' +import { deepParseVariables } from '../../../variables/deepParseVariables' export const executePixelBlock = ( state: SessionState, diff --git a/apps/viewer/src/features/blocks/integrations/sendEmail/constants.ts b/packages/bot-engine/blocks/integrations/sendEmail/constants.ts similarity index 100% rename from apps/viewer/src/features/blocks/integrations/sendEmail/constants.ts rename to packages/bot-engine/blocks/integrations/sendEmail/constants.ts diff --git a/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx b/packages/bot-engine/blocks/integrations/sendEmail/executeSendEmailBlock.tsx similarity index 96% rename from apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx rename to packages/bot-engine/blocks/integrations/sendEmail/executeSendEmailBlock.tsx index 53cb5a85e6..b37b9dd744 100644 --- a/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx +++ b/packages/bot-engine/blocks/integrations/sendEmail/executeSendEmailBlock.tsx @@ -1,7 +1,4 @@ -import { parseVariables } from '@/features/variables/parseVariables' -import prisma from '@/lib/prisma' -import { render } from '@faire/mjml-react/utils/render' -import { DefaultBotNotificationEmail } from '@typebot.io/emails' +import { DefaultBotNotificationEmail, render } from '@typebot.io/emails' import { AnswerInSessionState, ReplyLog, @@ -18,9 +15,11 @@ import { byId, isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib' import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results' import { decrypt } from '@typebot.io/lib/api' import { defaultFrom, defaultTransportOptions } from './constants' -import { ExecuteIntegrationResponse } from '@/features/chat/types' import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue' import { env } from '@typebot.io/env' +import { ExecuteIntegrationResponse } from '../../../types' +import prisma from '@typebot.io/lib/prisma' +import { parseVariables } from '../../../variables/parseVariables' export const executeSendEmailBlock = async ( state: SessionState, diff --git a/apps/viewer/src/features/blocks/integrations/webhook/executeWebhookBlock.ts b/packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts similarity index 97% rename from apps/viewer/src/features/blocks/integrations/webhook/executeWebhookBlock.ts rename to packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts index 7e11518529..c25fbfa0be 100644 --- a/apps/viewer/src/features/blocks/integrations/webhook/executeWebhookBlock.ts +++ b/packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts @@ -1,4 +1,3 @@ -import prisma from '@/lib/prisma' import { WebhookBlock, ZapierBlock, @@ -19,10 +18,11 @@ import { stringify } from 'qs' import { omit } from '@typebot.io/lib' import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results' import got, { Method, HTTPError, OptionsInit } from 'got' -import { ExecuteIntegrationResponse } from '@/features/chat/types' -import { parseVariables } from '@/features/variables/parseVariables' import { resumeWebhookExecution } from './resumeWebhookExecution' import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums' +import { ExecuteIntegrationResponse } from '../../../types' +import { parseVariables } from '../../../variables/parseVariables' +import prisma from '@typebot.io/lib/prisma' type ParsedWebhook = ExecutableWebhook & { basicAuth: { username?: string; password?: string } diff --git a/apps/viewer/src/features/blocks/integrations/webhook/parseSampleResult.ts b/packages/bot-engine/blocks/integrations/webhook/parseSampleResult.ts similarity index 100% rename from apps/viewer/src/features/blocks/integrations/webhook/parseSampleResult.ts rename to packages/bot-engine/blocks/integrations/webhook/parseSampleResult.ts diff --git a/apps/viewer/src/features/blocks/integrations/webhook/resumeWebhookExecution.ts b/packages/bot-engine/blocks/integrations/webhook/resumeWebhookExecution.ts similarity index 87% rename from apps/viewer/src/features/blocks/integrations/webhook/resumeWebhookExecution.ts rename to packages/bot-engine/blocks/integrations/webhook/resumeWebhookExecution.ts index 295ab23210..85f6faffa6 100644 --- a/apps/viewer/src/features/blocks/integrations/webhook/resumeWebhookExecution.ts +++ b/packages/bot-engine/blocks/integrations/webhook/resumeWebhookExecution.ts @@ -1,6 +1,3 @@ -import { ExecuteIntegrationResponse } from '@/features/chat/types' -import { parseVariables } from '@/features/variables/parseVariables' -import { updateVariables } from '@/features/variables/updateVariables' import { byId } from '@typebot.io/lib' import { MakeComBlock, @@ -11,6 +8,9 @@ import { ZapierBlock, } from '@typebot.io/schemas' import { SessionState } from '@typebot.io/schemas/features/chat/sessionState' +import { ExecuteIntegrationResponse } from '../../../types' +import { parseVariables } from '../../../variables/parseVariables' +import { updateVariablesInSession } from '../../../variables/updateVariablesInSession' type Props = { state: SessionState @@ -67,7 +67,7 @@ export const resumeWebhookExecution = ({ } }, []) if (newVariables.length > 0) { - const newSessionState = updateVariables(state)(newVariables) + const newSessionState = updateVariablesInSession(state)(newVariables) return { outgoingEdgeId: block.outgoingEdgeId, newSessionState, diff --git a/apps/viewer/src/features/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts b/packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts similarity index 90% rename from apps/viewer/src/features/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts rename to packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts index a0079af746..95f4881aed 100644 --- a/apps/viewer/src/features/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts +++ b/packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts @@ -1,5 +1,3 @@ -import { ExecuteIntegrationResponse } from '@/features/chat/types' -import prisma from '@/lib/prisma' import { SessionState } from '@typebot.io/schemas' import { ZemanticAiBlock, @@ -9,8 +7,10 @@ import { import got from 'got' import { decrypt } from '@typebot.io/lib/api/encryption' import { byId, isDefined, isEmpty } from '@typebot.io/lib' -import { updateVariables } from '@/features/variables/updateVariables' import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results' +import prisma from '@typebot.io/lib/prisma' +import { ExecuteIntegrationResponse } from '../../../types' +import { updateVariablesInSession } from '../../../variables/updateVariablesInSession' const URL = 'https://api.zemantic.ai/v1/search-documents' @@ -89,14 +89,14 @@ export const executeZemanticAiBlock = async ( switch (r.valueToExtract) { case 'Summary': if (isDefined(variable) && !isEmpty(res.summary)) { - newSessionState = updateVariables(newSessionState)([ + newSessionState = updateVariablesInSession(newSessionState)([ { ...variable, value: res.summary }, ]) } break case 'Results': if (isDefined(variable) && res.results.length) { - newSessionState = updateVariables(newSessionState)([ + newSessionState = updateVariablesInSession(newSessionState)([ { ...variable, value: JSON.stringify(res.results) }, ]) } diff --git a/apps/viewer/src/features/blocks/logic/abTest/executeAbTest.ts b/packages/bot-engine/blocks/logic/abTest/executeAbTest.ts similarity index 89% rename from apps/viewer/src/features/blocks/logic/abTest/executeAbTest.ts rename to packages/bot-engine/blocks/logic/abTest/executeAbTest.ts index 1b321eb107..a6ba7db387 100644 --- a/apps/viewer/src/features/blocks/logic/abTest/executeAbTest.ts +++ b/packages/bot-engine/blocks/logic/abTest/executeAbTest.ts @@ -1,5 +1,5 @@ import { AbTestBlock, SessionState } from '@typebot.io/schemas' -import { ExecuteLogicResponse } from '@/features/chat/types' +import { ExecuteLogicResponse } from '../../../types' export const executeAbTest = ( _: SessionState, diff --git a/apps/viewer/src/features/blocks/logic/condition/executeCondition.ts b/packages/bot-engine/blocks/logic/condition/executeCondition.ts similarity index 97% rename from apps/viewer/src/features/blocks/logic/condition/executeCondition.ts rename to packages/bot-engine/blocks/logic/condition/executeCondition.ts index 2a97b8a053..241efefb10 100644 --- a/apps/viewer/src/features/blocks/logic/condition/executeCondition.ts +++ b/packages/bot-engine/blocks/logic/condition/executeCondition.ts @@ -1,5 +1,3 @@ -import { findUniqueVariableValue } from '@/features/variables/findUniqueVariableValue' -import { parseVariables } from '@/features/variables/parseVariables' import { isNotDefined, isDefined } from '@typebot.io/lib' import { Comparison, @@ -8,6 +6,8 @@ import { LogicalOperator, Variable, } from '@typebot.io/schemas' +import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue' +import { parseVariables } from '../../../variables/parseVariables' export const executeCondition = (variables: Variable[]) => diff --git a/apps/viewer/src/features/blocks/logic/condition/executeConditionBlock.ts b/packages/bot-engine/blocks/logic/condition/executeConditionBlock.ts similarity index 89% rename from apps/viewer/src/features/blocks/logic/condition/executeConditionBlock.ts rename to packages/bot-engine/blocks/logic/condition/executeConditionBlock.ts index 2895410867..7f626e326b 100644 --- a/apps/viewer/src/features/blocks/logic/condition/executeConditionBlock.ts +++ b/packages/bot-engine/blocks/logic/condition/executeConditionBlock.ts @@ -1,5 +1,5 @@ import { ConditionBlock, SessionState } from '@typebot.io/schemas' -import { ExecuteLogicResponse } from '@/features/chat/types' +import { ExecuteLogicResponse } from '../../../types' import { executeCondition } from './executeCondition' export const executeConditionBlock = ( diff --git a/apps/viewer/src/features/blocks/logic/jump/executeJumpBlock.ts b/packages/bot-engine/blocks/logic/jump/executeJumpBlock.ts similarity index 85% rename from apps/viewer/src/features/blocks/logic/jump/executeJumpBlock.ts rename to packages/bot-engine/blocks/logic/jump/executeJumpBlock.ts index f46d0ab17a..69a9281b0b 100644 --- a/apps/viewer/src/features/blocks/logic/jump/executeJumpBlock.ts +++ b/packages/bot-engine/blocks/logic/jump/executeJumpBlock.ts @@ -1,8 +1,5 @@ -import { - addEdgeToTypebot, - createPortalEdge, -} from '@/features/chat/helpers/addEdgeToTypebot' -import { ExecuteLogicResponse } from '@/features/chat/types' +import { addEdgeToTypebot, createPortalEdge } from '../../../addEdgeToTypebot' +import { ExecuteLogicResponse } from '../../../types' import { TRPCError } from '@trpc/server' import { SessionState } from '@typebot.io/schemas' import { JumpBlock } from '@typebot.io/schemas/features/blocks/logic/jump' diff --git a/apps/viewer/src/features/blocks/logic/redirect/executeRedirect.ts b/packages/bot-engine/blocks/logic/redirect/executeRedirect.ts similarity index 82% rename from apps/viewer/src/features/blocks/logic/redirect/executeRedirect.ts rename to packages/bot-engine/blocks/logic/redirect/executeRedirect.ts index 96a2a842a8..1abc08a83b 100644 --- a/apps/viewer/src/features/blocks/logic/redirect/executeRedirect.ts +++ b/packages/bot-engine/blocks/logic/redirect/executeRedirect.ts @@ -1,7 +1,7 @@ -import { parseVariables } from '@/features/variables/parseVariables' import { RedirectBlock, SessionState } from '@typebot.io/schemas' import { sanitizeUrl } from '@typebot.io/lib' -import { ExecuteLogicResponse } from '@/features/chat/types' +import { ExecuteLogicResponse } from '../../../types' +import { parseVariables } from '../../../variables/parseVariables' export const executeRedirect = ( state: SessionState, diff --git a/apps/viewer/src/features/blocks/logic/script/executeScript.ts b/packages/bot-engine/blocks/logic/script/executeScript.ts similarity index 77% rename from apps/viewer/src/features/blocks/logic/script/executeScript.ts rename to packages/bot-engine/blocks/logic/script/executeScript.ts index 2ed0b38ac3..e97e7385fa 100644 --- a/apps/viewer/src/features/blocks/logic/script/executeScript.ts +++ b/packages/bot-engine/blocks/logic/script/executeScript.ts @@ -1,8 +1,8 @@ -import { ExecuteLogicResponse } from '@/features/chat/types' -import { extractVariablesFromText } from '@/features/variables/extractVariablesFromText' -import { parseGuessedValueType } from '@/features/variables/parseGuessedValueType' -import { parseVariables } from '@/features/variables/parseVariables' +import { ExecuteLogicResponse } from '../../../types' import { ScriptBlock, SessionState, Variable } from '@typebot.io/schemas' +import { extractVariablesFromText } from '../../../variables/extractVariablesFromText' +import { parseGuessedValueType } from '../../../variables/parseGuessedValueType' +import { parseVariables } from '../../../variables/parseVariables' export const executeScript = ( state: SessionState, diff --git a/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts b/packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts similarity index 91% rename from apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts rename to packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts index 1462e31859..891ed35a1a 100644 --- a/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts +++ b/packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts @@ -1,10 +1,10 @@ import { SessionState, SetVariableBlock, Variable } from '@typebot.io/schemas' import { byId } from '@typebot.io/lib' -import { ExecuteLogicResponse } from '@/features/chat/types' -import { updateVariables } from '@/features/variables/updateVariables' -import { parseVariables } from '@/features/variables/parseVariables' -import { parseGuessedValueType } from '@/features/variables/parseGuessedValueType' +import { ExecuteLogicResponse } from '../../../types' import { parseScriptToExecuteClientSideAction } from '../script/executeScript' +import { parseGuessedValueType } from '../../../variables/parseGuessedValueType' +import { parseVariables } from '../../../variables/parseVariables' +import { updateVariablesInSession } from '../../../variables/updateVariablesInSession' export const executeSetVariable = ( state: SessionState, @@ -48,7 +48,7 @@ export const executeSetVariable = ( ...existingVariable, value: evaluatedExpression, } - const newSessionState = updateVariables(state)([newVariable]) + const newSessionState = updateVariablesInSession(state)([newVariable]) return { outgoingEdgeId: block.outgoingEdgeId, newSessionState, diff --git a/apps/viewer/src/features/blocks/logic/typebotLink/executeTypebotLink.ts b/packages/bot-engine/blocks/logic/typebotLink/executeTypebotLink.ts similarity index 95% rename from apps/viewer/src/features/blocks/logic/typebotLink/executeTypebotLink.ts rename to packages/bot-engine/blocks/logic/typebotLink/executeTypebotLink.ts index 5bd96942b4..ea67c39a40 100644 --- a/apps/viewer/src/features/blocks/logic/typebotLink/executeTypebotLink.ts +++ b/packages/bot-engine/blocks/logic/typebotLink/executeTypebotLink.ts @@ -1,8 +1,4 @@ -import { - addEdgeToTypebot, - createPortalEdge, -} from '@/features/chat/helpers/addEdgeToTypebot' -import prisma from '@/lib/prisma' +import { addEdgeToTypebot, createPortalEdge } from '../../../addEdgeToTypebot' import { TypebotLinkBlock, SessionState, @@ -12,11 +8,12 @@ import { typebotInSessionStateSchema, TypebotInSession, } from '@typebot.io/schemas' -import { ExecuteLogicResponse } from '@/features/chat/types' +import { ExecuteLogicResponse } from '../../../types' import { createId } from '@paralleldrive/cuid2' import { isNotDefined } from '@typebot.io/lib/utils' -import { createResultIfNotExist } from '@/features/chat/queries/createResultIfNotExist' +import { createResultIfNotExist } from '../../../queries/createResultIfNotExist' import { executeJumpBlock } from '../jump/executeJumpBlock' +import prisma from '@typebot.io/lib/prisma' export const executeTypebotLink = async ( state: SessionState, diff --git a/packages/bot-engine/blocks/logic/typebotLink/fetchLinkedTypebots.ts b/packages/bot-engine/blocks/logic/typebotLink/fetchLinkedTypebots.ts new file mode 100644 index 0000000000..d41b788b10 --- /dev/null +++ b/packages/bot-engine/blocks/logic/typebotLink/fetchLinkedTypebots.ts @@ -0,0 +1,45 @@ +import prisma from '@typebot.io/lib/prisma' +import { User } from '@typebot.io/prisma' + +type Props = { + isPreview?: boolean + typebotIds: string[] + user?: User +} + +export const fetchLinkedTypebots = async ({ + user, + isPreview, + typebotIds, +}: Props) => { + if (!user || !isPreview) + return prisma.publicTypebot.findMany({ + where: { id: { in: typebotIds } }, + }) + const linkedTypebots = await prisma.typebot.findMany({ + where: { id: { in: typebotIds } }, + include: { + collaborators: { + select: { + userId: true, + }, + }, + workspace: { + select: { + members: { + select: { + userId: true, + }, + }, + }, + }, + }, + }) + + return linkedTypebots.filter( + (typebot) => + typebot.collaborators.some( + (collaborator) => collaborator.userId === user.id + ) || typebot.workspace.members.some((member) => member.userId === user.id) + ) +} diff --git a/apps/viewer/src/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots.ts b/packages/bot-engine/blocks/logic/typebotLink/getPreviouslyLinkedTypebots.ts similarity index 100% rename from apps/viewer/src/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots.ts rename to packages/bot-engine/blocks/logic/typebotLink/getPreviouslyLinkedTypebots.ts diff --git a/apps/viewer/src/features/blocks/logic/wait/executeWait.ts b/packages/bot-engine/blocks/logic/wait/executeWait.ts similarity index 86% rename from apps/viewer/src/features/blocks/logic/wait/executeWait.ts rename to packages/bot-engine/blocks/logic/wait/executeWait.ts index b8b7999b7c..55cf11a72d 100644 --- a/apps/viewer/src/features/blocks/logic/wait/executeWait.ts +++ b/packages/bot-engine/blocks/logic/wait/executeWait.ts @@ -1,6 +1,6 @@ -import { ExecuteLogicResponse } from '@/features/chat/types' -import { parseVariables } from '@/features/variables/parseVariables' +import { ExecuteLogicResponse } from '../../../types' import { SessionState, WaitBlock } from '@typebot.io/schemas' +import { parseVariables } from '../../../variables/parseVariables' export const executeWait = ( state: SessionState, diff --git a/packages/lib/computeTypingDuration.ts b/packages/bot-engine/computeTypingDuration.ts similarity index 100% rename from packages/lib/computeTypingDuration.ts rename to packages/bot-engine/computeTypingDuration.ts diff --git a/apps/viewer/src/features/chat/helpers/continueBotFlow.ts b/packages/bot-engine/continueBotFlow.ts similarity index 89% rename from apps/viewer/src/features/chat/helpers/continueBotFlow.ts rename to packages/bot-engine/continueBotFlow.ts index 595e09ff58..5988f34ce0 100644 --- a/apps/viewer/src/features/chat/helpers/continueBotFlow.ts +++ b/packages/bot-engine/continueBotFlow.ts @@ -14,21 +14,21 @@ import { import { isInputBlock, byId } from '@typebot.io/lib' import { executeGroup, parseInput } from './executeGroup' import { getNextGroup } from './getNextGroup' -import { validateEmail } from '@/features/blocks/inputs/email/validateEmail' -import { formatPhoneNumber } from '@/features/blocks/inputs/phone/formatPhoneNumber' -import { validateUrl } from '@/features/blocks/inputs/url/validateUrl' -import { updateVariables } from '@/features/variables/updateVariables' -import { parseVariables } from '@/features/variables/parseVariables' -import { resumeChatCompletion } from '@/features/blocks/integrations/openai/resumeChatCompletion' -import { resumeWebhookExecution } from '@/features/blocks/integrations/webhook/resumeWebhookExecution' -import { upsertAnswer } from '../queries/upsertAnswer' +import { validateEmail } from './blocks/inputs/email/validateEmail' +import { formatPhoneNumber } from './blocks/inputs/phone/formatPhoneNumber' +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 '@/features/blocks/inputs/buttons/parseButtonsReply' -import { ParsedReply } from '../types' -import { validateNumber } from '@/features/blocks/inputs/number/validateNumber' -import { parseDateReply } from '@/features/blocks/inputs/date/parseDateReply' -import { validateRatingReply } from '@/features/blocks/inputs/rating/validateRatingReply' -import { parsePictureChoicesReply } from '@/features/blocks/inputs/pictureChoice/parsePictureChoicesReply' +import { parseButtonsReply } from './blocks/inputs/buttons/parseButtonsReply' +import { ParsedReply } from './types' +import { validateNumber } from './blocks/inputs/number/validateNumber' +import { parseDateReply } from './blocks/inputs/date/parseDateReply' +import { validateRatingReply } from './blocks/inputs/rating/validateRatingReply' +import { parsePictureChoicesReply } from './blocks/inputs/pictureChoice/parsePictureChoicesReply' +import { parseVariables } from './variables/parseVariables' +import { updateVariablesInSession } from './variables/updateVariablesInSession' export const continueBotFlow = (state: SessionState) => @@ -57,7 +57,7 @@ export const continueBotFlow = ...existingVariable, value: safeJsonParse(reply), } - newSessionState = updateVariables(state)([newVariable]) + newSessionState = updateVariablesInSession(state)([newVariable]) } } else if (reply && block.type === IntegrationBlockType.WEBHOOK) { const result = resumeWebhookExecution({ @@ -170,7 +170,7 @@ const saveVariableValueIfAny = ) if (!foundVariable) return state - const newSessionState = updateVariables(state)([ + const newSessionState = updateVariablesInSession(state)([ { ...foundVariable, value: Array.isArray(foundVariable.value) diff --git a/apps/viewer/src/features/chat/helpers/executeGroup.ts b/packages/bot-engine/executeGroup.ts similarity index 92% rename from apps/viewer/src/features/chat/helpers/executeGroup.ts rename to packages/bot-engine/executeGroup.ts index 9d1f7bdd26..821c7b6363 100644 --- a/apps/viewer/src/features/chat/helpers/executeGroup.ts +++ b/packages/bot-engine/executeGroup.ts @@ -16,15 +16,15 @@ import { isLogicBlock, isNotEmpty, } from '@typebot.io/lib' -import { executeLogic } from './executeLogic' import { getNextGroup } from './getNextGroup' +import { executeLogic } from './executeLogic' import { executeIntegration } from './executeIntegration' -import { injectVariableValuesInButtonsInputBlock } from '@/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock' -import { deepParseVariables } from '@/features/variables/deepParseVariable' -import { computePaymentInputRuntimeOptions } from '@/features/blocks/inputs/payment/computePaymentInputRuntimeOptions' -import { injectVariableValuesInPictureChoiceBlock } from '@/features/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock' -import { parseDateInput } from '@/features/blocks/inputs/date/parseDateInput' +import { computePaymentInputRuntimeOptions } from './blocks/inputs/payment/computePaymentInputRuntimeOptions' +import { injectVariableValuesInButtonsInputBlock } from './blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock' +import { injectVariableValuesInPictureChoiceBlock } from './blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock' import { getPrefilledInputValue } from './getPrefilledValue' +import { parseDateInput } from './blocks/inputs/date/parseDateInput' +import { deepParseVariables } from './variables/deepParseVariables' export const executeGroup = ( diff --git a/apps/viewer/src/features/chat/helpers/executeIntegration.ts b/packages/bot-engine/executeIntegration.ts similarity index 56% rename from apps/viewer/src/features/chat/helpers/executeIntegration.ts rename to packages/bot-engine/executeIntegration.ts index e0e0484ca1..a5262655f2 100644 --- a/apps/viewer/src/features/chat/helpers/executeIntegration.ts +++ b/packages/bot-engine/executeIntegration.ts @@ -1,17 +1,17 @@ -import { executeOpenAIBlock } from '@/features/blocks/integrations/openai/executeOpenAIBlock' -import { executeSendEmailBlock } from '@/features/blocks/integrations/sendEmail/executeSendEmailBlock' -import { executeWebhookBlock } from '@/features/blocks/integrations/webhook/executeWebhookBlock' -import { executeChatwootBlock } from '@/features/blocks/integrations/chatwoot/executeChatwootBlock' -import { executeGoogleAnalyticsBlock } from '@/features/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock' -import { executeGoogleSheetBlock } from '@/features/blocks/integrations/googleSheets/executeGoogleSheetBlock' -import { executePixelBlock } from '@/features/blocks/integrations/pixel/executePixelBlock' -import { executeZemanticAiBlock } from '@/features/blocks/integrations/zemanticAi/executeZemanticAiBlock' +import { executeOpenAIBlock } from './blocks/integrations/openai/executeOpenAIBlock' +import { executeSendEmailBlock } from './blocks/integrations/sendEmail/executeSendEmailBlock' +import { executeWebhookBlock } from './blocks/integrations/webhook/executeWebhookBlock' +import { executeChatwootBlock } from './blocks/integrations/chatwoot/executeChatwootBlock' +import { executeGoogleAnalyticsBlock } from './blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock' +import { executeGoogleSheetBlock } from './blocks/integrations/googleSheets/executeGoogleSheetBlock' +import { executePixelBlock } from './blocks/integrations/pixel/executePixelBlock' +import { executeZemanticAiBlock } from './blocks/integrations/zemanticAi/executeZemanticAiBlock' import { IntegrationBlock, IntegrationBlockType, SessionState, } from '@typebot.io/schemas' -import { ExecuteIntegrationResponse } from '../types' +import { ExecuteIntegrationResponse } from './types' export const executeIntegration = (state: SessionState) => diff --git a/apps/viewer/src/features/chat/helpers/executeLogic.ts b/packages/bot-engine/executeLogic.ts similarity index 55% rename from apps/viewer/src/features/chat/helpers/executeLogic.ts rename to packages/bot-engine/executeLogic.ts index 818f93ad2b..d8ba4cb6c1 100644 --- a/apps/viewer/src/features/chat/helpers/executeLogic.ts +++ b/packages/bot-engine/executeLogic.ts @@ -1,13 +1,13 @@ -import { executeWait } from '@/features/blocks/logic/wait/executeWait' +import { executeWait } from './blocks/logic/wait/executeWait' import { LogicBlock, LogicBlockType, SessionState } from '@typebot.io/schemas' -import { ExecuteLogicResponse } from '../types' -import { executeScript } from '@/features/blocks/logic/script/executeScript' -import { executeJumpBlock } from '@/features/blocks/logic/jump/executeJumpBlock' -import { executeRedirect } from '@/features/blocks/logic/redirect/executeRedirect' -import { executeConditionBlock } from '@/features/blocks/logic/condition/executeConditionBlock' -import { executeSetVariable } from '@/features/blocks/logic/setVariable/executeSetVariable' -import { executeTypebotLink } from '@/features/blocks/logic/typebotLink/executeTypebotLink' -import { executeAbTest } from '@/features/blocks/logic/abTest/executeAbTest' +import { ExecuteLogicResponse } from './types' +import { executeScript } from './blocks/logic/script/executeScript' +import { executeJumpBlock } from './blocks/logic/jump/executeJumpBlock' +import { executeRedirect } from './blocks/logic/redirect/executeRedirect' +import { executeConditionBlock } from './blocks/logic/condition/executeConditionBlock' +import { executeSetVariable } from './blocks/logic/setVariable/executeSetVariable' +import { executeTypebotLink } from './blocks/logic/typebotLink/executeTypebotLink' +import { executeAbTest } from './blocks/logic/abTest/executeAbTest' export const executeLogic = (state: SessionState) => diff --git a/apps/viewer/src/features/chat/helpers/getNextGroup.ts b/packages/bot-engine/getNextGroup.ts similarity index 95% rename from apps/viewer/src/features/chat/helpers/getNextGroup.ts rename to packages/bot-engine/getNextGroup.ts index b2c81eb16c..b3826bf8bc 100644 --- a/apps/viewer/src/features/chat/helpers/getNextGroup.ts +++ b/packages/bot-engine/getNextGroup.ts @@ -1,7 +1,6 @@ -import { byId, isNotDefined } from '@typebot.io/lib' +import { byId, isDefined, isNotDefined } from '@typebot.io/lib' import { Group, SessionState, VariableWithValue } from '@typebot.io/schemas' -import { upsertResult } from '../queries/upsertResult' -import { isDefined } from '@udecode/plate-common' +import { upsertResult } from './queries/upsertResult' export type NextGroup = { group?: Group diff --git a/apps/viewer/src/features/chat/helpers/getPrefilledValue.ts b/packages/bot-engine/getPrefilledValue.ts similarity index 100% rename from apps/viewer/src/features/chat/helpers/getPrefilledValue.ts rename to packages/bot-engine/getPrefilledValue.ts diff --git a/apps/viewer/src/features/logs/helpers/formatLogDetails.ts b/packages/bot-engine/logs/helpers/formatLogDetails.ts similarity index 100% rename from apps/viewer/src/features/logs/helpers/formatLogDetails.ts rename to packages/bot-engine/logs/helpers/formatLogDetails.ts diff --git a/apps/viewer/src/features/logs/saveErrorLog.ts b/packages/bot-engine/logs/saveErrorLog.ts similarity index 100% rename from apps/viewer/src/features/logs/saveErrorLog.ts rename to packages/bot-engine/logs/saveErrorLog.ts diff --git a/apps/viewer/src/features/logs/saveLog.ts b/packages/bot-engine/logs/saveLog.ts similarity index 91% rename from apps/viewer/src/features/logs/saveLog.ts rename to packages/bot-engine/logs/saveLog.ts index 5e989c693e..c7e80b27a4 100644 --- a/apps/viewer/src/features/logs/saveLog.ts +++ b/packages/bot-engine/logs/saveLog.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { formatLogDetails } from './helpers/formatLogDetails' type Props = { diff --git a/apps/viewer/src/features/logs/saveSuccessLog.ts b/packages/bot-engine/logs/saveSuccessLog.ts similarity index 100% rename from apps/viewer/src/features/logs/saveSuccessLog.ts rename to packages/bot-engine/logs/saveSuccessLog.ts diff --git a/packages/bot-engine/package.json b/packages/bot-engine/package.json new file mode 100644 index 0000000000..1fcf84f080 --- /dev/null +++ b/packages/bot-engine/package.json @@ -0,0 +1,38 @@ +{ + "name": "@typebot.io/bot-engine", + "version": "1.0.0", + "license": "AGPL-3.0-or-later", + "private": true, + "main": "./index.ts", + "types": "./index.ts", + "dependencies": { + "@paralleldrive/cuid2": "2.2.1", + "@planetscale/database": "^1.8.0", + "@sentry/nextjs": "7.66.0", + "@trpc/server": "10.34.0", + "@typebot.io/emails": "workspace:*", + "@typebot.io/env": "workspace:*", + "@typebot.io/lib": "workspace:*", + "@typebot.io/prisma": "workspace:*", + "@typebot.io/schemas": "workspace:*", + "@typebot.io/tsconfig": "workspace:*", + "@udecode/plate-common": "^21.1.5", + "ai": "2.1.32", + "chrono-node": "2.6.6", + "date-fns": "^2.30.0", + "google-auth-library": "8.9.0", + "google-spreadsheet": "4.0.2", + "got": "12.6.0", + "libphonenumber-js": "1.10.37", + "node-html-parser": "^6.1.5", + "nodemailer": "6.9.3", + "openai-edge": "1.2.2", + "qs": "^6.11.2", + "remark-slate": "^1.8.6", + "stripe": "12.13.0" + }, + "devDependencies": { + "@types/nodemailer": "6.4.8", + "@types/qs": "6.9.7" + } +} diff --git a/apps/viewer/src/features/chat/helpers/parseDynamicTheme.ts b/packages/bot-engine/parseDynamicTheme.ts similarity index 87% rename from apps/viewer/src/features/chat/helpers/parseDynamicTheme.ts rename to packages/bot-engine/parseDynamicTheme.ts index 4916902997..8f1b291ba0 100644 --- a/apps/viewer/src/features/chat/helpers/parseDynamicTheme.ts +++ b/packages/bot-engine/parseDynamicTheme.ts @@ -1,5 +1,5 @@ -import { parseVariables } from '@/features/variables/parseVariables' import { SessionState, ChatReply } from '@typebot.io/schemas' +import { parseVariables } from './variables/parseVariables' export const parseDynamicTheme = ( state: SessionState | undefined diff --git a/apps/viewer/src/features/chat/queries/createResultIfNotExist.ts b/packages/bot-engine/queries/createResultIfNotExist.ts similarity index 94% rename from apps/viewer/src/features/chat/queries/createResultIfNotExist.ts rename to packages/bot-engine/queries/createResultIfNotExist.ts index ffc087b672..7d6ba65732 100644 --- a/apps/viewer/src/features/chat/queries/createResultIfNotExist.ts +++ b/packages/bot-engine/queries/createResultIfNotExist.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { getDefinedVariables } from '@typebot.io/lib/results' import { TypebotInSession } from '@typebot.io/schemas' diff --git a/apps/viewer/src/features/chat/queries/createSession.ts b/packages/bot-engine/queries/createSession.ts similarity index 84% rename from apps/viewer/src/features/chat/queries/createSession.ts rename to packages/bot-engine/queries/createSession.ts index 0ad2f03e25..3551dbd1ef 100644 --- a/apps/viewer/src/features/chat/queries/createSession.ts +++ b/packages/bot-engine/queries/createSession.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { SessionState } from '@typebot.io/schemas' type Props = { diff --git a/apps/viewer/src/features/chat/queries/deleteSession.ts b/packages/bot-engine/queries/deleteSession.ts similarity index 72% rename from apps/viewer/src/features/chat/queries/deleteSession.ts rename to packages/bot-engine/queries/deleteSession.ts index 7d190066e0..7e8531a51a 100644 --- a/apps/viewer/src/features/chat/queries/deleteSession.ts +++ b/packages/bot-engine/queries/deleteSession.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' export const deleteSession = (id: string) => prisma.chatSession.deleteMany({ diff --git a/apps/viewer/src/features/chat/queries/findPublicTypebot.ts b/packages/bot-engine/queries/findPublicTypebot.ts similarity index 94% rename from apps/viewer/src/features/chat/queries/findPublicTypebot.ts rename to packages/bot-engine/queries/findPublicTypebot.ts index 2ca7fc36b9..f616cedc04 100644 --- a/apps/viewer/src/features/chat/queries/findPublicTypebot.ts +++ b/packages/bot-engine/queries/findPublicTypebot.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' type Props = { publicId: string diff --git a/apps/viewer/src/features/chat/queries/findResult.ts b/packages/bot-engine/queries/findResult.ts similarity index 92% rename from apps/viewer/src/features/chat/queries/findResult.ts rename to packages/bot-engine/queries/findResult.ts index b0ace7ed93..37487ab881 100644 --- a/apps/viewer/src/features/chat/queries/findResult.ts +++ b/packages/bot-engine/queries/findResult.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { Answer, Result } from '@typebot.io/schemas' type Props = { diff --git a/apps/viewer/src/features/chat/queries/findTypebot.ts b/packages/bot-engine/queries/findTypebot.ts similarity index 89% rename from apps/viewer/src/features/chat/queries/findTypebot.ts rename to packages/bot-engine/queries/findTypebot.ts index 1f05245b36..721eade5d3 100644 --- a/apps/viewer/src/features/chat/queries/findTypebot.ts +++ b/packages/bot-engine/queries/findTypebot.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' type Props = { id: string diff --git a/apps/viewer/src/features/chat/queries/getSession.ts b/packages/bot-engine/queries/getSession.ts similarity index 90% rename from apps/viewer/src/features/chat/queries/getSession.ts rename to packages/bot-engine/queries/getSession.ts index ef20425e60..c7a00bc9cb 100644 --- a/apps/viewer/src/features/chat/queries/getSession.ts +++ b/packages/bot-engine/queries/getSession.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { ChatSession, sessionStateSchema } from '@typebot.io/schemas' export const getSession = async ( diff --git a/apps/viewer/src/features/chat/queries/restartSession.ts b/packages/bot-engine/queries/restartSession.ts similarity index 89% rename from apps/viewer/src/features/chat/queries/restartSession.ts rename to packages/bot-engine/queries/restartSession.ts index dddac7ee6f..ec5d619dc9 100644 --- a/apps/viewer/src/features/chat/queries/restartSession.ts +++ b/packages/bot-engine/queries/restartSession.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { SessionState } from '@typebot.io/schemas' type Props = { diff --git a/apps/viewer/src/features/chat/queries/saveLogs.ts b/packages/bot-engine/queries/saveLogs.ts similarity index 77% rename from apps/viewer/src/features/chat/queries/saveLogs.ts rename to packages/bot-engine/queries/saveLogs.ts index aefdd740ff..5ce54e266c 100644 --- a/apps/viewer/src/features/chat/queries/saveLogs.ts +++ b/packages/bot-engine/queries/saveLogs.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { Log } from '@typebot.io/schemas' export const saveLogs = (logs: Omit[]) => diff --git a/apps/viewer/src/features/chat/queries/updateSession.ts b/packages/bot-engine/queries/updateSession.ts similarity index 85% rename from apps/viewer/src/features/chat/queries/updateSession.ts rename to packages/bot-engine/queries/updateSession.ts index 0c5da4a7e1..8d60e0a6b3 100644 --- a/apps/viewer/src/features/chat/queries/updateSession.ts +++ b/packages/bot-engine/queries/updateSession.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { SessionState } from '@typebot.io/schemas' type Props = { diff --git a/apps/viewer/src/features/chat/queries/upsertAnswer.ts b/packages/bot-engine/queries/upsertAnswer.ts similarity index 95% rename from apps/viewer/src/features/chat/queries/upsertAnswer.ts rename to packages/bot-engine/queries/upsertAnswer.ts index 49a359a938..78c5e2d16c 100644 --- a/apps/viewer/src/features/chat/queries/upsertAnswer.ts +++ b/packages/bot-engine/queries/upsertAnswer.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { Prisma } from '@typebot.io/prisma' import { InputBlock, SessionState } from '@typebot.io/schemas' diff --git a/apps/viewer/src/features/chat/queries/upsertResult.ts b/packages/bot-engine/queries/upsertResult.ts similarity index 95% rename from apps/viewer/src/features/chat/queries/upsertResult.ts rename to packages/bot-engine/queries/upsertResult.ts index 88a7136869..65a4643656 100644 --- a/apps/viewer/src/features/chat/queries/upsertResult.ts +++ b/packages/bot-engine/queries/upsertResult.ts @@ -1,4 +1,4 @@ -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { getDefinedVariables } from '@typebot.io/lib/results' import { TypebotInSession } from '@typebot.io/schemas' diff --git a/apps/viewer/src/features/chat/helpers/saveStateToDatabase.ts b/packages/bot-engine/saveStateToDatabase.ts similarity index 79% rename from apps/viewer/src/features/chat/helpers/saveStateToDatabase.ts rename to packages/bot-engine/saveStateToDatabase.ts index b27ca1db87..e384dea366 100644 --- a/apps/viewer/src/features/chat/helpers/saveStateToDatabase.ts +++ b/packages/bot-engine/saveStateToDatabase.ts @@ -1,10 +1,10 @@ import { ChatReply, ChatSession } from '@typebot.io/schemas' -import { upsertResult } from '../queries/upsertResult' -import { saveLogs } from '../queries/saveLogs' -import { updateSession } from '../queries/updateSession' -import { formatLogDetails } from '@/features/logs/helpers/formatLogDetails' -import { createSession } from '../queries/createSession' -import { deleteSession } from '../queries/deleteSession' +import { upsertResult } from './queries/upsertResult' +import { saveLogs } from './queries/saveLogs' +import { updateSession } from './queries/updateSession' +import { formatLogDetails } from './logs/helpers/formatLogDetails' +import { createSession } from './queries/createSession' +import { deleteSession } from './queries/deleteSession' type Props = { isFirstSave?: boolean diff --git a/apps/viewer/src/features/chat/helpers/startBotFlow.ts b/packages/bot-engine/startBotFlow.ts similarity index 100% rename from apps/viewer/src/features/chat/helpers/startBotFlow.ts rename to packages/bot-engine/startBotFlow.ts diff --git a/apps/viewer/src/features/chat/helpers/startSession.ts b/packages/bot-engine/startSession.ts similarity index 96% rename from apps/viewer/src/features/chat/helpers/startSession.ts rename to packages/bot-engine/startSession.ts index e23e983c16..16748422d7 100644 --- a/apps/viewer/src/features/chat/helpers/startSession.ts +++ b/packages/bot-engine/startSession.ts @@ -1,6 +1,3 @@ -import { deepParseVariables } from '@/features/variables/deepParseVariable' -import { injectVariablesFromExistingResult } from '@/features/variables/injectVariablesFromExistingResult' -import { prefillVariables } from '@/features/variables/prefillVariables' import { createId } from '@paralleldrive/cuid2' import { TRPCError } from '@trpc/server' import { isDefined, omit, isNotEmpty, isInputBlock } from '@typebot.io/lib' @@ -19,13 +16,16 @@ import { StartTypebot, startTypebotSchema, } from '@typebot.io/schemas/features/chat/schema' -import { findPublicTypebot } from '../queries/findPublicTypebot' -import { findResult } from '../queries/findResult' -import { findTypebot } from '../queries/findTypebot' -import { startBotFlow } from './startBotFlow' import parse, { NodeType } from 'node-html-parser' -import { parseDynamicTheme } from './parseDynamicTheme' import { env } from '@typebot.io/env' +import { parseDynamicTheme } from './parseDynamicTheme' +import { findTypebot } from './queries/findTypebot' +import { findPublicTypebot } from './queries/findPublicTypebot' +import { findResult } from './queries/findResult' +import { startBotFlow } from './startBotFlow' +import { prefillVariables } from './variables/prefillVariables' +import { deepParseVariables } from './variables/deepParseVariables' +import { injectVariablesFromExistingResult } from './variables/injectVariablesFromExistingResult' type Props = { startParams: StartParams diff --git a/packages/bot-engine/tsconfig.json b/packages/bot-engine/tsconfig.json new file mode 100644 index 0000000000..f1c734a16a --- /dev/null +++ b/packages/bot-engine/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@typebot.io/tsconfig/base.json", + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"], + "compilerOptions": { + "jsx": "preserve", + "lib": ["ES2021"] + } +} diff --git a/apps/viewer/src/features/chat/types.ts b/packages/bot-engine/types.ts similarity index 100% rename from apps/viewer/src/features/chat/types.ts rename to packages/bot-engine/types.ts diff --git a/apps/viewer/src/features/variables/deepParseVariable.ts b/packages/bot-engine/variables/deepParseVariables.ts similarity index 100% rename from apps/viewer/src/features/variables/deepParseVariable.ts rename to packages/bot-engine/variables/deepParseVariables.ts diff --git a/apps/viewer/src/features/variables/extractVariablesFromText.ts b/packages/bot-engine/variables/extractVariablesFromText.ts similarity index 100% rename from apps/viewer/src/features/variables/extractVariablesFromText.ts rename to packages/bot-engine/variables/extractVariablesFromText.ts diff --git a/apps/viewer/src/features/variables/findUniqueVariableValue.ts b/packages/bot-engine/variables/findUniqueVariableValue.ts similarity index 100% rename from apps/viewer/src/features/variables/findUniqueVariableValue.ts rename to packages/bot-engine/variables/findUniqueVariableValue.ts diff --git a/apps/viewer/src/features/variables/hasVariable.ts b/packages/bot-engine/variables/hasVariable.ts similarity index 100% rename from apps/viewer/src/features/variables/hasVariable.ts rename to packages/bot-engine/variables/hasVariable.ts diff --git a/apps/viewer/src/features/variables/injectVariablesFromExistingResult.ts b/packages/bot-engine/variables/injectVariablesFromExistingResult.ts similarity index 100% rename from apps/viewer/src/features/variables/injectVariablesFromExistingResult.ts rename to packages/bot-engine/variables/injectVariablesFromExistingResult.ts diff --git a/apps/viewer/src/features/variables/parseGuessedTypeFromString.ts b/packages/bot-engine/variables/parseGuessedTypeFromString.ts similarity index 100% rename from apps/viewer/src/features/variables/parseGuessedTypeFromString.ts rename to packages/bot-engine/variables/parseGuessedTypeFromString.ts diff --git a/apps/viewer/src/features/variables/parseGuessedValueType.ts b/packages/bot-engine/variables/parseGuessedValueType.ts similarity index 100% rename from apps/viewer/src/features/variables/parseGuessedValueType.ts rename to packages/bot-engine/variables/parseGuessedValueType.ts diff --git a/apps/viewer/src/features/variables/parseVariableNumber.ts b/packages/bot-engine/variables/parseVariableNumber.ts similarity index 100% rename from apps/viewer/src/features/variables/parseVariableNumber.ts rename to packages/bot-engine/variables/parseVariableNumber.ts diff --git a/apps/viewer/src/features/variables/parseVariables.ts b/packages/bot-engine/variables/parseVariables.ts similarity index 97% rename from apps/viewer/src/features/variables/parseVariables.ts rename to packages/bot-engine/variables/parseVariables.ts index 612ec89e35..bea4450f6e 100644 --- a/apps/viewer/src/features/variables/parseVariables.ts +++ b/packages/bot-engine/variables/parseVariables.ts @@ -1,6 +1,6 @@ -import { isDefined } from '@typebot.io/lib' -import { Variable, VariableWithValue } from '@typebot.io/schemas' import { safeStringify } from '@typebot.io/lib/safeStringify' +import { isDefined } from '@typebot.io/lib/utils' +import { Variable, VariableWithValue } from '@typebot.io/schemas' export type ParseVariablesOptions = { fieldToParse?: 'value' | 'id' diff --git a/apps/viewer/src/features/variables/prefillVariables.ts b/packages/bot-engine/variables/prefillVariables.ts similarity index 100% rename from apps/viewer/src/features/variables/prefillVariables.ts rename to packages/bot-engine/variables/prefillVariables.ts index 47d32c571e..0c06ab5904 100644 --- a/apps/viewer/src/features/variables/prefillVariables.ts +++ b/packages/bot-engine/variables/prefillVariables.ts @@ -1,5 +1,5 @@ -import { StartParams, Variable } from '@typebot.io/schemas' import { safeStringify } from '@typebot.io/lib/safeStringify' +import { StartParams, Variable } from '@typebot.io/schemas' export const prefillVariables = ( variables: Variable[], diff --git a/apps/viewer/src/features/variables/transformVariablesToList.ts b/packages/bot-engine/variables/transformVariablesToList.ts similarity index 92% rename from apps/viewer/src/features/variables/transformVariablesToList.ts rename to packages/bot-engine/variables/transformVariablesToList.ts index 4f46e12c8b..9166ce598c 100644 --- a/apps/viewer/src/features/variables/transformVariablesToList.ts +++ b/packages/bot-engine/variables/transformVariablesToList.ts @@ -1,5 +1,5 @@ +import { isNotDefined } from '@typebot.io/lib/utils' import { Variable, VariableWithValue } from '@typebot.io/schemas' -import { isNotDefined } from '@typebot.io/lib' export const transformStringVariablesToList = (variables: Variable[]) => diff --git a/apps/viewer/src/features/variables/updateVariables.ts b/packages/bot-engine/variables/updateVariablesInSession.ts similarity index 96% rename from apps/viewer/src/features/variables/updateVariables.ts rename to packages/bot-engine/variables/updateVariablesInSession.ts index 6bfab19348..308c6e5f07 100644 --- a/apps/viewer/src/features/variables/updateVariables.ts +++ b/packages/bot-engine/variables/updateVariablesInSession.ts @@ -1,11 +1,11 @@ +import { safeStringify } from '@typebot.io/lib/safeStringify' import { SessionState, VariableWithUnknowValue, Variable, } from '@typebot.io/schemas' -import { safeStringify } from '@typebot.io/lib/safeStringify' -export const updateVariables = +export const updateVariablesInSession = (state: SessionState) => (newVariables: VariableWithUnknowValue[]): SessionState => ({ ...state, diff --git a/packages/lib/whatsApp/convertInputToWhatsAppMessage.ts b/packages/bot-engine/whatsapp/convertInputToWhatsAppMessage.ts similarity index 98% rename from packages/lib/whatsApp/convertInputToWhatsAppMessage.ts rename to packages/bot-engine/whatsapp/convertInputToWhatsAppMessage.ts index bf17774d7c..64ceacf991 100644 --- a/packages/lib/whatsApp/convertInputToWhatsAppMessage.ts +++ b/packages/bot-engine/whatsapp/convertInputToWhatsAppMessage.ts @@ -6,7 +6,7 @@ import { } from '@typebot.io/schemas' import { WhatsAppSendingMessage } from '@typebot.io/schemas/features/whatsapp' import { convertRichTextToWhatsAppText } from './convertRichTextToWhatsAppText' -import { isDefined, isEmpty } from '../utils' +import { isDefined, isEmpty } from '@typebot.io/lib/utils' export const convertInputToWhatsAppMessages = ( input: NonNullable, diff --git a/packages/lib/whatsApp/convertMessageToWhatsAppMessage.ts b/packages/bot-engine/whatsapp/convertMessageToWhatsAppMessage.ts similarity index 97% rename from packages/lib/whatsApp/convertMessageToWhatsAppMessage.ts rename to packages/bot-engine/whatsapp/convertMessageToWhatsAppMessage.ts index e2ec2cf6ff..0f49c955f8 100644 --- a/packages/lib/whatsApp/convertMessageToWhatsAppMessage.ts +++ b/packages/bot-engine/whatsapp/convertMessageToWhatsAppMessage.ts @@ -5,7 +5,7 @@ import { } from '@typebot.io/schemas' import { WhatsAppSendingMessage } from '@typebot.io/schemas/features/whatsapp' import { convertRichTextToWhatsAppText } from './convertRichTextToWhatsAppText' -import { isSvgSrc } from '../utils' +import { isSvgSrc } from '@typebot.io/lib/utils' const mp4HttpsUrlRegex = /^https:\/\/.*\.mp4$/ diff --git a/packages/lib/whatsApp/convertRichTextToWhatsAppText.ts b/packages/bot-engine/whatsapp/convertRichTextToWhatsAppText.ts similarity index 100% rename from packages/lib/whatsApp/convertRichTextToWhatsAppText.ts rename to packages/bot-engine/whatsapp/convertRichTextToWhatsAppText.ts diff --git a/apps/viewer/src/features/whatsApp/helpers/downloadMedia.ts b/packages/bot-engine/whatsapp/downloadMedia.ts similarity index 100% rename from apps/viewer/src/features/whatsApp/helpers/downloadMedia.ts rename to packages/bot-engine/whatsapp/downloadMedia.ts diff --git a/apps/viewer/src/features/whatsApp/helpers/resumeWhatsAppFlow.ts b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts similarity index 93% rename from apps/viewer/src/features/whatsApp/helpers/resumeWhatsAppFlow.ts rename to packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts index fcb14a8242..8c0603b8f9 100644 --- a/apps/viewer/src/features/whatsApp/helpers/resumeWhatsAppFlow.ts +++ b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts @@ -1,17 +1,17 @@ -import { continueBotFlow } from '@/features/chat/helpers/continueBotFlow' -import { saveStateToDatabase } from '@/features/chat/helpers/saveStateToDatabase' -import { getSession } from '@/features/chat/queries/getSession' import { SessionState } from '@typebot.io/schemas' import { WhatsAppCredentials, WhatsAppIncomingMessage, } from '@typebot.io/schemas/features/whatsapp' +import { env } from '@typebot.io/env' +import { sendChatReplyToWhatsApp } from './sendChatReplyToWhatsApp' import { startWhatsAppSession } from './startWhatsAppSession' -import prisma from '@/lib/prisma' -import { decrypt } from '@typebot.io/lib/api' import { downloadMedia } from './downloadMedia' -import { env } from '@typebot.io/env' -import { sendChatReplyToWhatsApp } from '@typebot.io/lib/whatsApp/sendChatReplyToWhatsApp' +import { getSession } from '../queries/getSession' +import { continueBotFlow } from '../continueBotFlow' +import { decrypt } from '@typebot.io/lib/api' +import { saveStateToDatabase } from '../saveStateToDatabase' +import prisma from '@typebot.io/lib/prisma' export const resumeWhatsAppFlow = async ({ receivedMessage, diff --git a/packages/lib/whatsApp/sendChatReplyToWhatsApp.ts b/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts similarity index 98% rename from packages/lib/whatsApp/sendChatReplyToWhatsApp.ts rename to packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts index b8d8cb5845..dbcf743c94 100644 --- a/packages/lib/whatsApp/sendChatReplyToWhatsApp.ts +++ b/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts @@ -13,7 +13,7 @@ import { sendWhatsAppMessage } from './sendWhatsAppMessage' import { captureException } from '@sentry/nextjs' import { HTTPError } from 'got' import { convertInputToWhatsAppMessages } from './convertInputToWhatsAppMessage' -import { isNotDefined } from '../utils' +import { isNotDefined } from '@typebot.io/lib/utils' import { computeTypingDuration } from '../computeTypingDuration' // Media can take some time to be delivered. This make sure we don't send a message before the media is delivered. diff --git a/packages/lib/whatsApp/sendWhatsAppMessage.ts b/packages/bot-engine/whatsapp/sendWhatsAppMessage.ts similarity index 100% rename from packages/lib/whatsApp/sendWhatsAppMessage.ts rename to packages/bot-engine/whatsapp/sendWhatsAppMessage.ts diff --git a/apps/viewer/src/features/whatsApp/helpers/startWhatsAppSession.ts b/packages/bot-engine/whatsapp/startWhatsAppSession.ts similarity index 97% rename from apps/viewer/src/features/whatsApp/helpers/startWhatsAppSession.ts rename to packages/bot-engine/whatsapp/startWhatsAppSession.ts index d47bc7e053..64ddbbc1e1 100644 --- a/apps/viewer/src/features/whatsApp/helpers/startWhatsAppSession.ts +++ b/packages/bot-engine/whatsapp/startWhatsAppSession.ts @@ -1,5 +1,4 @@ -import { startSession } from '@/features/chat/helpers/startSession' -import prisma from '@/lib/prisma' +import prisma from '@typebot.io/lib/prisma' import { ChatReply, ComparisonOperators, @@ -13,8 +12,9 @@ import { WhatsAppCredentials, WhatsAppIncomingMessage, } from '@typebot.io/schemas/features/whatsapp' -import { isNotDefined } from '@typebot.io/lib' +import { isNotDefined } from '@typebot.io/lib/utils' import { decrypt } from '@typebot.io/lib/api/encryption' +import { startSession } from '../startSession' type Props = { message: WhatsAppIncomingMessage diff --git a/packages/emails/src/index.ts b/packages/emails/src/index.ts index b6107af86d..078177093f 100644 --- a/packages/emails/src/index.ts +++ b/packages/emails/src/index.ts @@ -1 +1,2 @@ export * from './emails' +export { render } from '@faire/mjml-react/utils/render' diff --git a/packages/embeds/js/package.json b/packages/embeds/js/package.json index 021a7edb43..f439eb89bc 100644 --- a/packages/embeds/js/package.json +++ b/packages/embeds/js/package.json @@ -16,7 +16,8 @@ "@udecode/plate-common": "^21.1.5", "eventsource-parser": "^1.0.0", "solid-element": "1.7.1", - "solid-js": "1.7.8" + "solid-js": "1.7.8", + "@typebot.io/bot-engine": "workspace:*" }, "devDependencies": { "@babel/preset-typescript": "7.22.5", diff --git a/packages/embeds/js/src/features/blocks/bubbles/textBubble/components/TextBubble.tsx b/packages/embeds/js/src/features/blocks/bubbles/textBubble/components/TextBubble.tsx index a4b537c3fe..f7b7bbe3c6 100644 --- a/packages/embeds/js/src/features/blocks/bubbles/textBubble/components/TextBubble.tsx +++ b/packages/embeds/js/src/features/blocks/bubbles/textBubble/components/TextBubble.tsx @@ -5,7 +5,7 @@ import { PlateBlock } from './plate/PlateBlock' import { computePlainText } from '../helpers/convertRichTextToPlainText' import { clsx } from 'clsx' import { isMobile } from '@/utils/isMobileSignal' -import { computeTypingDuration } from '@typebot.io/lib/computeTypingDuration' +import { computeTypingDuration } from '@typebot.io/bot-engine/computeTypingDuration' type Props = { content: TextBubbleContent diff --git a/packages/embeds/js/tsconfig.json b/packages/embeds/js/tsconfig.json index 2b74782cc2..1c583257ef 100644 --- a/packages/embeds/js/tsconfig.json +++ b/packages/embeds/js/tsconfig.json @@ -14,6 +14,7 @@ "declaration": true, "declarationMap": true, "outDir": "dist", + "noEmit": false, "emitDeclarationOnly": true } } diff --git a/apps/viewer/src/helpers/api/isPlanetScale.ts b/packages/lib/isPlanetScale.ts similarity index 100% rename from apps/viewer/src/helpers/api/isPlanetScale.ts rename to packages/lib/isPlanetScale.ts diff --git a/packages/lib/package.json b/packages/lib/package.json index d27e833b51..e665d78c32 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@sentry/nextjs": "7.66.0", + "@trpc/server": "10.34.0", "@udecode/plate-common": "^21.1.5", "got": "12.6.0", "minio": "7.1.3", diff --git a/apps/viewer/src/lib/prisma.ts b/packages/lib/prisma.ts similarity index 100% rename from apps/viewer/src/lib/prisma.ts rename to packages/lib/prisma.ts diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index ce77240db1..075fbaa1fb 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -3,6 +3,6 @@ "include": ["**/*.ts"], "exclude": ["node_modules"], "compilerOptions": { - "target": "ES2021" + "lib": ["ES2021", "DOM"] } } diff --git a/packages/prisma/tsconfig.json b/packages/prisma/tsconfig.json index 57c1b52376..67f1988ce0 100644 --- a/packages/prisma/tsconfig.json +++ b/packages/prisma/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "@typebot.io/tsconfig/base.json", "include": ["**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules"], + "compilerOptions": { + "declaration": false + } } diff --git a/packages/tsconfig/base.json b/packages/tsconfig/base.json index 6d173f0c81..160553a823 100644 --- a/packages/tsconfig/base.json +++ b/packages/tsconfig/base.json @@ -3,8 +3,8 @@ "display": "Default", "compilerOptions": { "composite": false, - "declaration": true, - "declarationMap": true, + "declaration": false, + "declarationMap": false, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "inlineSources": false, @@ -15,7 +15,8 @@ "preserveWatchOutput": true, "skipLibCheck": true, "strict": true, - "downlevelIteration": true + "downlevelIteration": true, + "noEmit": true }, "exclude": ["node_modules"] } diff --git a/packages/tsconfig/nextjs.json b/packages/tsconfig/nextjs.json index 6af13474e6..42704671a9 100644 --- a/packages/tsconfig/nextjs.json +++ b/packages/tsconfig/nextjs.json @@ -4,13 +4,10 @@ "extends": "./base.json", "compilerOptions": { "allowJs": true, - "declaration": false, - "declarationMap": false, "incremental": true, "jsx": "preserve", "lib": ["dom", "dom.iterable", "esnext"], "module": "esnext", - "noEmit": true, "resolveJsonModule": true, "target": "es5" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 398f374803..55821b99cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,12 +74,6 @@ importers: '@sentry/nextjs': specifier: 7.66.0 version: 7.66.0(next@13.4.3)(react@18.2.0) - '@stripe/stripe-js': - specifier: 1.54.1 - version: 1.54.1 - '@t3-oss/env-nextjs': - specifier: ^0.6.0 - version: 0.6.0(typescript@5.1.6)(zod@3.21.4) '@tanstack/react-query': specifier: ^4.29.19 version: 4.29.19(react-dom@18.2.0)(react@18.2.0) @@ -98,6 +92,9 @@ importers: '@trpc/server': specifier: 10.34.0 version: 10.34.0 + '@typebot.io/bot-engine': + specifier: workspace:* + version: link:../../packages/bot-engine '@typebot.io/emails': specifier: workspace:* version: link:../../packages/emails @@ -107,9 +104,6 @@ importers: '@typebot.io/nextjs': specifier: workspace:* version: link:../../packages/embeds/nextjs - '@typebot.io/viewer': - specifier: workspace:* - version: link:../viewer '@udecode/plate-basic-marks': specifier: 21.1.5 version: 21.1.5(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-react@0.94.2)(slate@0.94.1) @@ -122,9 +116,6 @@ importers: '@udecode/plate-link': specifier: 21.2.0 version: 21.2.0(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-react@0.94.2)(slate@0.94.1) - '@udecode/plate-serializer-html': - specifier: 21.1.5 - version: 21.1.5(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-hyperscript@0.77.0)(slate-react@0.94.2)(slate@0.94.1) '@udecode/plate-ui-link': specifier: 21.2.0 version: 21.2.0(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-react@0.94.2)(slate@0.94.1)(styled-components@6.0.7) @@ -251,9 +242,6 @@ importers: slate-history: specifier: 0.93.0 version: 0.93.0(slate@0.94.1) - slate-hyperscript: - specifier: 0.77.0 - version: 0.77.0(slate@0.94.1) slate-react: specifier: 0.94.2 version: 0.94.2(react-dom@18.2.0)(react@18.2.0)(slate@0.94.1) @@ -524,51 +512,36 @@ importers: '@trpc/server': specifier: 10.34.0 version: 10.34.0 + '@typebot.io/bot-engine': + specifier: workspace:* + version: link:../../packages/bot-engine '@typebot.io/nextjs': specifier: workspace:* version: link:../../packages/embeds/nextjs '@typebot.io/prisma': specifier: workspace:* version: link:../../packages/prisma - '@udecode/plate-common': - specifier: ^21.1.5 - version: 21.1.5(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-react@0.94.2)(slate@0.94.1) ai: specifier: 2.1.32 version: 2.1.32(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.0)(vue@3.3.4) bot-engine: specifier: workspace:* version: link:../../packages/deprecated/bot-engine - chrono-node: - specifier: 2.6.6 - version: 2.6.6 cors: specifier: 2.8.5 version: 2.8.5 - date-fns: - specifier: ^2.30.0 - version: 2.30.0 - eventsource-parser: - specifier: ^1.0.0 - version: 1.0.0 google-spreadsheet: specifier: 4.0.2 version: 4.0.2(google-auth-library@8.9.0) got: specifier: 12.6.0 version: 12.6.0 - libphonenumber-js: - specifier: 1.10.37 - version: 1.10.37 next: specifier: 13.4.3 version: 13.4.3(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) nextjs-cors: specifier: 2.1.2 version: 2.1.2(next@13.4.3) - node-html-parser: - specifier: ^6.1.5 - version: 6.1.5 nodemailer: specifier: 6.9.3 version: 6.9.3 @@ -584,9 +557,6 @@ importers: react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) - remark-slate: - specifier: ^1.8.6 - version: 1.8.6 stripe: specifier: 12.13.0 version: 12.13.0 @@ -636,9 +606,6 @@ importers: '@types/react': specifier: 18.2.15 version: 18.2.15 - '@types/sanitize-html': - specifier: 2.9.0 - version: 2.9.0 dotenv-cli: specifier: ^7.2.1 version: 7.2.1 @@ -654,9 +621,6 @@ importers: next-runtime-env: specifier: ^1.6.2 version: 1.6.2 - node-fetch: - specifier: 3.3.1 - version: 3.3.1 papaparse: specifier: 5.4.1 version: 5.4.1 @@ -670,6 +634,88 @@ importers: specifier: 3.21.4 version: 3.21.4 + packages/bot-engine: + dependencies: + '@paralleldrive/cuid2': + specifier: 2.2.1 + version: 2.2.1 + '@planetscale/database': + specifier: ^1.8.0 + version: 1.8.0 + '@sentry/nextjs': + specifier: 7.66.0 + version: 7.66.0(next@13.4.3)(react@18.2.0) + '@trpc/server': + specifier: 10.34.0 + version: 10.34.0 + '@typebot.io/emails': + specifier: workspace:* + version: link:../emails + '@typebot.io/env': + specifier: workspace:* + version: link:../env + '@typebot.io/lib': + specifier: workspace:* + version: link:../lib + '@typebot.io/prisma': + specifier: workspace:* + version: link:../prisma + '@typebot.io/schemas': + specifier: workspace:* + version: link:../schemas + '@typebot.io/tsconfig': + specifier: workspace:* + version: link:../tsconfig + '@udecode/plate-common': + specifier: ^21.1.5 + version: 21.1.5(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-react@0.94.2)(slate@0.94.1) + ai: + specifier: 2.1.32 + version: 2.1.32(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.0)(vue@3.3.4) + chrono-node: + specifier: 2.6.6 + version: 2.6.6 + date-fns: + specifier: ^2.30.0 + version: 2.30.0 + google-auth-library: + specifier: 8.9.0 + version: 8.9.0 + google-spreadsheet: + specifier: 4.0.2 + version: 4.0.2(google-auth-library@8.9.0) + got: + specifier: 12.6.0 + version: 12.6.0 + libphonenumber-js: + specifier: 1.10.37 + version: 1.10.37 + node-html-parser: + specifier: ^6.1.5 + version: 6.1.5 + nodemailer: + specifier: 6.9.3 + version: 6.9.3 + openai-edge: + specifier: 1.2.2 + version: 1.2.2 + qs: + specifier: ^6.11.2 + version: 6.11.2 + remark-slate: + specifier: ^1.8.6 + version: 1.8.6 + stripe: + specifier: 12.13.0 + version: 12.13.0 + devDependencies: + '@types/nodemailer': + specifier: 6.4.8 + version: 6.4.8 + '@types/qs': + specifier: 6.9.7 + version: 6.9.7 + packages/deprecated/bot-engine: dependencies: '@stripe/react-stripe-js': @@ -847,6 +893,9 @@ importers: '@stripe/stripe-js': specifier: 1.54.1 version: 1.54.1 + '@typebot.io/bot-engine': + specifier: workspace:* + version: link:../../bot-engine '@udecode/plate-common': specifier: ^21.1.5 version: 21.1.5(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-react@0.94.2)(slate@0.94.1) @@ -1124,6 +1173,9 @@ importers: '@sentry/nextjs': specifier: 7.66.0 version: 7.66.0(next@13.4.3)(react@18.2.0) + '@trpc/server': + specifier: 10.34.0 + version: 10.34.0 '@udecode/plate-common': specifier: ^21.1.5 version: 21.1.5(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-react@0.94.2)(slate@0.94.1) @@ -8845,12 +8897,6 @@ packages: resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} dev: false - /@types/sanitize-html@2.9.0: - resolution: {integrity: sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==} - dependencies: - htmlparser2: 8.0.2 - dev: true - /@types/sax@1.2.4: resolution: {integrity: sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==} dependencies: @@ -9375,41 +9421,6 @@ packages: - scheduler dev: false - /@udecode/plate-serializer-html@21.1.5(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-hyperscript@0.77.0)(slate-react@0.94.2)(slate@0.94.1): - resolution: {integrity: sha512-V4P2o78Mpj7VvpvfgXMGG23XQZ30Kq/JXQTQxYxJufHF5hwy0MCYxxMK/oF8ED8zWM4G9hIKMHOqA4ZoFACjIA==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - slate: '>=0.94.0' - slate-history: '>=0.93.0' - slate-hyperscript: '>=0.66.0' - slate-react: '>=0.94.0' - dependencies: - '@udecode/plate-common': 21.1.5(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-react@0.94.2)(slate@0.94.1) - html-entities: 2.4.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - slate: 0.94.1 - slate-history: 0.93.0(slate@0.94.1) - slate-hyperscript: 0.77.0(slate@0.94.1) - slate-react: 0.94.2(react-dom@18.2.0)(react@18.2.0)(slate@0.94.1) - transitivePeerDependencies: - - '@babel/core' - - '@babel/template' - - '@types/react' - - jotai-devtools - - jotai-immer - - jotai-optics - - jotai-redux - - jotai-tanstack-query - - jotai-urql - - jotai-valtio - - jotai-xstate - - jotai-zustand - - react-native - - scheduler - dev: false - /@udecode/plate-styled-components@21.1.5(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-react@0.94.2)(slate@0.94.1)(styled-components@6.0.7): resolution: {integrity: sha512-/L212XVeywPoVzpu51NrUfli4ZeD7nc5JacN23UAKhqjpfgJafrRtgUMC0jkWO8cwaBUEhQEZ/sGh6Tg9T805Q==} peerDependencies: @@ -12051,11 +12062,6 @@ packages: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} dev: false - /data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - dev: true - /data-urls@3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} engines: {node: '>=12'} @@ -13690,14 +13696,6 @@ packages: xml-js: 1.6.11 dev: false - /fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.2.1 - dev: true - /fflate@0.4.8: resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} dev: false @@ -13892,13 +13890,6 @@ packages: combined-stream: 1.0.8 mime-types: 2.1.35 - /formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - dependencies: - fetch-blob: 3.2.0 - dev: true - /formidable@2.1.2: resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} dependencies: @@ -17340,11 +17331,6 @@ packages: resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} dev: false - /node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - dev: true - /node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} dependencies: @@ -17373,15 +17359,6 @@ packages: dependencies: whatwg-url: 5.0.0 - /node-fetch@3.3.1: - resolution: {integrity: sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - dev: true - /node-forge@1.3.1: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} @@ -20336,15 +20313,6 @@ packages: slate: 0.94.1 dev: false - /slate-hyperscript@0.77.0(slate@0.94.1): - resolution: {integrity: sha512-M6uRpttwKnosniQORNPYQABHQ9XWC7qaSr/127LWWPjTOR5MSSwrHGrghN81BhZVqpICHrI7jkPA2813cWdHNA==} - peerDependencies: - slate: '>=0.65.3' - dependencies: - is-plain-object: 5.0.0 - slate: 0.94.1 - dev: false - /slate-react@0.94.2(react-dom@18.2.0)(react@18.2.0)(slate@0.94.1): resolution: {integrity: sha512-4wDSuTuGBkdQ609CS55uc2Yhfa5but21usBgAtCVhPJQazL85kzN2vUUYTmGb7d/mpP9tdnJiVPopIyhqlRJ8Q==} peerDependencies: @@ -22338,11 +22306,6 @@ packages: transitivePeerDependencies: - encoding - /web-streams-polyfill@3.2.1: - resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} - engines: {node: '>= 8'} - dev: true - /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} From d7dc5fb5fb7bd07b477470c4941f9fc57d7f08b0 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Wed, 20 Sep 2023 16:06:53 +0200 Subject: [PATCH 011/233] :recycle: Remove storage limit related code --- .../billing/api/createCheckoutSession.ts | 13 +--- .../src/features/billing/api/getUsage.ts | 16 +---- .../billing/api/updateSubscription.ts | 24 +------ .../src/features/billing/billing.spec.ts | 4 +- .../billing/components/ChangePlanForm.tsx | 1 - .../billing/components/PreCheckoutModal.tsx | 1 - .../billing/components/ProPlanPricingCard.tsx | 50 +------------- .../components/StarterPlanPricingCard.tsx | 46 ------------- .../billing/helpers/parseSubscriptionItems.ts | 46 ++++--------- .../dashboard/components/DashboardPage.tsx | 4 +- apps/builder/src/pages/api/stripe/webhook.ts | 11 +-- .../builder/src/test/utils/databaseActions.ts | 5 +- apps/docs/openapi/builder/_spec_.json | 30 +------- .../PricingPage/PlanComparisonTables.tsx | 17 ----- .../components/PricingPage/ProPlanCard.tsx | 52 +------------- .../PricingPage/StarterPlanCard.tsx | 54 +-------------- apps/viewer/src/test/fileUpload.spec.ts | 28 +------- apps/viewer/src/test/sendEmail.spec.ts | 2 +- packages/bot-engine/continueBotFlow.ts | 1 - packages/lib/api/getUsage.ts | 17 +---- packages/lib/playwright/databaseActions.ts | 11 +-- packages/lib/pricing.ts | 68 +------------------ packages/schemas/features/telemetry.ts | 3 - packages/scripts/sendTotalResultsDigest.ts | 30 ++------ packages/scripts/tsconfig.json | 3 +- 25 files changed, 44 insertions(+), 493 deletions(-) diff --git a/apps/builder/src/features/billing/api/createCheckoutSession.ts b/apps/builder/src/features/billing/api/createCheckoutSession.ts index 7b8f9958dc..ec466b1c0b 100644 --- a/apps/builder/src/features/billing/api/createCheckoutSession.ts +++ b/apps/builder/src/features/billing/api/createCheckoutSession.ts @@ -27,7 +27,6 @@ export const createCheckoutSession = authenticatedProcedure plan: z.enum([Plan.STARTER, Plan.PRO]), returnUrl: z.string(), additionalChats: z.number(), - additionalStorage: z.number(), vat: z .object({ type: z.string(), @@ -53,7 +52,6 @@ export const createCheckoutSession = authenticatedProcedure plan, returnUrl, additionalChats, - additionalStorage, isYearly, }, ctx: { user }, @@ -119,7 +117,6 @@ export const createCheckoutSession = authenticatedProcedure plan, returnUrl, additionalChats, - additionalStorage, isYearly, }) @@ -142,7 +139,6 @@ type Props = { plan: 'STARTER' | 'PRO' returnUrl: string additionalChats: number - additionalStorage: number isYearly: boolean userId: string } @@ -156,7 +152,6 @@ export const createCheckoutSessionUrl = plan, returnUrl, additionalChats, - additionalStorage, isYearly, }: Props) => { const session = await stripe.checkout.sessions.create({ @@ -173,17 +168,11 @@ export const createCheckoutSessionUrl = workspaceId, plan, additionalChats, - additionalStorage, }, currency, billing_address_collection: 'required', automatic_tax: { enabled: true }, - line_items: parseSubscriptionItems( - plan, - additionalChats, - additionalStorage, - isYearly - ), + line_items: parseSubscriptionItems(plan, additionalChats, isYearly), }) return session.url diff --git a/apps/builder/src/features/billing/api/getUsage.ts b/apps/builder/src/features/billing/api/getUsage.ts index 0877e35af2..56cb51ecb7 100644 --- a/apps/builder/src/features/billing/api/getUsage.ts +++ b/apps/builder/src/features/billing/api/getUsage.ts @@ -19,9 +19,7 @@ export const getUsage = authenticatedProcedure workspaceId: z.string(), }) ) - .output( - z.object({ totalChatsUsed: z.number(), totalStorageUsed: z.number() }) - ) + .output(z.object({ totalChatsUsed: z.number() })) .query(async ({ input: { workspaceId }, ctx: { user } }) => { const workspace = await prisma.workspace.findFirst({ where: { @@ -55,20 +53,8 @@ export const getUsage = authenticatedProcedure }, }, }) - const { - _sum: { storageUsed: totalStorageUsed }, - } = await prisma.answer.aggregate({ - where: { - storageUsed: { gt: 0 }, - result: { - typebotId: { in: workspace.typebots.map((typebot) => typebot.id) }, - }, - }, - _sum: { storageUsed: true }, - }) return { totalChatsUsed, - totalStorageUsed: totalStorageUsed ?? 0, } }) diff --git a/apps/builder/src/features/billing/api/updateSubscription.ts b/apps/builder/src/features/billing/api/updateSubscription.ts index 26abfad57b..fe31eba07f 100644 --- a/apps/builder/src/features/billing/api/updateSubscription.ts +++ b/apps/builder/src/features/billing/api/updateSubscription.ts @@ -7,8 +7,8 @@ import { workspaceSchema } from '@typebot.io/schemas' import Stripe from 'stripe' import { isDefined } from '@typebot.io/lib' import { z } from 'zod' -import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing' -import { chatPriceIds, storagePriceIds } from './getSubscription' +import { getChatsLimit } from '@typebot.io/lib/pricing' +import { chatPriceIds } from './getSubscription' import { createCheckoutSessionUrl } from './createCheckoutSession' import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden' import { getUsage } from '@typebot.io/lib/api/getUsage' @@ -31,7 +31,6 @@ export const updateSubscription = authenticatedProcedure workspaceId: z.string(), plan: z.enum([Plan.STARTER, Plan.PRO]), additionalChats: z.number(), - additionalStorage: z.number(), currency: z.enum(['usd', 'eur']), isYearly: z.boolean(), }) @@ -48,7 +47,6 @@ export const updateSubscription = authenticatedProcedure workspaceId, plan, additionalChats, - additionalStorage, currency, isYearly, returnUrl, @@ -100,9 +98,6 @@ export const updateSubscription = authenticatedProcedure const currentAdditionalChatsItemId = subscription?.items.data.find( (item) => chatPriceIds.includes(item.price.id) )?.id - const currentAdditionalStorageItemId = subscription?.items.data.find( - (item) => storagePriceIds.includes(item.price.id) - )?.id const frequency = isYearly ? 'yearly' : 'monthly' const items = [ @@ -123,18 +118,6 @@ export const updateSubscription = authenticatedProcedure }), deleted: subscription ? additionalChats === 0 : undefined, }, - additionalStorage === 0 && !currentAdditionalStorageItemId - ? undefined - : { - id: currentAdditionalStorageItemId, - price: priceIds[plan].storage[frequency], - quantity: getStorageLimit({ - plan, - additionalStorageIndex: additionalStorage, - customStorageLimit: null, - }), - deleted: subscription ? additionalStorage === 0 : undefined, - }, ].filter(isDefined) if (subscription) { @@ -151,7 +134,6 @@ export const updateSubscription = authenticatedProcedure plan, returnUrl, additionalChats, - additionalStorage, isYearly, }) @@ -175,7 +157,6 @@ export const updateSubscription = authenticatedProcedure data: { plan, additionalChatsIndex: additionalChats, - additionalStorageIndex: additionalStorage, isQuarantined, }, }) @@ -188,7 +169,6 @@ export const updateSubscription = authenticatedProcedure data: { plan, additionalChatsIndex: additionalChats, - additionalStorageIndex: additionalStorage, }, }, ]) diff --git a/apps/builder/src/features/billing/billing.spec.ts b/apps/builder/src/features/billing/billing.spec.ts index e04fb1a0eb..6dcaf03938 100644 --- a/apps/builder/src/features/billing/billing.spec.ts +++ b/apps/builder/src/features/billing/billing.spec.ts @@ -85,7 +85,6 @@ test('should display valid usage', async ({ page }) => { await injectFakeResults({ count: 10, typebotId: usageTypebotId, - fakeStorage: 1100 * 1024 * 1024, }) await page.click('text=Free workspace') await page.click('text="Usage Workspace"') @@ -101,7 +100,6 @@ test('should display valid usage', async ({ page }) => { await injectFakeResults({ typebotId: usageTypebotId, count: 1090, - fakeStorage: 1200 * 1024 * 1024, }) await page.click('text="Settings"') await page.click('text="Billing & Usage"') @@ -140,7 +138,7 @@ test('plan changes should work', async ({ page }) => { quantity: 1, }, ], - { plan: Plan.STARTER, additionalChatsIndex: 0, additionalStorageIndex: 0 } + { plan: Plan.STARTER, additionalChatsIndex: 0 } ) // Update plan with additional quotas diff --git a/apps/builder/src/features/billing/components/ChangePlanForm.tsx b/apps/builder/src/features/billing/components/ChangePlanForm.tsx index 5f271b4716..2711d2356e 100644 --- a/apps/builder/src/features/billing/components/ChangePlanForm.tsx +++ b/apps/builder/src/features/billing/components/ChangePlanForm.tsx @@ -84,7 +84,6 @@ export const ChangePlanForm = ({ workspace }: Props) => { plan, workspaceId: workspace.id, additionalChats: selectedChatsLimitIndex, - additionalStorage: selectedStorageLimitIndex, currency: data?.subscription?.currency ?? (guessIfUserIsEuropean() ? 'eur' : 'usd'), diff --git a/apps/builder/src/features/billing/components/PreCheckoutModal.tsx b/apps/builder/src/features/billing/components/PreCheckoutModal.tsx index 22323647db..4332ad41f7 100644 --- a/apps/builder/src/features/billing/components/PreCheckoutModal.tsx +++ b/apps/builder/src/features/billing/components/PreCheckoutModal.tsx @@ -26,7 +26,6 @@ export type PreCheckoutModalProps = { plan: 'STARTER' | 'PRO' workspaceId: string additionalChats: number - additionalStorage: number currency: 'eur' | 'usd' isYearly: boolean } diff --git a/apps/builder/src/features/billing/components/ProPlanPricingCard.tsx b/apps/builder/src/features/billing/components/ProPlanPricingCard.tsx index 6a725d5f97..29bafea25d 100644 --- a/apps/builder/src/features/billing/components/ProPlanPricingCard.tsx +++ b/apps/builder/src/features/billing/components/ProPlanPricingCard.tsx @@ -23,8 +23,6 @@ import { computePrice, formatPrice, getChatsLimit, - getStorageLimit, - storageLimit, } from '@typebot.io/lib/pricing' import { FeaturesList } from './FeaturesList' import { MoreInfoTooltip } from '@/components/MoreInfoTooltip' @@ -35,7 +33,6 @@ type Props = { workspace: Pick< Workspace, | 'additionalChatsIndex' - | 'additionalStorageIndex' | 'plan' | 'customChatsLimit' | 'customStorageLimit' @@ -80,25 +77,18 @@ export const ProPlanPricingCard = ({ return } setSelectedChatsLimitIndex(workspace.additionalChatsIndex ?? 0) - setSelectedStorageLimitIndex(workspace.additionalStorageIndex ?? 0) }, [ selectedChatsLimitIndex, selectedStorageLimitIndex, workspace.additionalChatsIndex, - workspace.additionalStorageIndex, workspace?.plan, ]) const workspaceChatsLimit = workspace ? getChatsLimit(workspace) : undefined - const workspaceStorageLimit = workspace - ? getStorageLimit(workspace) - : undefined const isCurrentPlan = chatsLimit[Plan.PRO].graduatedPrice[selectedChatsLimitIndex ?? 0] .totalIncluded === workspaceChatsLimit && - storageLimit[Plan.PRO].graduatedPrice[selectedStorageLimitIndex ?? 0] - .totalIncluded === workspaceStorageLimit && isYearly === currentSubscription?.isYearly const getButtonLabel = () => { @@ -110,10 +100,7 @@ export const ProPlanPricingCard = ({ if (workspace?.plan === Plan.PRO) { if (isCurrentPlan) return scopedT('upgradeButton.current') - if ( - selectedChatsLimitIndex !== workspace.additionalChatsIndex || - selectedStorageLimitIndex !== workspace.additionalStorageIndex - ) + if (selectedChatsLimitIndex !== workspace.additionalChatsIndex) return t('update') } return t('upgrade') @@ -135,7 +122,6 @@ export const ProPlanPricingCard = ({ computePrice( Plan.PRO, selectedChatsLimitIndex ?? 0, - selectedStorageLimitIndex ?? 0, isYearly ? 'yearly' : 'monthly' ) ?? NaN @@ -238,40 +224,6 @@ export const ProPlanPricingCard = ({ {scopedT('chatsTooltip')} , - - - - } - size="sm" - isLoading={selectedStorageLimitIndex === undefined} - > - {selectedStorageLimitIndex !== undefined - ? parseNumberWithCommas( - storageLimit.PRO.graduatedPrice[ - selectedStorageLimitIndex - ].totalIncluded - ) - : undefined} - - - {storageLimit.PRO.graduatedPrice.map((price, index) => ( - setSelectedStorageLimitIndex(index)} - > - {parseNumberWithCommas(price.totalIncluded)} - - ))} - - {' '} - {scopedT('storageLimit')} - - - {scopedT('storageLimitTooltip')} - - , scopedT('pro.customDomains'), scopedT('pro.analytics'), ]} diff --git a/apps/builder/src/features/billing/components/StarterPlanPricingCard.tsx b/apps/builder/src/features/billing/components/StarterPlanPricingCard.tsx index 039a0aad38..f26256fb5a 100644 --- a/apps/builder/src/features/billing/components/StarterPlanPricingCard.tsx +++ b/apps/builder/src/features/billing/components/StarterPlanPricingCard.tsx @@ -19,8 +19,6 @@ import { computePrice, formatPrice, getChatsLimit, - getStorageLimit, - storageLimit, } from '@typebot.io/lib/pricing' import { FeaturesList } from './FeaturesList' import { MoreInfoTooltip } from '@/components/MoreInfoTooltip' @@ -31,7 +29,6 @@ type Props = { workspace: Pick< Workspace, | 'additionalChatsIndex' - | 'additionalStorageIndex' | 'plan' | 'customChatsLimit' | 'customStorageLimit' @@ -76,25 +73,18 @@ export const StarterPlanPricingCard = ({ return } setSelectedChatsLimitIndex(workspace.additionalChatsIndex ?? 0) - setSelectedStorageLimitIndex(workspace.additionalStorageIndex ?? 0) }, [ selectedChatsLimitIndex, selectedStorageLimitIndex, workspace.additionalChatsIndex, - workspace.additionalStorageIndex, workspace?.plan, ]) const workspaceChatsLimit = workspace ? getChatsLimit(workspace) : undefined - const workspaceStorageLimit = workspace - ? getStorageLimit(workspace) - : undefined const isCurrentPlan = chatsLimit[Plan.STARTER].graduatedPrice[selectedChatsLimitIndex ?? 0] .totalIncluded === workspaceChatsLimit && - storageLimit[Plan.STARTER].graduatedPrice[selectedStorageLimitIndex ?? 0] - .totalIncluded === workspaceStorageLimit && isYearly === currentSubscription?.isYearly const getButtonLabel = () => { @@ -109,7 +99,6 @@ export const StarterPlanPricingCard = ({ if ( selectedChatsLimitIndex !== workspace.additionalChatsIndex || - selectedStorageLimitIndex !== workspace.additionalStorageIndex || isYearly !== currentSubscription?.isYearly ) return t('update') @@ -133,7 +122,6 @@ export const StarterPlanPricingCard = ({ computePrice( Plan.STARTER, selectedChatsLimitIndex ?? 0, - selectedStorageLimitIndex ?? 0, isYearly ? 'yearly' : 'monthly' ) ?? NaN @@ -185,40 +173,6 @@ export const StarterPlanPricingCard = ({ {scopedT('chatsTooltip')} , - - - - } - size="sm" - isLoading={selectedStorageLimitIndex === undefined} - > - {selectedStorageLimitIndex !== undefined - ? parseNumberWithCommas( - storageLimit.STARTER.graduatedPrice[ - selectedStorageLimitIndex - ].totalIncluded - ) - : undefined} - - - {storageLimit.STARTER.graduatedPrice.map((price, index) => ( - setSelectedStorageLimitIndex(index)} - > - {parseNumberWithCommas(price.totalIncluded)} - - ))} - - {' '} - {scopedT('storageLimit')} - - - {scopedT('storageLimitTooltip')} - - , scopedT('starter.brandingRemoved'), scopedT('starter.fileUploadBlock'), scopedT('starter.createFolders'), diff --git a/apps/builder/src/features/billing/helpers/parseSubscriptionItems.ts b/apps/builder/src/features/billing/helpers/parseSubscriptionItems.ts index ba8911c9e8..276460bde5 100644 --- a/apps/builder/src/features/billing/helpers/parseSubscriptionItems.ts +++ b/apps/builder/src/features/billing/helpers/parseSubscriptionItems.ts @@ -1,10 +1,9 @@ -import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing' +import { getChatsLimit } from '@typebot.io/lib/pricing' import { priceIds } from '@typebot.io/lib/api/pricing' export const parseSubscriptionItems = ( plan: 'STARTER' | 'PRO', additionalChats: number, - additionalStorage: number, isYearly: boolean ) => { const frequency = isYearly ? 'yearly' : 'monthly' @@ -13,33 +12,18 @@ export const parseSubscriptionItems = ( price: priceIds[plan].base[frequency], quantity: 1, }, - ] - .concat( - additionalChats > 0 - ? [ - { - price: priceIds[plan].chats[frequency], - quantity: getChatsLimit({ - plan, - additionalChatsIndex: additionalChats, - customChatsLimit: null, - }), - }, - ] - : [] - ) - .concat( - additionalStorage > 0 - ? [ - { - price: priceIds[plan].storage[frequency], - quantity: getStorageLimit({ - plan, - additionalStorageIndex: additionalStorage, - customStorageLimit: null, - }), - }, - ] - : [] - ) + ].concat( + additionalChats > 0 + ? [ + { + price: priceIds[plan].chats[frequency], + quantity: getChatsLimit({ + plan, + additionalChatsIndex: additionalChats, + customChatsLimit: null, + }), + }, + ] + : [] + ) } diff --git a/apps/builder/src/features/dashboard/components/DashboardPage.tsx b/apps/builder/src/features/dashboard/components/DashboardPage.tsx index 58417781a1..428617b360 100644 --- a/apps/builder/src/features/dashboard/components/DashboardPage.tsx +++ b/apps/builder/src/features/dashboard/components/DashboardPage.tsx @@ -33,11 +33,10 @@ export const DashboardPage = () => { }) useEffect(() => { - const { subscribePlan, chats, storage, isYearly, claimCustomPlan } = + const { subscribePlan, chats, isYearly, claimCustomPlan } = router.query as { subscribePlan: Plan | undefined chats: string | undefined - storage: string | undefined isYearly: string | undefined claimCustomPlan: string | undefined } @@ -55,7 +54,6 @@ export const DashboardPage = () => { plan: subscribePlan as 'PRO' | 'STARTER', workspaceId: workspace.id, additionalChats: chats ? parseInt(chats) : 0, - additionalStorage: storage ? parseInt(storage) : 0, currency: guessIfUserIsEuropean() ? 'eur' : 'usd', isYearly: isYearly === 'false' ? false : true, }) diff --git a/apps/builder/src/pages/api/stripe/webhook.ts b/apps/builder/src/pages/api/stripe/webhook.ts index e279694271..0072c406b1 100644 --- a/apps/builder/src/pages/api/stripe/webhook.ts +++ b/apps/builder/src/pages/api/stripe/webhook.ts @@ -47,15 +47,13 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { | { plan: 'STARTER' | 'PRO' additionalChats: string - additionalStorage: string workspaceId: string userId: string } | { claimableCustomPlanId: string; userId: string } if ('plan' in metadata) { - const { workspaceId, plan, additionalChats, additionalStorage } = - metadata - if (!workspaceId || !plan || !additionalChats || !additionalStorage) + const { workspaceId, plan, additionalChats } = metadata + if (!workspaceId || !plan || !additionalChats) return res .status(500) .send({ message: `Couldn't retrieve valid metadata` }) @@ -65,7 +63,6 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { plan, stripeId: session.customer as string, additionalChatsIndex: parseInt(additionalChats), - additionalStorageIndex: parseInt(additionalStorage), isQuarantined: false, }, include: { @@ -88,7 +85,6 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { data: { plan, additionalChatsIndex: parseInt(additionalChats), - additionalStorageIndex: parseInt(additionalStorage), }, }, ]) @@ -124,7 +120,6 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { data: { plan: Plan.CUSTOM, additionalChatsIndex: 0, - additionalStorageIndex: 0, }, }, ]) @@ -154,7 +149,6 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { data: { plan: Plan.FREE, additionalChatsIndex: 0, - additionalStorageIndex: 0, customChatsLimit: null, customStorageLimit: null, customSeatsLimit: null, @@ -179,7 +173,6 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { data: { plan: Plan.FREE, additionalChatsIndex: 0, - additionalStorageIndex: 0, }, }, ]) diff --git a/apps/builder/src/test/utils/databaseActions.ts b/apps/builder/src/test/utils/databaseActions.ts index bf642c6971..d3872168d6 100644 --- a/apps/builder/src/test/utils/databaseActions.ts +++ b/apps/builder/src/test/utils/databaseActions.ts @@ -18,10 +18,7 @@ const stripe = new Stripe(env.STRIPE_SECRET_KEY ?? '', { export const addSubscriptionToWorkspace = async ( workspaceId: string, items: Stripe.SubscriptionCreateParams.Item[], - metadata: Pick< - Workspace, - 'additionalChatsIndex' | 'additionalStorageIndex' | 'plan' - > + metadata: Pick ) => { const { id: stripeId } = await stripe.customers.create({ email: 'test-user@gmail.com', diff --git a/apps/docs/openapi/builder/_spec_.json b/apps/docs/openapi/builder/_spec_.json index f8c4fa53e3..e4cff484d2 100644 --- a/apps/docs/openapi/builder/_spec_.json +++ b/apps/docs/openapi/builder/_spec_.json @@ -232,15 +232,11 @@ }, "additionalChatsIndex": { "type": "number" - }, - "additionalStorageIndex": { - "type": "number" } }, "required": [ "plan", - "additionalChatsIndex", - "additionalStorageIndex" + "additionalChatsIndex" ], "additionalProperties": false } @@ -320,21 +316,13 @@ "chatsLimit": { "type": "number" }, - "storageLimit": { - "type": "number" - }, "totalChatsUsed": { "type": "number" - }, - "totalStorageUsed": { - "type": "number" } }, "required": [ "chatsLimit", - "storageLimit", - "totalChatsUsed", - "totalStorageUsed" + "totalChatsUsed" ], "additionalProperties": false } @@ -30391,9 +30379,6 @@ "additionalChats": { "type": "number" }, - "additionalStorage": { - "type": "number" - }, "vat": { "type": "object", "properties": { @@ -30422,7 +30407,6 @@ "plan", "returnUrl", "additionalChats", - "additionalStorage", "isYearly" ], "additionalProperties": false @@ -30492,9 +30476,6 @@ "additionalChats": { "type": "number" }, - "additionalStorage": { - "type": "number" - }, "currency": { "type": "string", "enum": [ @@ -30511,7 +30492,6 @@ "workspaceId", "plan", "additionalChats", - "additionalStorage", "currency", "isYearly" ], @@ -30767,14 +30747,10 @@ "properties": { "totalChatsUsed": { "type": "number" - }, - "totalStorageUsed": { - "type": "number" } }, "required": [ - "totalChatsUsed", - "totalStorageUsed" + "totalChatsUsed" ], "additionalProperties": false } diff --git a/apps/landing-page/components/PricingPage/PlanComparisonTables.tsx b/apps/landing-page/components/PricingPage/PlanComparisonTables.tsx index fe6ec5a05a..a6f61cb969 100644 --- a/apps/landing-page/components/PricingPage/PlanComparisonTables.tsx +++ b/apps/landing-page/components/PricingPage/PlanComparisonTables.tsx @@ -24,7 +24,6 @@ import { formatPrice, prices, seatsLimit, - storageLimit, } from '@typebot.io/lib/pricing' import { parseNumberWithCommas } from '@typebot.io/lib' @@ -85,22 +84,6 @@ export const PlanComparisonTables = () => ( 2 GB 10 GB - - Additional Storage - - - {formatPrice(storageLimit.STARTER.graduatedPrice[1].price)} per{' '} - {storageLimit.STARTER.graduatedPrice[1].totalIncluded - - storageLimit.STARTER.graduatedPrice[0].totalIncluded}{' '} - GB - - - {formatPrice(storageLimit.PRO.graduatedPrice[1].price)} per{' '} - {storageLimit.PRO.graduatedPrice[1].totalIncluded - - storageLimit.PRO.graduatedPrice[0].totalIncluded}{' '} - GB - - Members Just you diff --git a/apps/landing-page/components/PricingPage/ProPlanCard.tsx b/apps/landing-page/components/PricingPage/ProPlanCard.tsx index 64ffc00f03..3f95a337f7 100644 --- a/apps/landing-page/components/PricingPage/ProPlanCard.tsx +++ b/apps/landing-page/components/PricingPage/ProPlanCard.tsx @@ -15,12 +15,7 @@ import { Plan } from '@typebot.io/prisma' import Link from 'next/link' import React, { useState } from 'react' import { parseNumberWithCommas } from '@typebot.io/lib' -import { - chatsLimit, - computePrice, - seatsLimit, - storageLimit, -} from '@typebot.io/lib/pricing' +import { chatsLimit, computePrice, seatsLimit } from '@typebot.io/lib/pricing' import { PricingCard } from './PricingCard' type Props = { @@ -30,14 +25,11 @@ type Props = { export const ProPlanCard = ({ isYearly }: Props) => { const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] = useState(0) - const [selectedStorageLimitIndex, setSelectedStorageLimitIndex] = - useState(0) const price = computePrice( Plan.PRO, selectedChatsLimitIndex ?? 0, - selectedStorageLimitIndex ?? 0, isYearly ? 'yearly' : 'monthly' ) ?? NaN @@ -93,46 +85,6 @@ export const ProPlanCard = ({ isYearly }: Props) => { , - - - } - size="sm" - variant="outline" - isLoading={selectedStorageLimitIndex === undefined} - > - {selectedStorageLimitIndex !== undefined - ? parseNumberWithCommas( - storageLimit.PRO.graduatedPrice[selectedStorageLimitIndex] - .totalIncluded - ) - : undefined} - - - {storageLimit.PRO.graduatedPrice.map((price, index) => ( - setSelectedStorageLimitIndex(index)} - > - {parseNumberWithCommas(price.totalIncluded)} - - ))} - - {' '} - GB of storage - - - - - - , 'Custom domains', 'In-depth analytics', ], @@ -142,7 +94,7 @@ export const ProPlanCard = ({ isYearly }: Props) => { button={ - + ) } diff --git a/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx b/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx index 3527603ca2..4cc89627d2 100644 --- a/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx +++ b/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx @@ -82,6 +82,7 @@ export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => { onClose={onClose} isOpen={isOpen} type={t('billing.limitMessage.analytics')} + excludedPlans={['STARTER']} /> diff --git a/apps/builder/src/features/billing/components/ChangePlanForm.tsx b/apps/builder/src/features/billing/components/ChangePlanForm.tsx index d39521e066..5a1f885861 100644 --- a/apps/builder/src/features/billing/components/ChangePlanForm.tsx +++ b/apps/builder/src/features/billing/components/ChangePlanForm.tsx @@ -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) => { - - handlePayClick({ ...props, plan: Plan.STARTER }) - } - isYearly={isYearly} - isLoading={isUpdatingSubscription} - currency={data.subscription?.currency} - /> + {excludedPlans?.includes('STARTER') ? null : ( + + handlePayClick({ ...props, plan: Plan.STARTER }) + } + isYearly={isYearly} + isLoading={isUpdatingSubscription} + currency={data.subscription?.currency} + /> + )} - - handlePayClick({ ...props, plan: Plan.PRO }) - } - isYearly={isYearly} - isLoading={isUpdatingSubscription} - currency={data.subscription?.currency} - /> + {excludedPlans?.includes('PRO') ? null : ( + + handlePayClick({ ...props, plan: Plan.PRO }) + } + isYearly={isYearly} + isLoading={isUpdatingSubscription} + currency={data.subscription?.currency} + /> + )} )} diff --git a/apps/builder/src/features/billing/components/ChangePlanModal.tsx b/apps/builder/src/features/billing/components/ChangePlanModal.tsx index 70a36d9c73..9bf467ea98 100644 --- a/apps/builder/src/features/billing/components/ChangePlanModal.tsx +++ b/apps/builder/src/features/billing/components/ChangePlanModal.tsx @@ -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 ( - + @@ -36,7 +42,12 @@ export const ChangePlanModal = ({ {t('billing.upgradeLimitLabel', { type: type })} )} - {workspace && } + {workspace && ( + + )} diff --git a/apps/builder/src/features/billing/components/ProPlanPricingCard.tsx b/apps/builder/src/features/billing/components/ProPlanPricingCard.tsx index 1fca6e7676..1a1516f2b4 100644 --- a/apps/builder/src/features/billing/components/ProPlanPricingCard.tsx +++ b/apps/builder/src/features/billing/components/ProPlanPricingCard.tsx @@ -200,6 +200,7 @@ export const ProPlanPricingCard = ({ {scopedT('chatsTooltip')} , + scopedT('pro.whatsAppIntegration'), scopedT('pro.customDomains'), scopedT('pro.analytics'), ]} diff --git a/apps/builder/src/features/billing/components/UpgradeButton.tsx b/apps/builder/src/features/billing/components/UpgradeButton.tsx index 385eb9c916..b48b9bd2f5 100644 --- a/apps/builder/src/features/billing/components/UpgradeButton.tsx +++ b/apps/builder/src/features/billing/components/UpgradeButton.tsx @@ -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} /> ) diff --git a/apps/builder/src/features/billing/helpers/isProPlan.ts b/apps/builder/src/features/billing/helpers/hasProPerks.ts similarity index 80% rename from apps/builder/src/features/billing/helpers/isProPlan.ts rename to apps/builder/src/features/billing/helpers/hasProPerks.ts index 6752191a07..a0ed05b3d3 100644 --- a/apps/builder/src/features/billing/helpers/isProPlan.ts +++ b/apps/builder/src/features/billing/helpers/hasProPerks.ts @@ -1,7 +1,7 @@ import { isDefined } from '@typebot.io/lib' import { Workspace, Plan } from '@typebot.io/prisma' -export const isProPlan = (workspace?: Pick) => +export const hasProPerks = (workspace?: Pick) => isDefined(workspace) && (workspace.plan === Plan.PRO || workspace.plan === Plan.LIFETIME || diff --git a/apps/builder/src/features/graph/components/edges/DropOffEdge.tsx b/apps/builder/src/features/graph/components/edges/DropOffEdge.tsx index 7a8f2cd1c3..c961bbb966 100644 --- a/apps/builder/src/features/graph/components/edges/DropOffEdge.tsx +++ b/apps/builder/src/features/graph/components/edges/DropOffEdge.tsx @@ -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) diff --git a/apps/builder/src/features/publish/components/SharePage.tsx b/apps/builder/src/features/publish/components/SharePage.tsx index c28821ca5d..f3dd57d9d6 100644 --- a/apps/builder/src/features/publish/components/SharePage.tsx +++ b/apps/builder/src/features/publish/components/SharePage.tsx @@ -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) ? ( @@ -138,6 +138,7 @@ export const SharePage = () => { Add my domain{' '} diff --git a/apps/builder/src/features/publish/components/embeds/EmbedButton.tsx b/apps/builder/src/features/publish/components/embeds/EmbedButton.tsx index 7bd359dd01..5e01607514 100644 --- a/apps/builder/src/features/publish/components/embeds/EmbedButton.tsx +++ b/apps/builder/src/features/publish/components/embeds/EmbedButton.tsx @@ -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 & { 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 = ({ > {logo} - {label} + + {label} + {lockTagPlan && ( + <> + {' '} + + + )} + - + {modal({ isOpen, onClose, ...modalProps })} ) } export const integrationsList = [ (props: Pick) => { + const { workspace } = useWorkspace() + if (isWhatsAppAvailable()) return ( } label="WhatsApp" - Modal={WhatsAppModal} + lockTagPlan={hasProPerks(workspace) ? undefined : 'PRO'} + modal={({ onClose, isOpen }) => + hasProPerks(workspace) ? ( + + ) : ( + + ) + } {...props} /> @@ -100,7 +127,9 @@ export const integrationsList = [ } label="Wordpress" - Modal={WordpressModal} + modal={({ onClose, isOpen }) => ( + + )} {...props} /> ), @@ -108,7 +137,7 @@ export const integrationsList = [ } label="Shopify" - Modal={ShopifyModal} + modal={(modalProps) => } {...props} /> ), @@ -116,7 +145,7 @@ export const integrationsList = [ } label="Wix" - Modal={WixModal} + modal={(modalProps) => } {...props} /> ), @@ -124,7 +153,7 @@ export const integrationsList = [ } label="Google Tag Manager" - Modal={GtmModal} + modal={(modalProps) => } {...props} /> ), @@ -132,7 +161,7 @@ export const integrationsList = [ } label="HTML & Javascript" - Modal={JavascriptModal} + modal={(modalProps) => } {...props} /> ), @@ -140,7 +169,7 @@ export const integrationsList = [ } label="React" - Modal={ReactModal} + modal={(modalProps) => } {...props} /> ), @@ -148,7 +177,7 @@ export const integrationsList = [ } label="Nextjs" - Modal={NextjsModal} + modal={(modalProps) => } {...props} /> ), @@ -156,7 +185,7 @@ export const integrationsList = [ } label="API" - Modal={ApiModal} + modal={(modalProps) => } {...props} /> ), @@ -164,7 +193,7 @@ export const integrationsList = [ } label="Notion" - Modal={NotionModal} + modal={(modalProps) => } {...props} /> ), @@ -172,7 +201,7 @@ export const integrationsList = [ } label="Webflow" - Modal={WebflowModal} + modal={(modalProps) => } {...props} /> ), @@ -180,7 +209,7 @@ export const integrationsList = [ } label="FlutterFlow" - Modal={FlutterFlowModal} + modal={(modalProps) => } {...props} /> ), @@ -194,7 +223,7 @@ export const integrationsList = [ /> } label="Script" - Modal={ScriptModal} + modal={(modalProps) => } {...props} /> ), @@ -202,15 +231,7 @@ export const integrationsList = [ } label="Iframe" - Modal={IframeModal} - {...props} - /> - ), - (props: Pick) => ( - } - label="Other" - Modal={OtherModal} + modal={(modalProps) => } {...props} /> ), diff --git a/apps/builder/src/features/publish/components/embeds/modals/OtherModal.tsx b/apps/builder/src/features/publish/components/embeds/modals/OtherModal.tsx deleted file mode 100644 index 04aa663cfb..0000000000 --- a/apps/builder/src/features/publish/components/embeds/modals/OtherModal.tsx +++ /dev/null @@ -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 ( - - {isDefined(selectedEmbedType) && ( - - )} - - ) -} diff --git a/apps/builder/src/features/publish/helpers/convertPublicTypebotToTypebot.ts b/apps/builder/src/features/publish/helpers/convertPublicTypebotToTypebot.ts index 7aded01fdf..84eb74389f 100644 --- a/apps/builder/src/features/publish/helpers/convertPublicTypebotToTypebot.ts +++ b/apps/builder/src/features/publish/helpers/convertPublicTypebotToTypebot.ts @@ -23,6 +23,5 @@ export const convertPublicTypebotToTypebot = ( isClosed: existingTypebot.isClosed, resultsTablePreferences: existingTypebot.resultsTablePreferences, selectedThemeTemplateId: existingTypebot.selectedThemeTemplateId, - whatsAppPhoneNumberId: existingTypebot.whatsAppPhoneNumberId, whatsAppCredentialsId: existingTypebot.whatsAppCredentialsId, }) diff --git a/apps/builder/src/features/typebot/api/updateTypebot.ts b/apps/builder/src/features/typebot/api/updateTypebot.ts index f41f21e3ae..b4fa39dfc5 100644 --- a/apps/builder/src/features/typebot/api/updateTypebot.ts +++ b/apps/builder/src/features/typebot/api/updateTypebot.ts @@ -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, }, }) diff --git a/apps/builder/src/locales/de.ts b/apps/builder/src/locales/de.ts index 548975c973..8a309a5f8d 100644 --- a/apps/builder/src/locales/de.ts +++ b/apps/builder/src/locales/de.ts @@ -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', diff --git a/apps/builder/src/locales/en.ts b/apps/builder/src/locales/en.ts index 71c7fc3946..fe351ae93e 100644 --- a/apps/builder/src/locales/en.ts +++ b/apps/builder/src/locales/en.ts @@ -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', diff --git a/apps/builder/src/locales/fr.ts b/apps/builder/src/locales/fr.ts index 9bdbe54f5f..28a7ee9a97 100644 --- a/apps/builder/src/locales/fr.ts +++ b/apps/builder/src/locales/fr.ts @@ -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', diff --git a/apps/builder/src/locales/pt-BR.ts b/apps/builder/src/locales/pt-BR.ts index e479882d48..7c3adb5338 100644 --- a/apps/builder/src/locales/pt-BR.ts +++ b/apps/builder/src/locales/pt-BR.ts @@ -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', diff --git a/apps/builder/src/locales/pt.ts b/apps/builder/src/locales/pt.ts index 9c7663db28..6e502dcd13 100644 --- a/apps/builder/src/locales/pt.ts +++ b/apps/builder/src/locales/pt.ts @@ -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', diff --git a/apps/builder/src/pages/api/stripe/webhook.ts b/apps/builder/src/pages/api/stripe/webhook.ts index 0072c406b1..290aa3bf06 100644 --- a/apps/builder/src/pages/api/stripe/webhook.ts +++ b/apps/builder/src/pages/api/stripe/webhook.ts @@ -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: { diff --git a/apps/docs/openapi/builder/_spec_.json b/apps/docs/openapi/builder/_spec_.json index 2f38237be3..ef15c9c972 100644 --- a/apps/docs/openapi/builder/_spec_.json +++ b/apps/docs/openapi/builder/_spec_.json @@ -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 diff --git a/apps/docs/openapi/chat/_spec_.json b/apps/docs/openapi/chat/_spec_.json index b7938b7084..d5b8d9c3bf 100644 --- a/apps/docs/openapi/chat/_spec_.json +++ b/apps/docs/openapi/chat/_spec_.json @@ -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 diff --git a/apps/landing-page/components/PricingPage/ProPlanCard.tsx b/apps/landing-page/components/PricingPage/ProPlanCard.tsx index 3f95a337f7..ed194f6fcd 100644 --- a/apps/landing-page/components/PricingPage/ProPlanCard.tsx +++ b/apps/landing-page/components/PricingPage/ProPlanCard.tsx @@ -85,6 +85,7 @@ export const ProPlanCard = ({ isYearly }: Props) => { , + 'WhatsApp integration', 'Custom domains', 'In-depth analytics', ], diff --git a/packages/bot-engine/continueBotFlow.ts b/packages/bot-engine/continueBotFlow.ts index 015affb06e..fd2d92b665 100644 --- a/packages/bot-engine/continueBotFlow.ts +++ b/packages/bot-engine/continueBotFlow.ts @@ -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' diff --git a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts index a25f25785c..26ca282040 100644 --- a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts +++ b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts @@ -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 }, diff --git a/packages/bot-engine/whatsapp/startWhatsAppSession.ts b/packages/bot-engine/whatsapp/startWhatsAppSession.ts index 4656e321fa..96b64699d0 100644 --- a/packages/bot-engine/whatsapp/startWhatsAppSession.ts +++ b/packages/bot-engine/whatsapp/startWhatsAppSession.ts @@ -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 @@ -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 '' - } - } -} diff --git a/packages/lib/playwright/databaseHelpers.ts b/packages/lib/playwright/databaseHelpers.ts index 240278e088..4abb2d8ca1 100644 --- a/packages/lib/playwright/databaseHelpers.ts +++ b/packages/lib/playwright/databaseHelpers.ts @@ -31,7 +31,6 @@ export const parseTestTypebot = ( isArchived: false, isClosed: false, resultsTablePreferences: null, - whatsAppPhoneNumberId: null, whatsAppCredentialsId: null, variables: [{ id: 'var1', name: 'var1' }], ...partialTypebot, diff --git a/packages/prisma/mysql/schema.prisma b/packages/prisma/mysql/schema.prisma index 9d0513ae24..1023617af5 100644 --- a/packages/prisma/mysql/schema.prisma +++ b/packages/prisma/mysql/schema.prisma @@ -198,7 +198,6 @@ model Typebot { webhooks Webhook[] isArchived Boolean @default(false) isClosed Boolean @default(false) - whatsAppPhoneNumberId String? whatsAppCredentialsId String? @@index([workspaceId]) diff --git a/packages/prisma/postgresql/migrations/20230925135118_remove_whatsapp_phone_number_id_field/migration.sql b/packages/prisma/postgresql/migrations/20230925135118_remove_whatsapp_phone_number_id_field/migration.sql new file mode 100644 index 0000000000..cced7384de --- /dev/null +++ b/packages/prisma/postgresql/migrations/20230925135118_remove_whatsapp_phone_number_id_field/migration.sql @@ -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"; diff --git a/packages/prisma/postgresql/schema.prisma b/packages/prisma/postgresql/schema.prisma index e6404645ca..65a73877dc 100644 --- a/packages/prisma/postgresql/schema.prisma +++ b/packages/prisma/postgresql/schema.prisma @@ -182,7 +182,6 @@ model Typebot { webhooks Webhook[] isArchived Boolean @default(false) isClosed Boolean @default(false) - whatsAppPhoneNumberId String? whatsAppCredentialsId String? @@index([workspaceId]) diff --git a/packages/schemas/features/typebot/typebot.ts b/packages/schemas/features/typebot/typebot.ts index 70f8c41e63..ae684b5940 100644 --- a/packages/schemas/features/typebot/typebot.ts +++ b/packages/schemas/features/typebot/typebot.ts @@ -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 ) From 7b3cbdb8e8a2dbdfa030ca782a186d9a449ff6d9 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 26 Sep 2023 08:07:33 +0200 Subject: [PATCH 025/233] :ambulance: (fileUpload) Fix file upload in linked typebots --- .../fileUpload/api/generateUploadUrl.ts | 92 +++++++++++++++++-- packages/embeds/js/package.json | 2 +- .../fileUpload/components/FileUploadForm.tsx | 8 +- .../inputs/fileUpload/helpers/uploadFiles.ts | 4 +- packages/embeds/nextjs/package.json | 2 +- packages/embeds/react/package.json | 2 +- 6 files changed, 89 insertions(+), 21 deletions(-) diff --git a/apps/viewer/src/features/fileUpload/api/generateUploadUrl.ts b/apps/viewer/src/features/fileUpload/api/generateUploadUrl.ts index d60d2a5213..1ef6061d5e 100644 --- a/apps/viewer/src/features/fileUpload/api/generateUploadUrl.ts +++ b/apps/viewer/src/features/fileUpload/api/generateUploadUrl.ts @@ -5,6 +5,7 @@ import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresigne import { env } from '@typebot.io/env' import { InputBlockType, publicTypebotSchema } from '@typebot.io/schemas' import prisma from '@typebot.io/lib/prisma' +import { getSession } from '@typebot.io/bot-engine/queries/getSession' export const generateUploadUrl = publicProcedure .meta({ @@ -17,12 +18,19 @@ export const generateUploadUrl = publicProcedure }) .input( z.object({ - filePathProps: z.object({ - typebotId: z.string(), - blockId: z.string(), - resultId: z.string(), - fileName: z.string(), - }), + filePathProps: z + .object({ + typebotId: z.string(), + blockId: z.string(), + resultId: z.string(), + fileName: z.string(), + }) + .or( + z.object({ + sessionId: z.string(), + fileName: z.string(), + }) + ), fileType: z.string().optional(), }) ) @@ -41,9 +49,73 @@ export const generateUploadUrl = publicProcedure 'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY', }) + // TODO: Remove (deprecated) + if ('typebotId' in filePathProps) { + const publicTypebot = await prisma.publicTypebot.findFirst({ + where: { + typebotId: filePathProps.typebotId, + }, + select: { + groups: true, + typebot: { + select: { + workspaceId: true, + }, + }, + }, + }) + + const workspaceId = publicTypebot?.typebot.workspaceId + + if (!workspaceId) + throw new TRPCError({ + code: 'BAD_REQUEST', + message: "Can't find workspaceId", + }) + + const filePath = `public/workspaces/${workspaceId}/typebots/${filePathProps.typebotId}/results/${filePathProps.resultId}/${filePathProps.fileName}` + + const fileUploadBlock = publicTypebotSchema._def.schema.shape.groups + .parse(publicTypebot.groups) + .flatMap((group) => group.blocks) + .find((block) => block.id === filePathProps.blockId) + + if (fileUploadBlock?.type !== InputBlockType.FILE) + throw new TRPCError({ + code: 'BAD_REQUEST', + message: "Can't find file upload block", + }) + + const presignedPostPolicy = await generatePresignedPostPolicy({ + fileType, + filePath, + maxFileSize: + fileUploadBlock.options.sizeLimit ?? + env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE, + }) + + return { + presignedUrl: presignedPostPolicy.postURL, + formData: presignedPostPolicy.formData, + fileUrl: env.S3_PUBLIC_CUSTOM_DOMAIN + ? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}` + : `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`, + } + } + + const session = await getSession(filePathProps.sessionId) + + if (!session) + throw new TRPCError({ + code: 'BAD_REQUEST', + message: "Can't find session", + }) + + const typebotId = session.state.typebotsQueue[0].typebot.id + const publicTypebot = await prisma.publicTypebot.findFirst({ where: { - typebotId: filePathProps.typebotId, + typebotId, }, select: { groups: true, @@ -63,12 +135,14 @@ export const generateUploadUrl = publicProcedure message: "Can't find workspaceId", }) - const filePath = `public/workspaces/${workspaceId}/typebots/${filePathProps.typebotId}/results/${filePathProps.resultId}/${filePathProps.fileName}` + const resultId = session.state.typebotsQueue[0].resultId + + const filePath = `public/workspaces/${workspaceId}/typebots/${typebotId}/results/${resultId}/${filePathProps.fileName}` const fileUploadBlock = publicTypebotSchema._def.schema.shape.groups .parse(publicTypebot.groups) .flatMap((group) => group.blocks) - .find((block) => block.id === filePathProps.blockId) + .find((block) => block.id === session.state.currentBlock?.blockId) if (fileUploadBlock?.type !== InputBlockType.FILE) throw new TRPCError({ diff --git a/packages/embeds/js/package.json b/packages/embeds/js/package.json index a2b7321a48..7b3e77b165 100644 --- a/packages/embeds/js/package.json +++ b/packages/embeds/js/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/js", - "version": "0.1.30", + "version": "0.1.31", "description": "Javascript library to display typebots on your website", "type": "module", "main": "dist/index.js", diff --git a/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx b/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx index 3bcd29e056..5d98d449ca 100644 --- a/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx +++ b/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx @@ -58,9 +58,7 @@ export const FileUploadForm = (props: Props) => { { file, input: { - resultId: props.context.resultId, - typebotId: props.context.typebot.id, - blockId: props.block.id, + sessionId: props.context.sessionId, fileName: file.name, }, }, @@ -86,9 +84,7 @@ export const FileUploadForm = (props: Props) => { files: files.map((file) => ({ file: file, input: { - resultId, - typebotId: props.context.typebot.id, - blockId: props.block.id, + sessionId: props.context.sessionId, fileName: file.name, }, })), diff --git a/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts b/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts index 2792739c64..d94bb09e6f 100644 --- a/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts +++ b/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts @@ -5,9 +5,7 @@ type UploadFileProps = { files: { file: File input: { - typebotId: string - blockId: string - resultId: string + sessionId: string fileName: string } }[] diff --git a/packages/embeds/nextjs/package.json b/packages/embeds/nextjs/package.json index 6ce9722c8d..e5f3503d35 100644 --- a/packages/embeds/nextjs/package.json +++ b/packages/embeds/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/nextjs", - "version": "0.1.30", + "version": "0.1.31", "description": "Convenient library to display typebots on your Next.js website", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/embeds/react/package.json b/packages/embeds/react/package.json index bb84059423..eeaec2afc9 100644 --- a/packages/embeds/react/package.json +++ b/packages/embeds/react/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/react", - "version": "0.1.30", + "version": "0.1.31", "description": "Convenient library to display typebots on your React app", "main": "dist/index.js", "types": "dist/index.d.ts", From 1ca742fc0b83e7dc429872816967d767101eedb2 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 26 Sep 2023 09:50:20 +0200 Subject: [PATCH 026/233] :zap: (setVariable) Add "Environment name" value in Set variable block (#850) Closes #848 ### Summary by CodeRabbit - New Feature: Added "Environment name" as a new value type in the SetVariable function, allowing users to distinguish between 'web' and 'whatsapp' environments. - Refactor: Simplified session state handling in `resumeWhatsAppFlow.ts` for improved code clarity. - Refactor: Updated `startWhatsAppSession.ts` to include an initial session state with WhatsApp contact and expiry timeout, enhancing session management. - Bug Fix: Improved null handling in `executeSetVariable.ts` for 'Contact name' and 'Phone number', preventing potential issues with falsy values. --- .../components/SetVariableContent.tsx | 1 + .../components/SetVariableSettings.tsx | 11 +++++ apps/docs/openapi/builder/_spec_.json | 7 +++ apps/docs/openapi/chat/_spec_.json | 1 + .../logic/setVariable/executeSetVariable.ts | 11 +++-- packages/bot-engine/startSession.ts | 3 ++ .../bot-engine/whatsapp/resumeWhatsAppFlow.ts | 14 +----- .../whatsapp/startWhatsAppSession.ts | 46 ++++++++++--------- .../features/blocks/logic/setVariable.ts | 1 + 9 files changed, 58 insertions(+), 37 deletions(-) diff --git a/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableContent.tsx b/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableContent.tsx index 7d2e4825a1..d8dc4bdc5b 100644 --- a/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableContent.tsx +++ b/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableContent.tsx @@ -62,6 +62,7 @@ const Expression = ({ case 'Tomorrow': case 'User ID': case 'Moment of the day': + case 'Environment name': case 'Yesterday': { return ( diff --git a/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableSettings.tsx b/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableSettings.tsx index 9920b5a567..7bb7d4414b 100644 --- a/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableSettings.tsx +++ b/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableSettings.tsx @@ -158,6 +158,17 @@ const SetVariableValue = ({ ) } + case 'Environment name': { + return ( + + + + Will return either web or{' '} + whatsapp. + + + ) + } case 'Contact name': case 'Phone number': case 'Random ID': diff --git a/apps/docs/openapi/builder/_spec_.json b/apps/docs/openapi/builder/_spec_.json index ef15c9c972..460dc0c089 100644 --- a/apps/docs/openapi/builder/_spec_.json +++ b/apps/docs/openapi/builder/_spec_.json @@ -1979,6 +1979,7 @@ "enum": [ "Custom", "Empty", + "Environment name", "User ID", "Now", "Today", @@ -6367,6 +6368,7 @@ "enum": [ "Custom", "Empty", + "Environment name", "User ID", "Now", "Today", @@ -10396,6 +10398,7 @@ "enum": [ "Custom", "Empty", + "Environment name", "User ID", "Now", "Today", @@ -14565,6 +14568,7 @@ "enum": [ "Custom", "Empty", + "Environment name", "User ID", "Now", "Today", @@ -18614,6 +18618,7 @@ "enum": [ "Custom", "Empty", + "Environment name", "User ID", "Now", "Today", @@ -22718,6 +22723,7 @@ "enum": [ "Custom", "Empty", + "Environment name", "User ID", "Now", "Today", @@ -26885,6 +26891,7 @@ "enum": [ "Custom", "Empty", + "Environment name", "User ID", "Now", "Today", diff --git a/apps/docs/openapi/chat/_spec_.json b/apps/docs/openapi/chat/_spec_.json index d5b8d9c3bf..8d5ef97f45 100644 --- a/apps/docs/openapi/chat/_spec_.json +++ b/apps/docs/openapi/chat/_spec_.json @@ -1574,6 +1574,7 @@ "enum": [ "Custom", "Empty", + "Environment name", "User ID", "Now", "Today", diff --git a/packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts b/packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts index 891ed35a1a..5584ec19d9 100644 --- a/packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts +++ b/packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts @@ -77,9 +77,11 @@ const getExpressionToEvaluate = (options: SetVariableBlock['options']): string | null => { switch (options.type) { case 'Contact name': - return state.whatsApp?.contact.name ?? '' - case 'Phone number': - return `"${state.whatsApp?.contact.phoneNumber}"` ?? '' + return state.whatsApp?.contact.name ?? null + case 'Phone number': { + const phoneNumber = state.whatsApp?.contact.phoneNumber + return phoneNumber ? `"${state.whatsApp?.contact.phoneNumber}"` : null + } case 'Now': case 'Today': return 'new Date().toISOString()' @@ -112,6 +114,9 @@ const getExpressionToEvaluate = if(now.getHours() >= 18) return 'evening' if(now.getHours() >= 22 || now.getHours() < 6) return 'night'` } + case 'Environment name': { + return state.whatsApp ? 'whatsapp' : 'web' + } case 'Custom': case undefined: { return options.expressionToEvaluate ?? null diff --git a/packages/bot-engine/startSession.ts b/packages/bot-engine/startSession.ts index 16748422d7..d451c0e33a 100644 --- a/packages/bot-engine/startSession.ts +++ b/packages/bot-engine/startSession.ts @@ -30,11 +30,13 @@ import { injectVariablesFromExistingResult } from './variables/injectVariablesFr type Props = { startParams: StartParams userId: string | undefined + initialSessionState?: Pick } export const startSession = async ({ startParams, userId, + initialSessionState, }: Props): Promise => { if (!startParams) throw new TRPCError({ @@ -108,6 +110,7 @@ export const startSession = async ({ dynamicTheme: parseDynamicThemeInState(typebot.theme), isStreamEnabled: startParams.isStreamEnabled, typingEmulation: typebot.settings.typingEmulation, + ...initialSessionState, } if (startParams.isOnlyRegistering) { diff --git a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts index 26ca282040..2cb7bddb79 100644 --- a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts +++ b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts @@ -46,16 +46,6 @@ export const resumeWhatsAppFlow = async ({ typebotId: typebot?.id, }) - const sessionState = - isPreview && session?.state - ? ({ - ...session?.state, - whatsApp: { - contact, - }, - } satisfies SessionState) - : session?.state - const credentials = await getCredentials({ credentialsId, isPreview }) if (!credentials) { @@ -71,8 +61,8 @@ export const resumeWhatsAppFlow = async ({ session?.updatedAt.getTime() + session.state.expiryTimeout < Date.now() const resumeResponse = - sessionState && !isSessionExpired - ? await continueBotFlow(sessionState)(messageContent) + session && !isSessionExpired + ? await continueBotFlow(session.state)(messageContent) : workspaceId ? await startWhatsAppSession({ incomingMessage: messageContent, diff --git a/packages/bot-engine/whatsapp/startWhatsAppSession.ts b/packages/bot-engine/whatsapp/startWhatsAppSession.ts index 96b64699d0..e8af5d19ae 100644 --- a/packages/bot-engine/whatsapp/startWhatsAppSession.ts +++ b/packages/bot-engine/whatsapp/startWhatsAppSession.ts @@ -10,7 +10,6 @@ import { } from '@typebot.io/schemas' import { WhatsAppCredentials, - WhatsAppIncomingMessage, defaultSessionExpiryTimeout, } from '@typebot.io/schemas/features/whatsapp' import { isInputBlock, isNotDefined } from '@typebot.io/lib/utils' @@ -73,47 +72,50 @@ export const startWhatsAppSession = async ({ if (isNotDefined(publicTypebot)) return - let session = await startSession({ + const sessionExpiryTimeoutHours = + publicTypebot.settings.whatsApp?.sessionExpiryTimeout ?? + defaultSessionExpiryTimeout + + const session = await startSession({ startParams: { typebot: publicTypebot.typebot.publicId as string, }, userId: undefined, + initialSessionState: { + whatsApp: { + contact, + }, + expiryTimeout: sessionExpiryTimeoutHours * 60 * 60 * 1000, + }, }) + let newSessionState: SessionState = session.newSessionState + // 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) + newSessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId + const nextGroup = await getNextGroup(newSessionState)(firstEdgeId) const firstBlock = nextGroup.group?.blocks.at(0) if (firstBlock && isInputBlock(firstBlock)) { - const resultId = session.newSessionState.typebotsQueue[0].resultId + const resultId = newSessionState.typebotsQueue[0].resultId if (resultId) await upsertResult({ hasStarted: true, isCompleted: false, resultId, - typebot: session.newSessionState.typebotsQueue[0].typebot, + typebot: newSessionState.typebotsQueue[0].typebot, }) - session = await continueBotFlow({ - ...session.newSessionState, - currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id }, - })(incomingMessage) + newSessionState = ( + await continueBotFlow({ + ...newSessionState, + currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id }, + })(incomingMessage) + ).newSessionState } - const sessionExpiryTimeoutHours = - publicTypebot.settings.whatsApp?.sessionExpiryTimeout ?? - defaultSessionExpiryTimeout - return { ...session, - newSessionState: { - ...session.newSessionState, - whatsApp: { - contact, - }, - expiryTimeout: sessionExpiryTimeoutHours * 60 * 60 * 1000, - }, + newSessionState, } } diff --git a/packages/schemas/features/blocks/logic/setVariable.ts b/packages/schemas/features/blocks/logic/setVariable.ts index 9984aa5633..27fda7379b 100644 --- a/packages/schemas/features/blocks/logic/setVariable.ts +++ b/packages/schemas/features/blocks/logic/setVariable.ts @@ -5,6 +5,7 @@ import { LogicBlockType } from './enums' export const valueTypes = [ 'Custom', 'Empty', + 'Environment name', 'User ID', 'Now', 'Today', From 801fea860a9940a6a87b6c4c147c6da6c3234df3 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 26 Sep 2023 14:02:53 +0200 Subject: [PATCH 027/233] :passport_control: Improve editor authorization feedback (#856) Closes #844, closes #839 ### Summary by CodeRabbit - New Feature: Added a `logOut` function to the user context for improved logout handling. - Refactor: Updated the redirect path in the `SignInForm` component for better user redirection after authentication. - New Feature: Enhanced the "Add" button and "Connect new" menu item in `CredentialsDropdown` with role-based access control. - Refactor: Replaced the `signOut` function with the `logOut` function from the `useUser` hook in `DashboardHeader`. - Bug Fix: Prevented execution of certain code blocks in `TypebotProvider` when `typebotData` is read-only. - Refactor: Optimized the `handleObserver` function in `ResultsTable` with a `useCallback` hook. - Bug Fix: Improved router readiness check in `WorkspaceProvider` to prevent premature execution of certain operations. --- .../src/features/account/UserProvider.tsx | 12 +++++++- .../features/auth/components/SignInForm.tsx | 2 +- .../components/CredentialsDropdown.tsx | 25 +++++++++------- .../dashboard/components/DashboardHeader.tsx | 9 ++---- .../editor/providers/TypebotProvider.tsx | 14 ++++++--- .../results/components/table/ResultsTable.tsx | 30 +++++++++++-------- .../features/workspace/WorkspaceProvider.tsx | 6 +++- 7 files changed, 61 insertions(+), 37 deletions(-) diff --git a/apps/builder/src/features/account/UserProvider.tsx b/apps/builder/src/features/account/UserProvider.tsx index 47bc533e51..4d3129428c 100644 --- a/apps/builder/src/features/account/UserProvider.tsx +++ b/apps/builder/src/features/account/UserProvider.tsx @@ -1,4 +1,4 @@ -import { useSession } from 'next-auth/react' +import { signOut, useSession } from 'next-auth/react' import { useRouter } from 'next/router' import { createContext, ReactNode, useEffect, useState } from 'react' import { isDefined, isNotDefined } from '@typebot.io/lib' @@ -15,9 +15,13 @@ export const userContext = createContext<{ user?: User isLoading: boolean currentWorkspaceId?: string + logOut: () => void updateUser: (newUser: Partial) => void }>({ isLoading: false, + logOut: () => { + console.log('logOut not implemented') + }, updateUser: () => { console.log('updateUser not implemented') }, @@ -91,6 +95,11 @@ export const UserProvider = ({ children }: { children: ReactNode }) => { env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout ) + const logOut = () => { + signOut() + setUser(undefined) + } + useEffect(() => { return () => { saveUser.flush() @@ -103,6 +112,7 @@ export const UserProvider = ({ children }: { children: ReactNode }) => { user, isLoading: status === 'loading', currentWorkspaceId, + logOut, updateUser, }} > diff --git a/apps/builder/src/features/auth/components/SignInForm.tsx b/apps/builder/src/features/auth/components/SignInForm.tsx index 1cd28b82fb..fd2daf570d 100644 --- a/apps/builder/src/features/auth/components/SignInForm.tsx +++ b/apps/builder/src/features/auth/components/SignInForm.tsx @@ -55,7 +55,7 @@ export const SignInForm = ({ useEffect(() => { if (status === 'authenticated') { - router.replace(router.query.callbackUrl?.toString() ?? '/typebots') + router.replace(router.query.redirectPath?.toString() ?? '/typebots') return } ;(async () => { diff --git a/apps/builder/src/features/credentials/components/CredentialsDropdown.tsx b/apps/builder/src/features/credentials/components/CredentialsDropdown.tsx index 9181fcc3e1..512a8cd5fa 100644 --- a/apps/builder/src/features/credentials/components/CredentialsDropdown.tsx +++ b/apps/builder/src/features/credentials/components/CredentialsDropdown.tsx @@ -15,6 +15,7 @@ import { useRouter } from 'next/router' import { useToast } from '../../../hooks/useToast' import { Credentials } from '@typebot.io/schemas' import { trpc } from '@/lib/trpc' +import { useWorkspace } from '@/features/workspace/WorkspaceProvider' type Props = Omit & { type: Credentials['type'] @@ -38,6 +39,7 @@ export const CredentialsDropdown = ({ }: Props) => { const router = useRouter() const { showToast } = useToast() + const { currentRole } = useWorkspace() const { data, refetch } = trpc.credentials.listCredentials.useQuery({ workspaceId, type, @@ -107,6 +109,7 @@ export const CredentialsDropdown = ({ textAlign="left" leftIcon={} onClick={onCreateNewClick} + isDisabled={currentRole === 'GUEST'} {...props} > Add {credentialsName} @@ -165,16 +168,18 @@ export const CredentialsDropdown = ({ /> ))} - } - onClick={onCreateNewClick} - > - Connect new - + {currentRole === 'GUEST' ? null : ( + } + onClick={onCreateNewClick} + > + Connect new + + )}
diff --git a/apps/builder/src/features/dashboard/components/DashboardHeader.tsx b/apps/builder/src/features/dashboard/components/DashboardHeader.tsx index daf21a6ea4..3bfdf64962 100644 --- a/apps/builder/src/features/dashboard/components/DashboardHeader.tsx +++ b/apps/builder/src/features/dashboard/components/DashboardHeader.tsx @@ -1,7 +1,6 @@ import React from 'react' import { HStack, Flex, Button, useDisclosure } from '@chakra-ui/react' import { HardDriveIcon, SettingsIcon } from '@/components/icons' -import { signOut } from 'next-auth/react' import { useUser } from '@/features/account/hooks/useUser' import { isNotDefined } from '@typebot.io/lib' import Link from 'next/link' @@ -13,15 +12,11 @@ import { WorkspaceSettingsModal } from '@/features/workspace/components/Workspac export const DashboardHeader = () => { const scopedT = useScopedI18n('dashboard.header') - const { user } = useUser() + const { user, logOut } = useUser() const { workspace, switchWorkspace, createWorkspace } = useWorkspace() const { isOpen, onOpen, onClose } = useDisclosure() - const handleLogOut = () => { - signOut() - } - const handleCreateNewWorkspace = () => createWorkspace(user?.name ?? undefined) @@ -59,7 +54,7 @@ export const DashboardHeader = () => { diff --git a/apps/builder/src/features/editor/providers/TypebotProvider.tsx b/apps/builder/src/features/editor/providers/TypebotProvider.tsx index 387f80961b..a636a2ca6f 100644 --- a/apps/builder/src/features/editor/providers/TypebotProvider.tsx +++ b/apps/builder/src/features/editor/providers/TypebotProvider.tsx @@ -168,7 +168,7 @@ export const TypebotProvider = ({ const saveTypebot = useCallback( async (updates?: Partial) => { - if (!localTypebot || !typebot) return + if (!localTypebot || !typebot || typebotData?.isReadOnly) return const typebotToSave = { ...localTypebot, ...updates } if (dequal(omit(typebot, 'updatedAt'), omit(typebotToSave, 'updatedAt'))) return @@ -180,7 +180,13 @@ export const TypebotProvider = ({ setLocalTypebot({ ...newTypebot }) return newTypebot }, - [localTypebot, setLocalTypebot, typebot, updateTypebot] + [ + localTypebot, + setLocalTypebot, + typebot, + typebotData?.isReadOnly, + updateTypebot, + ] ) useAutoSave( @@ -212,7 +218,7 @@ export const TypebotProvider = ({ ) useEffect(() => { - if (!localTypebot || !typebot) return + if (!localTypebot || !typebot || typebotData?.isReadOnly) return if (!areTypebotsEqual(localTypebot, typebot)) { window.addEventListener('beforeunload', preventUserFromRefreshing) } @@ -220,7 +226,7 @@ export const TypebotProvider = ({ return () => { window.removeEventListener('beforeunload', preventUserFromRefreshing) } - }, [localTypebot, typebot]) + }, [localTypebot, typebot, typebotData?.isReadOnly]) const updateLocalTypebot = async ({ updates, diff --git a/apps/builder/src/features/results/components/table/ResultsTable.tsx b/apps/builder/src/features/results/components/table/ResultsTable.tsx index dc2a43e4d5..85e9714014 100644 --- a/apps/builder/src/features/results/components/table/ResultsTable.tsx +++ b/apps/builder/src/features/results/components/table/ResultsTable.tsx @@ -9,7 +9,7 @@ import { } from '@chakra-ui/react' import { AlignLeftTextIcon } from '@/components/icons' import { ResultHeaderCell, ResultsTablePreferences } from '@typebot.io/schemas' -import React, { useEffect, useRef, useState } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import { LoadingRows } from './LoadingRows' import { useReactTable, @@ -48,7 +48,7 @@ export const ResultsTable = ({ onResultExpandIndex, }: ResultsTableProps) => { const background = useColorModeValue('white', colors.gray[900]) - const { updateTypebot } = useTypebot() + const { updateTypebot, isReadOnly } = useTypebot() const [rowSelection, setRowSelection] = useState>({}) const [isTableScrolled, setIsTableScrolled] = useState(false) const bottomElement = useRef(null) @@ -185,6 +185,14 @@ export const ResultsTable = ({ getCoreRowModel: getCoreRowModel(), }) + const handleObserver = useCallback( + (entities: IntersectionObserverEntry[]) => { + const target = entities[0] + if (target.isIntersecting) onScrollToBottom() + }, + [onScrollToBottom] + ) + useEffect(() => { if (!bottomElement.current) return const options: IntersectionObserverInit = { @@ -197,21 +205,17 @@ export const ResultsTable = ({ return () => { observer.disconnect() } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [bottomElement.current]) - - const handleObserver = (entities: IntersectionObserverEntry[]) => { - const target = entities[0] - if (target.isIntersecting) onScrollToBottom() - } + }, [handleObserver]) return ( - setRowSelection({})} - /> + {isReadOnly ? null : ( + setRowSelection({})} + /> + )} { - const { pathname, query, push } = useRouter() + const { pathname, query, push, isReady: isRouterReady } = useRouter() const { user } = useUser() const userId = user?.id const [workspaceId, setWorkspaceId] = useState() @@ -102,6 +102,8 @@ export const WorkspaceProvider = ({ useEffect(() => { if ( + pathname === '/signin' || + !isRouterReady || !workspaces || workspaces.length === 0 || workspaceId || @@ -122,7 +124,9 @@ export const WorkspaceProvider = ({ setWorkspaceIdInLocalStorage(newWorkspaceId) setWorkspaceId(newWorkspaceId) }, [ + isRouterReady, members, + pathname, query.workspaceId, typebot?.workspaceId, typebotId, From a176e23cc8c3d82c66df71b741527dd0210c5177 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 26 Sep 2023 10:04:28 +0200 Subject: [PATCH 028/233] :children_crossing: Better random IDs generation in setVariable --- .../blocks/logic/setVariable/executeSetVariable.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts b/packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts index 5584ec19d9..0271132880 100644 --- a/packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts +++ b/packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts @@ -5,6 +5,7 @@ import { parseScriptToExecuteClientSideAction } from '../script/executeScript' import { parseGuessedValueType } from '../../../variables/parseGuessedValueType' import { parseVariables } from '../../../variables/parseVariables' import { updateVariablesInSession } from '../../../variables/updateVariablesInSession' +import { createId } from '@paralleldrive/cuid2' export const executeSetVariable = ( state: SessionState, @@ -92,13 +93,10 @@ const getExpressionToEvaluate = return 'new Date(Date.now() - 86400000).toISOString()' } case 'Random ID': { - return 'Math.random().toString(36).substring(2, 15)' + return `"${createId()}"` } case 'User ID': { - return ( - state.typebotsQueue[0].resultId ?? - 'Math.random().toString(36).substring(2, 15)' - ) + return state.typebotsQueue[0].resultId ?? `"${createId()}"` } case 'Map item with same index': { return `const itemIndex = ${options.mapListItemParams?.baseListVariableId}.indexOf(${options.mapListItemParams?.baseItemVariableId}) From 56e175bda6df960f6a5f32d7fd5b5f4bdddd8d56 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 26 Sep 2023 10:22:02 +0200 Subject: [PATCH 029/233] :bug: (pixel) Fix multiple Meta pixels tracking Closes #846 --- packages/bot-engine/startSession.ts | 12 ++++++------ packages/embeds/js/src/lib/pixel.ts | 13 +++++-------- packages/embeds/js/src/utils/injectStartProps.ts | 12 +++++++++--- packages/schemas/features/chat/schema.ts | 3 ++- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/bot-engine/startSession.ts b/packages/bot-engine/startSession.ts index d451c0e33a..626a6d6da2 100644 --- a/packages/bot-engine/startSession.ts +++ b/packages/bot-engine/startSession.ts @@ -333,21 +333,21 @@ const parseStartClientSideAction = ( block.options.trackingId ) as GoogleAnalyticsBlock | undefined )?.options.trackingId, - pixelId: ( - blocks.find( + pixelIds: ( + blocks.filter( (block) => block.type === IntegrationBlockType.PIXEL && - block.options.pixelId && + isNotEmpty(block.options.pixelId) && block.options.isInitSkip !== true - ) as PixelBlock | undefined - )?.options.pixelId, + ) as PixelBlock[] + ).map((pixelBlock) => pixelBlock.options.pixelId as string), } if ( !startPropsToInject.customHeadCode && !startPropsToInject.gtmId && !startPropsToInject.googleAnalyticsId && - !startPropsToInject.pixelId + !startPropsToInject.pixelIds ) return diff --git a/packages/embeds/js/src/lib/pixel.ts b/packages/embeds/js/src/lib/pixel.ts index d3a3093be4..64f4acf4a2 100644 --- a/packages/embeds/js/src/lib/pixel.ts +++ b/packages/embeds/js/src/lib/pixel.ts @@ -3,12 +3,13 @@ import { PixelBlock } from '@typebot.io/schemas' declare const window: { fbq?: ( arg1: string, + arg4: string, arg2: string, arg3: Record | undefined ) => void } -export const initPixel = (pixelId: string) => { +export const initPixel = (pixelIds: string[]) => { const script = document.createElement('script') script.innerHTML = `!function(f,b,e,v,n,t,s) {if(f.fbq)return;n=f.fbq=function(){n.callMethod? @@ -18,13 +19,9 @@ export const initPixel = (pixelId: string) => { t.src=v;s=b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t,s)}(window, document,'script', 'https://connect.facebook.net/en_US/fbevents.js'); - fbq('init', '${pixelId}'); + ${pixelIds.map((pixelId) => `fbq('init', '${pixelId}');`).join('\n')} fbq('track', 'PageView');` document.head.appendChild(script) - - const noscript = document.createElement('noscript') - noscript.innerHTML = `` - document.head.appendChild(noscript) } export const trackPixelEvent = (options: PixelBlock['options']) => { @@ -41,7 +38,7 @@ export const trackPixelEvent = (options: PixelBlock['options']) => { : undefined if (options.eventType === 'Custom') { if (!options.name) return - window.fbq('trackCustom', options.name, params) + window.fbq('trackSingleCustom', options.pixelId, options.name, params) } - window.fbq('track', options.eventType, params) + window.fbq('trackSingle', options.pixelId, options.eventType, params) } diff --git a/packages/embeds/js/src/utils/injectStartProps.ts b/packages/embeds/js/src/utils/injectStartProps.ts index fc15b72bcf..1066008821 100644 --- a/packages/embeds/js/src/utils/injectStartProps.ts +++ b/packages/embeds/js/src/utils/injectStartProps.ts @@ -2,7 +2,11 @@ import { initGoogleAnalytics } from '@/lib/gtag' import { gtmBodyElement } from '@/lib/gtm' import { initPixel } from '@/lib/pixel' -import { injectCustomHeadCode, isNotEmpty } from '@typebot.io/lib/utils' +import { + injectCustomHeadCode, + isDefined, + isNotEmpty, +} from '@typebot.io/lib/utils' import { StartPropsToInject } from '@typebot.io/schemas' export const injectStartProps = async ( @@ -15,6 +19,8 @@ export const injectStartProps = async ( const googleAnalyticsId = startPropsToInject.googleAnalyticsId if (isNotEmpty(googleAnalyticsId)) await initGoogleAnalytics(googleAnalyticsId) - const pixelId = startPropsToInject.pixelId - if (isNotEmpty(pixelId)) initPixel(pixelId) + const pixelIds = startPropsToInject.pixelId + ? [startPropsToInject.pixelId] + : startPropsToInject.pixelIds + if (isDefined(pixelIds)) initPixel(pixelIds) } diff --git a/packages/schemas/features/chat/schema.ts b/packages/schemas/features/chat/schema.ts index 85bba1c378..3995ac8a0c 100644 --- a/packages/schemas/features/chat/schema.ts +++ b/packages/schemas/features/chat/schema.ts @@ -169,7 +169,8 @@ const runtimeOptionsSchema = paymentInputRuntimeOptionsSchema.optional() const startPropsToInjectSchema = z.object({ googleAnalyticsId: z.string().optional(), - pixelId: z.string().optional(), + pixelId: z.string().optional().describe('Deprecated'), + pixelIds: z.array(z.string()).optional(), gtmId: z.string().optional(), customHeadCode: z.string().optional(), }) From ec52fdc0ade4fd3acb651e8eae754269ac8cb6f7 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Wed, 27 Sep 2023 08:32:18 +0200 Subject: [PATCH 030/233] :pencil: (whatsapp) Add a "Create WhatsApp app" guide --- .../WhatsAppCredentialsModal.tsx | 11 ++------ apps/docs/docs/embed/whatsapp/_category_.json | 3 +++ .../docs/embed/whatsapp/create-meta-app.md | 19 ++++++++++++++ .../{whatsapp.md => whatsapp/overview.md} | 6 +++++ apps/docs/docs/self-hosting/configuration.md | 25 ++++--------------- 5 files changed, 35 insertions(+), 29 deletions(-) create mode 100644 apps/docs/docs/embed/whatsapp/_category_.json create mode 100644 apps/docs/docs/embed/whatsapp/create-meta-app.md rename apps/docs/docs/embed/{whatsapp.md => whatsapp/overview.md} (94%) diff --git a/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppCredentialsModal.tsx b/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppCredentialsModal.tsx index 6c1596f17d..5bd3992502 100644 --- a/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppCredentialsModal.tsx +++ b/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppCredentialsModal.tsx @@ -300,10 +300,10 @@ const Requirements = () => ( Make sure you have{' '} - created a WhatsApp Business Account + created a WhatsApp Meta app . You should be able to get to this page: @@ -312,13 +312,6 @@ const Requirements = () => ( alt="WhatsApp quickstart page" rounded="md" /> - - You can find your Meta apps here:{' '} - - https://developers.facebook.com/apps - - . - ) diff --git a/apps/docs/docs/embed/whatsapp/_category_.json b/apps/docs/docs/embed/whatsapp/_category_.json new file mode 100644 index 0000000000..f060749167 --- /dev/null +++ b/apps/docs/docs/embed/whatsapp/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "WhatsApp" +} diff --git a/apps/docs/docs/embed/whatsapp/create-meta-app.md b/apps/docs/docs/embed/whatsapp/create-meta-app.md new file mode 100644 index 0000000000..f7aa728c5f --- /dev/null +++ b/apps/docs/docs/embed/whatsapp/create-meta-app.md @@ -0,0 +1,19 @@ +# Create a WhatsApp Meta app + +## 1. Create a Facebook Business account + +1. Head over to https://business.facebook.com and log in +2. Create a new business account on the left side bar + +:::note +It is possible that Meta automatically restricts your newly created Business account. In that case, make sure to verify your identity to proceed. +::: + +## 2. Create a Meta app + +1. Head over to https://developers.facebook.com/apps +2. Click on Create App +3. "What do you want your app to do?", select `Other`. +4. Select `Business` type +5. Give it any name and select your newly created Business Account +6. On the app page, look for `WhatsApp` product and enable it diff --git a/apps/docs/docs/embed/whatsapp.md b/apps/docs/docs/embed/whatsapp/overview.md similarity index 94% rename from apps/docs/docs/embed/whatsapp.md rename to apps/docs/docs/embed/whatsapp/overview.md index 7fb5f3ad09..8942932e38 100644 --- a/apps/docs/docs/embed/whatsapp.md +++ b/apps/docs/docs/embed/whatsapp/overview.md @@ -1,3 +1,9 @@ +--- +sidebar_position: 1 +slug: /embed/whatsapp +title: Overview +--- + # WhatsApp WhatsApp is currently available as a private beta test. If you'd like to try it out, reach out to support@typebot.io. diff --git a/apps/docs/docs/self-hosting/configuration.md b/apps/docs/docs/self-hosting/configuration.md index 819b2fcb0a..483582a445 100644 --- a/apps/docs/docs/self-hosting/configuration.md +++ b/apps/docs/docs/self-hosting/configuration.md @@ -201,24 +201,9 @@ In order to be able to test your bot on WhatsApp from the Preview drawer, you ne

Requirements

-### Create a Facebook Business account +## 1. [Create a WhatsApp Meta app](../embed/whatsapp/create-meta-app) -1. Head over to https://business.facebook.com and log in -2. Create a new business account on the left side bar - -:::note -It is possible that Meta directly restricts your newly created Business account. In that case, make sure to verify your identity to proceed. -::: - -### Create a Meta app - -1. Head over to https://developers.facebook.com/apps -2. Click on Create App -3. Give it any name and select `Business` type -4. Select your newly created Business Account -5. On the app page, set up the `WhatsApp` product - -### Get the System User token +## 2. Get the System User token 1. Go to your [System users page](https://business.facebook.com/settings/system-users) and create a new system user that has access to the related. @@ -228,7 +213,7 @@ It is possible that Meta directly restricts your newly created Business account. 2. The generated token will be used as `META_SYSTEM_USER_TOKEN` in your viewer configuration. 3. Click on `Add assets`. Under `Apps`, look for your app, select it and check `Manage app` -### Get the phone number ID +## 3. Get the phone number ID 1. Go to your WhatsApp Dev Console @@ -237,12 +222,12 @@ It is possible that Meta directly restricts your newly created Business account. 2. Add your phone number by clicking on the `Add phone number` button. 3. Select the newly created phone number in the `From` dropdown list and you will see right below the associated `Phone number ID` This will be used as `WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID` in your viewer configuration. -### Set up the webhook +## 4. Set up the webhook 1. Head over to `Quickstart > Configuration`. Edit the webhook URL to `$NEXTAUTH_URL/api/v1/whatsapp/preview/webhook`. Set the Verify token to `$ENCRYPTION_SECRET` and click on `Verify and save`. 2. Add the `messages` webhook field. -### Set up the message template +## 5. Set up the message template 1. Head over to `Messaging > Message Templates` and click on `Create Template` 2. Select the `Utility` category From ccc34b30287cc26939d7435aae0e8773f5afa462 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Wed, 27 Sep 2023 14:22:51 +0200 Subject: [PATCH 031/233] :children_crossing: (whatsapp) Improve upgrade plan for whatsapp notice --- .../src/components/UnlockPlanAlertInfo.tsx | 4 +--- .../publish/components/embeds/EmbedButton.tsx | 16 +++------------- .../WhatsAppModal/WhatsAppCredentialsModal.tsx | 9 ++++++--- .../modals/WhatsAppModal/WhatsAppModal.tsx | 10 ++++++++++ .../results/components/UsageAlertBanners.tsx | 17 +++++------------ .../src/features/typebot/api/updateTypebot.ts | 4 ++-- .../workspace/components/MembersList.tsx | 4 +++- apps/builder/src/pages/api/stripe/webhook.ts | 7 ++++++- 8 files changed, 36 insertions(+), 35 deletions(-) diff --git a/apps/builder/src/components/UnlockPlanAlertInfo.tsx b/apps/builder/src/components/UnlockPlanAlertInfo.tsx index c73d41025b..2102e043dd 100644 --- a/apps/builder/src/components/UnlockPlanAlertInfo.tsx +++ b/apps/builder/src/components/UnlockPlanAlertInfo.tsx @@ -15,13 +15,11 @@ import { import { useI18n } from '@/locales' type Props = { - contentLabel: React.ReactNode buttonLabel?: string } & AlertProps & Pick export const UnlockPlanAlertInfo = ({ - contentLabel, buttonLabel, type, excludedPlans, @@ -39,7 +37,7 @@ export const UnlockPlanAlertInfo = ({ > - {contentLabel} + {props.children} diff --git a/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppModal.tsx b/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppModal.tsx index 3eeac1c0ca..f4b3adb1c2 100644 --- a/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppModal.tsx +++ b/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppModal.tsx @@ -39,6 +39,9 @@ import { NumberInput } from '@/components/inputs' import { defaultSessionExpiryTimeout } from '@typebot.io/schemas/features/whatsapp' import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings' import { isDefined } from '@typebot.io/lib/utils' +import { hasProPerks } from '@/features/billing/helpers/hasProPerks' +import { UnlockPlanAlertInfo } from '@/components/UnlockPlanAlertInfo' +import { PlanTag } from '@/features/billing/components/PlanTag' export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => { const { typebot, updateTypebot, isPublished } = useTypebot() @@ -175,6 +178,12 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => { + {!hasProPerks(workspace) && ( + + Upgrade your workspace to to be able to + enable WhatsApp integration. + + )} {!isPublished && phoneNumberData?.id && ( You have modifications that can be published. )} @@ -269,6 +278,7 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => { { <> {chatsLimitPercentage > ALERT_CHATS_PERCENT_THRESHOLD && ( - - Your workspace collected{' '} - {chatsLimitPercentage}% of your total chats - limit this month. Upgrade your plan to continue chatting with - your customers beyond this limit. - - } - buttonLabel="Upgrade" - /> + + Your workspace collected {chatsLimitPercentage}% of + your total chats limit this month. Upgrade your plan to continue + chatting with your customers beyond this limit. + )} diff --git a/apps/builder/src/features/typebot/api/updateTypebot.ts b/apps/builder/src/features/typebot/api/updateTypebot.ts index b4fa39dfc5..785dbcec45 100644 --- a/apps/builder/src/features/typebot/api/updateTypebot.ts +++ b/apps/builder/src/features/typebot/api/updateTypebot.ts @@ -119,12 +119,12 @@ export const updateTypebot = authenticatedProcedure } if ( - typebot.whatsAppCredentialsId && + typebot.settings?.whatsApp?.isEnabled && !hasProPerks(existingTypebot.workspace) ) { throw new TRPCError({ code: 'BAD_REQUEST', - message: 'WhatsApp is only available for Pro workspaces', + message: 'WhatsApp can be enabled only on a Pro workspaces', }) } diff --git a/apps/builder/src/features/workspace/components/MembersList.tsx b/apps/builder/src/features/workspace/components/MembersList.tsx index 2050bb79d2..defa31c705 100644 --- a/apps/builder/src/features/workspace/components/MembersList.tsx +++ b/apps/builder/src/features/workspace/components/MembersList.tsx @@ -103,7 +103,9 @@ export const MembersList = () => { return ( {!canInviteNewMember && ( - + + {scopedT('unlockBanner.label')} + )} {isDefined(seatsLimit) && ( diff --git a/apps/builder/src/pages/api/stripe/webhook.ts b/apps/builder/src/pages/api/stripe/webhook.ts index 290aa3bf06..7f7d181041 100644 --- a/apps/builder/src/pages/api/stripe/webhook.ts +++ b/apps/builder/src/pages/api/stripe/webhook.ts @@ -191,13 +191,18 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { await prisma.typebot.updateMany({ where: { id: typebot.id }, data: { - whatsAppCredentialsId: null, settings: { ...settings, general: { ...settings.general, isBrandingEnabled: true, }, + whatsApp: settings.whatsApp + ? { + ...settings.whatsApp, + isEnabled: false, + } + : undefined, }, }, }) From 99b0025a664f951629899160f8ecea42046221b4 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Wed, 27 Sep 2023 15:40:57 +0200 Subject: [PATCH 032/233] :bug: (preview) Fix always displayed start props toast --- packages/bot-engine/startSession.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/bot-engine/startSession.ts b/packages/bot-engine/startSession.ts index 626a6d6da2..536cc3558b 100644 --- a/packages/bot-engine/startSession.ts +++ b/packages/bot-engine/startSession.ts @@ -147,12 +147,12 @@ export const startSession = async ({ if (isDefined(startClientSideAction)) { if (!result) { if ('startPropsToInject' in startClientSideAction) { - const { customHeadCode, googleAnalyticsId, pixelId, gtmId } = + const { customHeadCode, googleAnalyticsId, pixelId, pixelIds, gtmId } = startClientSideAction.startPropsToInject let toolsList = '' if (customHeadCode) toolsList += 'Custom head code, ' if (googleAnalyticsId) toolsList += 'Google Analytics, ' - if (pixelId) toolsList += 'Pixel, ' + if (pixelId || pixelIds) toolsList += 'Pixel, ' if (gtmId) toolsList += 'Google Tag Manager, ' toolsList = toolsList.slice(0, -2) startLogs.push({ @@ -321,6 +321,15 @@ const parseStartClientSideAction = ( typebot: StartTypebot ): NonNullable[number] | undefined => { const blocks = typebot.groups.flatMap((group) => group.blocks) + const pixelBlocks = ( + blocks.filter( + (block) => + block.type === IntegrationBlockType.PIXEL && + isNotEmpty(block.options.pixelId) && + block.options.isInitSkip !== true + ) as PixelBlock[] + ).map((pixelBlock) => pixelBlock.options.pixelId as string) + const startPropsToInject = { customHeadCode: isNotEmpty(typebot.settings.metadata.customHeadCode) ? parseHeadCode(typebot.settings.metadata.customHeadCode) @@ -333,14 +342,7 @@ const parseStartClientSideAction = ( block.options.trackingId ) as GoogleAnalyticsBlock | undefined )?.options.trackingId, - pixelIds: ( - blocks.filter( - (block) => - block.type === IntegrationBlockType.PIXEL && - isNotEmpty(block.options.pixelId) && - block.options.isInitSkip !== true - ) as PixelBlock[] - ).map((pixelBlock) => pixelBlock.options.pixelId as string), + pixelIds: pixelBlocks.length > 0 ? pixelBlocks : undefined, } if ( From e10a506c9608dafd9462ea6d20d698551eb751c1 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Wed, 27 Sep 2023 16:45:14 +0200 Subject: [PATCH 033/233] =?UTF-8?q?:bug:=20(whatsapp)=20Fix=20preview=20fa?= =?UTF-8?q?iling=20to=20start=20and=20wait=20timeo=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/whatsapp/startWhatsAppPreview.ts | 1 + apps/docs/docs/embed/whatsapp/overview.md | 3 +- .../blocks/logic/wait/executeWait.ts | 19 ++- .../bot-engine/whatsapp/resumeWhatsAppFlow.ts | 22 ++-- .../whatsapp/sendChatReplyToWhatsApp.ts | 114 +++++++++++++----- 5 files changed, 110 insertions(+), 49 deletions(-) diff --git a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts index ebf78fc870..ada1c22382 100644 --- a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts +++ b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts @@ -109,6 +109,7 @@ export const startWhatsAppPreview = authenticatedProcedure phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID, systemUserAccessToken: env.META_SYSTEM_USER_TOKEN, }, + state: newSessionState, }) await saveStateToDatabase({ clientSideActions: [], diff --git a/apps/docs/docs/embed/whatsapp/overview.md b/apps/docs/docs/embed/whatsapp/overview.md index 8942932e38..8163de1425 100644 --- a/apps/docs/docs/embed/whatsapp/overview.md +++ b/apps/docs/docs/embed/whatsapp/overview.md @@ -21,14 +21,13 @@ Head over to the Share tab of your bot and click on the WhatsApp button to get t WhatsApp environment have some limitations that you need to keep in mind when building the bot: - GIF and SVG image files are not supported. They won't be displayed. -- Buttons content can't be longer than 20 characters +- Buttons content can't be longer than 20 characters. If the content is longer, it will be truncated. - Incompatible blocks, if present, they will be skipped: - Payment input block - Chatwoot block - Script block - Google Analytics block - Meta Pixel blocks - - Execute on client options ## Contact information diff --git a/packages/bot-engine/blocks/logic/wait/executeWait.ts b/packages/bot-engine/blocks/logic/wait/executeWait.ts index 55cf11a72d..0df209e033 100644 --- a/packages/bot-engine/blocks/logic/wait/executeWait.ts +++ b/packages/bot-engine/blocks/logic/wait/executeWait.ts @@ -7,22 +7,21 @@ export const executeWait = ( block: WaitBlock ): ExecuteLogicResponse => { const { variables } = state.typebotsQueue[0].typebot - if (!block.options.secondsToWaitFor) - return { outgoingEdgeId: block.outgoingEdgeId } const parsedSecondsToWaitFor = safeParseInt( parseVariables(variables)(block.options.secondsToWaitFor) ) return { outgoingEdgeId: block.outgoingEdgeId, - clientSideActions: parsedSecondsToWaitFor - ? [ - { - wait: { secondsToWaitFor: parsedSecondsToWaitFor }, - expectsDedicatedReply: block.options.shouldPause, - }, - ] - : undefined, + clientSideActions: + parsedSecondsToWaitFor || block.options.shouldPause + ? [ + { + wait: { secondsToWaitFor: parsedSecondsToWaitFor ?? 0 }, + expectsDedicatedReply: block.options.shouldPause, + }, + ] + : undefined, } } diff --git a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts index 2cb7bddb79..9b19eccd50 100644 --- a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts +++ b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts @@ -12,6 +12,15 @@ import { decrypt } from '@typebot.io/lib/api' import { saveStateToDatabase } from '../saveStateToDatabase' import prisma from '@typebot.io/lib/prisma' import { isDefined } from '@typebot.io/lib/utils' +import { startBotFlow } from '../startBotFlow' + +type Props = { + receivedMessage: WhatsAppIncomingMessage + sessionId: string + credentialsId?: string + workspaceId?: string + contact: NonNullable['contact'] +} export const resumeWhatsAppFlow = async ({ receivedMessage, @@ -19,13 +28,7 @@ export const resumeWhatsAppFlow = async ({ workspaceId, credentialsId, contact, -}: { - receivedMessage: WhatsAppIncomingMessage - sessionId: string - credentialsId?: string - workspaceId?: string - contact: NonNullable['contact'] -}) => { +}: Props): Promise<{ message: string }> => { const messageSendDate = new Date(Number(receivedMessage.timestamp) * 1000) const messageSentBefore3MinutesAgo = messageSendDate.getTime() < Date.now() - 180000 @@ -62,7 +65,9 @@ export const resumeWhatsAppFlow = async ({ const resumeResponse = session && !isSessionExpired - ? await continueBotFlow(session.state)(messageContent) + ? session.state.currentBlock + ? await continueBotFlow(session.state)(messageContent) + : await startBotFlow(session.state) : workspaceId ? await startWhatsAppSession({ incomingMessage: messageContent, @@ -90,6 +95,7 @@ export const resumeWhatsAppFlow = async ({ typingEmulation: newSessionState.typingEmulation, clientSideActions, credentials, + state: newSessionState, }) await saveStateToDatabase({ diff --git a/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts b/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts index dbcf743c94..4ad555a99a 100644 --- a/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts +++ b/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts @@ -15,6 +15,7 @@ import { HTTPError } from 'got' import { convertInputToWhatsAppMessages } from './convertInputToWhatsAppMessage' import { isNotDefined } from '@typebot.io/lib/utils' import { computeTypingDuration } from '../computeTypingDuration' +import { continueBotFlow } from '../continueBotFlow' // Media can take some time to be delivered. This make sure we don't send a message before the media is delivered. const messageAfterMediaTimeout = 5000 @@ -23,6 +24,7 @@ type Props = { to: string typingEmulation: SessionState['typingEmulation'] credentials: WhatsAppCredentials['data'] + state: SessionState } & Pick export const sendChatReplyToWhatsApp = async ({ @@ -32,13 +34,36 @@ export const sendChatReplyToWhatsApp = async ({ input, clientSideActions, credentials, -}: Props) => { + state, +}: Props): Promise => { const messagesBeforeInput = isLastMessageIncludedInInput(input) ? messages.slice(0, -1) : messages const sentMessages: WhatsAppSendingMessage[] = [] + const clientSideActionsBeforeMessages = + clientSideActions?.filter((action) => + isNotDefined(action.lastBubbleBlockId) + ) ?? [] + + for (const action of clientSideActionsBeforeMessages) { + const result = await executeClientSideAction({ to, credentials })(action) + if (!result) continue + const { input, newSessionState, messages, clientSideActions } = + await continueBotFlow(state)(result.replyToSend) + + return sendChatReplyToWhatsApp({ + to, + messages, + input, + typingEmulation: newSessionState.typingEmulation, + clientSideActions, + credentials, + state: newSessionState, + }) + } + for (const message of messagesBeforeInput) { const whatsAppMessage = convertMessageToWhatsAppMessage(message) if (isNotDefined(whatsAppMessage)) continue @@ -60,6 +85,28 @@ export const sendChatReplyToWhatsApp = async ({ credentials, }) sentMessages.push(whatsAppMessage) + const clientSideActionsAfterMessage = + clientSideActions?.filter( + (action) => action.lastBubbleBlockId === message.id + ) ?? [] + for (const action of clientSideActionsAfterMessage) { + const result = await executeClientSideAction({ to, credentials })( + action + ) + if (!result) continue + const { input, newSessionState, messages, clientSideActions } = + await continueBotFlow(state)(result.replyToSend) + + return sendChatReplyToWhatsApp({ + to, + messages, + input, + typingEmulation: newSessionState.typingEmulation, + clientSideActions, + credentials, + state: newSessionState, + }) + } } catch (err) { captureException(err, { extra: { message } }) console.log('Failed to send message:', JSON.stringify(message, null, 2)) @@ -68,34 +115,6 @@ export const sendChatReplyToWhatsApp = async ({ } } - if (clientSideActions) - for (const clientSideAction of clientSideActions) { - if ('redirect' in clientSideAction && clientSideAction.redirect.url) { - const message = { - type: 'text', - text: { - body: clientSideAction.redirect.url, - preview_url: true, - }, - } satisfies WhatsAppSendingMessage - try { - await sendWhatsAppMessage({ - to, - message, - credentials, - }) - } catch (err) { - captureException(err, { extra: { message } }) - console.log( - 'Failed to send message:', - JSON.stringify(message, null, 2) - ) - if (err instanceof HTTPError) - console.log('HTTPError', err.response.statusCode, err.response.body) - } - } - } - if (input) { const inputWhatsAppMessages = convertInputToWhatsAppMessages( input, @@ -160,3 +179,40 @@ const isLastMessageIncludedInInput = (input: ChatReply['input']): boolean => { if (isNotDefined(input)) return false return input.type === InputBlockType.CHOICE } + +const executeClientSideAction = + (context: { to: string; credentials: WhatsAppCredentials['data'] }) => + async ( + clientSideAction: NonNullable[number] + ): Promise<{ replyToSend: string | undefined } | void> => { + if ('wait' in clientSideAction) { + await new Promise((resolve) => + setTimeout(resolve, clientSideAction.wait.secondsToWaitFor * 1000) + ) + if (!clientSideAction.expectsDedicatedReply) return + return { + replyToSend: undefined, + } + } + if ('redirect' in clientSideAction && clientSideAction.redirect.url) { + const message = { + type: 'text', + text: { + body: clientSideAction.redirect.url, + preview_url: true, + }, + } satisfies WhatsAppSendingMessage + try { + await sendWhatsAppMessage({ + to: context.to, + message, + credentials: context.credentials, + }) + } catch (err) { + captureException(err, { extra: { message } }) + console.log('Failed to send message:', JSON.stringify(message, null, 2)) + if (err instanceof HTTPError) + console.log('HTTPError', err.response.statusCode, err.response.body) + } + } + } From d46e8013d4d3e28eea099f8de8da9055d7d27723 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Thu, 28 Sep 2023 15:35:21 +0200 Subject: [PATCH 034/233] :children_crossing: (pictureChoice) Improve single picture choice with same titles Closes #859 --- .../pictureChoice/parsePictureChoicesReply.ts | 16 ++++++---------- packages/embeds/js/package.json | 2 +- .../inputs/pictureChoice/SinglePictureChoice.tsx | 6 +++--- packages/embeds/nextjs/package.json | 2 +- packages/embeds/react/package.json | 2 +- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/bot-engine/blocks/inputs/pictureChoice/parsePictureChoicesReply.ts b/packages/bot-engine/blocks/inputs/pictureChoice/parsePictureChoicesReply.ts index f98a4ff0e6..7271948d2e 100644 --- a/packages/bot-engine/blocks/inputs/pictureChoice/parsePictureChoicesReply.ts +++ b/packages/bot-engine/blocks/inputs/pictureChoice/parsePictureChoicesReply.ts @@ -68,22 +68,18 @@ export const parsePictureChoicesReply = .join(', '), } } - if (state.whatsApp) { - const matchedItem = displayedItems.find((item) => item.id === inputValue) - if (!matchedItem) return { status: 'fail' } - return { - status: 'success', - reply: matchedItem.title ?? matchedItem.pictureSrc ?? '', - } - } const longestItemsFirst = [...displayedItems].sort( (a, b) => (b.title?.length ?? 0) - (a.title?.length ?? 0) ) const matchedItem = longestItemsFirst.find( (item) => - item.title && + item.id === inputValue || item.title - .toLowerCase() + ?.toLowerCase() + .trim() + .includes(inputValue.toLowerCase().trim()) || + item.pictureSrc + ?.toLowerCase() .trim() .includes(inputValue.toLowerCase().trim()) ) diff --git a/packages/embeds/js/package.json b/packages/embeds/js/package.json index 7b3e77b165..776793453f 100644 --- a/packages/embeds/js/package.json +++ b/packages/embeds/js/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/js", - "version": "0.1.31", + "version": "0.1.32", "description": "Javascript library to display typebots on your website", "type": "module", "main": "dist/index.js", diff --git a/packages/embeds/js/src/features/blocks/inputs/pictureChoice/SinglePictureChoice.tsx b/packages/embeds/js/src/features/blocks/inputs/pictureChoice/SinglePictureChoice.tsx index 33c618665e..0f4bc9a5e0 100644 --- a/packages/embeds/js/src/features/blocks/inputs/pictureChoice/SinglePictureChoice.tsx +++ b/packages/embeds/js/src/features/blocks/inputs/pictureChoice/SinglePictureChoice.tsx @@ -20,10 +20,10 @@ export const SinglePictureChoice = (props: Props) => { }) const handleClick = (itemIndex: number) => { - const pictureSrc = filteredItems()[itemIndex].pictureSrc - if (!pictureSrc) return + const item = filteredItems()[itemIndex] return props.onSubmit({ - value: filteredItems()[itemIndex].title ?? pictureSrc, + label: item.title ?? item.pictureSrc ?? item.id, + value: item.id, }) } diff --git a/packages/embeds/nextjs/package.json b/packages/embeds/nextjs/package.json index e5f3503d35..b50ae0a5c1 100644 --- a/packages/embeds/nextjs/package.json +++ b/packages/embeds/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/nextjs", - "version": "0.1.31", + "version": "0.1.32", "description": "Convenient library to display typebots on your Next.js website", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/embeds/react/package.json b/packages/embeds/react/package.json index eeaec2afc9..75630065c5 100644 --- a/packages/embeds/react/package.json +++ b/packages/embeds/react/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/react", - "version": "0.1.31", + "version": "0.1.32", "description": "Convenient library to display typebots on your React app", "main": "dist/index.js", "types": "dist/index.d.ts", From 76f4954540b523733f92db5db55f4de842515971 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Thu, 28 Sep 2023 16:39:48 +0200 Subject: [PATCH 035/233] =?UTF-8?q?:children=5Fcrossing:=20(pictureChoice)?= =?UTF-8?q?=20Allow=20dynamic=20picture=20choice=20with=E2=80=A6=20(#865)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … string variables ### Summary by CodeRabbit - Refactor: Updated `GoogleSheetsNodeContent` component to use the `options` prop instead of `action`, and integrated the `useTypebot` hook for better functionality. - Style: Improved UI text and layout in `GoogleSheetsSettings.tsx`, enhancing user experience when selecting rows. - Refactor: Simplified rendering logic in `BlockNodeContent.tsx` by directly calling `GoogleSheetsNodeContent` component, improving code readability. - Bug Fix: Enhanced `injectVariableValuesInPictureChoiceBlock` function to handle different types of values for titles, descriptions, and picture sources, fixing issues with variable value injection. --- .../components/GoogleSheetsNodeContent.tsx | 36 ++++++++++++++----- .../components/GoogleSheetsSettings.tsx | 14 ++++---- .../nodes/block/BlockNodeContent.tsx | 6 +--- ...njectVariableValuesInPictureChoiceBlock.ts | 35 +++++++++++------- 4 files changed, 59 insertions(+), 32 deletions(-) diff --git a/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsNodeContent.tsx b/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsNodeContent.tsx index b0d3006df4..f1009e7a8c 100644 --- a/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsNodeContent.tsx +++ b/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsNodeContent.tsx @@ -1,13 +1,33 @@ import React from 'react' -import { Text } from '@chakra-ui/react' -import { GoogleSheetsAction } from '@typebot.io/schemas' +import { Stack, Text } from '@chakra-ui/react' +import { GoogleSheetsAction, GoogleSheetsOptions } from '@typebot.io/schemas' +import { useTypebot } from '@/features/editor/providers/TypebotProvider' +import { SetVariableLabel } from '@/components/SetVariableLabel' type Props = { - action?: GoogleSheetsAction + options?: GoogleSheetsOptions } -export const GoogleSheetsNodeContent = ({ action }: Props) => ( - - {action ?? 'Configure...'} - -) +export const GoogleSheetsNodeContent = ({ options }: Props) => { + const { typebot } = useTypebot() + return ( + + + {options?.action ?? 'Configure...'} + + {typebot && + options?.action === GoogleSheetsAction.GET && + options?.cellsToExtract + .map((mapping) => mapping.variableId) + .map((variableId, idx) => + variableId ? ( + + ) : null + )} + + ) +} diff --git a/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings.tsx b/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings.tsx index cf69b3c1ce..1c089429f9 100644 --- a/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings.tsx +++ b/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings.tsx @@ -304,12 +304,17 @@ const ActionOptions = ({ - Rows to filter + Select row(s) - + + - )} diff --git a/apps/builder/src/features/graph/components/nodes/block/BlockNodeContent.tsx b/apps/builder/src/features/graph/components/nodes/block/BlockNodeContent.tsx index 60c3edb88e..753391198f 100644 --- a/apps/builder/src/features/graph/components/nodes/block/BlockNodeContent.tsx +++ b/apps/builder/src/features/graph/components/nodes/block/BlockNodeContent.tsx @@ -159,11 +159,7 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => { case LogicBlockType.CONDITION: return case IntegrationBlockType.GOOGLE_SHEETS: { - return ( - - ) + return } case IntegrationBlockType.GOOGLE_ANALYTICS: { return diff --git a/packages/bot-engine/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts b/packages/bot-engine/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts index 8d29496eaf..695310137d 100644 --- a/packages/bot-engine/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts +++ b/packages/bot-engine/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts @@ -20,8 +20,7 @@ export const injectVariableValuesInPictureChoiceBlock = variable.id === block.options.dynamicItems?.pictureSrcsVariableId && isDefined(variable.value) ) as VariableWithValue | undefined - if (!pictureSrcsVariable || typeof pictureSrcsVariable.value === 'string') - return block + if (!pictureSrcsVariable) return block const titlesVariable = block.options.dynamicItems.titlesVariableId ? (variables.find( (variable) => @@ -29,6 +28,10 @@ export const injectVariableValuesInPictureChoiceBlock = isDefined(variable.value) ) as VariableWithValue | undefined) : undefined + const titlesVariableValues = + typeof titlesVariable?.value === 'string' + ? [titlesVariable.value] + : titlesVariable?.value const descriptionsVariable = block.options.dynamicItems .descriptionsVariableId ? (variables.find( @@ -38,18 +41,26 @@ export const injectVariableValuesInPictureChoiceBlock = isDefined(variable.value) ) as VariableWithValue | undefined) : undefined + const descriptionsVariableValues = + typeof descriptionsVariable?.value === 'string' + ? [descriptionsVariable.value] + : descriptionsVariable?.value + + const variableValues = + typeof pictureSrcsVariable.value === 'string' + ? [pictureSrcsVariable.value] + : pictureSrcsVariable.value + return { ...block, - items: pictureSrcsVariable.value - .filter(isDefined) - .map((pictureSrc, idx) => ({ - id: idx.toString(), - type: ItemType.PICTURE_CHOICE, - blockId: block.id, - pictureSrc, - title: titlesVariable?.value?.[idx] ?? '', - description: descriptionsVariable?.value?.[idx] ?? '', - })), + items: variableValues.filter(isDefined).map((pictureSrc, idx) => ({ + id: idx.toString(), + type: ItemType.PICTURE_CHOICE, + blockId: block.id, + pictureSrc, + title: titlesVariableValues?.[idx] ?? '', + description: descriptionsVariableValues?.[idx] ?? '', + })), } } return deepParseVariables(variables)( From f9a14c0685826a00a1873d17baafbe950fab55e0 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Fri, 29 Sep 2023 09:59:38 +0200 Subject: [PATCH 036/233] =?UTF-8?q?:bug:=20(whatsapp)=20Fix=20auto=20start?= =?UTF-8?q?=20input=20where=20it=20didn't=20display=20next=20bu=E2=80=A6?= =?UTF-8?q?=20(#869)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …bbles ### Summary by CodeRabbit **Release Notes** - New Feature: Enhanced WhatsApp integration with improved phone number formatting and session ID generation. - Refactor: Updated the `startWhatsAppPreview` and `receiveMessagePreview` functions for better consistency and readability. - Bug Fix: Added a check for `phoneNumberId` in the `receiveMessage` function to prevent errors when it's undefined. - Documentation: Expanded the WhatsApp integration guide and FAQs in the docs, providing more detailed instructions and addressing common queries. - Chore: Introduced a new `metadata` field in the `whatsAppWebhookRequestBodySchema` to store the `phone_number_id`. --- .../WhatsAppPreviewInstructions.tsx | 34 ++++--- .../src/features/whatsapp/getPhoneNumber.ts | 2 +- .../whatsapp/receiveMessagePreview.ts | 2 +- .../features/whatsapp/startWhatsAppPreview.ts | 8 +- .../docs/embed/whatsapp/create-meta-app.md | 2 + apps/docs/docs/embed/whatsapp/overview.md | 24 +++++ apps/docs/openapi/builder/_spec_.json | 15 ++++ apps/docs/openapi/chat/_spec_.json | 84 +++++++++++++----- .../img/whatsapp/configure-integration.png | Bin 0 -> 115511 bytes .../features/whatsapp/api/receiveMessage.ts | 7 +- packages/bot-engine/queries/getSession.ts | 2 +- .../bot-engine/whatsapp/resumeWhatsAppFlow.ts | 1 - .../whatsapp/startWhatsAppSession.ts | 28 +++--- packages/schemas/features/whatsapp.ts | 3 + 14 files changed, 153 insertions(+), 59 deletions(-) create mode 100644 apps/docs/static/img/whatsapp/configure-integration.png diff --git a/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx b/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx index ca376dd1b3..87aad0d363 100644 --- a/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx +++ b/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx @@ -6,8 +6,8 @@ import { Alert, AlertIcon, Button, - Flex, HStack, + Link, SlideFade, Stack, StackProps, @@ -84,28 +84,38 @@ export const WhatsAppPreviewInstructions = (props: StackProps) => { defaultValue={phoneNumber} onChange={setPhoneNumber} /> - + {!isMessageSent && ( + + )} - + Chat started! - Open WhatsApp to test your bot. The first message can take up - to 2 min to be delivered. + The first message can take up to 2 min to be delivered. - + + ) diff --git a/apps/builder/src/features/whatsapp/getPhoneNumber.ts b/apps/builder/src/features/whatsapp/getPhoneNumber.ts index e1a2481332..56c1c291a8 100644 --- a/apps/builder/src/features/whatsapp/getPhoneNumber.ts +++ b/apps/builder/src/features/whatsapp/getPhoneNumber.ts @@ -47,7 +47,7 @@ export const getPhoneNumber = authenticatedProcedure const formattedPhoneNumber = `${ display_phone_number.startsWith('+') ? '' : '+' - }${display_phone_number.replace(/\s-/g, '')}` + }${display_phone_number.replace(/[\s-]/g, '')}` return { id: credentials.phoneNumberId, diff --git a/apps/builder/src/features/whatsapp/receiveMessagePreview.ts b/apps/builder/src/features/whatsapp/receiveMessagePreview.ts index b0cb6a435a..54e4580a39 100644 --- a/apps/builder/src/features/whatsapp/receiveMessagePreview.ts +++ b/apps/builder/src/features/whatsapp/receiveMessagePreview.ts @@ -34,7 +34,7 @@ export const receiveMessagePreview = publicProcedure const contactPhoneNumber = '+' + receivedMessage.from return resumeWhatsAppFlow({ receivedMessage, - sessionId: `wa-${receivedMessage.from}-preview`, + sessionId: `wa-preview-${receivedMessage.from}`, contact: { name: contactName, phoneNumber: contactPhoneNumber, diff --git a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts index ada1c22382..86a4c8d556 100644 --- a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts +++ b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts @@ -27,7 +27,9 @@ export const startWhatsAppPreview = authenticatedProcedure to: z .string() .min(1) - .transform((value) => value.replace(/\s/g, '').replace(/\+/g, '')), + .transform((value) => + value.replace(/\s/g, '').replace(/\+/g, '').replace(/-/g, '') + ), typebotId: z.string(), startGroupId: z.string().optional(), }) @@ -70,7 +72,7 @@ export const startWhatsAppPreview = authenticatedProcedure ) throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' }) - const sessionId = `wa-${to}-preview` + const sessionId = `wa-preview-${to}` const existingSession = await prisma.chatSession.findFirst({ where: { @@ -130,7 +132,7 @@ export const startWhatsAppPreview = authenticatedProcedure whatsApp: (existingSession?.state as SessionState | undefined) ?.whatsApp, }, - id: `wa-${to}-preview`, + id: sessionId, }) try { await sendWhatsAppMessage({ diff --git a/apps/docs/docs/embed/whatsapp/create-meta-app.md b/apps/docs/docs/embed/whatsapp/create-meta-app.md index f7aa728c5f..e389a59ab7 100644 --- a/apps/docs/docs/embed/whatsapp/create-meta-app.md +++ b/apps/docs/docs/embed/whatsapp/create-meta-app.md @@ -17,3 +17,5 @@ It is possible that Meta automatically restricts your newly created Business acc 4. Select `Business` type 5. Give it any name and select your newly created Business Account 6. On the app page, look for `WhatsApp` product and enable it + +You can then follow the instructions in the Share tab of your bot to connect your Meta app to Typebot. diff --git a/apps/docs/docs/embed/whatsapp/overview.md b/apps/docs/docs/embed/whatsapp/overview.md index 8163de1425..be84b170a1 100644 --- a/apps/docs/docs/embed/whatsapp/overview.md +++ b/apps/docs/docs/embed/whatsapp/overview.md @@ -29,8 +29,32 @@ WhatsApp environment have some limitations that you need to keep in mind when bu - Google Analytics block - Meta Pixel blocks +## Configuration + +You can customize how your bot behaves on WhatsApp in the `Configure integration` section + +WhatsApp configure integration + +**Session expiration timeout**: A number from 0 to 48 which is the number of hours after which the session will expire. If the user doesn't interact with the bot for more than the timeout, the session will expire and if user sends a new message, it will start a new chat. + +**Start bot condition**: A condition that will be evaluated when a user starts a conversation with your bot. If the condition is not met, the bot will not be triggered. + ## Contact information You can automatically assign contact name and phone number to a variable in your bot using a Set variable block with the dedicated system values: WhatsApp contact system variables + +## FAQ + +### How many WhatsApp numbers can I use? + +You can integrate as many numbers as you'd like. Keep in mind that Typebot does not provide those numbers. We work as a "Bring your own Meta application" and we give you clear instructions on [how to set up your Meta app](./whatsapp/create-meta-app). + +### Can I link multiple bots to the same WhatsApp number? + +Yes, you can. You will have to add a "Start bot condition" to each of your bots to make sure that the right bot is triggered when a user starts a conversation. + +### Does the integration with WhatsApp requires any paid API? + +You integrate your typebots with your own WhatsApp Business Platform which is the official service from Meta. At the moment, the first 1,000 Service conversations each month are free. For more information, refer to [their documentation](https://developers.facebook.com/docs/whatsapp/cloud-api/get-started#pricing---payment-methods) diff --git a/apps/docs/openapi/builder/_spec_.json b/apps/docs/openapi/builder/_spec_.json index 460dc0c089..02ab65a7a8 100644 --- a/apps/docs/openapi/builder/_spec_.json +++ b/apps/docs/openapi/builder/_spec_.json @@ -33087,6 +33087,18 @@ "value": { "type": "object", "properties": { + "metadata": { + "type": "object", + "properties": { + "phone_number_id": { + "type": "string" + } + }, + "required": [ + "phone_number_id" + ], + "additionalProperties": false + }, "contacts": { "type": "array", "items": { @@ -33388,6 +33400,9 @@ } } }, + "required": [ + "metadata" + ], "additionalProperties": false } }, diff --git a/apps/docs/openapi/chat/_spec_.json b/apps/docs/openapi/chat/_spec_.json index 8d5ef97f45..4ba7f441e8 100644 --- a/apps/docs/openapi/chat/_spec_.json +++ b/apps/docs/openapi/chat/_spec_.json @@ -5746,7 +5746,14 @@ "type": "string" }, "pixelId": { - "type": "string" + "type": "string", + "description": "Deprecated" + }, + "pixelIds": { + "type": "array", + "items": { + "type": "string" + } }, "gtmId": { "type": "string" @@ -6396,28 +6403,48 @@ "type": "object", "properties": { "filePathProps": { - "type": "object", - "properties": { - "typebotId": { - "type": "string" - }, - "blockId": { - "type": "string" - }, - "resultId": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "typebotId": { + "type": "string" + }, + "blockId": { + "type": "string" + }, + "resultId": { + "type": "string" + }, + "fileName": { + "type": "string" + } + }, + "required": [ + "typebotId", + "blockId", + "resultId", + "fileName" + ], + "additionalProperties": false }, - "fileName": { - "type": "string" + { + "type": "object", + "properties": { + "sessionId": { + "type": "string" + }, + "fileName": { + "type": "string" + } + }, + "required": [ + "sessionId", + "fileName" + ], + "additionalProperties": false } - }, - "required": [ - "typebotId", - "blockId", - "resultId", - "fileName" - ], - "additionalProperties": false + ] }, "fileType": { "type": "string" @@ -6604,6 +6631,18 @@ "value": { "type": "object", "properties": { + "metadata": { + "type": "object", + "properties": { + "phone_number_id": { + "type": "string" + } + }, + "required": [ + "phone_number_id" + ], + "additionalProperties": false + }, "contacts": { "type": "array", "items": { @@ -6905,6 +6944,9 @@ } } }, + "required": [ + "metadata" + ], "additionalProperties": false } }, diff --git a/apps/docs/static/img/whatsapp/configure-integration.png b/apps/docs/static/img/whatsapp/configure-integration.png new file mode 100644 index 0000000000000000000000000000000000000000..38e051eb26ae324a8f9433702a3641cbd9b7e7b5 GIT binary patch literal 115511 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 literal 0 HcmV?d00001 diff --git a/apps/viewer/src/features/whatsapp/api/receiveMessage.ts b/apps/viewer/src/features/whatsapp/api/receiveMessage.ts index 7353ffeda4..1c45848a28 100644 --- a/apps/viewer/src/features/whatsapp/api/receiveMessage.ts +++ b/apps/viewer/src/features/whatsapp/api/receiveMessage.ts @@ -23,16 +23,19 @@ export const receiveMessage = publicProcedure message: z.string(), }) ) - .mutation(async ({ input: { entry, workspaceId, credentialsId } }) => { + .mutation(async ({ input: { entry, credentialsId, workspaceId } }) => { const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0) if (isNotDefined(receivedMessage)) return { message: 'No message found' } const contactName = entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? '' const contactPhoneNumber = entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? '' + const phoneNumberId = entry.at(0)?.changes.at(0)?.value + .metadata.phone_number_id + if (!phoneNumberId) return { message: 'No phone number id found' } return resumeWhatsAppFlow({ receivedMessage, - sessionId: `wa-${credentialsId}-${receivedMessage.from}`, + sessionId: `wa-${phoneNumberId}-${receivedMessage.from}`, credentialsId, workspaceId, contact: { diff --git a/packages/bot-engine/queries/getSession.ts b/packages/bot-engine/queries/getSession.ts index 18167ad1a7..537767320e 100644 --- a/packages/bot-engine/queries/getSession.ts +++ b/packages/bot-engine/queries/getSession.ts @@ -1,5 +1,5 @@ import prisma from '@typebot.io/lib/prisma' -import { ChatSession, sessionStateSchema } from '@typebot.io/schemas' +import { sessionStateSchema } from '@typebot.io/schemas' export const getSession = async (sessionId: string) => { const session = await prisma.chatSession.findUnique({ diff --git a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts index 9b19eccd50..4b7d17e316 100644 --- a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts +++ b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts @@ -71,7 +71,6 @@ export const resumeWhatsAppFlow = async ({ : workspaceId ? await startWhatsAppSession({ incomingMessage: messageContent, - sessionId, workspaceId, credentials: { ...credentials, id: credentialsId as string }, contact, diff --git a/packages/bot-engine/whatsapp/startWhatsAppSession.ts b/packages/bot-engine/whatsapp/startWhatsAppSession.ts index e8af5d19ae..0140d7d6ee 100644 --- a/packages/bot-engine/whatsapp/startWhatsAppSession.ts +++ b/packages/bot-engine/whatsapp/startWhatsAppSession.ts @@ -20,7 +20,6 @@ import { upsertResult } from '../queries/upsertResult' type Props = { incomingMessage?: string - sessionId: string workspaceId?: string credentials: WhatsAppCredentials['data'] & Pick contact: NonNullable['contact'] @@ -76,7 +75,7 @@ export const startWhatsAppSession = async ({ publicTypebot.settings.whatsApp?.sessionExpiryTimeout ?? defaultSessionExpiryTimeout - const session = await startSession({ + let chatReply = await startSession({ startParams: { typebot: publicTypebot.typebot.publicId as string, }, @@ -89,34 +88,29 @@ export const startWhatsAppSession = async ({ }, }) - let newSessionState: SessionState = session.newSessionState + const sessionState: SessionState = chatReply.newSessionState // If first block is an input block, we can directly continue the bot flow const firstEdgeId = - newSessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId - const nextGroup = await getNextGroup(newSessionState)(firstEdgeId) + sessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId + const nextGroup = await getNextGroup(sessionState)(firstEdgeId) const firstBlock = nextGroup.group?.blocks.at(0) if (firstBlock && isInputBlock(firstBlock)) { - const resultId = newSessionState.typebotsQueue[0].resultId + const resultId = sessionState.typebotsQueue[0].resultId if (resultId) await upsertResult({ hasStarted: true, isCompleted: false, resultId, - typebot: newSessionState.typebotsQueue[0].typebot, + typebot: sessionState.typebotsQueue[0].typebot, }) - newSessionState = ( - await continueBotFlow({ - ...newSessionState, - currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id }, - })(incomingMessage) - ).newSessionState + chatReply = await continueBotFlow({ + ...sessionState, + currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id }, + })(incomingMessage) } - return { - ...session, - newSessionState, - } + return chatReply } export const messageMatchStartCondition = ( diff --git a/packages/schemas/features/whatsapp.ts b/packages/schemas/features/whatsapp.ts index 15487b428e..cd5acd6a77 100644 --- a/packages/schemas/features/whatsapp.ts +++ b/packages/schemas/features/whatsapp.ts @@ -142,6 +142,9 @@ export const whatsAppWebhookRequestBodySchema = z.object({ changes: z.array( z.object({ value: z.object({ + metadata: z.object({ + phone_number_id: z.string(), + }), contacts: z .array( z.object({ From cd97da2d34fcf576100d03fa30608ce26f1784c4 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Fri, 29 Sep 2023 11:23:04 +0200 Subject: [PATCH 037/233] :bug: (typebotLink) Fix nested typebot link pop --- packages/bot-engine/executeGroup.ts | 2 +- packages/bot-engine/getNextGroup.ts | 3 ++- packages/bot-engine/startBotFlow.ts | 14 ++++++++------ .../bot-engine/whatsapp/startWhatsAppSession.ts | 3 ++- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/bot-engine/executeGroup.ts b/packages/bot-engine/executeGroup.ts index 821c7b6363..ce519fbabb 100644 --- a/packages/bot-engine/executeGroup.ts +++ b/packages/bot-engine/executeGroup.ts @@ -119,7 +119,7 @@ export const executeGroup = } } - if (!nextEdgeId && state.typebotsQueue.length === 1) + if (!nextEdgeId && newSessionState.typebotsQueue.length === 1) return { messages, newSessionState, clientSideActions, logs } const nextGroup = await getNextGroup(newSessionState)( diff --git a/packages/bot-engine/getNextGroup.ts b/packages/bot-engine/getNextGroup.ts index b3826bf8bc..212331c049 100644 --- a/packages/bot-engine/getNextGroup.ts +++ b/packages/bot-engine/getNextGroup.ts @@ -23,7 +23,7 @@ export const getNextGroup = isCompleted: true, hasStarted: state.typebotsQueue[0].answers.length > 0, }) - const newSessionState = { + let newSessionState = { ...state, typebotsQueue: [ { @@ -69,6 +69,7 @@ export const getNextGroup = ], } satisfies SessionState const nextGroup = await getNextGroup(newSessionState)(nextEdgeId) + newSessionState = nextGroup.newSessionState if (!nextGroup) return { newSessionState, diff --git a/packages/bot-engine/startBotFlow.ts b/packages/bot-engine/startBotFlow.ts index 88ee436b97..68edf7d564 100644 --- a/packages/bot-engine/startBotFlow.ts +++ b/packages/bot-engine/startBotFlow.ts @@ -7,6 +7,7 @@ export const startBotFlow = async ( state: SessionState, startGroupId?: string ): Promise => { + let newSessionState = state if (startGroupId) { const group = state.typebotsQueue[0].typebot.groups.find( (group) => group.id === startGroupId @@ -16,12 +17,13 @@ export const startBotFlow = async ( code: 'BAD_REQUEST', message: "startGroupId doesn't exist", }) - return executeGroup(state)(group) + return executeGroup(newSessionState)(group) } const firstEdgeId = - state.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId - if (!firstEdgeId) return { messages: [], newSessionState: state } - const nextGroup = await getNextGroup(state)(firstEdgeId) - if (!nextGroup.group) return { messages: [], newSessionState: state } - return executeGroup(state)(nextGroup.group) + newSessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId + if (!firstEdgeId) return { messages: [], newSessionState } + const nextGroup = await getNextGroup(newSessionState)(firstEdgeId) + newSessionState = nextGroup.newSessionState + if (!nextGroup.group) return { messages: [], newSessionState } + return executeGroup(newSessionState)(nextGroup.group) } diff --git a/packages/bot-engine/whatsapp/startWhatsAppSession.ts b/packages/bot-engine/whatsapp/startWhatsAppSession.ts index 0140d7d6ee..c79731b79d 100644 --- a/packages/bot-engine/whatsapp/startWhatsAppSession.ts +++ b/packages/bot-engine/whatsapp/startWhatsAppSession.ts @@ -88,12 +88,13 @@ export const startWhatsAppSession = async ({ }, }) - const sessionState: SessionState = chatReply.newSessionState + let sessionState: SessionState = chatReply.newSessionState // If first block is an input block, we can directly continue the bot flow const firstEdgeId = sessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId const nextGroup = await getNextGroup(sessionState)(firstEdgeId) + sessionState = nextGroup.newSessionState const firstBlock = nextGroup.group?.blocks.at(0) if (firstBlock && isInputBlock(firstBlock)) { const resultId = sessionState.typebotsQueue[0].resultId From 1a4b8bb8fcc3fdf9d8dfae7bc31036eea9df99c0 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Fri, 29 Sep 2023 11:33:39 +0200 Subject: [PATCH 038/233] :pencil: (typebotLink) Add instructions about shared variables and merge answers --- .../docs/editor/blocks/logic/typebot-link.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/docs/docs/editor/blocks/logic/typebot-link.md b/apps/docs/docs/editor/blocks/logic/typebot-link.md index 7264753c71..ded6b13160 100644 --- a/apps/docs/docs/editor/blocks/logic/typebot-link.md +++ b/apps/docs/docs/editor/blocks/logic/typebot-link.md @@ -6,15 +6,14 @@ sidebar_position: 5 The typebot link logic block allows you to go into another typebot flow. This ultimately helps keep your flows clean and be able to reuse a flow in multiple places. -Link to typebot logic block +Link to typebot logic block -Variables are shared by default, you just need them to make sure they are present in the variables dropdown on both flows. +## Share variables between bots -## Link to the current typebot +The existing variable values are automatically shared to the linked bot. It means that if this linked bot contains similar variable names, it will be automatically pre-filled with the values from the previous bot. -You can also use this block to "clean" your flow of any long edges (connected arrows). For example, if you want the user to start the current bot again. Instead of connecting a gigantic arrow to the first group, you could add a "Link to typebot" block, choose "Current bot" and select the first group of your flow. +Example: My first bot asks for the user's name and stores it in the `Name` variable. Then, I link to another bot that displays a `Name` variable in a text bubble. This will display the name collected in the first bot. + +## Merge answers + +The Merge answers option allows you to merge the answers collected from a linked bot to the current bot. This is useful if you want to collect answers from multiple bots and then send them all at once to a third-party app. Or if you just want to collect all the answers into a unified results table. From 0e4e10c77b0d960ce268ebe2d25600812560e733 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Fri, 29 Sep 2023 15:10:39 +0200 Subject: [PATCH 039/233] :passport_control: (whatsapp) Remove feature flag Closes #401 --- .../preview/components/RuntimeMenu.tsx | 4 --- .../publish/components/embeds/EmbedButton.tsx | 28 +++++++++---------- .../WhatsAppCredentialsModal.tsx | 7 +++-- .../src/features/telemetry/posthog.tsx | 9 ------ 4 files changed, 18 insertions(+), 30 deletions(-) diff --git a/apps/builder/src/features/preview/components/RuntimeMenu.tsx b/apps/builder/src/features/preview/components/RuntimeMenu.tsx index 55a1e3ff9c..276b888163 100644 --- a/apps/builder/src/features/preview/components/RuntimeMenu.tsx +++ b/apps/builder/src/features/preview/components/RuntimeMenu.tsx @@ -10,7 +10,6 @@ import { Text, } from '@chakra-ui/react' import { runtimes } from '../data' -import { isWhatsAppAvailable } from '@/features/telemetry/posthog' type Runtime = (typeof runtimes)[number] @@ -37,9 +36,6 @@ export const RuntimeMenu = ({ selectedRuntime, onSelectRuntime }: Props) => { {runtimes .filter((runtime) => runtime.name !== selectedRuntime.name) - .filter((runtime) => - runtime.name === 'WhatsApp' ? isWhatsAppAvailable() : true - ) .map((runtime) => ( ) => { const { workspace } = useWorkspace() - if (isWhatsAppAvailable()) - return ( - - } - label="WhatsApp" - lockTagPlan={hasProPerks(workspace) ? undefined : 'PRO'} - modal={({ onClose, isOpen }) => ( - - )} - {...props} - /> - - ) + return ( + + } + label="WhatsApp" + lockTagPlan={hasProPerks(workspace) ? undefined : 'PRO'} + modal={({ onClose, isOpen }) => ( + + )} + {...props} + /> + + ) }, (props: Pick) => ( In your{' '} - } isExternal + size="sm" > WhatsApp Settings page - + , click on the Edit button and insert the following values: diff --git a/apps/builder/src/features/telemetry/posthog.tsx b/apps/builder/src/features/telemetry/posthog.tsx index 3be28d16e7..138c252ca7 100644 --- a/apps/builder/src/features/telemetry/posthog.tsx +++ b/apps/builder/src/features/telemetry/posthog.tsx @@ -24,13 +24,4 @@ export const identifyUser = (userId: string) => { posthog.identify(userId) } -export const isWhatsAppAvailable = () => { - if (!env.NEXT_PUBLIC_POSTHOG_KEY || !posthog) return true - const isWhatsAppEnabled = posthog.getFeatureFlag('whatsApp', { - send_event: false, - }) - if (isWhatsAppEnabled === undefined) return true - return posthog.__loaded && isWhatsAppEnabled -} - export { posthog } From 59cd79a4b85195e5d3573891f8a942c639cf2fe6 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Fri, 29 Sep 2023 16:02:10 +0200 Subject: [PATCH 040/233] :ambulance: (js) Fix dependency issue preventing user to install @typebot.io/js Closes #871 --- packages/embeds/js/package.json | 6 +++--- packages/embeds/nextjs/package.json | 2 +- packages/embeds/react/package.json | 2 +- pnpm-lock.yaml | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/embeds/js/package.json b/packages/embeds/js/package.json index 776793453f..a9ed482a46 100644 --- a/packages/embeds/js/package.json +++ b/packages/embeds/js/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/js", - "version": "0.1.32", + "version": "0.1.33", "description": "Javascript library to display typebots on your website", "type": "module", "main": "dist/index.js", @@ -16,8 +16,7 @@ "@udecode/plate-common": "^21.1.5", "eventsource-parser": "^1.0.0", "solid-element": "1.7.1", - "solid-js": "1.7.8", - "@typebot.io/bot-engine": "workspace:*" + "solid-js": "1.7.8" }, "devDependencies": { "@babel/preset-typescript": "7.22.5", @@ -29,6 +28,7 @@ "@typebot.io/env": "workspace:*", "@typebot.io/schemas": "workspace:*", "@typebot.io/tsconfig": "workspace:*", + "@typebot.io/bot-engine": "workspace:*", "autoprefixer": "10.4.14", "babel-preset-solid": "1.7.7", "clsx": "2.0.0", diff --git a/packages/embeds/nextjs/package.json b/packages/embeds/nextjs/package.json index b50ae0a5c1..927db90fea 100644 --- a/packages/embeds/nextjs/package.json +++ b/packages/embeds/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/nextjs", - "version": "0.1.32", + "version": "0.1.33", "description": "Convenient library to display typebots on your Next.js website", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/embeds/react/package.json b/packages/embeds/react/package.json index 75630065c5..a930dd28a3 100644 --- a/packages/embeds/react/package.json +++ b/packages/embeds/react/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/react", - "version": "0.1.32", + "version": "0.1.33", "description": "Convenient library to display typebots on your React app", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55821b99cf..1085cf9672 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -893,9 +893,6 @@ importers: '@stripe/stripe-js': specifier: 1.54.1 version: 1.54.1 - '@typebot.io/bot-engine': - specifier: workspace:* - version: link:../../bot-engine '@udecode/plate-common': specifier: ^21.1.5 version: 21.1.5(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-react@0.94.2)(slate@0.94.1) @@ -924,6 +921,9 @@ importers: '@rollup/plugin-typescript': specifier: 11.1.2 version: 11.1.2(rollup@3.26.2)(tslib@2.6.0)(typescript@5.1.6) + '@typebot.io/bot-engine': + specifier: workspace:* + version: link:../../bot-engine '@typebot.io/env': specifier: workspace:* version: link:../../env From f016072e3eef021eb21d32922ce3931bc7306616 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Fri, 29 Sep 2023 17:47:10 +0200 Subject: [PATCH 041/233] :children_crossing: (whatsapp) Improve how the whatsapp preview behaves (#873) ### Summary by CodeRabbit - New Feature: Updated WhatsApp logo with a new design and color scheme. - New Feature: Added a help button in the UI linking to documentation, enhancing user guidance. - New Feature: Introduced an alert message indicating that the WhatsApp integration is in beta testing. - New Feature: Implemented a button to open WhatsApp Web directly from the application, improving user convenience. - Refactor: Adjusted the retrieval of `contactPhoneNumber` in `receiveMessagePreview` function for better data structure compatibility. - Refactor: Optimized the initialization and management of the WhatsApp session in `startWhatsAppPreview`. - Refactor: Improved the `parseButtonsReply` function by refining condition checks. - Refactor: Enhanced the readability of serialized rich text in `convertRichTextToWhatsAppText` by introducing newline characters. - Bug Fix: Ensured preservation of `contact` information when resuming the WhatsApp flow in `resumeWhatsAppFlow`. --- .../src/components/logos/WhatsAppLogo.tsx | 60 +++---------------- .../WhatsAppPreviewInstructions.tsx | 36 +++++++---- .../publish/components/embeds/EmbedButton.tsx | 13 +++- .../whatsapp/receiveMessagePreview.ts | 3 +- .../features/whatsapp/startWhatsAppPreview.ts | 15 ++--- .../inputs/buttons/parseButtonsReply.ts | 13 +--- .../whatsapp/convertRichTextToWhatsAppText.ts | 2 +- .../bot-engine/whatsapp/resumeWhatsAppFlow.ts | 2 +- 8 files changed, 56 insertions(+), 88 deletions(-) diff --git a/apps/builder/src/components/logos/WhatsAppLogo.tsx b/apps/builder/src/components/logos/WhatsAppLogo.tsx index c2d1b9c319..4cd3beba6a 100644 --- a/apps/builder/src/components/logos/WhatsAppLogo.tsx +++ b/apps/builder/src/components/logos/WhatsAppLogo.tsx @@ -1,60 +1,14 @@ import { IconProps, Icon } from '@chakra-ui/react' +export const whatsAppBrandColor = '#25D366' + export const WhatsAppLogo = (props: IconProps) => ( - - - - - - + - - - - - - - - - - - ) diff --git a/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx b/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx index 87aad0d363..4067945006 100644 --- a/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx +++ b/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx @@ -20,6 +20,7 @@ import { setPhoneNumberInLocalStorage, } from '../helpers/phoneNumberFromLocalStorage' import { useEditor } from '@/features/editor/providers/EditorProvider' +import { BuoyIcon, ExternalLinkIcon } from '@/components/icons' export const WhatsAppPreviewInstructions = (props: StackProps) => { const { typebot, save } = useTypebot() @@ -70,10 +71,22 @@ export const WhatsAppPreviewInstructions = (props: StackProps) => { onSubmit={sendWhatsAppPreviewStartMessage} {...props} > + + Need help? + + - The WhatsApp integration is still experimental. -
I appreciate your bug reports 🧡 + The WhatsApp integration is still in beta test. +
+ Your bug reports are greatly appreciate 🧡
{ isDisabled={isEmpty(phoneNumber) || isMessageSent} isLoading={isSendingMessage} type="submit" + colorScheme="blue" > {hasMessageBeenSent ? 'Restart' : 'Start'} the chat )} + @@ -106,15 +129,6 @@ export const WhatsAppPreviewInstructions = (props: StackProps) => { - diff --git a/apps/builder/src/features/publish/components/embeds/EmbedButton.tsx b/apps/builder/src/features/publish/components/embeds/EmbedButton.tsx index c4067ae714..6e0f8862e4 100644 --- a/apps/builder/src/features/publish/components/embeds/EmbedButton.tsx +++ b/apps/builder/src/features/publish/components/embeds/EmbedButton.tsx @@ -37,7 +37,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 { + WhatsAppLogo, + whatsAppBrandColor, +} from '@/components/logos/WhatsAppLogo' import { WhatsAppModal } from './modals/WhatsAppModal/WhatsAppModal' import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider' import { useWorkspace } from '@/features/workspace/WorkspaceProvider' @@ -100,7 +103,13 @@ export const integrationsList = [ return ( } + logo={ + + } label="WhatsApp" lockTagPlan={hasProPerks(workspace) ? undefined : 'PRO'} modal={({ onClose, isOpen }) => ( diff --git a/apps/builder/src/features/whatsapp/receiveMessagePreview.ts b/apps/builder/src/features/whatsapp/receiveMessagePreview.ts index 54e4580a39..9ae10b17f6 100644 --- a/apps/builder/src/features/whatsapp/receiveMessagePreview.ts +++ b/apps/builder/src/features/whatsapp/receiveMessagePreview.ts @@ -31,7 +31,8 @@ export const receiveMessagePreview = publicProcedure if (isNotDefined(receivedMessage)) return { message: 'No message found' } const contactName = entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? '' - const contactPhoneNumber = '+' + receivedMessage.from + const contactPhoneNumber = + entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? '' return resumeWhatsAppFlow({ receivedMessage, sessionId: `wa-preview-${receivedMessage.from}`, diff --git a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts index 86a4c8d556..fbb49a85e5 100644 --- a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts +++ b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts @@ -98,6 +98,10 @@ export const startWhatsAppPreview = authenticatedProcedure startGroupId, }, userId: user.id, + initialSessionState: { + whatsApp: (existingSession?.state as SessionState | undefined) + ?.whatsApp, + }, }) if (canSendDirectMessagesToUser) { @@ -119,19 +123,12 @@ export const startWhatsAppPreview = authenticatedProcedure logs, session: { id: sessionId, - state: { - ...newSessionState, - currentBlock: !input ? undefined : newSessionState.currentBlock, - }, + state: newSessionState, }, }) } else { await restartSession({ - state: { - ...newSessionState, - whatsApp: (existingSession?.state as SessionState | undefined) - ?.whatsApp, - }, + state: newSessionState, id: sessionId, }) try { diff --git a/packages/bot-engine/blocks/inputs/buttons/parseButtonsReply.ts b/packages/bot-engine/blocks/inputs/buttons/parseButtonsReply.ts index 9624c12407..5dde38af2a 100644 --- a/packages/bot-engine/blocks/inputs/buttons/parseButtonsReply.ts +++ b/packages/bot-engine/blocks/inputs/buttons/parseButtonsReply.ts @@ -63,21 +63,14 @@ export const parseButtonsReply = reply: matchedItems.map((item) => item.content).join(', '), } } - if (state.whatsApp) { - const matchedItem = displayedItems.find((item) => item.id === inputValue) - if (!matchedItem) return { status: 'fail' } - return { - status: 'success', - reply: matchedItem.content ?? '', - } - } const longestItemsFirst = [...displayedItems].sort( (a, b) => (b.content?.length ?? 0) - (a.content?.length ?? 0) ) const matchedItem = longestItemsFirst.find( (item) => - item.content && - inputValue.toLowerCase().trim() === item.content.toLowerCase().trim() + item.id === inputValue || + (item.content && + inputValue.toLowerCase().trim() === item.content.toLowerCase().trim()) ) if (!matchedItem) return { status: 'fail' } return { diff --git a/packages/bot-engine/whatsapp/convertRichTextToWhatsAppText.ts b/packages/bot-engine/whatsapp/convertRichTextToWhatsAppText.ts index e2c5e1ff29..22a3015c02 100644 --- a/packages/bot-engine/whatsapp/convertRichTextToWhatsAppText.ts +++ b/packages/bot-engine/whatsapp/convertRichTextToWhatsAppText.ts @@ -6,4 +6,4 @@ export const convertRichTextToWhatsAppText = (richText: TElement[]): string => .map((chunk) => serialize(chunk)?.replaceAll('**', '*').replaceAll('&#39;', "'") ) - .join('') + .join('\n') diff --git a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts index 4b7d17e316..56885fb6db 100644 --- a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts +++ b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts @@ -67,7 +67,7 @@ export const resumeWhatsAppFlow = async ({ session && !isSessionExpired ? session.state.currentBlock ? await continueBotFlow(session.state)(messageContent) - : await startBotFlow(session.state) + : await startBotFlow({ ...session.state, whatsApp: { contact } }) : workspaceId ? await startWhatsAppSession({ incomingMessage: messageContent, From 129f5582db4e2970a7cb177ef2abb79f47159a99 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Fri, 29 Sep 2023 18:25:08 +0200 Subject: [PATCH 042/233] :pencil: Update About page content Closes #757 --- apps/landing-page/pages/about.tsx | 105 ++++++++++++++++++------------ 1 file changed, 65 insertions(+), 40 deletions(-) diff --git a/apps/landing-page/pages/about.tsx b/apps/landing-page/pages/about.tsx index baf1aa1877..77e89e4705 100644 --- a/apps/landing-page/pages/about.tsx +++ b/apps/landing-page/pages/about.tsx @@ -1,11 +1,9 @@ -import { Stack, Text, Box, Flex, Heading } from '@chakra-ui/react' +import { Stack, Text, Flex, Heading, List, ListItem } from '@chakra-ui/react' import { Header } from 'components/common/Header/Header' import { SocialMetaTags } from 'components/common/SocialMetaTags' import React from 'react' -import selfie from '../public/images/selfie.png' -import Image from 'next/image' import { Footer } from 'components/common/Footer' -import { TextLink } from 'components/common/TextLink' +import { EndCta } from 'components/Homepage/EndCta' const AboutPage = () => { return ( @@ -21,58 +19,85 @@ const AboutPage = () => { textAlign="justify" > - Typebot's story + Why Typebot? - - Typebot's team is composed of only me, Baptiste Arnaud, a - Software Engineer based in France. + I'm Baptiste, 28 years old. I'm a software product engineer. + I am passionated about great user experiences and beautiful + interfaces. - - - selfie - - - - I'm passionate about great product UX and, during the first COVID - lockdown, I decided to create my own Typeform alternative. + This is why I've started working on Typebot, 3 years ago. It is + my attempt on a great chatbot builder. + + + In France, people don't like chatbots. They always think about it + as the guard before getting the chance to talk to a human. You ask a + question to a robot and it tries to understand what you're saying + and help, but it does not a great job at this. (now, it is maybe not + that accurate since the rise of LLMs) + + But I think we undervalue the potential of chatbots. + + You chat with friends, colleagues and family on messaging platform + daily. You are used and you like this chat experience. That's why + businesses need to leverage this, it's a place where conversion + is high. + + + In an ideal world, a user should be able to chat with a human from a + company and have an instant answer. The problem is that it is + synchronous, time-consuming and it requires a huge customer support + team working 24/7. It doesn't scale at all. Waiting for an answer + from a human impacts the customer experience. - - Typebot was launched in July 2020. It is completely independent, - self-funded, and bootstrapped. At the current stage, I'm not - interested in raising funds or taking investments. + Chatbots are a solution. You can chat with your customers, at scale. - Because I love open-source SaaS, I decided in early 2022, alongside - the launch of a major 2.0 release, to open-source the project - entirely. Anyone can now read the source code and contribute to the - project. You can also self-host your own version of Typebot on your - server. + But, when built incorrectly, chatbots can be detrimental to your user + experience. Most solutions out there focus on customer support. It can + be so much more. + A great chatbot should: + + Provide a customised experience to the user + + Have a great user interface and beautiful animations + + Feel native to the business brand + Provide what the user is looking for + - With Typebot, I want to create the best bot-building experience. My - goal is to empower you as a user and help you build great user - experiences. Also, privacy comes first. While using Typebot, you - aren't tracked by some third-party analytics tool. + A chatbot is not necessarily tied to customer support. It can also do: + + Lead generation and qualification + Quizzes + Surveys + User onboarding + Product presentation + Registrations (newsletter, waiting list) + - I'm working hard on making a living from Typebot with a simple - business model:
-
You can use the tool for free but your forms will contain a - "Made with Typebot" small badge that potentially gets people - to know about the product. If you want to remove it and also have - access to other advanced features, you have to subscribe for $39 per - month. + To build that kind of chatbots, you need a tool that gives you enough + freedom to closely tie it to your business logic. The build experience + should be a reliable and fun experience. You also need a space where + you can analyse your results so that you can incrementally improve + your bots.
+ This is what Typebot provides. - If you have any questions, feel free to reach out to me at{' '} - - support@typebot.io - + I've built this tool by focusing on user empowering. Typebot is + extremely flexible and provides the building blocks to create great + chat experiences. Often times, the more freedom you give to the user, + the less intuitive the tool become. I try not to fall into that trap + with Typebot by providing the best defaults for each option. I also + try to help you learn master the tool with good templates and video + tutorials. +