@hellotext/hellotext 2.4.52 → 2.5.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.
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @typedef {'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'} Placement
3
+ * @description Valid placements for the WhatsApp widget.
4
+ */
5
+
6
+ /**
7
+ * @enum {Placement}
8
+ */
9
+ const placements = {
10
+ TOP_LEFT: 'top-left',
11
+ TOP_RIGHT: 'top-right',
12
+ BOTTOM_LEFT: 'bottom-left',
13
+ BOTTOM_RIGHT: 'bottom-right'
14
+ }
15
+
16
+ /**
17
+ * @typedef {Object} WhatsAppWidgetAppearance
18
+ * @property {Object} [launcher] - Launcher appearance overrides.
19
+ * @property {string} [launcher.iconUrl] - Image URL used for the launcher icon.
20
+ */
21
+
22
+ /**
23
+ * @class WhatsApp
24
+ * @classdesc Configuration for the standalone WhatsApp widget.
25
+ * @property {String} id - The id of the WhatsApp widget.
26
+ * @property {String} container - The container to append the widget to, defaults to 'body'.
27
+ * @property {Placement} placement - The placement of the widget, defaults to 'bottom-right'.
28
+ * @property {String} number - WhatsApp number used by the widget link.
29
+ * @property {String} body - Prefilled WhatsApp compose text.
30
+ * @property {WhatsAppWidgetAppearance} appearance - Appearance overrides.
31
+ */
32
+ class WhatsApp {
33
+ static _id
34
+ static _container = 'body'
35
+ static _placement = 'bottom-right'
36
+ static _appearance = {}
37
+ static _number = null
38
+ static _body = null
39
+
40
+ static set id(value) {
41
+ this._id = value
42
+ }
43
+
44
+ static get id() {
45
+ return this._id
46
+ }
47
+
48
+ static set container(value) {
49
+ this._container = value
50
+ }
51
+
52
+ static get container() {
53
+ return this._container
54
+ }
55
+
56
+ static set placement(value) {
57
+ if (!Object.values(placements).includes(value)) {
58
+ throw new Error(`Invalid placement value: ${value}`)
59
+ }
60
+
61
+ this._placement = value
62
+ }
63
+
64
+ static get placement() {
65
+ return this._placement
66
+ }
67
+
68
+ static get appearance() {
69
+ return this._appearance
70
+ }
71
+
72
+ static set appearance(value) {
73
+ if (!this.isPlainObject(value)) {
74
+ throw new Error('Appearance must be an object')
75
+ }
76
+
77
+ Object.entries(value).forEach(([key, nestedValue]) => {
78
+ if (key !== 'launcher') {
79
+ throw new Error(`Invalid appearance property: ${key}`)
80
+ }
81
+
82
+ if (!this.isPlainObject(nestedValue)) {
83
+ throw new Error(`Appearance ${key} must be an object`)
84
+ }
85
+
86
+ Object.entries(nestedValue).forEach(([nestedKey, propertyValue]) => {
87
+ if (nestedKey !== 'iconUrl') {
88
+ throw new Error(`Invalid appearance launcher property: ${nestedKey}`)
89
+ }
90
+
91
+ if (propertyValue == null) {
92
+ return
93
+ }
94
+
95
+ if (typeof propertyValue !== 'string') {
96
+ throw new Error(`Invalid appearance ${key}.${nestedKey} value: ${propertyValue}`)
97
+ }
98
+ })
99
+ })
100
+
101
+ this._appearance = value
102
+ }
103
+
104
+ static get number() {
105
+ return this._number
106
+ }
107
+
108
+ static set number(value) {
109
+ if (value != null && typeof value !== 'string') {
110
+ throw new Error(`Invalid number value: ${value}`)
111
+ }
112
+
113
+ this._number = value
114
+ }
115
+
116
+ static get body() {
117
+ return this._body
118
+ }
119
+
120
+ static set body(value) {
121
+ if (value != null && typeof value !== 'string') {
122
+ throw new Error(`Invalid body value: ${value}`)
123
+ }
124
+
125
+ this._body = value
126
+ }
127
+
128
+ static assign(props) {
129
+ if (props) {
130
+ Object.entries(props).forEach(([key, value]) => {
131
+ this[key] = value
132
+ })
133
+ }
134
+
135
+ return this
136
+ }
137
+
138
+ static isPlainObject(value) {
139
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
140
+ }
141
+ }
142
+
143
+ export { WhatsApp }
@@ -1,6 +1,7 @@
1
1
  import { Forms } from './configuration/forms'
2
2
  import { Locale } from './configuration/locale'
3
3
  import { Webchat } from './configuration/webchat'
4
+ import { WhatsApp } from './configuration/whatsapp'
4
5
 
