diff --git a/apps/builder/components/board/graph/BlockNode/BlockNode.tsx b/apps/builder/components/board/graph/BlockNode/BlockNode.tsx index 86d200cd3..89d2e6f07 100644 --- a/apps/builder/components/board/graph/BlockNode/BlockNode.tsx +++ b/apps/builder/components/board/graph/BlockNode/BlockNode.tsx @@ -10,7 +10,7 @@ import { Block, StartBlock } from 'bot-engine' import { useGraph } from 'contexts/GraphContext' import { useDnd } from 'contexts/DndContext' import { StepsList } from './StepsList' -import { isDefined } from 'services/utils' +import { isDefined } from 'utils' import { useTypebot } from 'contexts/TypebotContext' import { ContextMenu } from 'components/shared/ContextMenu' import { BlockNodeContextMenu } from './BlockNodeContextMenu' diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx index 43c27785c..c94e40956 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx @@ -4,7 +4,7 @@ import { Block, StartStep, Step, StepType } from 'bot-engine' import { SourceEndpoint } from './SourceEndpoint' import { useGraph } from 'contexts/GraphContext' import { StepIcon } from 'components/board/StepTypesList/StepIcon' -import { isDefined } from 'services/utils' +import { isDefined } from 'utils' import { Coordinates } from '@dnd-kit/core/dist/types' import { TextEditor } from './TextEditor/TextEditor' import { StepContent } from './StepContent' diff --git a/apps/builder/components/board/preview/PreviewDrawer.tsx b/apps/builder/components/board/preview/PreviewDrawer.tsx index 45a437988..97befc31d 100644 --- a/apps/builder/components/board/preview/PreviewDrawer.tsx +++ b/apps/builder/components/board/preview/PreviewDrawer.tsx @@ -91,7 +91,7 @@ export const PreviewDrawer = () => { > )} diff --git a/apps/builder/contexts/TypebotContext.tsx b/apps/builder/contexts/TypebotContext.tsx index dd5b1818e..2e1923940 100644 --- a/apps/builder/contexts/TypebotContext.tsx +++ b/apps/builder/contexts/TypebotContext.tsx @@ -35,11 +35,11 @@ import { import { fetcher, insertItemInList, - isDefined, omit, preventUserFromRefreshing, } from 'services/utils' import useSWR from 'swr' +import { isDefined } from 'utils' import { NewBlockPayload, Coordinates } from './GraphContext' const typebotContext = createContext<{ diff --git a/apps/builder/contexts/UserContext.tsx b/apps/builder/contexts/UserContext.tsx index 5b1f749de..857ea3284 100644 --- a/apps/builder/contexts/UserContext.tsx +++ b/apps/builder/contexts/UserContext.tsx @@ -9,7 +9,7 @@ import { useMemo, useState, } from 'react' -import { isDefined } from 'services/utils' +import { isDefined } from 'utils' import { updateUser as updateUserInDb } from 'services/user' import { useToast } from '@chakra-ui/react' import { deepEqual } from 'fast-equals' diff --git a/apps/builder/next.config.js b/apps/builder/next.config.js new file mode 100644 index 000000000..f4620f088 --- /dev/null +++ b/apps/builder/next.config.js @@ -0,0 +1,6 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const withTM = require('next-transpile-modules')(['utils']) + +module.exports = withTM({ + reactStrictMode: true, +}) diff --git a/apps/builder/package.json b/apps/builder/package.json index 9b6576f1b..f2e09bddc 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -32,6 +32,7 @@ "framer-motion": "^4", "htmlparser2": "^7.2.0", "kbar": "^0.1.0-beta.24", + "micro": "^9.3.4", "micro-cors": "^0.1.1", "next": "^12.0.7", "next-auth": "beta", @@ -50,7 +51,8 @@ "styled-components": "^5.3.3", "svg-round-corners": "^0.3.0", "swr": "^1.1.1", - "use-debounce": "^7.0.1" + "use-debounce": "^7.0.1", + "utils": "*" }, "devDependencies": { "@testing-library/cypress": "^8.0.2", @@ -69,6 +71,7 @@ "eslint-config-prettier": "^8.3.0", "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-prettier": "^4.0.0", + "next-transpile-modules": "^9.0.0", "prettier": "^2.5.1", "typescript": "^4.5.4" } diff --git a/apps/builder/pages/api/folders.ts b/apps/builder/pages/api/folders.ts index 8f96b6403..1f40be4b3 100644 --- a/apps/builder/pages/api/folders.ts +++ b/apps/builder/pages/api/folders.ts @@ -2,7 +2,7 @@ import { DashboardFolder, User } from 'db' import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { getSession } from 'next-auth/react' -import { methodNotAllowed } from 'services/api/utils' +import { methodNotAllowed } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getSession({ req }) diff --git a/apps/builder/pages/api/folders/[id].ts b/apps/builder/pages/api/folders/[id].ts index 3ec40299a..4b1adeb58 100644 --- a/apps/builder/pages/api/folders/[id].ts +++ b/apps/builder/pages/api/folders/[id].ts @@ -2,7 +2,7 @@ import { DashboardFolder } from 'db' import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { getSession } from 'next-auth/react' -import { methodNotAllowed } from 'services/api/utils' +import { methodNotAllowed } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getSession({ req }) diff --git a/apps/builder/pages/api/publicTypebots.ts b/apps/builder/pages/api/publicTypebots.ts index 929699962..7d4099b8c 100644 --- a/apps/builder/pages/api/publicTypebots.ts +++ b/apps/builder/pages/api/publicTypebots.ts @@ -1,7 +1,7 @@ import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { getSession } from 'next-auth/react' -import { methodNotAllowed } from 'services/api/utils' +import { methodNotAllowed } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getSession({ req }) diff --git a/apps/builder/pages/api/publicTypebots/[id].ts b/apps/builder/pages/api/publicTypebots/[id].ts index 8788282bb..0c78bbccb 100644 --- a/apps/builder/pages/api/publicTypebots/[id].ts +++ b/apps/builder/pages/api/publicTypebots/[id].ts @@ -1,7 +1,7 @@ import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { getSession } from 'next-auth/react' -import { methodNotAllowed } from 'services/api/utils' +import { methodNotAllowed } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getSession({ req }) diff --git a/apps/builder/pages/api/storage/upload-url.ts b/apps/builder/pages/api/storage/upload-url.ts index 11a4b8c41..75510f91f 100644 --- a/apps/builder/pages/api/storage/upload-url.ts +++ b/apps/builder/pages/api/storage/upload-url.ts @@ -1,7 +1,7 @@ import aws from 'aws-sdk' import { NextApiRequest, NextApiResponse } from 'next' import { getSession } from 'next-auth/react' -import { methodNotAllowed } from 'services/api/utils' +import { methodNotAllowed } from 'utils' const maxUploadFileSize = 10485760 // 10 MB const handler = async ( diff --git a/apps/builder/pages/api/stripe/checkout.ts b/apps/builder/pages/api/stripe/checkout.ts index 968d44381..9ba111ef5 100644 --- a/apps/builder/pages/api/stripe/checkout.ts +++ b/apps/builder/pages/api/stripe/checkout.ts @@ -1,5 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next' -import { methodNotAllowed } from 'services/api/utils' +import { methodNotAllowed } from 'utils' import Stripe from 'stripe' const usdPriceIdTest = 'price_1Jc4TQKexUFvKTWyGvsH4Ff5' diff --git a/apps/builder/pages/api/stripe/customer-portal.ts b/apps/builder/pages/api/stripe/customer-portal.ts index f0288d845..19909e778 100644 --- a/apps/builder/pages/api/stripe/customer-portal.ts +++ b/apps/builder/pages/api/stripe/customer-portal.ts @@ -1,7 +1,7 @@ import { User } from 'db' import { NextApiRequest, NextApiResponse } from 'next' import { getSession } from 'next-auth/react' -import { methodNotAllowed } from 'services/api/utils' +import { methodNotAllowed } from 'utils' import Stripe from 'stripe' const createCheckoutSession = async ( diff --git a/apps/builder/pages/api/stripe/webhook.ts b/apps/builder/pages/api/stripe/webhook.ts index 6c9e8979f..a9e1a3ce7 100644 --- a/apps/builder/pages/api/stripe/webhook.ts +++ b/apps/builder/pages/api/stripe/webhook.ts @@ -1,5 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next' -import { methodNotAllowed } from 'services/api/utils' +import { methodNotAllowed } from 'utils' import Stripe from 'stripe' import Cors from 'micro-cors' import { buffer } from 'micro' diff --git a/apps/builder/pages/api/typebots.ts b/apps/builder/pages/api/typebots.ts index dcec903ba..2fae467f8 100644 --- a/apps/builder/pages/api/typebots.ts +++ b/apps/builder/pages/api/typebots.ts @@ -3,7 +3,7 @@ import { User } from 'db' import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { getSession } from 'next-auth/react' -import { methodNotAllowed } from 'services/api/utils' +import { methodNotAllowed } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getSession({ req }) diff --git a/apps/builder/pages/api/typebots/[id].ts b/apps/builder/pages/api/typebots/[id].ts index 95cc30aad..906b022cf 100644 --- a/apps/builder/pages/api/typebots/[id].ts +++ b/apps/builder/pages/api/typebots/[id].ts @@ -1,7 +1,7 @@ import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { getSession } from 'next-auth/react' -import { methodNotAllowed } from 'services/api/utils' +import { methodNotAllowed } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getSession({ req }) diff --git a/apps/builder/pages/api/users/[id].ts b/apps/builder/pages/api/users/[id].ts index 34a9b318d..4aaf4939e 100644 --- a/apps/builder/pages/api/users/[id].ts +++ b/apps/builder/pages/api/users/[id].ts @@ -1,7 +1,7 @@ import prisma from 'libs/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { getSession } from 'next-auth/react' -import { methodNotAllowed } from 'services/api/utils' +import { methodNotAllowed } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getSession({ req }) diff --git a/apps/builder/services/graph.ts b/apps/builder/services/graph.ts index 9901ba3d6..7a1606814 100644 --- a/apps/builder/services/graph.ts +++ b/apps/builder/services/graph.ts @@ -9,7 +9,7 @@ import { firstStepOffsetY, } from 'contexts/GraphContext' import { roundCorners } from 'svg-round-corners' -import { isDefined } from './utils' +import { isDefined } from 'utils' export const computeFlowChartConnectorPath = ({ sourcePosition, diff --git a/apps/builder/services/utils.ts b/apps/builder/services/utils.ts index d981d97d8..7485a80c7 100644 --- a/apps/builder/services/utils.ts +++ b/apps/builder/services/utils.ts @@ -39,10 +39,6 @@ export const insertItemInList = ( newItem: T ): T[] => [...arr.slice(0, index), newItem, ...arr.slice(index)] -export const isDefined = (value: T | undefined | null): value is T => { - return value !== undefined && value !== null -} - export const preventUserFromRefreshing = (e: BeforeUnloadEvent) => { e.preventDefault() e.returnValue = '' diff --git a/apps/viewer/layouts/ErrorPage.tsx b/apps/viewer/layouts/ErrorPage.tsx index f86270b7a..d734f4af4 100644 --- a/apps/viewer/layouts/ErrorPage.tsx +++ b/apps/viewer/layouts/ErrorPage.tsx @@ -1,15 +1,6 @@ import React from 'react' -export const ErrorPage = ({ error }: { error: 'offline' | '500' | 'IE' }) => { - let errorLabel = - 'An error occured. Please try to refresh or contact the owner of this bot.' - if (error === 'offline') { - errorLabel = - 'Looks like your device is offline. Please, try to refresh the page.' - } - if (error === 'IE') { - errorLabel = "This bot isn't compatible with Internet Explorer." - } +export const ErrorPage = ({ error }: { error: Error }) => { return (
{ flexDirection: 'column', }} > - {error === '500' && ( -

500

- )} -

