Skip to content

Commit 0921832

Browse files
authored
Merge pull request #164 from intercom/p/generate-the-intercom-snippet
Add a snippet class for generating the Intercom snippet
2 parents b1da5b2 + 46eff5b commit 0921832

5 files changed

Lines changed: 3294 additions & 2 deletions

File tree

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export Client from './client';
22
export User from './user';
3+
export Snippet from './snippet';
34

45
import crypto from 'crypto';
56

lib/snippet.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { IdentityVerification } from './index';
2+
import htmlencode from 'htmlencode';
3+
4+
export default class Snippet {
5+
constructor(settings) {
6+
this.loggedOut = !settings.user_id && !settings.email;
7+
8+
if (!settings.app_id) {
9+
throw new Error('You must provide an app_id in your Intercom settings');
10+
}
11+
if (!this.loggedOut && !settings.verificationSecret) {
12+
throw new Error('You must provide your verification secret in your Intercom settings');
13+
}
14+
15+
this.settings = settings;
16+
}
17+
create() {
18+
const verificationSecret = this.getVerificationSecret();
19+
const identifier = this.getIdentifier();
20+
this.setUserHash(verificationSecret, identifier);
21+
return this.generateSnippetHTML();
22+
}
23+
getVerificationSecret() {
24+
const { verificationSecret } = this.settings;
25+
delete this.settings.verificationSecret;
26+
return verificationSecret;
27+
}
28+
getIdentifier() {
29+
if (this.settings.user_id) {
30+
return this.settings.user_id.toString();
31+
} else {
32+
return this.settings.email;
33+
}
34+
}
35+
setUserHash(verificationSecret, identifier) {
36+
if (this.loggedOut) {
37+
return;
38+
}
39+
40+
const userHash = IdentityVerification.userHash({
41+
secretKey: verificationSecret,
42+
identifier
43+
});
44+
this.settings.user_hash = userHash;
45+
}
46+
generateSnippetHTML() {
47+
return `
48+
<script>
49+
window.intercomSettings = {
50+
${this.settingsToString(this.settings)}
51+
};
52+
</script>
53+
<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/${this.settings.app_id}';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>
54+
`;
55+
}
56+
settingsToString(settings) {
57+
const intercomSettings = [];
58+
Object.keys(settings).map(key => {
59+
if (typeof settings[key] === 'object' && settings[key] !== null) {
60+
intercomSettings.push(`${key}: { ${this.settingsToString(settings[key])} }`);
61+
} else {
62+
const escapedKey = this.escapeString(key);
63+
const value = this.escapeString(settings[key]);
64+
if (typeof settings[key] === 'string') {
65+
intercomSettings.push(`${escapedKey}: "${value}"`);
66+
} else {
67+
intercomSettings.push(`${escapedKey}: ${value}`);
68+
}
69+
}
70+
});
71+
return intercomSettings.join(', ');
72+
}
73+
escapeString(string) {
74+
if (typeof string === 'string') {
75+
string = htmlencode.htmlEncode(string).replace(/\&quot;/gi, '\\"');
76+
}
77+
return string;
78+
}
79+
}

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
],
2020
"dependencies": {
2121
"bluebird": "^3.3.4",
22-
"request": "^2.83.0"
22+
"request": "^2.83.0",
23+
"htmlencode": "^0.0.4"
2324
},
2425
"devDependencies": {
2526
"babel-core": "^6.7.4",
@@ -32,7 +33,7 @@
3233
"gulp-exclude-gitignore": "^1.0.0",
3334
"gulp-istanbul": "^0.10.3",
3435
"gulp-mocha": "^2.0.0",
35-
"gulp-nsp": "^2.3.1",
36+
"gulp-nsp": "^2.4.2",
3637
"gulp-plumber": "^1.1.0",
3738
"nock": "7.5.0"
3839
},

