diff --git a/packages/bot-engine/src/components/ChatGroup/ChatGroup.tsx b/packages/bot-engine/src/components/ChatGroup/ChatGroup.tsx index a0b18e987..9cfe847d8 100644 --- a/packages/bot-engine/src/components/ChatGroup/ChatGroup.tsx +++ b/packages/bot-engine/src/components/ChatGroup/ChatGroup.tsx @@ -29,6 +29,7 @@ import { getLastChatBlockType } from '@/utils/chat' import { executeIntegration } from '@/utils/executeIntegration' import { executeLogic } from '@/utils/executeLogic' import { blockCanBeRetried, parseRetryBlock } from '@/utils/inputs' +import { PopupBlockedToast } from '../PopupBlockedToast' type ChatGroupProps = { blocks: Block[] @@ -72,6 +73,7 @@ export const ChatGroup = ({ const { scroll } = useChat() const [processedBlocks, setProcessedBlocks] = useState([]) const [displayedChunks, setDisplayedChunks] = useState([]) + const [blockedPopupUrl, setBlockedPopupUrl] = useState() const insertBlockInStack = (nextBlock: Block) => { setProcessedBlocks([...processedBlocks, nextBlock]) @@ -120,21 +122,25 @@ export const ChatGroup = ({ const currentBlock = [...processedBlocks].pop() if (!currentBlock) return if (isLogicBlock(currentBlock)) { - const { nextEdgeId, linkedTypebot } = await executeLogic(currentBlock, { - isPreview, - apiHost, - typebot, - linkedTypebots, - updateVariableValue, - updateVariables, - injectLinkedTypebot, - onNewLog, - createEdge, - setCurrentTypebotId, - pushEdgeIdInLinkedTypebotQueue, - currentTypebotId, - pushParentTypebotId, - }) + const { nextEdgeId, linkedTypebot, blockedPopupUrl } = await executeLogic( + currentBlock, + { + isPreview, + apiHost, + typebot, + linkedTypebots, + updateVariableValue, + updateVariables, + injectLinkedTypebot, + onNewLog, + createEdge, + setCurrentTypebotId, + pushEdgeIdInLinkedTypebotQueue, + currentTypebotId, + pushParentTypebotId, + } + ) + if (blockedPopupUrl) setBlockedPopupUrl(blockedPopupUrl) const isRedirecting = currentBlock.type === LogicBlockType.REDIRECT && currentBlock.options.isNewTab === false @@ -224,6 +230,8 @@ export const ChatGroup = ({ hasGuestAvatar={typebot.theme.chat.guestAvatar?.isEnabled ?? false} onDisplayNextBlock={displayNextBlock} keepShowingHostAvatar={keepShowingHostAvatar} + blockedPopupUrl={blockedPopupUrl} + onBlockedPopupLinkClick={() => setBlockedPopupUrl(undefined)} /> ))} @@ -236,6 +244,8 @@ type Props = { hostAvatar: { isEnabled: boolean; src?: string } hasGuestAvatar: boolean keepShowingHostAvatar: boolean + blockedPopupUrl?: string + onBlockedPopupLinkClick: () => void onDisplayNextBlock: ( answerContent?: InputSubmitContent, isRetry?: boolean @@ -246,6 +256,8 @@ const ChatChunks = ({ hostAvatar, hasGuestAvatar, keepShowingHostAvatar, + blockedPopupUrl, + onBlockedPopupLinkClick, onDisplayNextBlock, }: Props) => { const [isSkipped, setIsSkipped] = useState(false) @@ -320,6 +332,14 @@ const ChatChunks = ({ )} )} + {blockedPopupUrl ? ( +
+ +
+ ) : null} ) } diff --git a/packages/bot-engine/src/components/PopupBlockedToast.tsx b/packages/bot-engine/src/components/PopupBlockedToast.tsx new file mode 100644 index 000000000..74cd603dd --- /dev/null +++ b/packages/bot-engine/src/components/PopupBlockedToast.tsx @@ -0,0 +1,30 @@ +type Props = { + url: string + onLinkClick: () => void +} + +export const PopupBlockedToast = ({ url, onLinkClick }: Props) => { + return ( +
+ + Popup blocked + +
+ The bot wants to open a new tab but it was blocked by your broswer. It + needs a manual approval. +
+ + Continue in new tab + +
+ ) +} diff --git a/packages/bot-engine/src/features/blocks/logic/redirect/utils/executeRedirect.ts b/packages/bot-engine/src/features/blocks/logic/redirect/utils/executeRedirect.ts index ef8c646a2..fc247d01f 100644 --- a/packages/bot-engine/src/features/blocks/logic/redirect/utils/executeRedirect.ts +++ b/packages/bot-engine/src/features/blocks/logic/redirect/utils/executeRedirect.ts @@ -7,21 +7,33 @@ import { sanitizeUrl } from 'utils' export const executeRedirect = ( block: RedirectBlock, { typebot: { variables } }: LogicState -): EdgeId | undefined => { - if (!block.options?.url) return block.outgoingEdgeId +): { + nextEdgeId?: EdgeId + blockedPopupUrl?: string +} => { + if (!block.options?.url) return { nextEdgeId: block.outgoingEdgeId } const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url)) const isEmbedded = window.parent && window.location !== window.top?.location + let newWindow: Window | null = null if (isEmbedded) { - if (!block.options.isNewTab) - return ((window.top as Window).location.href = formattedUrl) + if (!block.options.isNewTab) { + ;(window.top as Window).location.href = formattedUrl + return { nextEdgeId: block.outgoingEdgeId } + } try { - window.open(formattedUrl) + newWindow = window.open(formattedUrl) } catch (err) { sendEventToParent({ redirectUrl: formattedUrl }) } } else { - window.open(formattedUrl, block.options.isNewTab ? '_blank' : '_self') + newWindow = window.open( + formattedUrl, + block.options.isNewTab ? '_blank' : '_self' + ) + } + return { + nextEdgeId: block.outgoingEdgeId, + blockedPopupUrl: newWindow ? undefined : formattedUrl, } - return block.outgoingEdgeId } diff --git a/packages/bot-engine/src/utils/executeLogic.ts b/packages/bot-engine/src/utils/executeLogic.ts index 969044753..30cbef8ea 100644 --- a/packages/bot-engine/src/utils/executeLogic.ts +++ b/packages/bot-engine/src/utils/executeLogic.ts @@ -15,6 +15,7 @@ export const executeLogic = async ( ): Promise<{ nextEdgeId?: EdgeId linkedTypebot?: TypebotViewerProps['typebot'] | LinkedTypebot + blockedPopupUrl?: string }> => { switch (block.type) { case LogicBlockType.SET_VARIABLE: @@ -22,7 +23,7 @@ export const executeLogic = async ( case LogicBlockType.CONDITION: return { nextEdgeId: executeCondition(block, context) } case LogicBlockType.REDIRECT: - return { nextEdgeId: executeRedirect(block, context) } + return executeRedirect(block, context) case LogicBlockType.SCRIPT: return { nextEdgeId: await executeScript(block, context) } case LogicBlockType.TYPEBOT_LINK: diff --git a/packages/js/src/components/ConversationContainer/ConversationContainer.tsx b/packages/js/src/components/ConversationContainer/ConversationContainer.tsx index 6f6e10082..3768f5d8e 100644 --- a/packages/js/src/components/ConversationContainer/ConversationContainer.tsx +++ b/packages/js/src/components/ConversationContainer/ConversationContainer.tsx @@ -6,6 +6,7 @@ import { BotContext, InitialChatReply } from '@/types' import { isNotDefined } from 'utils' import { executeClientSideAction } from '@/utils/executeClientSideActions' import { LoadingChunk } from './LoadingChunk' +import { PopupBlockedToast } from './PopupBlockedToast' const parseDynamicTheme = ( initialTheme: Theme, @@ -57,6 +58,7 @@ export const ConversationContainer = (props: Props) => { >(props.initialChatReply.dynamicTheme) const [theme, setTheme] = createSignal(props.initialChatReply.typebot.theme) const [isSending, setIsSending] = createSignal(false) + const [blockedPopupUrl, setBlockedPopupUrl] = createSignal() createEffect(() => { setTheme( @@ -112,7 +114,8 @@ export const ConversationContainer = (props: Props) => { isNotDefined(action.lastBubbleBlockId) ) for (const action of actionsToExecute) { - await executeClientSideAction(action) + const response = await executeClientSideAction(action) + if (response) setBlockedPopupUrl(response.blockedPopupUrl) } } if (isNotDefined(lastChunk.input)) { @@ -128,7 +131,8 @@ export const ConversationContainer = (props: Props) => { (action) => action.lastBubbleBlockId === blockId ) for (const action of actionsToExecute) { - await executeClientSideAction(action) + const response = await executeClientSideAction(action) + if (response) setBlockedPopupUrl(response.blockedPopupUrl) } } } @@ -161,6 +165,16 @@ export const ConversationContainer = (props: Props) => { + + {(blockedPopupUrl) => ( +
+ setBlockedPopupUrl(undefined)} + /> +
+ )} +
) diff --git a/packages/js/src/components/ConversationContainer/PopupBlockedToast.tsx b/packages/js/src/components/ConversationContainer/PopupBlockedToast.tsx new file mode 100644 index 000000000..f8a0f3907 --- /dev/null +++ b/packages/js/src/components/ConversationContainer/PopupBlockedToast.tsx @@ -0,0 +1,30 @@ +type Props = { + url: string + onLinkClick: () => void +} + +export const PopupBlockedToast = (props: Props) => { + return ( + + ) +} diff --git a/packages/js/src/components/bubbles/GuestBubble.tsx b/packages/js/src/components/bubbles/GuestBubble.tsx index 9370d2a90..9c1eaf0db 100644 --- a/packages/js/src/components/bubbles/GuestBubble.tsx +++ b/packages/js/src/components/bubbles/GuestBubble.tsx @@ -13,7 +13,7 @@ export const GuestBubble = (props: Props) => ( style={{ 'margin-left': '50px' }} > {props.message} diff --git a/packages/js/src/features/blocks/logic/redirect/utils/executeRedirect.ts b/packages/js/src/features/blocks/logic/redirect/utils/executeRedirect.ts index b115c43ce..3aee2119f 100644 --- a/packages/js/src/features/blocks/logic/redirect/utils/executeRedirect.ts +++ b/packages/js/src/features/blocks/logic/redirect/utils/executeRedirect.ts @@ -1,6 +1,13 @@ import type { RedirectOptions } from 'models' -export const executeRedirect = ({ url, isNewTab }: RedirectOptions) => { +export const executeRedirect = ({ + url, + isNewTab, +}: RedirectOptions): { blockedPopupUrl: string } | undefined => { if (!url) return - window.open(url, isNewTab ? '_blank' : '_self') + const updatedWindow = window.open(url, isNewTab ? '_blank' : '_self') + if (!updatedWindow) + return { + blockedPopupUrl: url, + } } diff --git a/packages/js/src/utils/executeClientSideActions.ts b/packages/js/src/utils/executeClientSideActions.ts index afdb9f362..0f9e8c40c 100644 --- a/packages/js/src/utils/executeClientSideActions.ts +++ b/packages/js/src/utils/executeClientSideActions.ts @@ -7,20 +7,20 @@ import type { ChatReply } from 'models' export const executeClientSideAction = async ( clientSideAction: NonNullable[0] -) => { +): Promise<{ blockedPopupUrl: string } | void> => { if ('chatwoot' in clientSideAction) { - executeChatwoot(clientSideAction.chatwoot) + return executeChatwoot(clientSideAction.chatwoot) } if ('googleAnalytics' in clientSideAction) { - executeGoogleAnalyticsBlock(clientSideAction.googleAnalytics) + return executeGoogleAnalyticsBlock(clientSideAction.googleAnalytics) } if ('scriptToExecute' in clientSideAction) { - await executeScript(clientSideAction.scriptToExecute) + return executeScript(clientSideAction.scriptToExecute) } if ('redirect' in clientSideAction) { - executeRedirect(clientSideAction.redirect) + return executeRedirect(clientSideAction.redirect) } if ('wait' in clientSideAction) { - await executeWait(clientSideAction.wait) + return executeWait(clientSideAction.wait) } }