{errorLabel}

+

{error.name}

+

{error.message}

) } diff --git a/apps/viewer/layouts/TypebotPage.tsx b/apps/viewer/layouts/TypebotPage.tsx index c66c431ee..aaa0430e0 100644 --- a/apps/viewer/layouts/TypebotPage.tsx +++ b/apps/viewer/layouts/TypebotPage.tsx @@ -1,6 +1,7 @@ -import { PublicTypebot, TypebotViewer } from 'bot-engine' -import React from 'react' +import { Answer, PublicTypebot, TypebotViewer } from 'bot-engine' +import React, { useEffect, useState } from 'react' import { SEO } from '../components/Seo' +import { createResult, updateResult } from '../services/result' import { ErrorPage } from './ErrorPage' import { NotFoundPage } from './NotFoundPage' @@ -10,17 +11,61 @@ export type TypebotPageProps = { isIE: boolean } +const sessionStorageKey = 'resultId' + export const TypebotPage = ({ typebot, isIE, url }: TypebotPageProps) => { + const [error, setError] = useState( + isIE ? new Error('Internet explorer is not supported') : undefined + ) + const [resultId, setResultId] = useState() + + useEffect(() => { + initializeResult() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const initializeResult = async () => { + if (!typebot) return + const resultIdFromSession = sessionStorage.getItem(sessionStorageKey) + if (resultIdFromSession) setResultId(resultIdFromSession) + else { + const { error, data: result } = await createResult(typebot.typebotId) + if (error) setError(error) + if (result) { + setResultId(result.id) + sessionStorage.setItem(sessionStorageKey, result.id) + } + } + } + + const handleAnswersUpdate = async (answers: Answer[]) => { + if (!resultId) return setError(new Error('Result was not created')) + const { error } = await updateResult(resultId, { answers }) + if (error) setError(error) + } + + const handleCompleted = async () => { + if (!resultId) return setError(new Error('Result was not created')) + const { error } = await updateResult(resultId, { isCompleted: true }) + if (error) setError(error) + } + if (!typebot) { return } - if (isIE) { - return + if (error) { + return } return (
- + {resultId && ( + + )}
) } diff --git a/apps/viewer/next.config.js b/apps/viewer/next.config.js new file mode 100644 index 000000000..f4620f088 --- /dev/null +++ b/apps/viewer/next.config.js @@ -0,0 +1,6 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const withTM = require('next-transpile-modules')(['utils']) + +module.exports = withTM({ + reactStrictMode: true, +}) diff --git a/apps/viewer/package.json b/apps/viewer/package.json index 8ca8320ec..1a6de1fe5 100644 --- a/apps/viewer/package.json +++ b/apps/viewer/package.json @@ -13,7 +13,8 @@ "db": "*", "next": "^12.0.7", "react": "^17.0.2", - "react-dom": "^17.0.2" + "react-dom": "^17.0.2", + "utils": "*" }, "devDependencies": { "@types/node": "^17.0.4", @@ -24,6 +25,7 @@ "eslint-config-prettier": "^8.3.0", "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-prettier": "^4.0.0", + "next-transpile-modules": "^9.0.0", "prettier": "^2.5.1", "typescript": "^4.5.4" } diff --git a/apps/viewer/pages/[publicId].tsx b/apps/viewer/pages/[publicId].tsx index 3d4f943e0..f8ddfc994 100644 --- a/apps/viewer/pages/[publicId].tsx +++ b/apps/viewer/pages/[publicId].tsx @@ -11,7 +11,7 @@ export const getServerSideProps: GetServerSideProps = async ( const pathname = context.resolvedUrl.split('?')[0] try { if (!context.req.headers.host) return { props: {} } - typebot = await getTypebotFromPublicId(context.query.publicId.toString()) + typebot = await getTypebotFromPublicId(context.query.publicId?.toString()) if (!typebot) return { props: {} } return { props: { @@ -32,8 +32,9 @@ export const getServerSideProps: GetServerSideProps = async ( } const getTypebotFromPublicId = async ( - publicId: string + publicId?: string ): Promise => { + if (!publicId) return const typebot = await prisma.publicTypebot.findUnique({ where: { publicId }, }) diff --git a/apps/viewer/pages/api/results.ts b/apps/viewer/pages/api/results.ts new file mode 100644 index 000000000..80192340a --- /dev/null +++ b/apps/viewer/pages/api/results.ts @@ -0,0 +1,16 @@ +import prisma from 'libs/prisma' +import { NextApiRequest, NextApiResponse } from 'next' +import { methodNotAllowed } from 'utils' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'POST') { + const { typebotId } = JSON.parse(req.body) + const result = await prisma.result.create({ + data: { typebotId }, + }) + return res.send(result) + } + return methodNotAllowed(res) +} + +export default handler diff --git a/apps/viewer/pages/api/results/[id].ts b/apps/viewer/pages/api/results/[id].ts new file mode 100644 index 000000000..0c8664e5e --- /dev/null +++ b/apps/viewer/pages/api/results/[id].ts @@ -0,0 +1,18 @@ +import prisma from 'libs/prisma' +import { NextApiRequest, NextApiResponse } from 'next' +import { methodNotAllowed } from 'utils' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'PATCH') { + const data = JSON.parse(req.body) + const id = req.query.id.toString() + const result = await prisma.result.update({ + where: { id }, + data: { ...data, updatedAt: new Date() }, + }) + return res.send(result) + } + return methodNotAllowed(res) +} + +export default handler diff --git a/apps/viewer/services/result.ts b/apps/viewer/services/result.ts new file mode 100644 index 000000000..864a47959 --- /dev/null +++ b/apps/viewer/services/result.ts @@ -0,0 +1,21 @@ +import { Result } from 'db' +import { sendRequest } from 'utils' + +export const createResult = async (typebotId: string) => { + return sendRequest({ + url: `/api/results`, + method: 'POST', + body: { typebotId }, + }) +} + +export const updateResult = async ( + resultId: string, + result: Partial +) => { + return sendRequest({ + url: `/api/results/${resultId}`, + method: 'PATCH', + body: result, + }) +} diff --git a/apps/viewer/tsconfig.json b/apps/viewer/tsconfig.json index b29b6b742..53c073685 100644 --- a/apps/viewer/tsconfig.json +++ b/apps/viewer/tsconfig.json @@ -4,7 +4,7 @@ "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, - "strict": false, + "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "incremental": true, @@ -14,8 +14,9 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", + "baseUrl": ".", "composite": true }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "cypress"] } diff --git a/packages/bot-engine/package.json b/packages/bot-engine/package.json index cf39d48e5..cb6ecc68c 100644 --- a/packages/bot-engine/package.json +++ b/packages/bot-engine/package.json @@ -7,6 +7,7 @@ "types": "dist/index.d.ts", "dependencies": { "db": "*", + "fast-equals": "^2.0.4", "react-frame-component": "^5.2.1", "react-scroll": "^1.8.4", "react-transition-group": "^4.4.2" @@ -15,18 +16,18 @@ "@rollup/plugin-commonjs": "^21.0.1", "@rollup/plugin-node-resolve": "^13.1.1", "@rollup/plugin-typescript": "^8.3.0", - "@types/react": "^17.0.37", + "@types/react": "^17.0.38", "@types/react-scroll": "^1.8.3", "@types/react-transition-group": "^4.4.4", "autoprefixer": "^10.4.0", "npm-run-all": "^4.1.5", "postcss": "^8.4.5", - "rollup": "^2.61.1", - "rollup-plugin-dts": "^4.0.1", + "rollup": "^2.62.0", + "rollup-plugin-dts": "^4.1.0", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-terser": "^7.0.2", - "tailwindcss": "^3.0.7", + "tailwindcss": "^3.0.8", "typescript": "^4.5.4" }, "peerDependencies": { diff --git a/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx b/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx index 9e78fe642..87a64b62f 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx @@ -8,7 +8,7 @@ import { HostAvatarsContext } from '../../contexts/HostAvatarsContext' type ChatBlockProps = { block: Block - onBlockEnd: (nextBlockId: string) => void + onBlockEnd: (nextBlockId?: string) => void } export const ChatBlock = ({ block, onBlockEnd }: ChatBlockProps) => { @@ -31,7 +31,10 @@ export const ChatBlock = ({ block, onBlockEnd }: ChatBlockProps) => { const displayNextStep = () => { const currentStep = [...displayedSteps].pop() - if (currentStep?.target?.blockId) + if ( + currentStep?.target?.blockId || + displayedSteps.length === block.steps.length + ) return onBlockEnd(currentStep?.target?.blockId) const nextStep = block.steps[displayedSteps.length] if (nextStep) setDisplayedSteps([...displayedSteps, nextStep]) diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx index 4b00022ff..5623bbfa3 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react' +import { useAnswers } from '../../../contexts/AnswersContext' import { useHostAvatars } from '../../../contexts/HostAvatarsContext' import { Step } from '../../../models' import { isTextInputStep, isTextStep } from '../../../services/utils' @@ -13,13 +14,21 @@ export const ChatStep = ({ step: Step onTransitionEnd: () => void }) => { + const { addAnswer } = useAnswers() + + const handleInputSubmit = (content: string) => { + addAnswer({ stepId: step.id, blockId: step.blockId, content }) + onTransitionEnd() + } + if (isTextStep(step)) return - if (isTextInputStep(step)) return + if (isTextInputStep(step)) + return return No step } -const InputChatStep = ({ onSubmit }: { onSubmit: () => void }) => { +const InputChatStep = ({ onSubmit }: { onSubmit: (value: string) => void }) => { const { addNewAvatarOffset } = useHostAvatars() const [answer, setAnswer] = useState() @@ -29,7 +38,7 @@ const InputChatStep = ({ onSubmit }: { onSubmit: () => void }) => { const handleSubmit = (value: string) => { setAnswer(value) - onSubmit() + onSubmit(value) } if (answer) { diff --git a/packages/bot-engine/src/components/ConversationContainer.tsx b/packages/bot-engine/src/components/ConversationContainer.tsx index 0a8620eca..ee2ebeddd 100644 --- a/packages/bot-engine/src/components/ConversationContainer.tsx +++ b/packages/bot-engine/src/components/ConversationContainer.tsx @@ -1,28 +1,36 @@ import React, { useEffect, useRef, useState } from 'react' -import { PublicTypebot } from '..' +import { Answer, PublicTypebot } from '..' import { Block } from '..' import { ChatBlock } from './ChatBlock/ChatBlock' import { useFrame } from 'react-frame-component' import { setCssVariablesValue } from '../services/theme' +import { useAnswers } from '../contexts/AnswersContext' +import { deepEqual } from 'fast-equals' +type Props = { + typebot: PublicTypebot + onNewBlockVisible: (blockId: string) => void + onAnswersUpdate: (answers: Answer[]) => void + onCompleted: () => void +} export const ConversationContainer = ({ typebot, - onNewBlockVisisble, -}: { - typebot: PublicTypebot - onNewBlockVisisble: (blockId: string) => void -}) => { + onNewBlockVisible, + onAnswersUpdate, + onCompleted, +}: Props) => { const { document: frameDocument } = useFrame() const [displayedBlocks, setDisplayedBlocks] = useState([]) - - const [isConversationEnded, setIsConversationEnded] = useState(false) + const [localAnswers, setLocalAnswers] = useState([]) + const { answers } = useAnswers() const bottomAnchor = useRef(null) - const displayNextBlock = (blockId: string) => { + const displayNextBlock = (blockId?: string) => { + if (!blockId) return onCompleted() const nextBlock = typebot.blocks.find((b) => b.id === blockId) - if (!nextBlock) return - onNewBlockVisisble(blockId) + if (!nextBlock) return onCompleted() + onNewBlockVisible(blockId) setDisplayedBlocks([...displayedBlocks, nextBlock]) } @@ -35,6 +43,12 @@ export const ConversationContainer = ({ setCssVariablesValue(typebot.theme, frameDocument.body.style) }, [typebot.theme, frameDocument]) + useEffect(() => { + if (deepEqual(localAnswers, answers)) return + setLocalAnswers(answers) + onAnswersUpdate(answers) + }, [answers]) + return (
))} {/* We use a block to simulate padding because it makes iOS scroll flicker */} -
+
) } diff --git a/packages/bot-engine/src/components/TypebotViewer.tsx b/packages/bot-engine/src/components/TypebotViewer.tsx index dabc067b9..7da9b0c9d 100644 --- a/packages/bot-engine/src/components/TypebotViewer.tsx +++ b/packages/bot-engine/src/components/TypebotViewer.tsx @@ -1,19 +1,23 @@ import React, { useMemo } from 'react' -import { BackgroundType, PublicTypebot } from '../models' +import { Answer, BackgroundType, PublicTypebot } from '../models' import { TypebotContext } from '../contexts/TypebotContext' import Frame from 'react-frame-component' //@ts-ignore import style from '../assets/style.css' import { ConversationContainer } from './ConversationContainer' -import { ResultContext } from '../contexts/ResultsContext' +import { AnswersContext } from '../contexts/AnswersContext' export type TypebotViewerProps = { typebot: PublicTypebot - onNewBlockVisisble?: (blockId: string) => void + onNewBlockVisible?: (blockId: string) => void + onAnswersUpdate?: (answers: Answer[]) => void + onCompleted?: () => void } export const TypebotViewer = ({ typebot, - onNewBlockVisisble, + onNewBlockVisible, + onAnswersUpdate, + onCompleted, }: TypebotViewerProps) => { const containerBgColor = useMemo( () => @@ -23,7 +27,13 @@ export const TypebotViewer = ({ [typebot.theme.general.background] ) const handleNewBlockVisible = (blockId: string) => { - if (onNewBlockVisisble) onNewBlockVisisble(blockId) + if (onNewBlockVisible) onNewBlockVisible(blockId) + } + const handleAnswersUpdate = (answers: Answer[]) => { + if (onAnswersUpdate) onAnswersUpdate(answers) + } + const handleCompleted = () => { + if (onCompleted) onCompleted() } return ( @@ -38,22 +48,24 @@ export const TypebotViewer = ({ }} /> - +
-
+
) diff --git a/packages/bot-engine/src/contexts/AnswersContext.tsx b/packages/bot-engine/src/contexts/AnswersContext.tsx new file mode 100644 index 000000000..90d3fc646 --- /dev/null +++ b/packages/bot-engine/src/contexts/AnswersContext.tsx @@ -0,0 +1,34 @@ +import { Answer } from '../models' +import React, { createContext, ReactNode, useContext, useState } from 'react' + +const answersContext = createContext<{ + answers: Answer[] + addAnswer: (answer: Answer) => void + //@ts-ignore +}>({}) + +export const AnswersContext = ({ + children, + typebotId, +}: { + children: ReactNode + typebotId: string +}) => { + const [answers, setAnswers] = useState([]) + + const addAnswer = (answer: Answer) => + setAnswers((answers) => [...answers, answer]) + + return ( + + {children} + + ) +} + +export const useAnswers = () => useContext(answersContext) diff --git a/packages/bot-engine/src/contexts/ResultsContext.tsx b/packages/bot-engine/src/contexts/ResultsContext.tsx deleted file mode 100644 index 3a3f6c663..000000000 --- a/packages/bot-engine/src/contexts/ResultsContext.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Answer, Result } from '../models' -import React, { - createContext, - Dispatch, - ReactNode, - SetStateAction, - useContext, - useState, -} from 'react' - -const resultContext = createContext<{ - result: Result - setResult: Dispatch> - addAnswer: (answer: Answer) => void - //@ts-ignore -}>({}) - -export const ResultContext = ({ - children, - typebotId, -}: { - children: ReactNode - typebotId: string -}) => { - const [result, setResult] = useState({ - id: 'tmp', - createdAt: new Date(), - updatedAt: new Date(), - answers: [], - typebotId, - isCompleted: false, - }) - - const addAnswer = (answer: Answer) => - setResult({ ...result, answers: [...result.answers, answer] }) - - return ( - - {children} - - ) -} - -export const useResult = () => useContext(resultContext) diff --git a/apps/builder/services/api/utils.ts b/packages/utils/apiUtils.ts similarity index 100% rename from apps/builder/services/api/utils.ts rename to packages/utils/apiUtils.ts diff --git a/packages/utils/index.ts b/packages/utils/index.ts new file mode 100644 index 000000000..e5c865a06 --- /dev/null +++ b/packages/utils/index.ts @@ -0,0 +1,2 @@ +export * from './utils' +export * from './apiUtils' diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 000000000..c5f40cc33 --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,13 @@ +{ + "name": "utils", + "version": "1.0.0", + "main": "index.ts", + "license": "AGPL-3.0-or-later", + "private": true, + "devDependencies": { + "typescript": "^4.5.4" + }, + "dependencies": { + "next": "^12.0.7" + } +} diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 000000000..3ffa57bad --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/packages/utils/utils.ts b/packages/utils/utils.ts new file mode 100644 index 000000000..91565d808 --- /dev/null +++ b/packages/utils/utils.ts @@ -0,0 +1,27 @@ +export const sendRequest = async ({ + url, + method, + body, +}: { + url: string + method: string + body?: Record +}): Promise<{ data?: ResponseData; error?: Error }> => { + try { + const response = await fetch(url, { + method, + mode: 'cors', + body: body ? JSON.stringify(body) : undefined, + }) + if (!response.ok) throw new Error(response.statusText) + const data = await response.json() + return { data } + } catch (e) { + console.error(e) + return { error: e as Error } + } +} + +export const isDefined = (value: T | undefined | null): value is T => { + return value !== undefined && value !== null +} diff --git a/yarn.lock b/yarn.lock index 8af517cd9..bb8b046d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1903,6 +1903,11 @@ arch@^2.2.0: resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== +arg@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0" + integrity sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg== + arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -2309,6 +2314,11 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + bytes@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" @@ -2588,6 +2598,11 @@ constants-browserify@1.0.0: resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= +content-type@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + convert-source-map@1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -2999,6 +3014,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +depd@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" + integrity sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k= + depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -3204,6 +3224,14 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +enhanced-resolve@^5.7.0: + version "5.8.3" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0" + integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + enquirer@^2.3.5, enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" @@ -3986,7 +4014,7 @@ globby@^11.0.4: merge2 "^1.3.0" slash "^3.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.8" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== @@ -4083,6 +4111,16 @@ htmlparser2@^7.2.0: domutils "^2.8.0" entities "^3.0.1" +http-errors@1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" + integrity sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY= + dependencies: + depd "1.1.1" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + http-errors@1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" @@ -4126,6 +4164,11 @@ hyphenate-style-name@^1.0.2: resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== +iconv-lite@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + integrity sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ== + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -4232,6 +4275,11 @@ inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, i resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + ini@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" @@ -4418,6 +4466,11 @@ is-shared-array-buffer@^1.0.1: resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== +is-stream@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -4862,6 +4915,16 @@ micro-cors@^0.1.1: resolved "https://registry.yarnpkg.com/micro-cors/-/micro-cors-0.1.1.tgz#af7a480182c114ffd1ada84ad9dffc52bb4f4054" integrity sha512-6WqIahA5sbQR1Gjexp1VuWGFDKbZZleJb/gy1khNGk18a6iN1FdTcr3Q8twaxkV5H94RjxIBjirYbWCehpMBFw== +micro@^9.3.4: + version "9.3.4" + resolved "https://registry.yarnpkg.com/micro/-/micro-9.3.4.tgz#745a494e53c8916f64fb6a729f8cbf2a506b35ad" + integrity sha512-smz9naZwTG7qaFnEZ2vn248YZq9XR+XoOH3auieZbkhDL4xLOxiE+KqG8qqnBeKfXA9c1uEFGCxPN1D+nT6N7w== + dependencies: + arg "4.1.0" + content-type "1.0.4" + is-stream "1.1.0" + raw-body "2.3.2" + micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" @@ -4983,6 +5046,14 @@ next-auth@beta: preact-render-to-string "^5.1.19" uuid "^8.3.2" +next-transpile-modules@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/next-transpile-modules/-/next-transpile-modules-9.0.0.tgz#133b1742af082e61cc76b02a0f12ffd40ce2bf90" + integrity sha512-VCNFOazIAnXn1hvgYYSTYMnoWgKgwlYh4lm1pKbSfiB3kj5ZYLcKVhfh3jkPOg1cnd9DP+pte9yCUocdPEUBTQ== + dependencies: + enhanced-resolve "^5.7.0" + escalade "^3.1.1" + next@^12.0.7: version "12.0.7" resolved "https://registry.yarnpkg.com/next/-/next-12.0.7.tgz#33ebf229b81b06e583ab5ae7613cffe1ca2103fc" @@ -6029,6 +6100,16 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" +raw-body@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" + integrity sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k= + dependencies: + bytes "3.0.0" + http-errors "1.6.2" + iconv-lite "0.4.19" + unpipe "1.0.0" + raw-body@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" @@ -6347,7 +6428,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rollup-plugin-dts@^4.0.1: +rollup-plugin-dts@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/rollup-plugin-dts/-/rollup-plugin-dts-4.1.0.tgz#63b1e7de3970bb6d50877e60df2150a3892bc49c" integrity sha512-rriXIm3jdUiYeiAAd1Fv+x2AxK6Kq6IybB2Z/IdoAW95fb4uRUurYsEYKa8L1seedezDeJhy8cfo8FEL9aZzqg== @@ -6397,7 +6478,7 @@ rollup-pluginutils@^2.8.2: dependencies: estree-walker "^0.6.1" -rollup@^2.61.1: +rollup@^2.62.0: version "2.62.0" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.62.0.tgz#9e640b419fc5b9e0241844f6d55258bd79986ecc" integrity sha512-cJEQq2gwB0GWMD3rYImefQTSjrPYaC6s4J9pYqnstVLJ1CHa/aZNVkD4Epuvg4iLeMA4KRiq7UM7awKK6j7jcw== @@ -6509,6 +6590,11 @@ setimmediate@^1.0.4: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + integrity sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ= + setprototypeof@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" @@ -6778,7 +6864,7 @@ stacktrace-parser@0.1.10: dependencies: type-fest "^0.7.1" -"statuses@>= 1.5.0 < 2": +"statuses@>= 1.3.1 < 2", "statuses@>= 1.5.0 < 2": version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= @@ -7028,10 +7114,10 @@ table@^6.0.9: string-width "^4.2.3" strip-ansi "^6.0.1" -tailwindcss@^3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.7.tgz#15936881f042a7eb8d6f2b6a454bac9f51181bbd" - integrity sha512-rZdKNHtC64jcQncLoWOuCzj4lQDTAgLtgK3WmQS88tTdpHh9OwLqULTQxI3tw9AMJsqSpCKlmcjW/8CSnni6zQ== +tailwindcss@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.8.tgz#6c751c3d2ee8e1fa18b108303b73f44a5e868992" + integrity sha512-Yww1eRYO1AxITJmW/KduZPxNvYdHuedeKwPju9Oakp7MdiixRi5xkpLhirsc81QCxHL0eoce6qKmxXwYGt4Cjw== dependencies: arg "^5.0.1" chalk "^4.1.2" @@ -7055,6 +7141,11 @@ tailwindcss@^3.0.7: resolve "^1.20.0" tmp "^0.2.1" +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + terser@^5.0.0: version "5.10.0" resolved "https://registry.yarnpkg.com/terser/-/terser-5.10.0.tgz#b86390809c0389105eb0a0b62397563096ddafcc"