-
Notifications
You must be signed in to change notification settings - Fork 731
chore: add intercom (IN-1028) #3945
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -9,6 +9,7 @@ import { watch } from 'vue'; | |||||
| import config from '@/config'; | ||||||
| import { setRumUser } from '@/utils/datadog/rum'; | ||||||
| import useSessionTracking from '@/shared/modules/monitoring/useSessionTracking'; | ||||||
| import { boot as bootIntercom, shutdown as shutdownIntercom } from '@/utils/intercom'; | ||||||
|
|
||||||
| export default { | ||||||
| init() { | ||||||
|
|
@@ -36,6 +37,19 @@ export default { | |||||
| if (user) { | ||||||
| setRumUser(user); | ||||||
| lfxHeader.authuser = user; | ||||||
|
|
||||||
| const intercomJwt = user[config.intercom.auth0IntercomClaim]; | ||||||
| const userId = user[config.intercom.auth0UsernameClaim]; | ||||||
| if (userId && intercomJwt) { | ||||||
| bootIntercom({ | ||||||
| user_id: userId, | ||||||
| name: user.name, | ||||||
| email: user.email, | ||||||
| intercom_user_jwt: intercomJwt, | ||||||
| }).catch((error: any) => { | ||||||
| console.error('Intercom: Boot failed', error); | ||||||
|
||||||
| console.error('Intercom: Boot failed', error); | |
| console.error('Intercom: Boot failed:', (error && error.message) ? error.message : String(error)); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,167 @@ | ||||||||||||||||||||
| /* eslint-disable @typescript-eslint/no-unused-vars */ | ||||||||||||||||||||
| /* eslint-disable no-use-before-define */ | ||||||||||||||||||||
| import config from '@/config'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| declare global { | ||||||||||||||||||||
| interface Window { | ||||||||||||||||||||
| Intercom?: any; | ||||||||||||||||||||
| intercomSettings?: any; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let isLoaded = false; | ||||||||||||||||||||
| let isBooted = false; | ||||||||||||||||||||
| let isLoading = false; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export interface IntercomBootOptions { | ||||||||||||||||||||
| user_id: string; | ||||||||||||||||||||
| name?: string; | ||||||||||||||||||||
| email?: string; | ||||||||||||||||||||
| intercom_user_jwt?: string; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const loadScript = (): void => { | ||||||||||||||||||||
| if (isLoaded || isLoading || typeof window === 'undefined') { | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| isLoading = true; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Create stub so queued commands work before script loads | ||||||||||||||||||||
| const w = window as any; | ||||||||||||||||||||
| const ic = w.Intercom; | ||||||||||||||||||||
| if (typeof ic === 'function') { | ||||||||||||||||||||
| ic('reattach_activator'); | ||||||||||||||||||||
| ic('update', w.intercomSettings); | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| const i: any = (...args: any[]) => { i.c(args); }; | ||||||||||||||||||||
| i.q = []; | ||||||||||||||||||||
| i.c = (args: any) => { i.q.push(args); }; | ||||||||||||||||||||
| w.Intercom = i; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Pre-set app settings | ||||||||||||||||||||
| window.intercomSettings = { | ||||||||||||||||||||
| api_base: config.intercom.apiBase, | ||||||||||||||||||||
| app_id: config.intercom.appId, | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const script = document.createElement('script'); | ||||||||||||||||||||
| script.type = 'text/javascript'; | ||||||||||||||||||||
| script.async = true; | ||||||||||||||||||||
| script.src = `https://widget.intercom.io/widget/${config.intercom.appId}`; | ||||||||||||||||||||
| script.onload = () => { | ||||||||||||||||||||
| isLoaded = true; | ||||||||||||||||||||
| isLoading = false; | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| script.onerror = (error) => { | ||||||||||||||||||||
| isLoading = false; | ||||||||||||||||||||
| console.error('Intercom: Failed to load script', error); | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
Comment on lines
+56
to
+59
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| const firstScript = document.getElementsByTagName('script')[0]; | ||||||||||||||||||||
| if (firstScript?.parentNode) { | ||||||||||||||||||||
| firstScript.parentNode.insertBefore(script, firstScript); | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| (document.head || document.body).appendChild(script); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export const boot = (options: IntercomBootOptions): Promise<void> => new Promise((resolve, reject) => { | ||||||||||||||||||||
| if (typeof window === 'undefined') { | ||||||||||||||||||||
| reject(new Error('Window is undefined')); | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (!config.intercom.appId) { | ||||||||||||||||||||
| console.info('Intercom: Disabled (no appId configured)'); | ||||||||||||||||||||
| reject(new Error('No Intercom app ID configured')); | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+75
to
+79
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| if (isBooted) { | ||||||||||||||||||||
| const { intercom_user_jwt: _jwt, ...updateOptions } = options; | ||||||||||||||||||||
| update(updateOptions); | ||||||||||||||||||||
| resolve(); | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (!isLoaded && !isLoading) { | ||||||||||||||||||||
| loadScript(); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Set JWT in intercomSettings before boot — required for identity verification | ||||||||||||||||||||
| if (options.intercom_user_jwt) { | ||||||||||||||||||||
| window.intercomSettings = window.intercomSettings || {}; | ||||||||||||||||||||
| window.intercomSettings.intercom_user_jwt = options.intercom_user_jwt; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+92
to
+96
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| const checkLoaded = setInterval(() => { | ||||||||||||||||||||
| if (isLoaded && window.Intercom) { | ||||||||||||||||||||
| clearInterval(checkLoaded); | ||||||||||||||||||||
| clearTimeout(timeoutHandle); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (isBooted) { | ||||||||||||||||||||
| const { intercom_user_jwt: _jwt, ...updateOptions } = options; | ||||||||||||||||||||
| update(updateOptions); | ||||||||||||||||||||
| resolve(); | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| isBooted = true; | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| const { intercom_user_jwt: _jwt, ...bootOptions } = options; | ||||||||||||||||||||
| window.Intercom('boot', { | ||||||||||||||||||||
| api_base: config.intercom.apiBase, | ||||||||||||||||||||
| app_id: config.intercom.appId, | ||||||||||||||||||||
| ...bootOptions, | ||||||||||||||||||||
|
Comment on lines
+112
to
+116
|
||||||||||||||||||||
| const { intercom_user_jwt: _jwt, ...bootOptions } = options; | |
| window.Intercom('boot', { | |
| api_base: config.intercom.apiBase, | |
| app_id: config.intercom.appId, | |
| ...bootOptions, | |
| window.Intercom('boot', { | |
| api_base: config.intercom.apiBase, | |
| app_id: config.intercom.appId, | |
| ...options, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JWT stripped from Intercom boot call breaks verification
High Severity
The intercom_user_jwt is explicitly destructured out and excluded from the options passed to window.Intercom('boot', ...). It's only set on window.intercomSettings, but Intercom's boot API call uses its own argument object — not window.intercomSettings — for identity verification. Per Intercom's SPA documentation, the JWT must be passed directly in the boot call for authenticated sessions to work. Without it, identity verification will silently fail and users won't get an authenticated Intercom experience.
Additional Locations (1)
Copilot
AI
Mar 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the script fails quickly (onerror), boot() will still poll for up to 10s before rejecting, which delays auth lifecycle completion and adds avoidable background work. Consider making loadScript() return a shared Promise (cached across calls) that resolves on onload and rejects on onerror, and have boot() await that instead of polling; this will fail fast, simplify control flow, and avoid repeated intervals/timeouts for concurrent boots.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shutdown doesn't cancel a pending boot operation
Low Severity
shutdown() only acts when isBooted is true, but if it's called while boot() is still waiting for the script to load (polling via setInterval), the pending interval is never cleared. Once the script finishes loading, the interval callback will still execute and boot Intercom with the previous user's credentials. Adding a cancellation flag (e.g., a cancelled boolean checked inside the interval) would close this lifecycle gap.
Additional Locations (1)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.env.dist.localis typically a template; committing a concrete app id can cause accidental usage against the wrong Intercom workspace in local/dev setups. Consider leaving this blank or using an obvious placeholder value (with a short comment) to encourage explicit configuration per environment.