Static white-label banking demo app for Genesys Cloud demonstrations. No backend, no build step, no framework — plain HTML/CSS/vanilla JS served as static files.
Keep the documentation up to date automatically — as part of the same change, without being asked. Whenever a change alters documented behaviour, update every affected doc in the same commit. This is part of the definition of done; a change that leaves the docs stale is incomplete.
Treat the following as documentation that must track the code:
README.mdandREADME.en.md— always updated together and mirrored (FR/EN); any change to one must be applied to the other in the same edit.AGENTS.md— architecture, module list, script load order, settings panels, storage schema, localStorage keys, public APIs, and the patterns/anti-patterns tables.js-api.html— the custom-JS API reference: any new/changed variable or helper exposed toadditionalJS/advisorAdditionalJS(e.g. a newSalesforce.*method) must be reflected here, with an example when it is non-trivial.
Trigger points that almost always require a doc update: adding/removing a page, JS module,
or settings panel; changing the DEFAULT_PROFILE schema or a localStorage key; adding or
renaming a public method on DemoConfig / DemoGenesys / DemoSalesforce / DemoAudioCodes;
changing the variables/functions injected into the custom-JS snippets; or changing an OAuth
/ integration setup requirement.
node server.js # starts at http://localhost:5500/
PORT=5500 node server.js # or any other port
# or via npm:
npm startserver.js is a zero-dependency static file server (Node built-ins only, no npm install).
It serves the app/ directory as the web root. Requires Node 18+.
crypto.subtle (used by PKCE) requires a secure context — localhost qualifies.
Register the following URIs in Genesys Cloud Admin → OAuth → your client:
For live demo on https://poninsor.github.io/demobanque/
https://poninsor.github.io/demobanque/app/index.html
https://poninsor.github.io/demobanque/app/settings.html
https://poninsor.github.io/demobanque/app/contact.html
For local development
http://localhost/index.html
http://localhost/settings.html
http://localhost/contact.html
Genesys Cloud ignores the port number for localhost URIs — the three localhost
entries above cover all local ports (5500, 5500, 8080, etc.).
Why
index.htmlexplicitly?window.location.pathnamereturns/index.htmlwhen navigating to that page directly, not/. The registered redirect URI must match exactly what the app sends.
app/
index.html Login page (account number + PIN)
dashboard.html Dashboard — total assets, transactions, chart
account.html Checking account — transaction list
transfer.html Transfer wizard (3 steps)
cards.html Credit cards
beneficiaries.html Saved recipients
credits.html Loans
messages.html Secure messaging — client view, file attachments, advisor presence
advisor.html Advisor view — all conversations, real-time replies, presence heartbeat
contact.html Contact — channels, appointment booking, ASAP callback
settings.html Settings — 10 panels (backup, brand, persona, balances, products,
language, security, Genesys Cloud, AudioCodes WebRTC, Salesforce)
js-api.html Custom-JS API reference — variables and helpers available in the
additionalJS / advisorAdditionalJS snippets (Genesys + Salesforce),
with copyable examples; linked from the Settings info-bubbles
config.js DemoConfig — all localStorage read/write, auth, branding
genesys.js DemoGenesys — OAuth PKCE flow, Genesys Cloud API calls,
unified call entry point (delegates to AudioCodes or tel:)
salesforce.js DemoSalesforce — Salesforce OAuth (Authorization Code + PKCE) and
REST helpers (create/get/update for Task, Contact, Case; query);
depends on config.js only, so it is safe on advisor.html
audiocodes.js DemoAudioCodes — AudioCodes WebRTC integration (custom UI on top
of the standalone SDK), with SIP extraHeaders and call restore
shell.js Global shell behaviours (mobile sheet, Messenger snippet)
shell.css Shared layout styles (nav, bottom sheet, profile card)
audiocodes.css Floating call button + call panel styles, brand-aligned
colors.css CSS custom properties (brand palette, semantic tokens)
lib/
audiocodes/ AudioCodes SDK — vendor files, never modify
click-to-call.js Standalone AudioCodesUA SDK + JsSIP. THIS is the file
loaded at runtime by audiocodes.js — the app builds its
own UI on top so call state and persistence are in our hands.
click-to-call-widget.js Self-contained widget bundle (Web Component + SDK + JsSIP).
NOT used — the widget's "Restore Call after Page Refresh"
feature is broken (unbound beforeunload handler) and the
widget cannot reconnect to the same agent after a refresh
per official AudioCodes guidance.
basic.js / advanced.js / advancedauthUrl.js
Reference snippets only. Never loaded.
Every JS file exports a single IIFE-based object (DemoConfig, DemoGenesys,
DemoSalesforce, DemoAudioCodes). No ES modules, no bundler. Scripts are loaded via
<script src="..."> in order:
shell.js → config.js → genesys.js → salesforce.js → audiocodes.js → inline page script.
Each page loads only what it needs — the full chain is the pattern, not a requirement:
advisor.htmlloadsconfig.js+salesforce.jsonly (no shell, no Genesys, no AudioCodes)index.htmlloadsconfig.js+genesys.js(no shell.js, no AudioCodes)index.htmllinkscolors.cssdirectly; all other pages linkshell.css, which pulls in the tokens via@import url("colors.css")
audiocodes.js and audiocodes.css are loaded on every authenticated page (all except
index.html and advisor.html) so that an active SIP call survives navigation — the SDK
state and the active-call snapshot are persisted to localStorage on beforeunload /
pagehide and the SDK is re-initialised on the next page's auto-init.
- All code comments must be in English. No French in
//or/* */blocks. - UI strings (labels, toasts, error messages) remain bilingual (FR/EN) using the
t(fr, en)helper ori18n-fr/i18n-enCSS classes. - Do not translate or touch visible UI text unless the task explicitly requires it.
- Vanilla JS only. No TypeScript, no React, no Vue, no lodash.
- Prefer
const/let. Nevervar. - Arrow functions for callbacks; named functions for anything called by name.
- No unnecessary abstractions. Three similar lines beat a premature helper.
- Do not add error handling for scenarios that cannot happen (trust internal guarantees).
- Do not add feature flags, backwards-compatibility shims, or dead code.
Dynamic values (API responses, user input, err.message) must be set via DOM methods
to prevent XSS:
// Correct
const el = document.createElement('span');
el.textContent = userValue;
parent.appendChild(el);
// Wrong — XSS risk
parent.innerHTML = `<span>${userValue}</span>`;Static markup (hardcoded SVGs, fixed templates with no dynamic parts) may use innerHTML.
Prefer el.replaceChildren(...nodes) over el.innerHTML = '' followed by appends.
Attach listeners via addEventListener, not onclick="..." inline attributes —
except in static markup where the handler is a simple named function call.
| Store | Usage |
|---|---|
localStorage['demobank_v1'] |
User profiles — main object (config, persona, messages) |
localStorage['demobank_gc_token'] |
Genesys OAuth token cache { token, expiry, clientId } |
localStorage['demobank_lang'] |
Global language preference, read before the profile loads |
localStorage['demobank_adv_active'] |
Advisor presence heartbeat — { accountId, ts } written every 15 s by advisor.html; read by messages.html to show online/offline status; TTL 30 s |
localStorage['demobank_client_active'] |
Client presence heartbeat — { accountId, ts } written every 15 s by messages.html; read by advisor.html to show online/offline status; TTL 30 s |
localStorage['demobank_ac_restore'] |
AudioCodes call snapshot for cross-page restoration |
localStorage['demobank_sf_token'] |
Salesforce OAuth token cache { token, instanceUrl, expiry, clientId } |
localStorage['demobank_settings_collapsed'] |
Settings page — array of collapsed panel ids (fold state) |
sessionStorage |
Transient OAuth state: _gc_code_verifier, _gc_oauth_state, _gc_post_login_redirect, _gc_pending |
sessionStorage['demobank_ac_mic'] / ['demobank_ac_spk'] |
Selected microphone / speaker device index in the AudioCodes call panel |
Always read the profile once at the start of a function and reuse the local variable.
Do not call DemoConfig.getProfile() multiple times in a single operation.
The app uses OAuth 2.0 Authorization Code with PKCE (no client secret). The Genesys Cloud OAuth client type must be set to "Code Authorization" with "PKCE Required" enabled.
_buildAuthUrl(gc, redirectUri)— generatescode_verifier,code_challenge(SHA-256),state; stores verifier + state insessionStorage; returns the authorization URL.- Page navigates to Genesys login.
- Genesys redirects back to the originating page with
?code=&state=in the query string (never in the hash). - The IIFE in
genesys.jsruns async on page load, detects?code=, validatesstate, exchanges the code viaPOST /oauth/token, saves the token tolocalStorage. - Page redirects to
_gc_post_login_redirect(set before step 2).
- Never use
response_type=token(Implicit Grant is disabled). redirect_urimust bewindow.location.origin + window.location.pathname(no hash, no query string) and must be registered in the Genesys Cloud OAuth client settings.- Pages that auto-call
fetchTokenon load (e.g.fetchWaitTime) must always pass{ noRedirect: true }to avoid triggering a redirect loop during the PKCE callback. - Pages with an auto-redirect on existing profile (e.g.
index.html) must skip that redirect when?code=is present in the URL:if (DemoConfig.getProfile() && !new URLSearchParams(location.search).has('code')) window.location.href = 'dashboard.html';
Token is stored in localStorage under key demobank_gc_token as
{ token, expiry, clientId }. It is loaded by _loadToken() on every page init
and validated against the current clientId and expiry timestamp.
Managed by DemoAudioCodes in audiocodes.js. The app uses the standalone SDK
(lib/audiocodes/click-to-call.js, AudioCodesUA singleton) and renders its own UI
(floating FAB + call panel) styled with the project's brand variables. The bundled
widget (click-to-call-widget.js) is intentionally NOT used because its
"Restore Call after Page Refresh" feature is broken and, per official AudioCodes
guidance, custom UI is required for cross-page call continuity.
audiocodes.jsauto-invokesinit()onDOMContentLoadedon every authenticated page._initializedguards against re-entry.- Guard: returns immediately if
audiocodes.enabledis false, ordomain/calleris blank. - The UI is built once in the DOM: hidden
<audio id="dac-audio">, floating button#dac-fab, and call panel#dac-panel. All elements use CSS classes defined inaudiocodes.cssand inherit brand variables (--brand,--brand-fg,--success-500,--danger-500,--font-body). lib/audiocodes/click-to-call.jsis loaded dynamically as a<script defer>.- On
load:AudioCodesUA.instanceis configured viasetServerConfig,setAccount,setListeners,setModes,setWebSocketKeepAlive, theninit(false). - SDK events drive UI state machine:
idle → connecting → ringing → in-call ↔ on-hold. Audio playback is attached oncallShowStreamsviaaudio.srcObject = call.data._remoteMediaStream. ?call=1on the URL auto-triggersDemoGenesys.call(_urlHeaders)after init (chatbot use case), which in turn callsDemoAudioCodes.call(urlHeaders)when AudioCodes is enabled. Any additional URL params whose name starts withX-User-are collected and forwarded as extra SIP headers on top of the auto-generated profile headers:→ addscontact.html?call=1&X-User-ParentConnID=abc123&X-User-Segment=premiumX-User-ParentConnID: abc123andX-User-Segment: premiumto the INVITE headers.
DemoAudioCodes.call(urlHeaders?) // dial genesys.internalCallNumber (fallback: callNumber);
// urlHeaders is an optional string[] of 'Name: Value' headers
DemoAudioCodes.callTo(number, headers?) // dial arbitrary number; headers override auto-built set
DemoAudioCodes.hangup() // terminate active call
DemoAudioCodes.mute(on?) // toggle or set audio mute
DemoAudioCodes.hold(on?) // toggle or set local hold
DemoAudioCodes.sendDTMF(digit) // send a single DTMF tone
DemoAudioCodes.isEnabled() // bool — config is valid
DemoAudioCodes.getState() // 'idle'|'connecting'|'ringing'|'in-call'|'on-hold'Every outgoing call attaches a set of custom SIP headers built from the current profile:
| Header | Source |
|---|---|
X-User-FirstName |
persona.firstName |
X-User-LastName |
persona.lastName |
X-User-Email |
persona.email |
X-User-Phone |
persona.phone |
X-User-ProfileType |
persona.profileType |
X-User-AdvisorName |
persona.advisor |
X-User-AccountId |
DemoConfig.getCurrentAccountId() |
Additional headers are appended from the textarea #ac-extra-headers in the settings
panel (audiocodes.extraHeaders, one per line, format Name: Value). These are passed
as the third argument to phone.call(phone.AUDIO, number, headers). Use the
X-User-* prefix and never X-Genesys-* / X-inin-* / X-pcv-* / X-gcv-*
(those prefixes are reserved by Genesys internals).
In a Genesys Cloud Architect flow, retrieve these headers with the "Get SIP Headers" action and route on the values.
On beforeunload / pagehide, if _activeCall && _activeCall.isEstablished(), the
module snapshots the call to localStorage['demobank_ac_restore']:
{
callTo, video, replaces, time, hold, mute, extraHeaders
}On the next page's init, the snapshot is read (and deleted) before the SDK script
loads. After the SDK reports loginStateChanged: cause === 'connected', the module
issues a new INVITE with ['Replaces: <dialog>', ...extraHeaders] as extra headers
to take over the existing dialog. The 20-second restoreCallMaxDelay window from the
doc is enforced. Restoration depends on the SBC keeping the dialog alive long enough;
if the dialog has been torn down server-side, the SBC returns 481 Call/Transaction Does Not Exist and the call cannot be resumed (frontend has done everything correctly).
audiocodes: {
enabled: false, // master toggle
domain: '', // SIP server FQDN
wssAddress: '', // optional WSS override; default = 'wss://' + domain
caller: '', // SIP username
password: '', // SIP password
extraHeaders: '' // raw text, one header per line ('Name: Value')
}All phone call triggers must go through DemoGenesys.call(urlHeaders?), not directly to DemoAudioCodes.call(). DemoGenesys.call() inspects DemoAudioCodes.isEnabled() and delegates accordingly:
// genesys.js
call(urlHeaders) {
if (DemoAudioCodes.isEnabled()) { DemoAudioCodes.call(urlHeaders); return; }
const digits = (gc.callNumber || '').replace(/\D/g, '');
if (digits) window.location.href = `tel:${digits}`;
else showNotif(t('Aucun numéro configuré', 'No number configured'), true);
}- Phone buttons in
contact.htmlcallDemoGenesys.call() - The
?call=1auto-trigger oncontact.htmlcallsDemoGenesys.call(_urlHeaders) DemoAudioCodes.call()is never called directly from page code; only fromDemoGenesys.call()
When AudioCodes is disabled or not configured, DemoGenesys.call() falls back to
window.location.href = 'tel:' + digits using genesys.callNumber. The FAB is not
rendered in this case.
app/lib/audiocodes/click-to-call.js — standalone SDK + JsSIP, loaded at runtime.
Never modify. click-to-call-widget.js, basic.js, advanced.js, advancedauthUrl.js
are references only and never loaded by the app.
Managed by DemoSalesforce in salesforce.js. Provides REST helpers for the Task,
Contact and Case objects, usable directly from the custom-JS snippets via the injected
Salesforce variable. salesforce.js depends on config.js only (no genesys.js, no
shell.js) so it is safe to load on advisor.html. Loaded on settings.html,
messages.html and advisor.html.
Same shape as the Genesys flow in genesys.js. The token-exchange POST to
${loginUrl}/services/oauth2/token and every REST call run in the browser, so:
- The Connected App must have PKCE required and no client secret.
- The
settings.htmlURL must be a registered Callback URL. - The app origin must be on the org's CORS allowlist (Setup → CORS). Without it, the token exchange and REST calls fail with a CORS error — this is an org-side config gap, not a frontend bug.
salesforce: {
enabled: false, // master toggle
loginUrl: 'https://login.salesforce.com', // or https://test.salesforce.com (sandbox)
clientId: '', // Connected App Consumer Key
apiVersion: 'v60.0', // REST API version
contactId: '' // Salesforce Contact ID linked to the persona;
// configured in the Persona panel (shown only when
// enabled=true); injected as salesforce.contactId
// in all custom JS snippets; used as ContactId /
// AccountId when creating Cases, Opportunities, Tasks
}{ token, instanceUrl, expiry, clientId }. instanceUrl comes from the token response
(instance_url) and is the base for REST calls. Salesforce does not return expires_in,
so expiry is a soft 2h hint; a 401 from the API is the real source of truth.
Both providers redirect back to settings.html?code=&state=. Each load-time IIFE only
acts on the callback when it initiated the flow — i.e. its own state is present in
sessionStorage (_gc_oauth_state / _sf_oauth_state). If that key is absent, the IIFE
returns without wiping the URL, leaving the ?code= for the other provider's handler.
genesys.js has an explicit guard for this; do not remove it.
DemoSalesforce.create(sobject, fields) // POST /sobjects/{sobject} → { id, success }
DemoSalesforce.get(sobject, id, fields?) // GET /sobjects/{sobject}/{id} → record
DemoSalesforce.update(sobject, id, fields) // PATCH /sobjects/{sobject}/{id} → true (204)
DemoSalesforce.query(soql) // GET /query?q=… → { totalSize, records }
DemoSalesforce.sfFetch(path, init) // low-level fetch (auto Bearer, JSON body)
DemoSalesforce.sfFetchJSON(path, init) // same, throws on non-2xx, parses JSON
DemoSalesforce.isEnabled() // config.enabled && clientId
DemoSalesforce.getTokenStatus() // { connected, expiresAt, instanceUrl }
DemoSalesforce.clearToken()
DemoSalesforce.redirectForAuth(sf, redirectUri)sobject accepts any standard or custom API name: 'Case', 'Opportunity', 'Contact',
'Task', 'Custom_Object__c', etc. update is PATCH (HTTP 204, returns true).
The full reference and examples live in js-api.html.
The lightbox (openViewer / closeViewer + #img-viewer* CSS), escapeHtml, getTime
and the attachment CSS (.attach-card, .attach-img*) are deliberately duplicated in
both pages: advisor.html loads only config.js, there is no module system, and the two
copies are small enough that a shared file is not worth the extra script tag. Rule: any
change to one copy must be mirrored in the other. Accepted divergences: .attach-card
max-width is 320px in messages.html vs 280px in advisor.html (narrower panel), and
renderMessage() differs by design (client vs advisor perspective flip, avatar handling).
Clients can attach files to messages. Attachment state is held in _pendingFile before send.
Allowed types (_ALLOWED_TYPES / _isAllowedType()):
image/*— JPEG, PNG, WebP, GIF — rendered as inline thumbnail, full-screen in lightboxapplication/pdf— rendered as an attach-card; PDF preview via<iframe>+ blob URL in lightboxapplication/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document— card only, direct download on clickapplication/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet— card only, direct download on click
Size limit: 2 MB per file (checked before FileReader). Toast error if exceeded.
Quota guard: if getStorageUsageBytes() > 4 MB, a confirm() dialog offers to purge fileData from old messages before sending. purgeMessageAttachments(accountId) removes binary payloads while preserving the message card.
Message schema (type 'file'):
{ id, from, type: 'file', fileName, fileSize, mimeType, fileData /* data URL */, time }Rendering (renderMessage()):
image/*withfileData→<button class="attach-img-btn">wrapping<img class="attach-img">→ click opens lightboxapplication/pdfwithfileData→<button>wrapping.attach-card(withline-height:normal) → click opens lightbox with<iframe>+ blob URL- Non-previewable with
fileData→.attach-cardwrapped in<a download>→ direct download - Any type without
fileData(purged or legacy) → static.attach-card, no click action
Both messages.html and advisor.html include an identical #img-viewer overlay:
- Images:
<img id="img-viewer-img">—srcset tofileData - PDFs:
<iframe id="img-viewer-pdf">—srcset to a blob URL created from the base64 data; blob URL is revoked oncloseViewer()to prevent memory leaks - Download button wraps the file data as an
<a download>link - Close: backdrop click, ✕ button, or Escape key
advisor.html maintains a heartbeat in localStorage so messages.html can show whether the advisor is currently viewing the conversation.
In advisor.html:
_writeAdvisorActive(accountId)writes{ accountId, ts: Date.now() }todemobank_adv_activeselectAccount(id)calls_writeAdvisorActive()immediately and starts asetInterval(15 s)deselectAccount()calls_clearAdvisorActive()and stops the intervalbeforeunloadcalls_clearAdvisorActive()—visibilitychangedoes NOT clear it (tab switch is not a disconnect)
In messages.html:
_advisorIsPresent()reads the key and returnstrueifaccountIdmatches andtsis < 30 s old_updateAdvisorStatus()updates the#conv-rolebadge (green dot = online, grey = offline)- A
storageevent listener onwindowdetectsdemobank_adv_activechanges in real time - A 30 s
setIntervalis a safety net for crash scenarios where nostorageevent fires sendMessage()skipsexecuteAdditionalJS()when_advisorIsPresent()is true
Account deselection in advisor.html:
Clicking empty space inside #threads-panel (anywhere that is not a .thread element) calls deselectAccount(), which resets selectedAccountId, clears the heartbeat, removes the ?account= URL param, and hides the active conversation panel.
messages.html maintains a heartbeat in localStorage so advisor.html can show whether the client is currently viewing the secure conversation.
In messages.html:
_writeClientActive()writes{ accountId, ts: Date.now() }todemobank_client_activeDOMContentLoadedcalls_writeClientActive()immediately and starts asetInterval(15 s)beforeunloadclears the interval and calls_clearClientActive()visibilitychangemust NOT clear it (tab switching is not a disconnect)
In advisor.html:
_clientIsPresent(accountId)reads the key and returnstrueifaccountIdmatches andtsis < 30 s old_updateClientStatus()updates the#conv-client-statusbadge (green dot = online, grey = offline)- A
storageevent listener onwindowdetectsdemobank_client_activechanges in real time - A 30 s
setIntervalis a safety net for crash scenarios where nostorageevent fires
Three configurable JavaScript snippets are grouped in the Personnalisation & Automatisation
panel in settings.html (panel id #automation). All are stored in the profile and executed
via DemoConfig.runConfiguredJS() through dedicated executor functions.
| Profile key | Executor | Trigger |
|---|---|---|
additionalJS |
executeAdditionalJS(runtime) |
messages.html — client sends a message, advisor not present |
advisorAdditionalJS |
executeAdvisorAdditionalJS(accountId, runtime) |
advisor.html — advisor sends a message, client not present |
creditSimulationJS |
executeCreditSimulationJS(runtime) |
credits.html — client clicks "Soumettre ma simulation" |
All three snippets execute through DemoConfig.runConfiguredJS() with a shared injected
runtime context. Variables available in every snippet:
token,apiBaseUrl,apiUrl(path),fetchGenesys(path, init),fetchGenesysJSON(path, init)profile/settings— full profile objectpersona,genesys,balances,products,audiocodes,salesforcesalesforce.contactId— Salesforce Contact ID linked to the persona; configured in the Persona panel (visible when Salesforce is enabled); used asContactId/AccountIdwhen creating Cases, Opportunities, TasksSalesforce— theDemoSalesforceobject (ornullifsalesforce.jsis not loaded)accountId,threadId,language,tutoiement,role
Additional variables injected per snippet:
additionalJS/advisorAdditionalJS:message,messageTextcreditSimulationJS:loanType,loanLabel,projectNature,amount,months,rate,monthly,totalCost
Rules:
- Keep snippet examples ASCII-only unless the surrounding file already requires otherwise
- Do not add any new
new Function(...)execution path outsidemessengerSnippet,additionalJS,advisorAdditionalJS, andcreditSimulationJS advisor.htmldoes not loadshell.js; advisor-side snippets must work withconfig.jsonly- Snippets must stay non-blocking from the page UX perspective: log errors, do not crash the flow
Two mechanisms coexist:
CSS classes (preferred for static markup):
<span class="i18n-fr">Bonjour</span>
<span class="i18n-en">Hello</span>html[lang="en"] .i18n-fr { display: none } and vice-versa.
t(fr, en) helper (JS strings only, inside genesys.js):
showNotif(t('Connexion réussie', 'Connection successful'), false);syncI18nInputs() pattern (for <option> and <textarea> whose value must
change with language): uses data-fr / data-en attributes and a MutationObserver
on document.documentElement[lang].
The MutationObserver should only call syncI18nInputs. Do not piggyback unrelated
logic (e.g. updateOAuthStatus) onto that observer.
All colours come from colors.css via CSS custom properties (--brand, --brand-500,
--fg, --bg, etc.). Never hardcode hex values in JS or inline styles except in SVG
icons that require a stroke/fill attribute.
DemoConfig.applyBranding() regenerates the palette from the stored primaryColor and
writes it to :root. Call it after any profile update that may change the colour.
In-page toast (settings.html, messages.html, and other pages): showToast(msg, isError).
Must update the icon attribute (data-lucide) to reflect the error state, then call
lucide.createIcons(). Each page that needs toasts must include its own #toast element and the showToast function — there is no shared implementation.
Genesys overlay notification (genesys.js): showNotif(message, isError).
Text is set via createTextNode (not innerHTML). Supports \n for line breaks.
| Anti-pattern | Why |
|---|---|
innerHTML with dynamic data |
XSS |
new Function(code)() for anything other than messengerSnippet, additionalJS, advisorAdditionalJS, or creditSimulationJS |
arbitrary code execution |
response_type=token |
Implicit Grant is deprecated and disabled |
{ noRedirect: false } in auto-called fetchToken |
triggers redirect loops on callback pages |
p.persona.firstName without guard |
TypeError if profile is incomplete |
parseInt(str) without radix |
implicit base-8 for strings starting with 0 |
Date.getMonth() >= 3 && <= 9 for Paris DST |
wrong boundary; use getTimezoneOffset() |
Hardcoded Paris UTC offset (+02:00 / +01:00) |
DST transitions are date-dependent |
MutationObserver with mixed responsibilities |
can create side-effect loops |
Calling DemoConfig.getProfile() more than once per function |
redundant JSON.parse |
Calling DemoAudioCodes.call() directly from page code |
bypasses the unified entry point; always use DemoGenesys.call() |
visibilitychange to clear advisor heartbeat |
tab-switching is not a disconnect; use beforeunload only |
| Blob URL not revoked after lightbox close | memory leak; always call URL.revokeObjectURL() in closeViewer() |