Skip to content

Commit 8a05d36

Browse files
authored
chore: add intercom (IN-1028) (#3945)
Signed-off-by: Joana Maia <[email protected]>
1 parent 44c6b90 commit 8a05d36

5 files changed

Lines changed: 196 additions & 0 deletions

File tree

frontend/.env.dist.local

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ VUE_APP_DISCORD_INSTALLATION_URL=
99
VUE_APP_CONVERSATIONS_PUBLIC_URL=http://localhost:3000
1010
VUE_APP_NANGO_URL=http://localhost:3003
1111
VUE_APP_ENV=local
12+
VUE_APP_INTERCOM_APP_ID=mxl90k6y

frontend/scripts/docker-entrypoint.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ declare -a ENV_VARIABLES=(
3030
"VUE_APP_DATADOG_RUM_APPLICATION_ID"
3131
"VUE_APP_DATADOG_RUM_CLIENT_TOKEN"
3232
"VUE_APP_TEAM_USER_IDS"
33+
"VUE_APP_INTERCOM_APP_ID"
3334
)
3435

3536
for ENV_VAR in "${ENV_VARIABLES[@]}"; do

frontend/src/config.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ const defaultConfig = {
5656
permissions: {
5757
teamUserIds: import.meta.env.VUE_APP_TEAM_USER_IDS,
5858
},
59+
intercom: {
60+
appId: import.meta.env.VUE_APP_INTERCOM_APP_ID,
61+
apiBase: 'https://api-iam.intercom.io',
62+
auth0IntercomClaim: 'http://lfx.dev/claims/intercom',
63+
auth0UsernameClaim: 'https://sso.linuxfoundation.org/claims/username',
64+
},
5965
};
6066

6167
const composedConfig = {
@@ -104,6 +110,12 @@ const composedConfig = {
104110
permissions: {
105111
teamUserIds: 'CROWD_VUE_APP_TEAM_USER_IDS',
106112
},
113+
intercom: {
114+
appId: 'CROWD_VUE_APP_INTERCOM_APP_ID',
115+
apiBase: 'https://api-iam.intercom.io',
116+
auth0IntercomClaim: 'http://lfx.dev/claims/intercom',
117+
auth0UsernameClaim: 'https://sso.linuxfoundation.org/claims/username',
118+
},
107119
};
108120

109121
const config = defaultConfig.backendUrl ? defaultConfig : composedConfig;

frontend/src/modules/auth/store/auth.actions.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { watch } from 'vue';
99
import config from '@/config';
1010
import { setRumUser } from '@/utils/datadog/rum';
1111
import useSessionTracking from '@/shared/modules/monitoring/useSessionTracking';
12+
import { boot as bootIntercom, shutdown as shutdownIntercom } from '@/utils/intercom';
1213

1314
export default {
1415
init() {
@@ -36,6 +37,19 @@ export default {
3637
if (user) {
3738
setRumUser(user);
3839
lfxHeader.authuser = user;
40+
41+
const intercomJwt = user[config.intercom.auth0IntercomClaim];
42+
const userId = user[config.intercom.auth0UsernameClaim];
43+
if (userId && intercomJwt) {
44+
bootIntercom({
45+
user_id: userId,
46+
name: user.name,
47+
email: user.email,
48+
intercom_user_jwt: intercomJwt,
49+
}).catch((error: any) => {
50+
console.error('Intercom: Boot failed', error);
51+
});
52+
}
3953
}
4054
});
4155
},
@@ -155,6 +169,7 @@ export default {
155169
},
156170
logout() {
157171
disconnectSocket();
172+
shutdownIntercom();
158173
this.user = null;
159174
return Auth0Service.logout();
160175
},
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
/* eslint-disable no-use-before-define */
3+
import config from '@/config';
4+
5+
declare global {
6+
interface Window {
7+
Intercom?: any;
8+
intercomSettings?: any;
9+
}
10+
}
11+
12+
let isLoaded = false;
13+
let isBooted = false;
14+
let isLoading = false;
15+
16+
export interface IntercomBootOptions {
17+
user_id: string;
18+
name?: string;
19+
email?: string;
20+
intercom_user_jwt?: string;
21+
}
22+
23+
const loadScript = (): void => {
24+
if (isLoaded || isLoading || typeof window === 'undefined') {
25+
return;
26+
}
27+
isLoading = true;
28+
29+
// Create stub so queued commands work before script loads
30+
const w = window as any;
31+
const ic = w.Intercom;
32+
if (typeof ic === 'function') {
33+
ic('reattach_activator');
34+
ic('update', w.intercomSettings);
35+
} else {
36+
const i: any = (...args: any[]) => { i.c(args); };
37+
i.q = [];
38+
i.c = (args: any) => { i.q.push(args); };
39+
w.Intercom = i;
40+
}
41+
42+
// Pre-set app settings
43+
window.intercomSettings = {
44+
api_base: config.intercom.apiBase,
45+
app_id: config.intercom.appId,
46+
};
47+
48+
const script = document.createElement('script');
49+
script.type = 'text/javascript';
50+
script.async = true;
51+
script.src = `https://widget.intercom.io/widget/${config.intercom.appId}`;
52+
script.onload = () => {
53+
isLoaded = true;
54+
isLoading = false;
55+
};
56+
script.onerror = (error) => {
57+
isLoading = false;
58+
console.error('Intercom: Failed to load script', error);
59+
};
60+
61+
const firstScript = document.getElementsByTagName('script')[0];
62+
if (firstScript?.parentNode) {
63+
firstScript.parentNode.insertBefore(script, firstScript);
64+
} else {
65+
(document.head || document.body).appendChild(script);
66+
}
67+
};
68+
69+
export const boot = (options: IntercomBootOptions): Promise<void> => new Promise((resolve, reject) => {
70+
if (typeof window === 'undefined') {
71+
reject(new Error('Window is undefined'));
72+
return;
73+
}
74+
75+
if (!config.intercom.appId) {
76+
console.info('Intercom: Disabled (no appId configured)');
77+
reject(new Error('No Intercom app ID configured'));
78+
return;
79+
}
80+
81+
if (isBooted) {
82+
const { intercom_user_jwt: _jwt, ...updateOptions } = options;
83+
update(updateOptions);
84+
resolve();
85+
return;
86+
}
87+
88+
if (!isLoaded && !isLoading) {
89+
loadScript();
90+
}
91+
92+
// Set JWT in intercomSettings before boot — required for identity verification
93+
if (options.intercom_user_jwt) {
94+
window.intercomSettings = window.intercomSettings || {};
95+
window.intercomSettings.intercom_user_jwt = options.intercom_user_jwt;
96+
}
97+
98+
const checkLoaded = setInterval(() => {
99+
if (isLoaded && window.Intercom) {
100+
clearInterval(checkLoaded);
101+
clearTimeout(timeoutHandle);
102+
103+
if (isBooted) {
104+
const { intercom_user_jwt: _jwt, ...updateOptions } = options;
105+
update(updateOptions);
106+
resolve();
107+
return;
108+
}
109+
110+
isBooted = true;
111+
try {
112+
const { intercom_user_jwt: _jwt, ...bootOptions } = options;
113+
window.Intercom('boot', {
114+
api_base: config.intercom.apiBase,
115+
app_id: config.intercom.appId,
116+
...bootOptions,
117+
});
118+
resolve();
119+
} catch (error) {
120+
isBooted = false;
121+
console.error('Intercom: Boot failed', error);
122+
reject(error);
123+
}
124+
}
125+
}, 100);
126+
127+
const timeoutHandle = setTimeout(() => {
128+
clearInterval(checkLoaded);
129+
if (!isBooted) {
130+
isLoading = false;
131+
reject(new Error('Intercom script failed to load'));
132+
}
133+
}, 10000);
134+
});
135+
136+
export const update = (data?: Partial<IntercomBootOptions>): void => {
137+
if (typeof window !== 'undefined' && window.Intercom && isBooted) {
138+
try {
139+
window.Intercom('update', data || {});
140+
} catch (error) {
141+
console.error('Intercom: Update failed', error);
142+
}
143+
}
144+
};
145+
146+
export const shutdown = (): void => {
147+
if (typeof window === 'undefined') {
148+
return;
149+
}
150+
if (window.intercomSettings?.intercom_user_jwt) {
151+
delete window.intercomSettings.intercom_user_jwt;
152+
}
153+
if (window.Intercom && isBooted) {
154+
try {
155+
window.Intercom('shutdown');
156+
isBooted = false;
157+
} catch (error) {
158+
console.error('Intercom: Shutdown failed', error);
159+
}
160+
}
161+
};
162+
163+
export const useIntercom = () => ({
164+
boot,
165+
update,
166+
shutdown,
167+
});

0 commit comments

Comments
 (0)