diff --git a/CHANGELOG.md b/CHANGELOG.md index ccfea8d919..0a1991a8de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,31 @@ All notable changes to Botonic will be documented in this file. +## [0.26.0-beta.0] - 2024-04-16 + +### Added + +- [@botonic/plugin-flow-builder](https://www.npmjs.com/package/@botonic/plugin-flow-builder) + + - [Add `WebviewContentsContext` and `useWebviewContents`](https://github.com/hubtype/botonic/pull/2810) + +- [@botonic/react](https://www.npmjs.com/package/@botonic/react) + - [Add `closeWebview` in browser API](https://github.com/hubtype/botonic/pull/2810) + +### Changed + +- [@botonic/plugin-flow-builder](https://www.npmjs.com/package/@botonic/plugin-flow-builder) + + - [Remove `flowUrl` and add `apiUrl`, `jsonVersion` in BotonicPluginFlowBuilderOptions](https://github.com/hubtype/botonic/pull/2810) + +- [@botonic/react](https://www.npmjs.com/package/@botonic/react) + - [Remove `RequestContext` and create `WebviewRequestContext` for webview](https://github.com/hubtype/botonic/pull/2810) + +### Fixed + +- [@botonic/core](https://www.npmjs.com/package/@botonic/core) + - [Add `organization_id` in Session](https://github.com/hubtype/botonic/pull/2810) + ## [0.25.0] - 2024-03-27 **NOTE**: [Required version has been updated to be run with Node 20 and npm 10](https://github.com/hubtype/botonic/pull/2780). diff --git a/packages/botonic-core/package.json b/packages/botonic-core/package.json index 998347da66..0be4ec6c5a 100644 --- a/packages/botonic-core/package.json +++ b/packages/botonic-core/package.json @@ -1,6 +1,6 @@ { "name": "@botonic/core", - "version": "0.25.0", + "version": "0.26.0-beta.0", "license": "MIT", "description": "Build Chatbots using React", "main": "./lib/cjs/index.js", diff --git a/packages/botonic-core/src/models/legacy-types.ts b/packages/botonic-core/src/models/legacy-types.ts index 58c05e3411..78ab574e3c 100644 --- a/packages/botonic-core/src/models/legacy-types.ts +++ b/packages/botonic-core/src/models/legacy-types.ts @@ -170,7 +170,8 @@ export interface Session { __retries: number is_first_interaction: boolean last_session?: any - organization?: string + organization: string + organization_id: string user: SessionUser // after handoff _hubtype_case_status?: CaseStatusType diff --git a/packages/botonic-core/tests/helpers/routing.ts b/packages/botonic-core/tests/helpers/routing.ts index bf6ada0b8b..7817eeb148 100644 --- a/packages/botonic-core/tests/helpers/routing.ts +++ b/packages/botonic-core/tests/helpers/routing.ts @@ -9,6 +9,8 @@ export function testSession(): Session { user: { id: 'userid', provider: PROVIDER.DEV }, bot: { id: 'bot_id' }, is_first_interaction: true, + organization: 'test_org', + organization_id: '1234567890', __retries: 0, } } diff --git a/packages/botonic-plugin-flow-builder/package.json b/packages/botonic-plugin-flow-builder/package.json index 6001b9c14e..4af1828711 100644 --- a/packages/botonic-plugin-flow-builder/package.json +++ b/packages/botonic-plugin-flow-builder/package.json @@ -1,6 +1,6 @@ { "name": "@botonic/plugin-flow-builder", - "version": "0.25.0", + "version": "0.26.0-beta.0", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.js", "description": "Use Flow Builder to show your contents", @@ -14,7 +14,7 @@ "lint_core": "../../node_modules/.bin/eslint_d --cache --quiet 'src/**/*.ts*'" }, "dependencies": { - "@botonic/react": "^0.25.0", + "@botonic/react": "^0.26.0-beta.0", "axios": "^1.6.8", "uuid": "^9.0.1" }, diff --git a/packages/botonic-plugin-flow-builder/src/constants.ts b/packages/botonic-plugin-flow-builder/src/constants.ts index af94804695..40ee626699 100644 --- a/packages/botonic-plugin-flow-builder/src/constants.ts +++ b/packages/botonic-plugin-flow-builder/src/constants.ts @@ -1,3 +1,5 @@ +export const FLOW_BUILDER_API_URL_PROD = + 'https://api.ent0.flowbuilder.prod.hubtype.com' export const SEPARATOR = '|' export const SOURCE_INFO_SEPARATOR = `${SEPARATOR}source_` export const VARIABLE_PATTERN = /{([^}]+)}/g diff --git a/packages/botonic-plugin-flow-builder/src/index.ts b/packages/botonic-plugin-flow-builder/src/index.ts index 24bf1766a0..7ff3c474af 100644 --- a/packages/botonic-plugin-flow-builder/src/index.ts +++ b/packages/botonic-plugin-flow-builder/src/index.ts @@ -2,7 +2,11 @@ import { Plugin, PluginPreRequest, Session } from '@botonic/core' import { ActionRequest } from '@botonic/react' import { FlowBuilderApi } from './api' -import { SEPARATOR, SOURCE_INFO_SEPARATOR } from './constants' +import { + FLOW_BUILDER_API_URL_PROD, + SEPARATOR, + SOURCE_INFO_SEPARATOR, +} from './constants' import { FlowCarousel, FlowContent, @@ -24,6 +28,7 @@ import { import { DEFAULT_FUNCTIONS } from './functions' import { BotonicPluginFlowBuilderOptions, + FlowBuilderJSONVersion, KnowledgeBaseResponse, PayloadParamsBase, } from './types' @@ -48,7 +53,9 @@ export default class BotonicPluginFlowBuilder implements Plugin { ) => Promise constructor(readonly options: BotonicPluginFlowBuilderOptions) { - this.flowUrl = options.flowUrl + const apiUrl = options.apiUrl || FLOW_BUILDER_API_URL_PROD + const jsonVersion = options.jsonVersion || FlowBuilderJSONVersion.LATEST + this.flowUrl = `${apiUrl}/flow/${jsonVersion}` this.flow = options.flow this.getLocale = options.getLocale this.getAccessToken = resolveGetAccessToken(options) @@ -229,4 +236,9 @@ export default class BotonicPluginFlowBuilder implements Plugin { export * from './action' export * from './content-fields' -export { BotonicPluginFlowBuilderOptions, PayloadParamsBase } from './types' +export { + BotonicPluginFlowBuilderOptions, + FlowBuilderJSONVersion, + PayloadParamsBase, +} from './types' +export * from './webview' diff --git a/packages/botonic-plugin-flow-builder/src/types.ts b/packages/botonic-plugin-flow-builder/src/types.ts index 47e1a5d950..9c552a504b 100644 --- a/packages/botonic-plugin-flow-builder/src/types.ts +++ b/packages/botonic-plugin-flow-builder/src/types.ts @@ -4,7 +4,8 @@ import { ActionRequest } from '@botonic/react' import { HtFlowBuilderData } from './content-fields/hubtype-fields' export interface BotonicPluginFlowBuilderOptions { - flowUrl: string + apiUrl?: string + jsonVersion?: FlowBuilderJSONVersion flow?: HtFlowBuilderData customFunctions?: Record getLocale: (session: Session) => string @@ -31,6 +32,11 @@ export enum ProcessEnvNodeEnvs { DEVELOPMENT = 'development', } +export enum FlowBuilderJSONVersion { + DRAFT = 'draft', + LATEST = 'latest', +} + export interface KnowledgeBaseResponse { answer: string hasKnowledge: boolean diff --git a/packages/botonic-plugin-flow-builder/src/webview/contents-context.ts b/packages/botonic-plugin-flow-builder/src/webview/contents-context.ts new file mode 100644 index 0000000000..4b81210beb --- /dev/null +++ b/packages/botonic-plugin-flow-builder/src/webview/contents-context.ts @@ -0,0 +1,11 @@ +import { createContext } from 'react' + +import { WebviewContentsContextType } from './types' + +export const WebviewContentsContext = createContext( + { + getTextContent: () => undefined, + getImageSrc: () => undefined, + setCurrentLocale: () => undefined, + } +) diff --git a/packages/botonic-plugin-flow-builder/src/webview/index.ts b/packages/botonic-plugin-flow-builder/src/webview/index.ts new file mode 100644 index 0000000000..a86f3ed76d --- /dev/null +++ b/packages/botonic-plugin-flow-builder/src/webview/index.ts @@ -0,0 +1,3 @@ +export { WebviewContentsContext } from './contents-context' +export * from './types' +export { useWebviewContents } from './use-webview-contents' diff --git a/packages/botonic-plugin-flow-builder/src/webview/types.ts b/packages/botonic-plugin-flow-builder/src/webview/types.ts new file mode 100644 index 0000000000..c1972307ef --- /dev/null +++ b/packages/botonic-plugin-flow-builder/src/webview/types.ts @@ -0,0 +1,47 @@ +import { FlowBuilderJSONVersion } from '../types' + +export enum WebviewContentType { + TEXT = 'webview-text', + IMAGE = 'webview-image', +} + +export interface WebviewContentsResponse { + webview_contents: (WebviewTextContent | WebviewImageContent)[] +} + +export interface WebviewTextContent { + code: string + type: WebviewContentType.TEXT + content: { + text: { message: string; locale: string }[] + } +} + +export interface WebviewImageContent { + code: string + type: WebviewContentType.IMAGE + content: { + image: { file: string; locale: string }[] + } +} + +export interface UseWebviewContentsProps { + apiUrl?: string + version?: FlowBuilderJSONVersion + orgId: string + botId: string + webviewId: string + locale: string +} + +export interface UseWebviewContents { + isLoading: boolean + error: boolean + webviewContentsContext: WebviewContentsContextType +} + +export interface WebviewContentsContextType { + getTextContent: (code: string) => string | undefined + getImageSrc: (code: string) => string | undefined + setCurrentLocale: (locale: string) => void +} diff --git a/packages/botonic-plugin-flow-builder/src/webview/use-webview-contents.ts b/packages/botonic-plugin-flow-builder/src/webview/use-webview-contents.ts new file mode 100644 index 0000000000..455cbac3d4 --- /dev/null +++ b/packages/botonic-plugin-flow-builder/src/webview/use-webview-contents.ts @@ -0,0 +1,78 @@ +import axios from 'axios' +import { useEffect, useState } from 'react' + +import { FLOW_BUILDER_API_URL_PROD } from '../constants' +import { FlowBuilderJSONVersion } from '../types' +import { + UseWebviewContents, + UseWebviewContentsProps, + WebviewContentsResponse, + WebviewContentType, + WebviewImageContent, + WebviewTextContent, +} from './types' + +export function useWebviewContents({ + apiUrl = FLOW_BUILDER_API_URL_PROD, + version = FlowBuilderJSONVersion.LATEST, + orgId, + botId, + webviewId, + locale, +}: UseWebviewContentsProps): UseWebviewContents { + const [textContents, setTextContents] = useState() + const [imageContents, setImageContents] = useState() + const [currentLocale, setCurrentLocale] = useState(locale) + const [isLoading, setLoading] = useState(false) + const [error, setError] = useState(false) + + useEffect(() => { + const getResponseContents = async () => { + setLoading(true) + const url = `${apiUrl}/webview/${version}` + try { + const response = await axios.get(url, { + params: { org: orgId, bot: botId, webview: webviewId }, + }) + + const textResponseContents = response.data.webview_contents.filter( + webviewContent => webviewContent.type === WebviewContentType.TEXT + ) as WebviewTextContent[] + setTextContents(textResponseContents) + + const imageResponseContents = response.data.webview_contents.filter( + webviewContent => webviewContent.type === WebviewContentType.IMAGE + ) as WebviewImageContent[] + setImageContents(imageResponseContents) + } catch (error) { + console.error('Error fetching webview contents:', error) + setError(true) + } finally { + setLoading(false) + } + } + getResponseContents() + }, []) + + const getTextContent = (contentID: string): string | undefined => { + return textContents + ?.find(textContent => textContent.code === contentID) + ?.content.text.find(text => text.locale === currentLocale)?.message + } + + const getImageSrc = (contentID: string): string | undefined => { + return imageContents + ?.find(imageContent => imageContent.code === contentID) + ?.content.image.find(image => image.locale === currentLocale)?.file + } + + return { + isLoading, + error, + webviewContentsContext: { + getTextContent, + getImageSrc, + setCurrentLocale, + }, + } +} diff --git a/packages/botonic-plugin-flow-builder/tsconfig.base.json b/packages/botonic-plugin-flow-builder/tsconfig.base.json deleted file mode 100644 index 27de99656c..0000000000 --- a/packages/botonic-plugin-flow-builder/tsconfig.base.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "es2017", - "module": "commonjs", - "declaration": true, - "sourceMap": true, - "strict": true, - "noUnusedLocals": false, - "noUnusedParameters": true, - "noImplicitReturns": false, - "noFallthroughCasesInSwitch": true, - "forceConsistentCasingInFileNames": true, - "strictPropertyInitialization": false, - "importHelpers": true, - "noImplicitAny": false, - "outDir": "./lib", - "pretty": true - } -} diff --git a/packages/botonic-react/package.json b/packages/botonic-react/package.json index 22d528d9ee..3518389088 100644 --- a/packages/botonic-react/package.json +++ b/packages/botonic-react/package.json @@ -1,6 +1,6 @@ { "name": "@botonic/react", - "version": "0.25.0", + "version": "0.26.0-beta.0", "license": "MIT", "description": "Build Chatbots using React", "main": "./lib/cjs", @@ -20,7 +20,7 @@ "lint_core": "../../node_modules/.bin/eslint_d --cache --quiet '.*.js' '*.js' 'src/**/*.js*' --fix" }, "dependencies": { - "@botonic/core": "^0.25.0", + "@botonic/core": "^0.26.0-beta.0", "axios": "^1.6.8", "emoji-picker-react": "^4.4.9", "framer-motion": "^3.1.1", diff --git a/packages/botonic-react/src/components/message/styles.ts b/packages/botonic-react/src/components/message/styles.ts index a82c727bea..5b8d26597a 100644 --- a/packages/botonic-react/src/components/message/styles.ts +++ b/packages/botonic-react/src/components/message/styles.ts @@ -2,7 +2,11 @@ import styled from 'styled-components' import { COLORS } from '../../constants' -export const MessageContainer = styled.div` +interface MessageContainerProps { + isSentByUser: boolean +} + +export const MessageContainer = styled.div` display: flex; justify-content: ${props => (props.isSentByUser ? 'flex-end' : 'flex-start')}; position: relative; @@ -17,8 +21,13 @@ export const BotMessageImageContainer = styled.div` align-items: center; justify-content: center; ` +interface BolbContainerProps { + bgcolor: string + blob: boolean + blobwidth?: string +} -export const BlobContainer = styled.div` +export const BlobContainer = styled.div` position: relative; margin: 8px; border-radius: 8px; @@ -31,8 +40,12 @@ export const BlobContainer = styled.div` : '60%' : 'calc(100% - 16px)'}; ` +interface BlobTextProps { + blob: boolean + markdownstyle: any +} -export const BlobText = styled.div` +export const BlobText = styled.div` padding: ${props => (props.blob ? '8px 12px' : '0px')}; display: flex; flex-direction: column; @@ -49,13 +62,21 @@ export const BlobTickContainer = styled.div` top: 0; align-items: center; ` -export const BlobTick = styled.div` + +interface BlobTickProps { + pointerSize: number +} + +export const BlobTick = styled.div` position: relative; margin: -${props => props.pointerSize}px 0px; border: ${props => props.pointerSize}px solid ${COLORS.TRANSPARENT}; ` +interface TimestampContainerProps { + isSentByUser: boolean +} -export const TimestampContainer = styled.div` +export const TimestampContainer = styled.div` display: flex; position: relative; justify-content: ${props => (props.isSentByUser ? 'flex-end' : 'flex-start')}; @@ -71,8 +92,11 @@ export const TimestampContainer = styled.div` max-width: 20px; } ` +interface TimestampTextProps { + isSentByUser: boolean +} -export const TimestampText = styled.div` +export const TimestampText = styled.div` @import url('https://fonts.googleapis.com/css?family=Lato'); font-family: Lato; font-size: 10px; diff --git a/packages/botonic-react/src/contexts.tsx b/packages/botonic-react/src/contexts.tsx index 17ae1461bd..04568567bd 100644 --- a/packages/botonic-react/src/contexts.tsx +++ b/packages/botonic-react/src/contexts.tsx @@ -1,18 +1,42 @@ +import { Input as CoreInput, Session as CoreSession } from '@botonic/core' import { createContext } from 'react' -import { WebchatContextProps } from './index-types' +import { ActionRequest, WebchatContextProps } from './index-types' import { webchatInitialState } from './webchat/hooks' -export const RequestContext = createContext({ +export const RequestContext = createContext< + Partial & { + getString: () => string + setLocale: () => void + } +>({ getString: () => '', - setLocale: () => '', - session: {}, + setLocale: () => undefined, + session: {} as CoreSession, params: {}, - input: {}, + input: {} as CoreInput, defaultDelay: 0, defaultTyping: 0, }) +export interface CloseWebviewOptions { + payload?: string + path?: string + params?: Record +} + +export const WebviewRequestContext = createContext<{ + closeWebview: (options?: CloseWebviewOptions) => undefined + getString: (stringId: string) => string + params: Record + session: CoreSession +}>({ + closeWebview: () => undefined, + getString: () => '', + params: {} as Record, + session: {} as CoreSession, +}) + export const WebchatContext = createContext({ addMessage: () => { return diff --git a/packages/botonic-react/src/index.ts b/packages/botonic-react/src/index.ts index a3b2137afa..45c6545794 100644 --- a/packages/botonic-react/src/index.ts +++ b/packages/botonic-react/src/index.ts @@ -1,6 +1,10 @@ export { BotonicInputTester, BotonicOutputTester } from './botonic-tester' export * from './components' -export { RequestContext, WebchatContext } from './contexts' +export { + RequestContext, + WebchatContext, + WebviewRequestContext, +} from './contexts' export { DevApp } from './dev-app' export * from './index-types' export { msgsToBotonic, msgToBotonic } from './msg-to-botonic' diff --git a/packages/botonic-react/src/webchat-app.jsx b/packages/botonic-react/src/webchat-app.jsx index 0405c4ebaf..b524501a98 100644 --- a/packages/botonic-react/src/webchat-app.jsx +++ b/packages/botonic-react/src/webchat-app.jsx @@ -209,6 +209,10 @@ export class WebchatApp { this.webchatRef.current.closeWebchat() } + closeWebview() { + this.webchatRef.current.closeWebview() + } + toggle() { this.webchatRef.current.toggleWebchat() } diff --git a/packages/botonic-react/src/webchat/message-list/index.tsx b/packages/botonic-react/src/webchat/message-list/index.tsx index b9d370a391..5664e96f8c 100644 --- a/packages/botonic-react/src/webchat/message-list/index.tsx +++ b/packages/botonic-react/src/webchat/message-list/index.tsx @@ -93,6 +93,7 @@ export const WebchatMessageList = props => { role={ROLES.MESSAGE_LIST} // TODO: Distinguis between multiple instances of webchat, e.g. `${uniqueId}-botonic-scrollable` id='botonic-scrollable-content' + // @ts-ignore scrollbar={scrollbarOptions} autoHide={scrollbarOptions.autoHide} ismessagescontainer={true.toString()} diff --git a/packages/botonic-react/src/webchat/webchat.jsx b/packages/botonic-react/src/webchat/webchat.jsx index be271ff9bf..ddf9f9e561 100644 --- a/packages/botonic-react/src/webchat/webchat.jsx +++ b/packages/botonic-react/src/webchat/webchat.jsx @@ -16,7 +16,7 @@ import { Audio, Document, Image, Text, Video } from '../components' import { Handoff } from '../components/handoff' import { normalizeWebchatSettings } from '../components/webchat-settings' import { COLORS, MAX_ALLOWED_SIZE_MB, ROLES, WEBCHAT } from '../constants' -import { RequestContext, WebchatContext } from '../contexts' +import { WebchatContext, WebviewRequestContext } from '../contexts' import { SENDERS } from '../index-types' import { getFullMimeWhitelist, @@ -367,8 +367,7 @@ export const Webchat = forwardRef((props, ref) => { if (options && options.payload) { sendPayload(options.payload) } else if (options && options.path) { - let params = '' - if (options.params) params = params2queryString(options.params) + const params = options.params ? params2queryString(options.params) : '' sendPayload(`__PATH_PAYLOAD__${options.path}?${params}`) } } @@ -604,6 +603,7 @@ export const Webchat = forwardRef((props, ref) => { updateTheme(merge(webchatState.theme, themeUpdates), themeUpdates) updateTyping(false) }, + closeWebview: closeWebview, })) const resolveCase = () => { @@ -694,13 +694,10 @@ export const Webchat = forwardRef((props, ref) => { } const webviewRequestContext = { + closeWebview: closeWebview, getString: stringId => props.getString(stringId, webchatState.session), - setLocale: locale => props.getString(locale, webchatState.session), - session: webchatState.session || {}, params: webchatState.webviewParams || {}, - closeWebview: closeWebview, - defaultDelay: props.defaultDelay || 0, - defaultTyping: props.defaultTyping || 0, + session: webchatState.session || {}, } useEffect(() => { @@ -808,7 +805,7 @@ export const Webchat = forwardRef((props, ref) => { } const webchatWebview = () => ( - + { }} webview={webchatState.webview} /> - + ) let mobileStyle = {} if (isMobile(getThemeProperty(WEBCHAT.CUSTOM_PROPERTIES.mobileBreakpoint))) { diff --git a/packages/botonic-react/src/webchat/webview.jsx b/packages/botonic-react/src/webchat/webview.jsx index fa34fd541b..df5a9cefc3 100644 --- a/packages/botonic-react/src/webchat/webview.jsx +++ b/packages/botonic-react/src/webchat/webview.jsx @@ -3,7 +3,7 @@ import Frame from 'react-frame-component' import styled from 'styled-components' import { COLORS, ROLES, WEBCHAT } from '../constants' -import { RequestContext, WebchatContext } from '../contexts' +import { WebchatContext, WebviewRequestContext } from '../contexts' const StyledWebview = styled.div` position: absolute; @@ -62,7 +62,7 @@ const WebviewMode = props => { } export const WebviewHeader = () => { - const { closeWebview } = useContext(RequestContext) + const { closeWebview } = useContext(WebviewRequestContext) const { getThemeProperty } = useContext(WebchatContext) return ( { export const WebviewContainer = props => { const { webchatState } = useContext(WebchatContext) - const { closeWebview } = useContext(RequestContext) + const { closeWebview } = useContext(WebviewRequestContext) const Webview = webchatState.webview - const close = e => e.data == 'botonicCloseWebview' && closeWebview() + const close = e => e.data === 'botonicCloseWebview' && closeWebview() useEffect(() => { if (window.addEventListener) { diff --git a/packages/botonic-react/src/webview-app.tsx b/packages/botonic-react/src/webview-app.tsx index 9ea1842c63..1d09f95241 100644 --- a/packages/botonic-react/src/webview-app.tsx +++ b/packages/botonic-react/src/webview-app.tsx @@ -5,7 +5,7 @@ import React from 'react' import { render } from 'react-dom' import { BrowserRouter, Route } from 'react-router-dom' -import { RequestContext } from './contexts' +import { CloseWebviewOptions, WebviewRequestContext } from './contexts' class App extends React.Component { constructor(props) { @@ -21,7 +21,7 @@ class App extends React.Component { this.state = { session, params } } - async close(options) { + async close(options?: CloseWebviewOptions) { let payload = options ? options.payload : null if (options.path) payload = `__PATH_PAYLOAD__${options.path}` if (payload) { @@ -76,8 +76,8 @@ class App extends React.Component { } render() { - const requestContext = { - getString: stringId => + const webviewRequestContext = { + getString: (stringId: string) => getString(this.props.locales, this.state.session.__locale, stringId), session: this.state.session || {}, params: this.state.params || {}, @@ -85,11 +85,11 @@ class App extends React.Component { } return ( - + {this.props.webviews.map((Webview, i) => ( ))} - + ) } }