diff --git a/packages/journey-manager/src/index.ts b/packages/journey-manager/src/index.ts index 6b3f5250..5eb60810 100644 --- a/packages/journey-manager/src/index.ts +++ b/packages/journey-manager/src/index.ts @@ -355,7 +355,10 @@ export const run = async (props: RunProps): Promise => { await waitUntilDomContentLoaded(); - const triggeredSteps = getTriggeredSteps(props.config.conversationId); + let triggeredSteps = getTriggeredSteps(props.config.conversationId); + + // TODO: type this more accurately + let uiElement: any; /** * Digression detection @@ -379,10 +382,6 @@ export const run = async (props: RunProps): Promise => { }, ); - if (isDigressionDetectable && !urlConditions.some(matchesUrlCondition)) { - props.onDigression?.(client); - } - const sendStep = (stepId: string, once: boolean): void => { if ( triggeredSteps.some((triggeredStep) => triggeredStep.stepId === stepId) @@ -391,7 +390,13 @@ export const run = async (props: RunProps): Promise => { return; } } else { - triggeredSteps.push({ stepId, url: window.location.toString() }); + triggeredSteps = [ + ...triggeredSteps, + { stepId, url: window.location.toString() }, + ]; + if (uiElement != null) { + uiElement.triggeredSteps = triggeredSteps; + } saveTriggeredSteps(props.config.conversationId, triggeredSteps); } props.onStep?.(stepId); @@ -524,9 +529,6 @@ export const run = async (props: RunProps): Promise => { * UI management */ - // TODO: type this more accurately - let uiElement: any; - const setHighlights = debounce((): void => { const highlightElements = findActiveTriggers("click").flatMap( (activeTrigger) => activeTrigger.elements, @@ -543,41 +545,10 @@ export const run = async (props: RunProps): Promise => { uiElement.style.zIndex = 1000; uiElement.config = props.ui; uiElement.client = client; - const handleAction = (ev: any): void => { - const action = ev.detail?.action; - if (action == null) { - return; - } - if (action === "escalate" && props.ui?.onEscalation != null) { - props.ui.onEscalation({ sendStep: client.sendStep }); - return; - } - if (action === "end" && props.ui?.onEnd != null) { - props.ui.onEnd({ sendStep: client.sendStep }); - return; - } - if (action === "previous") { - if (props.ui?.onPreviousStep != null) { - props.ui.onPreviousStep({ - sendStep: client.sendStep, - triggeredSteps, - }); - } else { - const lastTriggeredStep = triggeredSteps[triggeredSteps.length - 1]; - if (lastTriggeredStep != null) { - sendStep(lastTriggeredStep.stepId, false); - // Redirect to previous page if the last triggered step occurred on it - if (lastTriggeredStep.url !== window.location.toString()) { - window.location.href = lastTriggeredStep.url; - } - } - } - } - }; + uiElement.triggeredSteps = triggeredSteps; if (props.ui.highlights ?? false) { setHighlights(); } - uiElement.addEventListener("action", handleAction); document.body.appendChild(uiElement); teardownUiElement = () => { document.body.removeChild(uiElement); @@ -621,7 +592,23 @@ export const run = async (props: RunProps): Promise => { * Change detection */ + let digressionCallbackCalled = false; + const documentObserver = new MutationObserver((mutations) => { + if (isDigressionDetectable) { + const userHasDigressed = !urlConditions.some(matchesUrlCondition); + if (userHasDigressed) { + // Avoid calling the digression callback multiple times after a digression has been detected + if (!digressionCallbackCalled) { + props.onDigression?.(client); + } + digressionCallbackCalled = true; + uiElement.digression = true; + } else { + digressionCallbackCalled = false; + uiElement.digression = false; + } + } // If any of the added nodes are inside matches on appear events, trigger those events const targets = withElementsSync(appearSteps); mutations.forEach((mutation) => { diff --git a/packages/journey-manager/src/ui.tsx b/packages/journey-manager/src/ui.tsx index e589e8a8..68c84579 100644 --- a/packages/journey-manager/src/ui.tsx +++ b/packages/journey-manager/src/ui.tsx @@ -38,6 +38,33 @@ export interface Theme { fontFamily: string; } +interface HandlerArg { + sendStep: Client["sendStep"]; + triggeredSteps: Array<{ stepId: string; url: string }>; +} + +/** + * Button configuration + */ +export interface ButtonConfig { + /** + * Button label + */ + label: string; + /** + * Button confirmation: if present, the button click handler only triggers after the confirmation button is hit + */ + confirmation?: string; + /** + * Icon URL + */ + iconUrl?: string; + /** + * Click handler + */ + onClick: (config: HandlerArg) => void; +} + /** * Deep partial variant of the UI theme, input by the library user */ @@ -103,10 +130,7 @@ export interface UiConfig { /** * On previous step */ - onPreviousStep?: (config: { - sendStep: Client["sendStep"]; - triggeredSteps: Array<{ stepId: string; url: string }>; - }) => void; + onPreviousStep?: (config: HandlerArg) => void; /** * Previous step button label */ @@ -114,11 +138,7 @@ export interface UiConfig { /** * Custom buttons */ - customButtons?: Array<{ - label: string; - iconUrl?: string; - onClick: () => void; - }>; + buttons?: ButtonConfig[]; /** * If this is set, the journey manager will show a call-to-action tooltip to invite the user to interact with the overlay pin. * it will be shown only if the user never interacts with the overlay pin, after `tooltipShowAfterMs` milliseconds. @@ -563,8 +583,6 @@ const Highlight: FunctionComponent<{ element: HTMLElement }> = ({ ); }; -type Action = "end" | "escalate" | "previous"; - type ControlCenterStatus = | null | "pending-escalation" @@ -574,10 +592,11 @@ type ControlCenterStatus = const ControlCenter: FunctionComponent<{ config: UiConfig; + client: Client; + triggeredSteps: TriggeredStep[]; digression: boolean; highlightElements: HTMLElement[]; - onAction: (action: Action) => void; -}> = ({ config, highlightElements, digression, onAction }) => { +}> = ({ config, client, triggeredSteps, highlightElements, digression }) => { const [hasBeenOpened, setHasBeenOpened] = useState(false); const [isOpen, setIsOpen] = useState(false); const [status, setStatus] = useState(null); @@ -622,6 +641,27 @@ const ControlCenter: FunctionComponent<{ return null; }, [status]); + const onPreviousStep = config.onPreviousStep + ? () => { + config.onPreviousStep?.({ + sendStep: client.sendStep, + triggeredSteps, + }); + } + : () => { + const lastTriggeredStep = triggeredSteps[triggeredSteps.length - 1]; + if (lastTriggeredStep != null) { + client.sendStep(lastTriggeredStep.stepId).catch((err) => { + // eslint-disable-next-line no-console + console.warn(err); + }); + // Redirect to previous page if the last triggered step occurred on it + if (lastTriggeredStep.url !== window.location.toString()) { + window.location.href = lastTriggeredStep.url; + } + } + }; + return ( <> @@ -673,19 +713,19 @@ const ControlCenter: FunctionComponent<{
{config.onEscalation != null ? ( ) : null} @@ -704,7 +744,7 @@ const ControlCenter: FunctionComponent<{ ) : null} + {(config.buttons ?? []).map((buttonConfig, buttonIndex) => ( + + ))}
)}
@@ -741,21 +794,30 @@ const ControlCenter: FunctionComponent<{ ); }; +interface TriggeredStep { + stepId: string; + url: string; +} + /** * @hidden @internal */ export class JourneyManagerElement extends HTMLElement { _shadowRoot: ShadowRoot | null = null; + _client: Client | null = null; + _triggeredSteps: TriggeredStep[] | null = null; _config: UiConfig | null = null; - _digression?: boolean; + _digression: boolean = false; _highlightElements: HTMLElement[] = []; /** * Set digression attribute */ set digression(value: boolean) { - this._digression = value; - this.render(); + if (this._digression !== value) { + this._digression = value; + this.render(); + } } /** @@ -766,6 +828,22 @@ export class JourneyManagerElement extends HTMLElement { this.render(); } + /** + * Set SDK client + */ + set client(value: Client) { + this._client = value; + this.render(); + } + + /** + * Set triggered steps + */ + set triggeredSteps(value: TriggeredStep[]) { + this._triggeredSteps = value; + this.render(); + } + /** * Set UI configuration */ @@ -779,23 +857,20 @@ export class JourneyManagerElement extends HTMLElement { */ render(): void { this._shadowRoot = this._shadowRoot ?? this.attachShadow({ mode: "open" }); - if (this._config == null) { + if ( + this._config == null || + this._client == null || + this._triggeredSteps == null + ) { return; } render( { - this.dispatchEvent( - new CustomEvent("action", { - detail: { - action, - }, - }), - ); - }} />, this._shadowRoot, ); diff --git a/packages/website/src/components/Prototyping.tsx b/packages/website/src/components/Prototyping.tsx index 7db06486..348cc7dc 100644 --- a/packages/website/src/components/Prototyping.tsx +++ b/packages/website/src/components/Prototyping.tsx @@ -120,11 +120,21 @@ const runJourneyManager = async (): Promise => { fontFamily: "'Neue Haas Grotesk'", colors: { highlight: "#7dd3fc" }, }, - onEscalation: () => { + escalationButtonLabel: "Hand Off To Specialist", + onEscalation: (args) => { // eslint-disable-next-line no-console - console.log("escalation"); + console.log("escalation", args); }, nudgeContent: "click me! click me!", + buttons: [ + { + label: "Google Chat", + onClick: () => { + // eslint-disable-next-line no-console + console.log("Google Chat button click"); + }, + }, + ], }, triggers: triggersForRun(), onStep: (stepId) => {