diff --git a/frontend/.env.dist.local b/frontend/.env.dist.local index 5f52478ffb..b1e23953ba 100644 --- a/frontend/.env.dist.local +++ b/frontend/.env.dist.local @@ -9,3 +9,4 @@ VUE_APP_DISCORD_INSTALLATION_URL= VUE_APP_CONVERSATIONS_PUBLIC_URL=http://localhost:3000 VUE_APP_NANGO_URL=http://localhost:3003 VUE_APP_ENV=local +VUE_APP_INTERCOM_APP_ID=mxl90k6y diff --git a/frontend/src/config.js b/frontend/src/config.js index dea89f7828..19125dc29d 100644 --- a/frontend/src/config.js +++ b/frontend/src/config.js @@ -56,6 +56,12 @@ const defaultConfig = { permissions: { teamUserIds: import.meta.env.VUE_APP_TEAM_USER_IDS, }, + intercom: { + appId: import.meta.env.VUE_APP_INTERCOM_APP_ID, + apiBase: 'https://api-iam.intercom.io', + auth0IntercomClaim: 'http://lfx.dev/claims/intercom', + auth0UsernameClaim: 'https://sso.linuxfoundation.org/claims/username', + }, }; const composedConfig = { @@ -104,6 +110,12 @@ const composedConfig = { permissions: { teamUserIds: 'CROWD_VUE_APP_TEAM_USER_IDS', }, + intercom: { + appId: 'CROWD_VUE_APP_INTERCOM_APP_ID', + apiBase: 'https://api-iam.intercom.io', + auth0IntercomClaim: 'http://lfx.dev/claims/intercom', + auth0UsernameClaim: 'https://sso.linuxfoundation.org/claims/username', + }, }; const config = defaultConfig.backendUrl ? defaultConfig : composedConfig; diff --git a/frontend/src/modules/auth/store/auth.actions.ts b/frontend/src/modules/auth/store/auth.actions.ts index bdebe95e44..9ab490163d 100644 --- a/frontend/src/modules/auth/store/auth.actions.ts +++ b/frontend/src/modules/auth/store/auth.actions.ts @@ -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); + }); + } } }); }, @@ -155,6 +169,7 @@ export default { }, logout() { disconnectSocket(); + shutdownIntercom(); this.user = null; return Auth0Service.logout(); }, diff --git a/frontend/src/utils/intercom/index.ts b/frontend/src/utils/intercom/index.ts new file mode 100644 index 0000000000..6e54d9591c --- /dev/null +++ b/frontend/src/utils/intercom/index.ts @@ -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); + }; + + 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 => 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; + } + + 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; + } + + 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, + }); + resolve(); + } catch (error) { + isBooted = false; + console.error('Intercom: Boot failed', error); + reject(error); + } + } + }, 100); + + const timeoutHandle = setTimeout(() => { + clearInterval(checkLoaded); + if (!isBooted) { + isLoading = false; + reject(new Error('Intercom script failed to load')); + } + }, 10000); +}); + +export const update = (data?: Partial): void => { + if (typeof window !== 'undefined' && window.Intercom && isBooted) { + try { + window.Intercom('update', data || {}); + } catch (error) { + console.error('Intercom: Update failed', error); + } + } +}; + +export const shutdown = (): void => { + if (typeof window === 'undefined') { + return; + } + if (window.intercomSettings?.intercom_user_jwt) { + delete window.intercomSettings.intercom_user_jwt; + } + if (window.Intercom && isBooted) { + try { + window.Intercom('shutdown'); + isBooted = false; + } catch (error) { + console.error('Intercom: Shutdown failed', error); + } + } +}; + +export const useIntercom = () => ({ + boot, + update, + shutdown, +});