🚸 (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 { 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<Block[]>([])
|
||||
const [displayedChunks, setDisplayedChunks] = useState<ChatDisplayChunk[]>([])
|
||||
const [blockedPopupUrl, setBlockedPopupUrl] = useState<string>()
|
||||
|
||||
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)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -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 = ({
|
||||
)}
|
||||
</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 = (
|
||||
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
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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<string>()
|
||||
|
||||
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) => {
|
||||
<Show when={isSending()}>
|
||||
<LoadingChunk theme={theme()} />
|
||||
</Show>
|
||||
<Show when={blockedPopupUrl()} keyed>
|
||||
{(blockedPopupUrl) => (
|
||||
<div class="flex justify-end">
|
||||
<PopupBlockedToast
|
||||
url={blockedPopupUrl}
|
||||
onLinkClick={() => setBlockedPopupUrl(undefined)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<BottomSpacer ref={bottomSpacer} />
|
||||
</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' }}
|
||||
>
|
||||
<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"
|
||||
>
|
||||
{props.message}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -7,20 +7,20 @@ import type { ChatReply } from 'models'
|
||||
|
||||
export const executeClientSideAction = async (
|
||||
clientSideAction: NonNullable<ChatReply['clientSideActions']>[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)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user