@segment/analytics-browser-actions-intercom 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@segment/analytics-browser-actions-intercom",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "main": "./dist/cjs",
6
+ "module": "./dist/esm",
7
+ "scripts": {
8
+ "build": "yarn build:esm && yarn build:cjs",
9
+ "build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
10
+ "build:esm": "tsc --outDir ./dist/esm"
11
+ },
12
+ "typings": "./dist/esm",
13
+ "dependencies": {
14
+ "@segment/actions-core": "^3.71.0",
15
+ "@segment/actions-shared": "^1.53.0",
16
+ "@segment/browser-destination-runtime": "^1.0.0"
17
+ },
18
+ "peerDependencies": {
19
+ "@segment/analytics-next": "*"
20
+ }
21
+ }
@@ -0,0 +1,55 @@
1
+ import { Subscription } from '@segment/browser-destination-runtime/types'
2
+ import { Analytics, Context } from '@segment/analytics-next'
3
+ import intercomDestination, { destination } from '../index'
4
+
5
+ const subscriptions: Subscription[] = [
6
+ {
7
+ partnerAction: 'trackEvent',
8
+ name: 'Show',
9
+ enabled: true,
10
+ subscribe: 'type = "track"',
11
+ mapping: {
12
+ event_name: {
13
+ '@path': '$.event'
14
+ },
15
+ event_metadata: {
16
+ '@path': '$.properties'
17
+ },
18
+ revenue: {
19
+ '@path': '$.properties.revenue'
20
+ },
21
+ currency: {
22
+ '@path': '$.properties.currency'
23
+ }
24
+ }
25
+ }
26
+ ]
27
+
28
+ describe('Intercom (actions)', () => {
29
+ test('loads Intercom with just appID', async () => {
30
+ const [event] = await intercomDestination({
31
+ appId: 'topSecretKey',
32
+ richLinkProperties: ['article'],
33
+ activator: '#test',
34
+ subscriptions
35
+ })
36
+
37
+ jest.spyOn(destination, 'initialize')
38
+
39
+ await event.load(Context.system(), {} as Analytics)
40
+ expect(destination.initialize).toHaveBeenCalled()
41
+
42
+ const scripts = window.document.querySelectorAll('script')
43
+ expect(scripts).toMatchInlineSnapshot(`
44
+ NodeList [
45
+ <script
46
+ src="https://widget.intercom.io/widget/topSecretKey"
47
+ type="text/javascript"
48
+ />,
49
+ <script>
50
+ // the emptiness
51
+ </script>,
52
+ ]
53
+ `)
54
+ })
55
+ })
@@ -0,0 +1,90 @@
1
+ import dayjs from 'dayjs'
2
+ import { convertDateToUnix, filterCustomTraits, getWidgetOptions, isEmpty } from '../utils'
3
+
4
+ describe('Utils test', () => {
5
+ describe('date handling tests', () => {
6
+ test('handles ISO datestrings', () => {
7
+ const date = new Date()
8
+ const isoDate = date.toISOString()
9
+ const unixDate = dayjs(isoDate).unix()
10
+
11
+ expect(convertDateToUnix(isoDate)).toEqual(unixDate)
12
+ })
13
+
14
+ test('accepts Unix timestamps in seconds', () => {
15
+ const date = Math.floor(new Date().getTime() / 1000)
16
+
17
+ expect(convertDateToUnix(date)).toEqual(date)
18
+ })
19
+
20
+ test('accepts Unix timestamps in milliseconds', () => {
21
+ const dateInMS = Math.floor(new Date().getTime())
22
+ const dateInS = Math.floor(dateInMS / 1000)
23
+
24
+ expect(convertDateToUnix(dateInMS)).toEqual(dateInS)
25
+ })
26
+ })
27
+
28
+ describe('custom trait filtering tests', () => {
29
+ test('objects & arrays will be filtered out of traits', () => {
30
+ const traits = {
31
+ name: 'ibum',
32
+ badObj: {
33
+ badKey: 'badValue'
34
+ },
35
+ badArray: ['i will be dropped']
36
+ }
37
+
38
+ expect(filterCustomTraits(traits)).toEqual({
39
+ name: 'ibum'
40
+ })
41
+ })
42
+
43
+ test('custom traits will be filtered with undefined traits object', () => {
44
+ const traits = undefined
45
+ expect(filterCustomTraits(traits)).toEqual({})
46
+ })
47
+ })
48
+
49
+ describe('isEmpty tests', () => {
50
+ test('isEmpty returns true if object is empty', () => {
51
+ const obj = {}
52
+ expect(isEmpty(obj)).toBe(true)
53
+ })
54
+
55
+ test('isEmpty returns false if object is not empty', () => {
56
+ const obj = { prop: 'value' }
57
+ expect(isEmpty(obj)).toBe(false)
58
+ })
59
+
60
+ test('isEmpty works for undefined obj', () => {
61
+ const obj = undefined
62
+ expect(isEmpty(obj)).toBe(true)
63
+ })
64
+ })
65
+
66
+ describe('widget options tests', () => {
67
+ test('attaches `activator` if activator is not default', () => {
68
+ const activator = '#my-widget'
69
+ const hide_default_launcher = undefined
70
+
71
+ expect(getWidgetOptions(hide_default_launcher, activator)).toEqual({
72
+ widget: {
73
+ activator: activator
74
+ }
75
+ })
76
+ })
77
+
78
+ test('attaches `hide_default_launcher` if its not undefined ', () => {
79
+ const activator = '#my-widget'
80
+ const hide_default_launcher = false
81
+
82
+ expect(getWidgetOptions(hide_default_launcher, activator)).toEqual({
83
+ widget: {
84
+ activator: activator
85
+ },
86
+ hide_default_launcher: false
87
+ })
88
+ })
89
+ })
90
+ })
package/src/api.ts ADDED
@@ -0,0 +1,11 @@
1
+ type method = 'trackEvent' | 'update'
2
+
3
+ type IntercomApi = {
4
+ richLinkProperties: string[] | undefined
5
+ appId: string
6
+ activator: string | undefined
7
+ }
8
+
9
+ type IntercomFunction = (method: method, ...args: unknown[]) => void
10
+
11
+ export type Intercom = IntercomFunction & IntercomApi
@@ -0,0 +1,20 @@
1
+ // Generated file. DO NOT MODIFY IT BY HAND.
2
+
3
+ export interface Settings {
4
+ /**
5
+ * The app_id of your Intercom app which will indicate where to store any data.
6
+ */
7
+ appId: string
8
+ /**
9
+ * By default, Intercom will inject their own inbox button onto the page, but you can choose to use your own custom button instead by providing a CSS selector, e.g. #my-button. You must have the "Show the Intercom Inbox" setting enabled for this to work. The default value is #IntercomDefaultWidget.
10
+ */
11
+ activator?: string
12
+ /**
13
+ * A list of rich link property keys.
14
+ */
15
+ richLinkProperties?: string[]
16
+ /**
17
+ * The regional API to use for processing the data
18
+ */
19
+ apiBase?: string
20
+ }
@@ -0,0 +1,308 @@
1
+ import { Analytics, Context } from '@segment/analytics-next'
2
+ import { Subscription } from '@segment/browser-destination-runtime'
3
+ import intercomDestination, { destination } from '../../index'
4
+
5
+ const subscriptions: Subscription[] = [
6
+ {
7
+ partnerAction: 'identifyCompany',
8
+ name: 'Show',
9
+ enabled: true,
10
+ subscribe: 'type = "group"',
11
+ mapping: {
12
+ company: {
13
+ company_id: { '@path': '$.groupId' },
14
+ company_custom_traits: {
15
+ city: {
16
+ '@path': '$.traits.city'
17
+ },
18
+ tech: {
19
+ '@path': '$.traits.tech'
20
+ }
21
+ },
22
+ name: { '@path': '$.traits.name' },
23
+ plan: { '@path': '$.traits.plan' },
24
+ monthly_spend: { '@path': '$.traits.monthlySpend' },
25
+ created_at: {
26
+ '@if': {
27
+ exists: { '@path': '$.traits.createdAt' },
28
+ then: { '@path': '$.traits.createdAt' },
29
+ else: { '@path': '$.traits.created_at' }
30
+ }
31
+ },
32
+ size: { '@path': '$.traits.size' },
33
+ website: { '@path': '$.traits.website' },
34
+ industry: { '@path': '$.traits.industry' }
35
+ },
36
+ hide_default_launcher: {
37
+ '@if': {
38
+ exists: { '@path': '$.context.Intercom.hideDefaultLauncher' },
39
+ then: { '@path': '$.context.Intercom.hideDefaultLauncher' },
40
+ else: { '@path': '$.context.Intercom.hide_default_launcher' }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ]
46
+
47
+ describe('Intercom.update (Company)', () => {
48
+ const settings = {
49
+ appId: 'superSecretAppID',
50
+ activator: '#IntercomDefaultWidget'
51
+ }
52
+
53
+ let mockIntercom: jest.Mock<any, any>
54
+ let identifyCompany: any
55
+ beforeEach(async () => {
56
+ jest.restoreAllMocks()
57
+
58
+ const [identifyCompanyPlugin] = await intercomDestination({
59
+ ...settings,
60
+ subscriptions
61
+ })
62
+ identifyCompany = identifyCompanyPlugin
63
+
64
+ mockIntercom = jest.fn()
65
+ jest.spyOn(destination, 'initialize').mockImplementation(() => {
66
+ const mockedWithProps = Object.assign(mockIntercom as any, settings)
67
+ return Promise.resolve(mockedWithProps)
68
+ })
69
+ await identifyCompany.load(Context.system(), {} as Analytics)
70
+ })
71
+
72
+ test('sends an id', async () => {
73
+ const context = new Context({
74
+ type: 'group',
75
+ groupId: 'id'
76
+ })
77
+
78
+ await identifyCompany.group?.(context)
79
+
80
+ expect(mockIntercom).toHaveBeenCalledWith('update', {
81
+ company: {
82
+ company_id: 'id'
83
+ }
84
+ })
85
+ })
86
+
87
+ test('sends an id & properties', async () => {
88
+ const context = new Context({
89
+ type: 'group',
90
+ groupId: 'id',
91
+ traits: {
92
+ name: 'Jeff'
93
+ }
94
+ })
95
+
96
+ await identifyCompany.group?.(context)
97
+
98
+ expect(mockIntercom).toHaveBeenCalledWith('update', {
99
+ company: {
100
+ company_id: 'id',
101
+ name: 'Jeff'
102
+ }
103
+ })
104
+ })
105
+
106
+ test('converts created_at from ISO-8601 to Unix', async () => {
107
+ const context = new Context({
108
+ type: 'group',
109
+ groupId: 'id',
110
+ traits: {
111
+ createdAt: '2018-01-23T22:28:55.111Z'
112
+ }
113
+ })
114
+
115
+ await identifyCompany.group?.(context)
116
+
117
+ expect(mockIntercom).toHaveBeenCalledWith('update', {
118
+ company: {
119
+ company_id: 'id',
120
+ created_at: 1516746535
121
+ }
122
+ })
123
+ })
124
+
125
+ test('maps created_at properly regardless of it being sent in snake_case or camelCase', async () => {
126
+ const context = new Context({
127
+ type: 'group',
128
+ groupId: 'id',
129
+ traits: {
130
+ created_at: '2018-01-23T22:28:55.111Z'
131
+ }
132
+ })
133
+
134
+ await identifyCompany.group?.(context)
135
+
136
+ expect(mockIntercom).toHaveBeenCalledWith('update', {
137
+ company: {
138
+ company_id: 'id',
139
+ created_at: 1516746535
140
+ }
141
+ })
142
+ })
143
+
144
+ test('sends custom traits', async () => {
145
+ const context = new Context({
146
+ type: 'group',
147
+ groupId: 'id',
148
+ traits: {
149
+ wave: 'Capitola',
150
+ city: 'SF',
151
+ tech: true
152
+ }
153
+ })
154
+
155
+ await identifyCompany.group?.(context)
156
+
157
+ expect(mockIntercom).toHaveBeenCalledWith('update', {
158
+ company: {
159
+ company_id: 'id',
160
+ city: 'SF',
161
+ tech: true
162
+ }
163
+ })
164
+ })
165
+
166
+ test('drops arrays or objects in traits', async () => {
167
+ const context = new Context({
168
+ type: 'group',
169
+ groupId: 'id',
170
+ traits: {
171
+ badArray: ['i', 'shall', 'be', 'dropped'],
172
+ badObject: {
173
+ rip: 'i will cease to exist'
174
+ },
175
+ city: 'Belmar'
176
+ }
177
+ })
178
+
179
+ await identifyCompany.group?.(context)
180
+
181
+ expect(mockIntercom).toHaveBeenCalledWith('update', {
182
+ company: {
183
+ company_id: 'id',
184
+ city: 'Belmar'
185
+ }
186
+ })
187
+ })
188
+
189
+ test('should set hide_default_launcher if the setting is there', async () => {
190
+ const context = new Context({
191
+ type: 'group',
192
+ groupId: 'id',
193
+ traits: {
194
+ name: 'Segment'
195
+ },
196
+ context: {
197
+ Intercom: {
198
+ hideDefaultLauncher: true
199
+ }
200
+ }
201
+ })
202
+
203
+ await identifyCompany.group?.(context)
204
+
205
+ expect(mockIntercom).toHaveBeenCalledWith('update', {
206
+ company: {
207
+ company_id: 'id',
208
+ name: 'Segment'
209
+ },
210
+ hide_default_launcher: true
211
+ })
212
+ })
213
+ })
214
+
215
+ describe('Intercom.update (user) widget options', () => {
216
+ const settings = {
217
+ appId: 'superSecretAppID',
218
+ activator: '#customWidget'
219
+ }
220
+
221
+ let mockIntercom: jest.Mock<any, any>
222
+ let identifyCompany: any
223
+ beforeEach(async () => {
224
+ jest.restoreAllMocks()
225
+
226
+ const [identifyCompanyPlugin] = await intercomDestination({
227
+ ...settings,
228
+ subscriptions
229
+ })
230
+ identifyCompany = identifyCompanyPlugin
231
+
232
+ mockIntercom = jest.fn()
233
+ jest.spyOn(destination, 'initialize').mockImplementation(() => {
234
+ const mockedWithProps = Object.assign(mockIntercom as any, settings)
235
+ return Promise.resolve(mockedWithProps)
236
+ })
237
+ await identifyCompany.load(Context.system(), {} as Analytics)
238
+ })
239
+
240
+ test('sets activator if activator is not #IntercomDefaultWidget', async () => {
241
+ const context = new Context({
242
+ type: 'group',
243
+ groupId: 'id',
244
+ traits: {}
245
+ })
246
+
247
+ await identifyCompany.group?.(context)
248
+
249
+ expect(mockIntercom).toHaveBeenCalledWith('update', {
250
+ company: {
251
+ company_id: 'id'
252
+ },
253
+ widget: {
254
+ activator: '#customWidget'
255
+ }
256
+ })
257
+ })
258
+
259
+ test('should set hide_default_launcher if the setting is there', async () => {
260
+ const context = new Context({
261
+ type: 'group',
262
+ groupId: 'id',
263
+ traits: {},
264
+ context: {
265
+ Intercom: {
266
+ hideDefaultLauncher: false
267
+ }
268
+ }
269
+ })
270
+
271
+ await identifyCompany.identify?.(context)
272
+
273
+ expect(mockIntercom).toHaveBeenCalledWith('update', {
274
+ company: {
275
+ company_id: 'id'
276
+ },
277
+ hide_default_launcher: false,
278
+ widget: {
279
+ activator: '#customWidget'
280
+ }
281
+ })
282
+ })
283
+
284
+ test('maps hide_default_launcher correctly regardless of it being sent in snake_case or camelCase', async () => {
285
+ const context = new Context({
286
+ type: 'group',
287
+ groupId: 'id',
288
+ traits: {},
289
+ context: {
290
+ Intercom: {
291
+ hideDefaultLauncher: false
292
+ }
293
+ }
294
+ })
295
+
296
+ await identifyCompany.identify?.(context)
297
+
298
+ expect(mockIntercom).toHaveBeenCalledWith('update', {
299
+ company: {
300
+ company_id: 'id'
301
+ },
302
+ hide_default_launcher: false,
303
+ widget: {
304
+ activator: '#customWidget'
305
+ }
306
+ })
307
+ })
308
+ })
@@ -0,0 +1,51 @@
1
+ // Generated file. DO NOT MODIFY IT BY HAND.
2
+
3
+ export interface Payload {
4
+ /**
5
+ * The user's company.
6
+ */
7
+ company: {
8
+ /**
9
+ * The unique identifier of the company.
10
+ */
11
+ company_id: string
12
+ /**
13
+ * The name of the company.
14
+ */
15
+ name: string
16
+ /**
17
+ * The time the company was created in your system.
18
+ */
19
+ created_at?: string | number
20
+ /**
21
+ * The name of the plan you have associated with the company.
22
+ */
23
+ plan?: string
24
+ /**
25
+ * The monthly spend of the company, e.g. how much revenue the company generates for your business.
26
+ */
27
+ monthly_spend?: number
28
+ /**
29
+ * The number of employees in the company.
30
+ */
31
+ size?: number
32
+ /**
33
+ * The URL for the company website.
34
+ */
35
+ website?: string
36
+ /**
37
+ * The industry that the company operates in.
38
+ */
39
+ industry?: string
40
+ /**
41
+ * The custom attributes for the company object.
42
+ */
43
+ company_custom_traits?: {
44
+ [k: string]: unknown
45
+ }
46
+ }
47
+ /**
48
+ * Selectively show the chat widget. As per [Intercom docs](https://www.intercom.com/help/en/articles/189-turn-off-show-or-hide-the-intercom-messenger), you want to first hide the Messenger for all users inside the Intercom UI using Messenger settings. Then think about how you want to programmatically decide which users you would like to show the widget to.
49
+ */
50
+ hide_default_launcher?: boolean
51
+ }
@@ -0,0 +1,82 @@
1
+ import { InputField } from '@segment/actions-core'
2
+ import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
3
+ import { Intercom } from '../api'
4
+ import type { Settings } from '../generated-types'
5
+ import { getCompanyProperties } from '../sharedCompanyProperties'
6
+ import { convertDateToUnix, filterCustomTraits, getWidgetOptions } from '../utils'
7
+ import type { Payload } from './generated-types'
8
+
9
+ const companyProperties: Record<string, InputField> = getCompanyProperties()
10
+
11
+ const action: BrowserActionDefinition<Settings, Intercom, Payload> = {
12
+ title: 'Identify Company',
13
+ description: 'Create or update a company in Intercom.',
14
+ defaultSubscription: 'type = "group"',
15
+ platform: 'web',
16
+ fields: {
17
+ company: {
18
+ description: "The user's company.",
19
+ label: 'Company',
20
+ type: 'object',
21
+ required: true,
22
+ properties: companyProperties,
23
+ default: {
24
+ company_id: { '@path': '$.groupId' },
25
+ name: { '@path': '$.traits.name' },
26
+ created_at: {
27
+ '@if': {
28
+ exists: { '@path': '$.traits.createdAt' },
29
+ then: { '@path': '$.traits.createdAt' },
30
+ else: { '@path': '$.traits.created_at' }
31
+ }
32
+ },
33
+ plan: { '@path': '$.traits.plan' },
34
+ size: { '@path': '$.traits.size' },
35
+ website: { '@path': '$.traits.website' },
36
+ industry: { '@path': '$.traits.industry' },
37
+ monthly_spend: { '@path': '$.traits.monthly_spend' }
38
+ }
39
+ },
40
+ hide_default_launcher: {
41
+ description:
42
+ 'Selectively show the chat widget. As per [Intercom docs](https://www.intercom.com/help/en/articles/189-turn-off-show-or-hide-the-intercom-messenger), you want to first hide the Messenger for all users inside the Intercom UI using Messenger settings. Then think about how you want to programmatically decide which users you would like to show the widget to.',
43
+ label: 'Hide Default Launcher',
44
+ type: 'boolean',
45
+ required: false,
46
+ default: {
47
+ '@if': {
48
+ exists: { '@path': '$.context.Intercom.hideDefaultLauncher' },
49
+ then: { '@path': '$.context.Intercom.hideDefaultLauncher' },
50
+ else: { '@path': '$.context.Intercom.hide_default_launcher' }
51
+ }
52
+ }
53
+ }
54
+ },
55
+ perform: (Intercom, event) => {
56
+ // remove properties that require extra handling
57
+ const { company_custom_traits, ...rest } = event.payload.company
58
+ let company = { ...rest }
59
+
60
+ // convert date from ISO-8601 to UNIX
61
+ if (company?.created_at) {
62
+ company.created_at = convertDateToUnix(company.created_at)
63
+ }
64
+
65
+ // drop custom objects & arrays
66
+ const filteredCustomTraits = filterCustomTraits(company_custom_traits)
67
+
68
+ // merge filtered custom traits back into company object
69
+ company = { ...company, ...filteredCustomTraits }
70
+
71
+ // get user's widget options
72
+ const widgetOptions = getWidgetOptions(event.payload.hide_default_launcher, Intercom.activator)
73
+
74
+ //API call
75
+ Intercom('update', {
76
+ company,
77
+ ...widgetOptions
78
+ })
79
+ }
80
+ }
81
+
82
+ export default action