customerio-gist-web
Version:
Build beautiful in-app flows with no code and deliver them instantly to your app. http://customer.io
410 lines (368 loc) • 14.7 kB
JavaScript
import Gist from '../gist';
import { log } from "../utilities/log";
import { logMessageView, logUserMessageView } from "../services/log-service";
import { v4 as uuidv4 } from 'uuid';
import { settings } from "../services/settings";
import {
loadOverlayComponent,
showOverlayComponent,
hideOverlayComponent,
removeOverlayComponent,
loadEmbedComponent,
showEmbedComponent,
hideEmbedComponent,
resizeComponent,
elementHasHeight,
isElementLoaded,
changeOverlayTitle
} from "./message-component-manager";
import { resolveMessageProperties } from "./gist-properties-manager";
import { positions, addPageElement } from "./page-component-manager";
import { getAllCustomAttributes } from "./custom-attribute-manager";
import { checkMessageQueue } from "./queue-manager";
import { isMessageBroadcast, markBroadcastAsSeen, markBroadcastAsDismissed, isShowAlwaysBroadcast } from './message-broadcast-manager';
import { markUserQueueMessageAsSeen, saveMessageState, clearMessageState } from './message-user-queue-manager';
import { setMessageLoaded } from './message-user-queue-manager';
import {
fetchMessageByInstanceId,
fetchMessageByElementId,
isQueueIdAlreadyShowing,
removeMessageByInstanceId,
updateMessageByInstanceId,
hasDisplayChanged,
applyDisplaySettings
} from '../utilities/message-utils';
export async function showMessage(message) {
if (Gist.isDocumentVisible) {
if (isQueueIdAlreadyShowing(message.queueId)) {
log(`Message with queueId ${message.queueId} is already showing.`);
return null;
}
if (Gist.overlayInstanceId) {
log(`Message ${Gist.overlayInstanceId} already showing.`);
return null;
} else {
var properties = resolveMessageProperties(message);
message.instanceId = uuidv4();
message.overlay = true;
message.firstLoad = true;
message.shouldResizeHeight = true;
message.shouldScale = properties.shouldScale;
message.renderStartTime = new Date().getTime();
Gist.overlayInstanceId = message.instanceId;
Gist.currentMessages.push(message);
// Use saved step if available (set by queue manager)
const savedStep = message.savedStepName || null;
return loadMessageComponent(message, null, savedStep);
}
} else {
log("Document hidden, not showing message now.");
return null;
}
}
export async function embedMessage(message, elementId) {
if (Gist.isDocumentVisible) {
if (isQueueIdAlreadyShowing(message.queueId)) {
log(`Message with queueId ${message.queueId} is already showing.`);
return null;
}
message.instanceId = uuidv4();
message.overlay = false;
message.firstLoad = true;
message.shouldScale = false;
message.elementId = elementId;
message.shouldResizeHeight = !elementHasHeight(elementId);
message.renderStartTime = new Date().getTime();
Gist.currentMessages.push(message);
// Use saved step if available (set by queue manager)
const savedStep = message.savedStepName || null;
return loadMessageComponent(message, elementId, savedStep);
} else {
log("Document hidden, not showing message now.");
return null;
}
}
export async function hideMessage(message) {
if (message) {
Gist.messageDismissed(message);
if (message.overlay) {
await resetOverlayState(true, message);
} else {
resetEmbedState(message);
}
} else {
log(`Message with instance id: ${message.instanceId} not found`);
}
}
export async function removePersistentMessage(message) {
var messageProperties = resolveMessageProperties(message);
if (message) {
if (messageProperties.persistent) {
log(`Persistent message dismissed, logging view`);
await logUserMessageViewLocally(message);
await reportMessageView(message);
// Clear saved message state when persistent message is removed
await clearMessageState(message.queueId);
}
} else {
log(`Message with instance id: ${message.instanceId} not found`);
}
}
function resetEmbedState(message) {
removeMessageByInstanceId(message.instanceId);
hideEmbedComponent(message.elementId);
}
async function resetOverlayState(hideFirst, message) {
if (hideFirst) {
await hideOverlayComponent();
} else {
removeOverlayComponent();
}
if (Gist.currentMessages.length == 0) {
window.removeEventListener('message', handleGistEvents);
window.removeEventListener('touchstart', handleTouchStartEvents);
}
removeMessageByInstanceId(message.instanceId);
Gist.overlayInstanceId = null;
}
function loadMessageComponent(message, elementId = null, stepName = null) {
if (elementId && isElementLoaded(elementId)) {
log(`Message ${message.messageId} already showing in element ${elementId}.`);
return null;
}
var options = {
endpoint: settings.ENGINE_API_ENDPOINT[Gist.config.env],
siteId: Gist.config.siteId,
dataCenter: Gist.config.dataCenter,
messageId: message.messageId,
instanceId: message.instanceId,
livePreview: false,
properties: message.properties,
customAttributes: Object.fromEntries(getAllCustomAttributes())
}
var url = `${settings.GIST_VIEW_ENDPOINT[Gist.config.env]}/index.html`
window.addEventListener('message', handleGistEvents);
window.addEventListener('touchstart', handleTouchStartEvents);
if (elementId) {
if (positions.includes(elementId)) { addPageElement(elementId); }
loadEmbedComponent(elementId, url, message, options, stepName);
} else {
loadOverlayComponent(url, message, options, stepName);
}
return message;
}
async function reportMessageView(message) {
log(`Message shown, logging view for: ${message.messageId}`);
var response = {};
if (message.queueId != null) {
await logUserMessageViewLocally(message);
response = await logUserMessageView(message.queueId);
} else {
response = await logMessageView(message.messageId);
}
if (response.status === 200) {
log(`Message view logged`);
} else {
log(`Problem logging message: ${response.status}`);
}
}
function handleTouchStartEvents() {
// Added this to avoid errors in the console
}
async function handleGistEvents(e) {
if (e.data.gist && e.origin === settings.RENDERER_HOST) {
var currentInstanceId = e.data.gist.instanceId;
var currentMessage = fetchMessageByInstanceId(currentInstanceId);
if (!currentMessage) { return; }
var messageProperties = resolveMessageProperties(currentMessage);
switch (e.data.gist.method) {
case "routeLoaded": {
var timeElapsed = (new Date().getTime() - currentMessage.renderStartTime) * 0.001;
log(`Engine render for message: ${currentMessage.messageId} timer elapsed in ${timeElapsed.toFixed(3)} seconds`);
setMessageLoaded(currentMessage.queueId);
currentMessage.currentRoute = e.data.gist.parameters.route;
// Show component for first load or display change reload
if (currentMessage.firstLoad || currentMessage.isDisplayChange) {
if (currentMessage.overlay) {
showOverlayComponent(currentMessage);
} else {
showEmbedComponent(currentMessage.elementId);
}
// Only trigger events for actual first load, not display changes
if (currentMessage.firstLoad && !currentMessage.isDisplayChange) {
Gist.messageShown(currentMessage);
if (messageProperties.persistent) {
log(`Persistent message shown, skipping logging view`);
} else {
await reportMessageView(currentMessage);
}
}
currentMessage.firstLoad = false;
currentMessage.isDisplayChange = false;
}
updateMessageByInstanceId(currentInstanceId, currentMessage);
break;
}
case "tap": {
var action = e.data.gist.parameters.action;
var name = e.data.gist.parameters.name;
Gist.messageAction(currentMessage, action, name);
if (e.data.gist.parameters.system && !messageProperties.persistent) {
await hideMessage(currentMessage);
break;
}
try {
var url = new URL(action);
if (url && url.protocol === "gist:") {
var gistAction = url.href.replace("gist://", "").split('?')[0];
switch (gistAction) {
case "close":
await hideMessage(currentMessage);
await removePersistentMessage(currentMessage);
await logBroadcastDismissedLocally(currentMessage);
await checkMessageQueue();
break;
case "showMessage":
var messageId = url.searchParams.get('messageId');
var properties = url.searchParams.get('properties');
if (messageId) {
if (properties) {
properties = JSON.parse(atob(properties));
}
await Gist.showMessage({ messageId: messageId, properties: properties });
}
break;
case "loadPage":
url = url.href.substring(url.href.indexOf('?url=') + 5);
if (url) {
if (url.startsWith("mailto:") || url.startsWith("https://") || url.startsWith("http://") || url.startsWith("/")) {
window.location.href = url;
} else {
window.location.href = window.location + url;
}
}
break;
}
}
} catch {
// If the action is not a URL, we don't need to do anything.
}
break;
}
case "changeMessageStep": {
var displaySettings = e.data.gist.parameters.displaySettings;
var messageStepName = e.data.gist.parameters.messageStepName;
// Save message state (step + display settings) for persistent messages or show-always broadcasts
if (messageProperties.persistent || isShowAlwaysBroadcast(currentMessage)) {
await saveMessageState(currentMessage.queueId, messageStepName, displaySettings);
}
if (displaySettings && hasDisplayChanged(currentMessage, displaySettings)) {
log(`Display settings changed, reloading message`);
// Hide visually without side effects
await hideMessageVisually(currentMessage);
// Apply new display settings
applyDisplaySettings(currentMessage, displaySettings);
// Re-show message with new settings
await reloadMessageWithNewDisplay(currentMessage, messageStepName);
}
break;
}
case "routeChanged": {
currentMessage.currentRoute = e.data.gist.parameters.route;
currentMessage.renderStartTime = new Date().getTime();
updateMessageByInstanceId(currentInstanceId, currentMessage);
log(`Route changed to: ${currentMessage.currentRoute}`);
break;
}
case "sizeChanged": {
log(`Size Changed Width: ${e.data.gist.parameters.width} - Height: ${e.data.gist.parameters.height}`);
if (!currentMessage.elementId || currentMessage.shouldResizeHeight) {
resizeComponent(currentMessage, e.data.gist.parameters);
}
break;
}
case "titleChanged": {
log(`Overlay title changed to: ${e.data.gist.parameters.title}`);
changeOverlayTitle(currentInstanceId, e.data.gist.parameters.title);
break;
}
case "eventDispatched": {
Gist.events.dispatch("eventDispatched", { "name": e.data.gist.parameters.name, "payload": e.data.gist.parameters.payload });
break;
}
case "error":
case "routeError": {
Gist.messageError(currentMessage);
if (Gist.overlayInstanceId) {
resetOverlayState(false, currentMessage);
} else {
resetEmbedState(currentMessage);
}
break;
}
}
}
}
// Reload message with new display settings
async function reloadMessageWithNewDisplay(message, stepName) {
// Mark as display change reload to show component when routeLoaded is received
// but without triggering messageShown event or logging view
message.isDisplayChange = true;
message.renderStartTime = new Date().getTime();
// Determine elementId based on display type
var elementId = message.elementId || null;
// If moving to an elementId position, check if there's already a message there and dismiss it
if (elementId) {
const existingMessage = fetchMessageByElementId(elementId);
if (existingMessage && existingMessage.instanceId !== message.instanceId) {
log(`Dismissing existing message at ${elementId} to make room for multi-step message`);
await hideMessage(existingMessage);
}
}
// Update Gist.overlayInstanceId based on new display type
if (message.overlay) {
Gist.overlayInstanceId = message.instanceId;
// Set properties for overlay display
var properties = resolveMessageProperties(message);
message.shouldScale = properties.shouldScale;
message.shouldResizeHeight = true;
} else {
Gist.overlayInstanceId = null;
// Set properties for embedded display
message.shouldScale = false;
message.shouldResizeHeight = !elementHasHeight(elementId);
}
// Add page element if it's an overlay position
if (elementId && positions.includes(elementId)) {
addPageElement(elementId);
}
// Reload the message component with new settings
// Component will be shown when routeLoaded event is received
loadMessageComponent(message, elementId, stepName);
}
// Visual-only hide without side effects
async function hideMessageVisually(message) {
if (message.overlay) {
await hideOverlayComponent();
} else {
hideEmbedComponent(message.elementId);
}
// Note: We don't call removeMessageByInstanceId or clear Gist.overlayInstanceId
// to keep the message in memory for re-rendering
}
async function logUserMessageViewLocally(message) {
log(`Logging user message view locally for: ${message.queueId}`);
if (isMessageBroadcast(message)) {
await markBroadcastAsSeen(message.queueId);
} else {
await markUserQueueMessageAsSeen(message.queueId);
}
}
export async function logBroadcastDismissedLocally(message) {
if (isMessageBroadcast(message)) {
log(`Logging broadcast dismissed locally for: ${message.queueId}`);
await markBroadcastAsDismissed(message.queueId);
// Clear saved message state when broadcast is dismissed
await clearMessageState(message.queueId);
}
}