5
6
  /**
6
7
  * @class Configuration
@@ -10,6 +11,7 @@ import { Webchat } from './configuration/webchat'
10
11
  * @property {String} [session] - session id
11
12
  * @property {Forms} [forms] - form configuration
12
13
  * @property {Webchat} [webchat] - webchat configuration
14
+ * @property {WhatsApp} [whatsappWidget] - WhatsApp widget configuration
13
15
  * @property {Locale} [locale] - locale configuration
14
16
  */
15
17
  class Configuration {
@@ -21,6 +23,7 @@ class Configuration {
21
23
 
22
24
  static forms = Forms
23
25
  static webchat = Webchat
26
+ static whatsapp = WhatsApp
24
27
 
25
28
  /**
26
29
  *
@@ -42,6 +45,8 @@ class Configuration {
42
45
  this.forms = Forms.assign(value)
43
46
  } else if (key === 'webchat') {
44
47
  this.webchat = Webchat.assign(value)
48
+ } else if (key === 'whatsappWidget') {
49
+ this.whatsapp = WhatsApp.assign(value)
45
50
  } else {
46
51
  this[key] = value
47
52
  }
package/src/core/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Configuration } from './configuration'
2
2
  export { Locale } from './configuration/locale'
3
3
  export { Webchat } from './configuration/webchat'
4
+ export { WhatsApp } from './configuration/whatsapp'
4
5
  export { default as Event } from './event'
package/src/hellotext.js CHANGED
@@ -1,7 +1,17 @@
1
1
  import { Configuration, Event } from './core'
2
2
 
3
3
  import API, { Response, keepaliveFor } from './api'
4
- import { Business, Fingerprint, FormCollection, Page, Query, Session, User, Webchat } from './models'
4
+ import {
5
+ Business,
6
+ Fingerprint,
7
+ FormCollection,
8
+ Page,
9
+ Query,
10
+ Session,
11
+ User,
12
+ Webchat,
13
+ WhatsAppWidget,
14
+ } from './models'
5
15
 
6
16
  import { NotInitializedError } from './errors'
7
17
 
@@ -10,6 +20,7 @@ class Hellotext {
10
20
  static forms
11
21
  static business
12
22
  static webchat
23
+ static whatsapp
13
24
 
14
25
  /**
15
26
  * initialize the module.
@@ -18,25 +29,35 @@ class Hellotext {
18
29
  */
19
30
  static async initialize(business, config = {}) {
20
31
  this.business = new Business(business)
32
+ this.page = new Page()
21
33
 
22
34
  Configuration.assign(config)
23
- Session.initialize()
35
+ Session.initialize(this.page)
24
36
 
25
- this.page = new Page()
26
37
  this.forms = new FormCollection()
27
38
 
28
39
  this.query = new Query()
29
40
 
30
41
  const businessData = await this.business.hydrate()
31
- const webchatConfig = config.webchat === false ?
32
- false :
33
- this.mergeWebchatConfig((businessData && businessData.webchat) || {}, config.webchat || {})
34
-
35
- const hasExplicitBehaviourOverride = (
42
+ const webchatConfig =
43
+ config.webchat === false
44
+ ? false
45
+ : this.mergeWebchatConfig(
46
+ (businessData && businessData.webchat) || {},
47
+ config.webchat || {},
48
+ )
49
+ const whatsappConfig =
50
+ config.whatsappWidget === false
51
+ ? false
52
+ : this.mergeWhatsAppConfig(
53
+ (businessData && businessData.whatsapp) || {},
54
+ config.whatsappWidget || {},
55
+ )
56
+
57
+ const hasExplicitBehaviourOverride =
36
58
  config.webchat &&
37
59
  config.webchat !== false &&
38
60
  Object.prototype.hasOwnProperty.call(config.webchat, 'behaviour')
39
- )
40
61
  Configuration.webchat.behaviourOverride = hasExplicitBehaviourOverride
41
62
 
42
63
  if (webchatConfig && webchatConfig.id) {
@@ -44,6 +65,11 @@ class Hellotext {
44
65
  this.webchat = await Webchat.load(webchatConfig.id)
45
66
  }
46
67
 
68
+ if (whatsappConfig && whatsappConfig.id) {
69
+ Configuration.whatsapp.assign(whatsappConfig)
70
+ this.whatsapp = await WhatsAppWidget.load(whatsappConfig.id)
71
+ }
72
+
47
73
  if (typeof MutationObserver !== 'undefined') {
48
74
  this.forms.collectExistingFormsOnPage()
49
75
  }
@@ -53,6 +79,10 @@ class Hellotext {
53
79
  return this.deepMergePlainObjects(dashboardConfig, localConfig)
54
80
  }
55
81
 
82
+ static mergeWhatsAppConfig(dashboardConfig, localConfig) {
83
+ return this.deepMergePlainObjects(dashboardConfig, localConfig)
84
+ }
85
+
56
86
  static deepMergePlainObjects(base, override) {
57
87
  const result = { ...base }
58
88
 
@@ -147,11 +177,7 @@ class Hellotext {
147
177
  })
148
178
 
149
179
  if (response.succeeded) {
150
- User.remember(
151
- externalId,
152
- options.source,
153
- fingerprint,
154
- )
180
+ User.remember(externalId, options.source, fingerprint)
155
181
  }
156
182
 
157
183
  return response
@@ -9,3 +9,4 @@ export { Session } from './session'
9
9
  export { User } from './user'
10
10
  export { UTM } from './utm'
11
11
  export { Webchat } from './webchat'
12
+ export { WhatsAppWidget } from './whatsapp_widget'
@@ -1,16 +1,24 @@
1
1
  import { Configuration } from '../core'
2
2
  import { Cookies } from './cookies'
3
+ import { Page } from './page'
3
4
  import { Query } from './query'
4
5
  import API from '../api'
5
6
 
6
7
  class Session {
7
8
  static #session
8
9
  static #query
10
+ static #page
9
11
 
10
12
  static get session() {
11
13
  return this.#session
12
14
  }
13
15
 
16
+ static get ackPayload() {
17
+ return {
18
+ utm_params: this.#page?.utmParams || {},
19
+ }
20
+ }
21
+
14
22
  static set session(value) {
15
23
  const oldSession = Cookies.get('hello_session')
16
24
 
@@ -22,14 +30,15 @@ class Session {
22
30
  }
23
31
 
24
32
  if (!Cookies.get('hello_session_ack_at')) {
25
- API.acks.send()
33
+ API.acks.send(this.ackPayload)
26
34
  Cookies.set('hello_session_ack_at', new Date().toISOString())
27
35
  }
28
36
 
29
37
  return this.#session
30
38
  }
31
39
 
32
- static initialize() {
40
+ static initialize(page = new Page()) {
41
+ this.#page = page
33
42
  this.#query = new Query()
34
43
 
35
44
  this.session = this.#query.session || Configuration.session || Cookies.get('hello_session')
@@ -30,6 +30,7 @@ class Webchat {
30
30
  }
31
31
 
32
32
  this.containerToAppendTo.appendChild(this.data.html)
33
+ this.markCoexistingWidgets()
33
34
  this.mounted = true
34
35
 
35
36
  return true
@@ -69,6 +70,16 @@ class Webchat {
69
70
  get stylesheetLoaded() {
70
71
  return Business.waitForStylesheet(Business.latestStylesheet)
71
72
  }
73
+
74
+ markCoexistingWidgets() {
75
+ const webchat = this.data.html
76
+ const whatsapp = document.querySelector('.hellotext--whatsapp-widget')
77
+
78
+ if (!webchat || !whatsapp) return
79
+
80
+ webchat.classList.add('hellotext--with-whatsapp-widget')
81
+ whatsapp.classList.add('hellotext--with-webchat')
82
+ }
72
83
  }
73
84
 
74
85
  export { Webchat }
@@ -0,0 +1,68 @@
1
+ import { Configuration } from '../core'
2
+
3
+ import API from '../api'
4
+ import { Business } from './business'
5
+
6
+ class WhatsAppWidget {
7
+ static async load(id) {
8
+ const widget = new WhatsAppWidget({
9
+ id,
10
+ html: await API.whatsappWidgets.get(id)
11
+ })
12
+
13
+ widget.rendered = widget.render()
14
+
15
+ return widget
16
+ }
17
+
18
+ constructor(data) {
19
+ this.data = data
20
+ this.mounted = false
21
+ this.rendered = Promise.resolve(false)
22
+ }
23
+
24
+ async render() {
25
+ if (!this.data.html) return false
26
+
27
+ const container = this.containerToAppendTo
28
+ if (!container) {
29
+ console.warn(`Hellotext WhatsApp widget was not mounted because the container ${Configuration.whatsapp.container} was not found.`)
30
+ return false
31
+ }
32
+
33
+ if (!await this.stylesheetLoaded) {
34
+ console.warn('Hellotext WhatsApp widget was not mounted because its stylesheet failed to load.')
35
+ return false
36
+ }
37
+
38
+ container.appendChild(this.data.html)
39
+ this.markCoexistingWidgets()
40
+ this.mounted = true
41
+
42
+ return true
43
+ }
44
+
45
+ get containerToAppendTo() {
46
+ try {
47
+ return document.querySelector(Configuration.whatsapp.container)
48
+ } catch (_) {
49
+ return null
50
+ }
51
+ }
52
+
53
+ get stylesheetLoaded() {
54
+ return Business.waitForStylesheet(Business.latestStylesheet)
55
+ }
56
+
57
+ markCoexistingWidgets() {
58
+ const webchat = document.querySelector('.hellotext--webchat:not(.hellotext--whatsapp-widget)')
59
+ const whatsapp = this.data.html
60
+
61
+ if (!webchat || !whatsapp) return
62
+
63
+ webchat.classList.add('hellotext--with-whatsapp-widget')
64
+ whatsapp.classList.add('hellotext--with-webchat')
65
+ }
66
+ }
67
+
68
+ export { WhatsAppWidget }