🚸 (bot) Show a popup when the redirect is blocked by browser
Allows us to show a link button to redirect the user anyway
This commit is contained in:
@ -29,6 +29,7 @@ import { getLastChatBlockType } from '@/utils/chat'
|
|||||||
import { executeIntegration } from '@/utils/executeIntegration'
|
import { executeIntegration } from '@/utils/executeIntegration'
|
||||||
import { executeLogic } from '@/utils/executeLogic'
|
import { executeLogic } from '@/utils/executeLogic'
|
||||||
import { blockCanBeRetried, parseRetryBlock } from '@/utils/inputs'
|
import { blockCanBeRetried, parseRetryBlock } from '@/utils/inputs'
|
||||||
|
import { PopupBlockedToast } from '../PopupBlockedToast'
|
||||||
|
|
||||||
type ChatGroupProps = {
|
type ChatGroupProps = {
|
||||||
blocks: Block[]
|
blocks: Block[]
|
||||||
@ -72,6 +73,7 @@ export const ChatGroup = ({
|
|||||||
const { scroll } = useChat()
|
const { scroll } = useChat()
|
||||||
const [processedBlocks, setProcessedBlocks] = useState<Block[]>([])
|
const [processedBlocks, setProcessedBlocks] = useState<Block[]>([])
|
||||||
const [displayedChunks, setDisplayedChunks] = useState<ChatDisplayChunk[]>([])
|
const [displayedChunks, setDisplayedChunks] = useState<ChatDisplayChunk[]>([])
|
||||||
|
const [blockedPopupUrl, setBlockedPopupUrl] = useState<string>()
|
||||||
|
|
||||||
const insertBlockInStack = (nextBlock: Block) => {
|
const insertBlockInStack = (nextBlock: Block) => {
|
||||||
setProcessedBlocks([...processedBlocks, nextBlock])
|
setProcessedBlocks([...processedBlocks, nextBlock])
|
||||||
@ -120,21 +122,25 @@ export const ChatGroup = ({
|
|||||||
const currentBlock = [...processedBlocks].pop()
|
const currentBlock = [...processedBlocks].pop()
|
||||||
if (!currentBlock) return
|
if (!currentBlock) return
|
||||||
if (isLogicBlock(currentBlock)) {
|
if (isLogicBlock(currentBlock)) {
|
||||||
const { nextEdgeId, linkedTypebot } = await executeLogic(currentBlock, {
|
const { nextEdgeId, linkedTypebot, blockedPopupUrl } = await executeLogic(
|
||||||
isPreview,
|
currentBlock,
|
||||||
apiHost,
|
{
|
||||||
typebot,
|
isPreview,
|
||||||
linkedTypebots,
|
apiHost,
|
||||||
updateVariableValue,
|
typebot,
|
||||||
updateVariables,
|
linkedTypebots,
|
||||||
injectLinkedTypebot,
|
updateVariableValue,
|
||||||
onNewLog,
|
updateVariables,
|
||||||
createEdge,
|
injectLinkedTypebot,
|
||||||
setCurrentTypebotId,
|
onNewLog,
|
||||||
pushEdgeIdInLinkedTypebotQueue,
|
createEdge,
|
||||||
currentTypebotId,
|
setCurrentTypebotId,
|
||||||
pushParentTypebotId,
|
pushEdgeIdInLinkedTypebotQueue,
|
||||||
})
|
currentTypebotId,
|
||||||
|
pushParentTypebotId,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (blockedPopupUrl) setBlockedPopupUrl(blockedPopupUrl)
|
||||||
const isRedirecting =
|
const isRedirecting =
|
||||||
currentBlock.type === LogicBlockType.REDIRECT &&
|
currentBlock.type === LogicBlockType.REDIRECT &&
|
||||||
currentBlock.options.isNewTab === false
|
currentBlock.options.isNewTab === false
|
||||||
@ -224,6 +230,8 @@ export const ChatGroup = ({
|
|||||||
hasGuestAvatar={typebot.theme.chat.guestAvatar?.isEnabled ?? false}
|
hasGuestAvatar={typebot.theme.chat.guestAvatar?.isEnabled ?? false}
|
||||||
onDisplayNextBlock={displayNextBlock}
|
onDisplayNextBlock={displayNextBlock}
|
||||||
keepShowingHostAvatar={keepShowingHostAvatar}
|
keepShowingHostAvatar={keepShowingHostAvatar}
|
||||||
|
blockedPopupUrl={blockedPopupUrl}
|
||||||
|
onBlockedPopupLinkClick={() => setBlockedPopupUrl(undefined)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -236,6 +244,8 @@ type Props = {
|
|||||||
hostAvatar: { isEnabled: boolean; src?: string }
|
hostAvatar: { isEnabled: boolean; src?: string }
|
||||||
hasGuestAvatar: boolean
|
hasGuestAvatar: boolean
|
||||||
keepShowingHostAvatar: boolean
|
keepShowingHostAvatar: boolean
|
||||||
|
blockedPopupUrl?: string
|
||||||
|
onBlockedPopupLinkClick: () => void
|
||||||
onDisplayNextBlock: (
|
onDisplayNextBlock: (
|
||||||
answerContent?: InputSubmitContent,
|
answerContent?: InputSubmitContent,
|
||||||
isRetry?: boolean
|
isRetry?: boolean
|
||||||
@ -246,6 +256,8 @@ const ChatChunks = ({
|
|||||||
hostAvatar,
|
hostAvatar,
|
||||||
hasGuestAvatar,
|
hasGuestAvatar,
|
||||||
keepShowingHostAvatar,
|
keepShowingHostAvatar,
|
||||||
|
blockedPopupUrl,
|
||||||
|
onBlockedPopupLinkClick,
|
||||||
onDisplayNextBlock,
|
onDisplayNextBlock,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isSkipped, setIsSkipped] = useState(false)
|
const [isSkipped, setIsSkipped] = useState(false)
|
||||||
@ -320,6 +332,14 @@ const ChatChunks = ({
|
|||||||
)}
|
)}
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
)}
|
)}
|
||||||
|
{blockedPopupUrl ? (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<PopupBlockedToast
|
||||||
|
url={blockedPopupUrl}
|
||||||
|
onLinkClick={onBlockedPopupLinkClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
30
packages/bot-engine/src/components/PopupBlockedToast.tsx
Normal file
30
packages/bot-engine/src/components/PopupBlockedToast.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
onLinkClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PopupBlockedToast = ({ url, onLinkClick }: Props) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow flex flex-col gap-2"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<span className="mb-1 text-sm font-semibold text-gray-900">
|
||||||
|
Popup blocked
|
||||||
|
</span>
|
||||||
|
<div className="mb-2 text-sm font-normal">
|
||||||
|
The bot wants to open a new tab but it was blocked by your broswer. It
|
||||||
|
needs a manual approval.
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
className="py-1 px-4 justify-center text-sm font-semibold rounded-md text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 typebot-button"
|
||||||
|
rel="noreferrer"
|
||||||
|
onClick={onLinkClick}
|
||||||
|
>
|
||||||
|
Continue in new tab
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -7,21 +7,33 @@ import { sanitizeUrl } from 'utils'
|
|||||||
export const executeRedirect = (
|
export const executeRedirect = (
|
||||||
block: RedirectBlock,
|
block: RedirectBlock,
|
||||||
{ typebot: { variables } }: LogicState
|
{ 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 formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url))
|
||||||
const isEmbedded = window.parent && window.location !== window.top?.location
|
const isEmbedded = window.parent && window.location !== window.top?.location
|
||||||
|
let newWindow: Window | null = null
|
||||||
if (isEmbedded) {
|
if (isEmbedded) {
|
||||||
if (!block.options.isNewTab)
|
if (!block.options.isNewTab) {
|
||||||
return ((window.top as Window).location.href = formattedUrl)
|
;(window.top as Window).location.href = formattedUrl
|
||||||
|
return { nextEdgeId: block.outgoingEdgeId }
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.open(formattedUrl)
|
newWindow = window.open(formattedUrl)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sendEventToParent({ redirectUrl: formattedUrl })
|
sendEventToParent({ redirectUrl: formattedUrl })
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ export const executeLogic = async (
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
nextEdgeId?: EdgeId
|
nextEdgeId?: EdgeId
|
||||||
linkedTypebot?: TypebotViewerProps['typebot'] | LinkedTypebot
|
linkedTypebot?: TypebotViewerProps['typebot'] | LinkedTypebot
|
||||||
|
blockedPopupUrl?: string
|
||||||
}> => {
|
}> => {
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case LogicBlockType.SET_VARIABLE:
|
case LogicBlockType.SET_VARIABLE:
|
||||||
@ -22,7 +23,7 @@ export const executeLogic = async (
|
|||||||
case LogicBlockType.CONDITION:
|
case LogicBlockType.CONDITION:
|
||||||
return { nextEdgeId: executeCondition(block, context) }
|
return { nextEdgeId: executeCondition(block, context) }
|
||||||
case LogicBlockType.REDIRECT:
|
case LogicBlockType.REDIRECT:
|
||||||
return { nextEdgeId: executeRedirect(block, context) }
|
return executeRedirect(block, context)
|
||||||
case LogicBlockType.SCRIPT:
|
case LogicBlockType.SCRIPT:
|
||||||
return { nextEdgeId: await executeScript(block, context) }
|
return { nextEdgeId: await executeScript(block, context) }
|
||||||
case LogicBlockType.TYPEBOT_LINK:
|
case LogicBlockType.TYPEBOT_LINK:
|
||||||
|
@ -6,6 +6,7 @@ import { BotContext, InitialChatReply } from '@/types'
|
|||||||
import { isNotDefined } from 'utils'
|
import { isNotDefined } from 'utils'
|
||||||
import { executeClientSideAction } from '@/utils/executeClientSideActions'
|
import { executeClientSideAction } from '@/utils/executeClientSideActions'
|
||||||
import { LoadingChunk } from './LoadingChunk'
|
import { LoadingChunk } from './LoadingChunk'
|
||||||
|
import { PopupBlockedToast } from './PopupBlockedToast'
|
||||||
|
|
||||||
const parseDynamicTheme = (
|
const parseDynamicTheme = (
|
||||||
initialTheme: Theme,
|
initialTheme: Theme,
|
||||||
@ -57,6 +58,7 @@ export const ConversationContainer = (props: Props) => {
|
|||||||
>(props.initialChatReply.dynamicTheme)
|
>(props.initialChatReply.dynamicTheme)
|
||||||
const [theme, setTheme] = createSignal(props.initialChatReply.typebot.theme)
|
const [theme, setTheme] = createSignal(props.initialChatReply.typebot.theme)
|
||||||
const [isSending, setIsSending] = createSignal(false)
|
const [isSending, setIsSending] = createSignal(false)
|
||||||
|
const [blockedPopupUrl, setBlockedPopupUrl] = createSignal<string>()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setTheme(
|
setTheme(
|
||||||
@ -112,7 +114,8 @@ export const ConversationContainer = (props: Props) => {
|
|||||||
isNotDefined(action.lastBubbleBlockId)
|
isNotDefined(action.lastBubbleBlockId)
|
||||||
)
|
)
|
||||||
for (const action of actionsToExecute) {
|
for (const action of actionsToExecute) {
|
||||||
await executeClientSideAction(action)
|
const response = await executeClientSideAction(action)
|
||||||
|
if (response) setBlockedPopupUrl(response.blockedPopupUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isNotDefined(lastChunk.input)) {
|
if (isNotDefined(lastChunk.input)) {
|
||||||
@ -128,7 +131,8 @@ export const ConversationContainer = (props: Props) => {
|
|||||||
(action) => action.lastBubbleBlockId === blockId
|
(action) => action.lastBubbleBlockId === blockId
|
||||||
)
|
)
|
||||||
for (const action of actionsToExecute) {
|
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) => {
|
|||||||
<Show when={isSending()}>
|
<Show when={isSending()}>
|
||||||
<LoadingChunk theme={theme()} />
|
<LoadingChunk theme={theme()} />
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={blockedPopupUrl()} keyed>
|
||||||
|
{(blockedPopupUrl) => (
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<PopupBlockedToast
|
||||||
|
url={blockedPopupUrl}
|
||||||
|
onLinkClick={() => setBlockedPopupUrl(undefined)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
<BottomSpacer ref={bottomSpacer} />
|
<BottomSpacer ref={bottomSpacer} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
onLinkClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PopupBlockedToast = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow flex flex-col gap-2"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<span class="mb-1 text-sm font-semibold text-gray-900">
|
||||||
|
Popup blocked
|
||||||
|
</span>
|
||||||
|
<div class="mb-2 text-sm font-normal">
|
||||||
|
The bot wants to open a new tab but it was blocked by your broswer. It
|
||||||
|
needs a manual approval.
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={props.url}
|
||||||
|
target="_blank"
|
||||||
|
class="py-1 px-4 justify-center text-sm font-semibold rounded-md text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 typebot-button"
|
||||||
|
rel="noreferrer"
|
||||||
|
onClick={() => props.onLinkClick()}
|
||||||
|
>
|
||||||
|
Continue in new tab
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -13,7 +13,7 @@ export const GuestBubble = (props: Props) => (
|
|||||||
style={{ 'margin-left': '50px' }}
|
style={{ 'margin-left': '50px' }}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="px-4 py-2 rounded-lg mr-2 whitespace-pre-wrap max-w-full typebot-guest-bubble cursor-pointer"
|
class="px-4 py-2 rounded-lg mr-2 whitespace-pre-wrap max-w-full typebot-guest-bubble"
|
||||||
data-testid="guest-bubble"
|
data-testid="guest-bubble"
|
||||||
>
|
>
|
||||||
{props.message}
|
{props.message}
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
import type { RedirectOptions } from 'models'
|
import type { RedirectOptions } from 'models'
|
||||||
|
|
||||||
export const executeRedirect = ({ url, isNewTab }: RedirectOptions) => {
|
export const executeRedirect = ({
|
||||||
|
url,
|
||||||
|
isNewTab,
|
||||||
|
}: RedirectOptions): { blockedPopupUrl: string } | undefined => {
|
||||||
if (!url) return
|
if (!url) return
|
||||||
window.open(url, isNewTab ? '_blank' : '_self')
|
const updatedWindow = window.open(url, isNewTab ? '_blank' : '_self')
|
||||||
|
if (!updatedWindow)
|
||||||
|
return {
|
||||||
|
blockedPopupUrl: url,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,20 +7,20 @@ import type { ChatReply } from 'models'
|
|||||||
|
|
||||||
export const executeClientSideAction = async (
|
export const executeClientSideAction = async (
|
||||||
clientSideAction: NonNullable<ChatReply['clientSideActions']>[0]
|
clientSideAction: NonNullable<ChatReply['clientSideActions']>[0]
|
||||||
) => {
|
): Promise<{ blockedPopupUrl: string } | void> => {
|
||||||
if ('chatwoot' in clientSideAction) {
|
if ('chatwoot' in clientSideAction) {
|
||||||
executeChatwoot(clientSideAction.chatwoot)
|
return executeChatwoot(clientSideAction.chatwoot)
|
||||||
}
|
}
|
||||||
if ('googleAnalytics' in clientSideAction) {
|
if ('googleAnalytics' in clientSideAction) {
|
||||||
executeGoogleAnalyticsBlock(clientSideAction.googleAnalytics)
|
return executeGoogleAnalyticsBlock(clientSideAction.googleAnalytics)
|
||||||
}
|
}
|
||||||
if ('scriptToExecute' in clientSideAction) {
|
if ('scriptToExecute' in clientSideAction) {
|
||||||
await executeScript(clientSideAction.scriptToExecute)
|
return executeScript(clientSideAction.scriptToExecute)
|
||||||
}
|
}
|
||||||
if ('redirect' in clientSideAction) {
|
if ('redirect' in clientSideAction) {
|
||||||
executeRedirect(clientSideAction.redirect)
|
return executeRedirect(clientSideAction.redirect)
|
||||||
}
|
}
|
||||||
if ('wait' in clientSideAction) {
|
if ('wait' in clientSideAction) {
|
||||||
await executeWait(clientSideAction.wait)
|
return executeWait(clientSideAction.wait)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user