/** * @file zendesk-config.js * @desc Zendesk Messenger - Central Configuration * * This script provides central configuration for the Zendesk Messenger widget, * defining visual themes and dynamic conversation tagging rules based on URL * parameters and hostnames. * * === WHAT THIS FILE DOES ===================================================== * * 1. THEME: Defines all color tokens used by the Zendesk Messenger widget. * Modifying values here automatically updates the widget's appearance. * * 2. TAG RULES: Determines which conversation tags are sent to Zendesk when a * chat initiates. These tags are crucial for routing, reporting, * and bot behavior. * * === HOW TO USE ============================================================== * * Load this file once on every page, ensuring it executes before * `zendesk-bootstrap.js`. Upon loading, the following are exposed globally: * * `window.ZENDESK_CONFIG.theme` A frozen object containing color tokens. * `window.ZENDESK_CONFIG.getTags()` A function that returns an array of active tags. * * === HOW TO ADD A NEW TAG RULE =============================================== * * To add a new tag rule, append an object to the `TAG_RULES` array in Section 2. * Each rule object must contain two functions: * * `test(location)`: A function that returns a boolean, indicating whether * this rule should apply for the given URL. * `tags(location)`: A function that returns an array of strings, representing * the tags to be added if the `test` function returns true. * * No modifications to page templates are required; all tag logic is managed * within this file. Refer to the commented examples within `TAG_RULES` for * common patterns. * * === EVENTS ================================================================== * * This script dispatches a `zendesk-config:ready` CustomEvent on `window` once * fully initialized. Bootstrap scripts can listen for this event before * accessing `window.ZENDESK_CONFIG`. * * === BROWSER SUPPORT ========================================================= * * Requires `URLSearchParams` and `Array.from`. Compatible with all modern * browsers; Internet Explorer is not supported. * * ============================================================================= */ (function bootstrap(global) { 'use strict'; // ========================================================================= // SECTION 1 - VISUAL THEME // // Color tokens consumed directly by Zendesk Messenger. // Naming convention: // `someElement` → the background or fill color of that element // `onSomeElement` → the text / icon color drawn ON TOP of it // // Every token is a hex string. Adjust values here only - never inline // colors elsewhere. // ========================================================================= const THEME = Object.freeze({ // == Widget launcher & conversation header ================= primary: '#8a2ce8', // header + launcher background onPrimary: '#ffffff', // header/launcher text & icons // == End-user (visitor) message bubbles ==================== message: '#a456f6', // end-user message bubbles onMessage: '#ffffff', // end-user message text // == Agent / bot message bubbles =========================== businessMessage: '#f5f6f8', // bot/agent message bubbles onBusinessMessage: '#192642', // bot/agent message text // == Buttons & quick-reply chips =========================== action: '#1d2124', // button / quick-reply background onAction: '#ffffff', // button / quick-reply text // == Chat window background ================================ background: '#ffffff', // chat window background onBackground: '#1d2124', // timestamps, names, meta text // == Error / failure states ================================ error: '#cd3642', // error / failure states onError: '#ffffff', // error / failure text // == System / notification banners ======================== notify: '#cce0f0', // system / notification banners onNotify: '#13456d', // system / notification text // == Secondary / disabled actions ========================= onSecondaryAction: '#1d2124', // secondary / disabled action text }); // ========================================================================= // SECTION 2 - CONVERSATION TAG RULES // // `TAG_RULES` is the definitive list of routing decisions for this widget. // // How evaluation works: // • Rules are processed sequentially from top to bottom on every chat open. // • Any rule whose `test()` function returns `true` will contribute its tags. // • Rules are additive; multiple rules can fire simultaneously. // • Duplicate tags are automatically removed before being sent to Zendesk. // // The `location` parameter passed to `test()` and `tags()` is the standard // browser `Location` / `URL` object. For unit testing, a synthetic URL object // can be injected to decouple from the real browser address bar. // // Rule structure: // `{ // test: (location) => boolean, // tags: (location) => string[] // }` // // Both `test()` and `tags()` functions are mandatory for each rule. // ========================================================================= /** * Detects the recruiting feature-flag in the URL query string. * This function supports two formats for backward compatibility: * * 1. Standard (new links): `?recruiting_page=universal-testing` * 2. Legacy (old links): `?parameter1+recruiting_page=universal-testing` * (The browser decodes `+` in a key as a space, so the key becomes * `"parameter1 recruiting_page"` - its suffix is still `"recruiting_page"`.) * * The parameter's value is not checked; its mere presence is sufficient. * * @param {URLSearchParams} params - An already-parsed `URLSearchParams` object. * @returns {boolean} - True if the recruiting page flag is present, false otherwise. */ const isRecruitingPageFlagSet = (params) => { // Fast path: check for the standard key directly. if (params.has('recruiting_page')) { return true; } // Slow path: iterate through all keys to find the legacy format. // This is acceptable as real URLs typically have a small number of parameters. for (const key of params.keys()) { if (key.endsWith('recruiting_page')) { return true; } } return false; }; /** * Defines the structure for a tag rule. * @typedef {Object} TagRule * @property {(location: Location | URL) => boolean} test - Function to test if the rule applies. * @property {(location: Location | URL) => string[]} tags - Function to return tags if the rule applies. */ /** * The list of conversation tag rules. * @type {TagRule[]} */ const TAG_RULES = [ // == Rule 1: Recruiting pages ========================================= // // This rule fires under two conditions: // 1. The current hostname matches a known recruiting domain (production or staging). // 2. The URL contains the `recruiting_page` feature-flag query parameter. // // The query-parameter path is intentionally unrestricted by hostname, // allowing testers and QA to activate recruiting behavior from any environment. // // Standard flag: `?recruiting_page=universal-testing` // Legacy flag: `?oldparam+recruiting_page=universal-testing` // // Result tag: `'recruiting_page'` { test: (location) => { const host = location.hostname; const params = new URLSearchParams(location.search); const isKnownRecruitingHost = host === 'recruiting.xing.com' || host === 'staging.recruiting.xing.com'; return isKnownRecruitingHost || isRecruitingPageFlagSet(params); }, tags: () => ['recruiting_page'], }, // == Rule 2: Audience segment ========================================= // // This rule reads the `?segment=` query parameter and maps it to a routing tag. // Only the values `'b2b'` and `'b2c'` are recognized; any other value is // ignored (no tag added, no error). // // This rule works on any path, and additional query parameters do not interfere. // // Matching examples: // `https://help.xing.com/hc/en-us?segment=b2b` // `https://help.xing.com/hc/de/categories/123?utm_source=nl&segment=b2c` // // Result tags: `'segment_b2b'` or `'segment_b2c'` { test: (location) => { const params = new URLSearchParams(location.search); const segment = params.get('segment'); return segment === 'b2b' || segment === 'b2c'; }, tags: (location) => { const params = new URLSearchParams(location.search); const segment = params.get('segment'); return [`segment_${segment}`]; // e.g., 'segment_b2b' or 'segment_b2c' }, }, // == Add new rules here =============================================== // // Append new rule objects below. Avoid modifying page templates. // Each rule MUST define both `test()` and `tags()`. A missing `tags()` // will result in a console warning and the rule being skipped, preventing // a crash of the entire widget. // // Pattern examples: // // Country routing (URL path prefix) // `{ test: (l) => l.pathname.startsWith('/de'), // tags: () => ['country_de'] }` // // A/B experiment (50 / 50 random split) // `{ test: () => Math.random() < 0.5, // tags: () => ['ab_variant_b'] }` // // Authenticated users (session storage flag) // `{ test: () => !!sessionStorage.getItem('uid'), // tags: () => ['authenticated'] }` // // Product line (URL path segment) // `{ test: (l) => l.pathname.includes('/pro'), // tags: () => ['product_pro'] }` // // Multi-brand (second hostname) // `{ test: (l) => l.hostname === 'brand2.xing.com', // tags: () => ['brand_2'] }` // ]; // ========================================================================= // SECTION 3 - getTags( [location] ) // // Public function. Executes every rule in `TAG_RULES` against the provided // (or current) URL and returns a flat, deduplicated array of tag strings. // // Rules with a missing `tags()` function are gracefully skipped with a // console warning, ensuring that a misconfigured rule does not break the // entire widget. // // @param {Location|URL} [location=global.location] - The URL object to test against. // Defaults to `window.location`. // Inject a synthetic URL object for unit tests. // @returns {string[]} - An array of deduplicated tags, preserving their original // collection order. // ========================================================================= const getTags = (location = global.location) => { let activeTags = []; for (let i = 0; i < TAG_RULES.length; i++) { const rule = TAG_RULES[i]; // Safety guard: Ensure `tags()` function exists to prevent runtime errors. if (typeof rule.tags !== 'function') { console.warn( `[ZENDESK_CONFIG] TAG_RULES[${i}] is missing a tags() function and will be skipped. ` + `Every rule must define both test() and tags().`, ); continue; } if (rule.test(location)) { activeTags = activeTags.concat(rule.tags(location)); } } // Remove duplicates while preserving insertion order. // Using `Array.filter` for consistency with the original code's ES5 approach. return activeTags.filter( (tag, index) => activeTags.indexOf(tag) === index, ); }; // ========================================================================= // SECTION 4 - PUBLIC API // // Exposed as a frozen object to prevent accidental runtime modification of // theme tokens or the `getTags` reference from outside this file. // // Accessible via: `window.ZENDESK_CONFIG` // ========================================================================= global.ZENDESK_CONFIG = Object.freeze({ theme: THEME, getTags: getTags, }); // Notify any bootstrap scripts that are waiting for this file to finish. // Listen with: `window.addEventListener('zendesk-config:ready', fn)` global.dispatchEvent(new CustomEvent('zendesk-config:ready')); })(window);