Skip to content

Commit 3798dd5

Browse files
committed
chore: add intercom (IN-1028)
Signed-off-by: Joana Maia <jmaia@contractor.linuxfoundation.org>
1 parent e76f62c commit 3798dd5

4 files changed

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

0 commit comments

Comments
 (0)