test/snippet.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import Snippet from '../lib/snippet';
2+
import assert from 'assert';
3+
4+
describe('snippet', () => {
5+
it('should be able to grab the verification secret', () => {
6+
const settings = {
7+
verificationSecret: 'abc123',
8+
app_id: 'xyz789',
9+
user_id: 1
10+
};
11+
const snippet = new Snippet(settings);
12+
assert.equal(snippet.getVerificationSecret(), 'abc123');
13+
});
14+
15+
it('should grab the user_id as the identifier', () => {
16+
const settings = {
17+
verificationSecret: 'abc123',
18+
app_id: 'xyz789',
19+
user_id: 1,
20+
email: 'peter@intercom.io'
21+
};
22+
const snippet = new Snippet(settings);
23+
assert.equal(snippet.getIdentifier(), 1);
24+
});
25+
26+
it('should grab the email as the identifier if no user_id', () => {
27+
const settings = {
28+
verificationSecret: 'abc123',
29+
app_id: 'xyz789',
30+
email: 'peter@intercom.io'
31+
};
32+
const snippet = new Snippet(settings);
33+
assert.equal(snippet.getIdentifier(), 'peter@intercom.io');
34+
});
35+
36+
it('should throw an error if there\'s no verification secret', () => {
37+
const settings = {
38+
app_id: 'xyz789',
39+
user_id: 1
40+
};
41+
assert.throws(() => new Snippet(settings), Error);
42+
});
43+
44+
it('should error if there\'s no app_id', () => {
45+
const settings = {
46+
verificationSecret: 'abc123',
47+
user_id: 1
48+
};
49+
assert.throws(() => new Snippet(settings), Error);
50+
});
51+
52+
it('should return the logged out snippet if no identifier', () => {
53+
const settings = {
54+
app_id: 'xyz789'
55+
};
56+
const snippet = new Snippet(settings);
57+
assert.equal(snippet.settingsToString(settings), 'app_id: "xyz789"');
58+
});
59+
60+
it('should escape bad stuff', () => {
61+
const settings = {
62+
verificationSecret: 'abc123',
63+
app_id: 'xyz789',
64+
email: 'peter@"<script>alert(1)</script>intercom.io'
65+
};
66+
const snippet = new Snippet(settings);
67+
assert.equal(snippet.settingsToString(settings), 'verificationSecret: "abc123", app_id: "xyz789", email: "peter@\\"&lt;script&gt;alert(1)&lt;/script&gt;intercom.io"');
68+
});
69+
70+
it('should not include the verification secret in the snippet html', () => {
71+
const settings = {
72+
verificationSecret: 'abc123',
73+
app_id: 'xyz789',
74+
user_id: 1,
75+
email: 'peter@intercom.io',
76+
name: 'Peter McKenna',
77+
company: {
78+
id: 123,
79+
name: 'Intercom'
80+
}
81+
};
82+
const snippet = new Snippet(settings);
83+
assert.equal(snippet.create().indexOf(settings.verificationSecret), -1);
84+
});
85+
86+
it('should return the snippet html', () => {
87+
const settings = {
88+
verificationSecret: 'abc123',
89+
app_id: 'xyz789',
90+
user_id: 1,
91+
email: 'peter@intercom.io',
92+
name: 'Peter McKenna',
93+
company: {
94+
id: 123,
95+
name: 'Intercom'
96+
}
97+
};
98+
const snippet = new Snippet(settings);
99+
assert.equal(snippet.create(), `
100+
<script>
101+
window.intercomSettings = {
102+
app_id: "xyz789", user_id: 1, email: "peter@intercom.io", name: "Peter McKenna", company: { id: 123, name: "Intercom" }, user_hash: "f02877f24c9dd37542268a28627ebaf2e07d0d114d9482abcdc20f60874b40b3"
103+
};
104+
</script>
105+
<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/xyz789';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>
106+
`);
107+
});
108+
});

0 commit comments

Comments
 (0)