@hellotext/hellotext 1.8.7 → 2.0.1

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.
Files changed (112) hide show
  1. package/README.md +27 -2
  2. package/dist/hellotext.js +1 -1
  3. package/index.d.ts +61 -0
  4. package/lib/api/businesses.cjs +37 -0
  5. package/lib/api/businesses.js +3 -9
  6. package/lib/api/events.cjs +45 -0
  7. package/lib/api/events.js +8 -14
  8. package/lib/api/forms.cjs +51 -0
  9. package/lib/api/forms.js +10 -17
  10. package/lib/api/index.cjs +51 -0
  11. package/lib/api/index.js +10 -23
  12. package/lib/api/response.cjs +76 -0
  13. package/lib/api/response.js +1 -7
  14. package/lib/api/submissions.cjs +47 -0
  15. package/lib/api/submissions.js +9 -17
  16. package/lib/api/webchat/messages.cjs +70 -0
  17. package/lib/api/webchat/messages.js +10 -18
  18. package/lib/api/webchats.cjs +44 -0
  19. package/lib/api/webchats.js +8 -16
  20. package/lib/builders/input_builder.cjs +61 -0
  21. package/lib/builders/input_builder.js +4 -11
  22. package/lib/builders/logo_builder.cjs +50 -0
  23. package/lib/builders/logo_builder.js +5 -12
  24. package/lib/channels/application_channel.cjs +67 -0
  25. package/lib/channels/application_channel.js +1 -8
  26. package/lib/channels/{web_chat_channel.js → webchat_channel.cjs} +35 -20
  27. package/lib/channels/webchat_channel.js +3 -11
  28. package/lib/controllers/form_controller.cjs +141 -0
  29. package/lib/controllers/form_controller.js +15 -22
  30. package/lib/controllers/mixins/usePopover.cjs +59 -0
  31. package/lib/controllers/mixins/usePopover.js +6 -14
  32. package/lib/controllers/{web_chat/pagination_controller.js → webchat/emoji_picker_controller.cjs} +74 -29
  33. package/lib/controllers/webchat/emoji_picker_controller.js +11 -17
  34. package/lib/controllers/webchat_controller.cjs +433 -0
  35. package/lib/controllers/webchat_controller.js +49 -51
  36. package/lib/core/configuration/forms.cjs +49 -0
  37. package/lib/core/configuration/forms.js +2 -8
  38. package/lib/core/configuration/{web_chat.js → webchat.cjs} +53 -38
  39. package/lib/core/configuration/webchat.js +2 -9
  40. package/lib/core/configuration.cjs +65 -0
  41. package/lib/core/configuration.js +8 -13
  42. package/lib/core/event.cjs +71 -0
  43. package/lib/core/event.js +5 -11
  44. package/lib/core/index.cjs +20 -0
  45. package/lib/core/index.js +2 -20
  46. package/lib/errors/index.cjs +19 -0
  47. package/lib/errors/index.js +2 -19
  48. package/lib/errors/invalid_event.cjs +34 -0
  49. package/lib/errors/invalid_event.js +1 -7
  50. package/lib/errors/not_initialized_error.cjs +34 -0
  51. package/lib/errors/not_initialized_error.js +1 -7
  52. package/lib/hellotext.cjs +153 -0
  53. package/lib/hellotext.js +19 -27
  54. package/lib/index.cjs +19 -0
  55. package/lib/index.js +11 -20
  56. package/lib/locales/en.cjs +22 -0
  57. package/lib/locales/en.js +2 -9
  58. package/lib/locales/es.cjs +22 -0
  59. package/lib/locales/es.js +2 -9
  60. package/lib/locales/index.cjs +14 -0
  61. package/lib/locales/index.js +6 -14
  62. package/lib/models/business.cjs +62 -0
  63. package/lib/models/business.js +22 -24
  64. package/lib/models/cookies.cjs +40 -0
  65. package/lib/models/cookies.js +3 -10
  66. package/lib/models/form.cjs +166 -0
  67. package/lib/models/form.js +10 -17
  68. package/lib/models/form_collection.cjs +122 -0
  69. package/lib/models/form_collection.js +18 -22
  70. package/lib/models/index.cjs +54 -0
  71. package/lib/models/index.js +7 -54
  72. package/lib/models/query.cjs +51 -0
  73. package/lib/models/query.js +3 -9
  74. package/lib/models/session.cjs +58 -0
  75. package/lib/models/session.js +12 -17
  76. package/lib/models/{web_chat.js → webchat.cjs} +14 -22
  77. package/lib/models/webchat.js +6 -13
  78. package/lib/vanilla.cjs +13 -0
  79. package/lib/vanilla.js +1 -13
  80. package/package.json +41 -7
  81. package/src/controllers/webchat_controller.js +72 -47
  82. package/src/hellotext.js +5 -4
  83. package/src/index.bundle.js +5 -0
  84. package/src/index.js +1 -3
  85. package/src/models/business.js +3 -6
  86. package/src/models/form_collection.js +10 -4
  87. package/src/models/session.js +4 -3
  88. package/styles/index.css +22 -30
  89. package/.babelrc +0 -24
  90. package/.github/workflows/ci.yml +0 -24
  91. package/.prettierignore +0 -3
  92. package/.prettierrc.json +0 -18
  93. package/MIT-LICENSE +0 -20
  94. package/__tests__/builders/input_builder_test.js +0 -117
  95. package/__tests__/core/configuration/forms_test.js +0 -30
  96. package/__tests__/core/configuration/webchat_test.js +0 -163
  97. package/__tests__/core/configuration_test.js +0 -28
  98. package/__tests__/core/event_test.js +0 -56
  99. package/__tests__/hellotext_test.js +0 -197
  100. package/__tests__/models/business_test.js +0 -26
  101. package/__tests__/models/cookies_test.js +0 -25
  102. package/__tests__/models/form_collection_test.js +0 -103
  103. package/__tests__/models/form_test.js +0 -131
  104. package/__tests__/models/query_test.js +0 -97
  105. package/docs/forms.md +0 -176
  106. package/docs/tracking.md +0 -203
  107. package/docs/webchat.md +0 -101
  108. package/jest.config.js +0 -4
  109. package/jest.setup.js +0 -16
  110. package/lib/api/web_chat/messages.js +0 -74
  111. package/lib/api/web_chats.js +0 -53
  112. package/webpack.config.js +0 -24
@@ -1,103 +0,0 @@
1
- /**
2
- * @jest-environment jsdom
3
- */
4
-
5
- import { Business, FormCollection } from '../../src/models'
6
- import Hellotext from '../../src/hellotext'
7
-
8
- beforeEach(() => {
9
- Business.prototype.fetchPublicData = jest.fn().mockResolvedValue({ whitelist: 'disabled' })
10
-
11
- Hellotext.initialize('M01az53K', {
12
- autoGenerateSession: false,
13
- forms: {
14
- autoMount: false,
15
- }
16
- })
17
- })
18
-
19
- describe('collect', () => {
20
- it('does not fetch forms when there are no forms to fetch', async () => {
21
- const fetch = jest.fn()
22
- global.fetch = fetch
23
-
24
- await Hellotext.forms.collect()
25
-
26
- expect(fetch).not.toHaveBeenCalled()
27
- })
28
-
29
- it('fetches forms when there are forms to fetch', async () => {
30
- global.fetch = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ id: 1 }) })
31
-
32
- document.body.innerHTML = `
33
- <form data-hello-form="1"></form>
34
- `
35
-
36
- await Hellotext.forms.collect()
37
-
38
- expect(fetch).toHaveBeenCalledTimes(1)
39
- })
40
- })
41
-
42
- describe('add', () => {
43
- const forms = new FormCollection()
44
- const form = { id: 1, name: 'Form 1' }
45
-
46
- it('adds a form to the forms array', () => {
47
- forms.add(form)
48
- expect(forms.length).toEqual(1)
49
- })
50
-
51
- it('does not add a form that is already in the forms array', () => {
52
- forms.add(form)
53
- forms.add(form)
54
-
55
- expect(forms.length).toEqual(1)
56
- })
57
- })
58
-
59
- describe('getting elements', () => {
60
- const forms = new FormCollection()
61
- forms.add({ id: 1, name: 'Form 1' })
62
- forms.add({ id: 2, name: 'Form 1' })
63
-
64
- describe('getById', () => {
65
- it('returns the form with the given id', () => {
66
- expect(forms.getById(1).id).toEqual(1)
67
- })
68
- })
69
-
70
- describe('getByIndex', () => {
71
- it('returns the form at the given index', () => {
72
- expect(forms.getByIndex(1).id).toEqual(2)
73
- })
74
- })
75
- })
76
-
77
- describe('includes', () => {
78
- const forms = new FormCollection()
79
- const form = { id: 1, name: 'Form 1' }
80
-
81
- it('is true when the form is in the forms array', () => {
82
- forms.add(form)
83
- expect(forms.includes(1)).toEqual(true)
84
- })
85
-
86
- it('is false when the form is not in the forms array', () => {
87
- expect(forms.includes(2)).toEqual(false)
88
- })
89
- })
90
-
91
- describe('excludes', () => {
92
- const forms = new FormCollection()
93
- const form = { id: 1, name: 'Form 1' }
94
-
95
- it('is true when the form is not in the forms array', () => {
96
- expect(forms.excludes(2)).toEqual(true)
97
- })
98
-
99
- it('is false when the form is in the forms array', () => {
100
- forms.add(form)
101
- expect(forms.excludes(1)).toEqual(false)
102
- })
103
- })
@@ -1,131 +0,0 @@
1
- /**
2
- * @jest-environment jsdom
3
- */
4
-
5
- import Hellotext from '../../src/hellotext'
6
- import { Form } from '../../src/models'
7
-
8
- describe('id', () => {
9
- it('is the form id', () => {
10
- const form = new Form({ id: 1 })
11
- expect(form.id).toEqual(1)
12
- })
13
- })
14
-
15
- describe('mount', () => {
16
- const data = {
17
- id: 1,
18
- steps: [
19
- {
20
- header: { content: 'Header' },
21
- inputs: [],
22
- button: { content: 'Button' },
23
- footer: { content: 'Footer' },
24
- },
25
- ]
26
- }
27
-
28
- const form = new Form(data)
29
-
30
- beforeEach(() => {
31
- Hellotext.business = {
32
- locale: {
33
- white_label: {
34
- powered_by: 'Powered by Hellotext',
35
- }
36
- },
37
- features: {
38
- white_label: false,
39
- }
40
- }
41
-
42
- document.body.innerHTML = ''
43
- })
44
-
45
- it('mounts the form', () => {
46
- form.mount()
47
- expect(document.body.querySelector('form')).not.toBeNull()
48
- })
49
-
50
- describe('when form has been completed', () => {
51
- beforeEach(() => {
52
- localStorage.setItem('hello-form-1', 'true')
53
- })
54
-
55
- it('does not mount the form automatically', () => {
56
- form.mount()
57
- expect(document.body.querySelector('form')).toBeNull()
58
- })
59
-
60
- it('mounts the form when ifCompleted is false', () => {
61
- form.mount({ ifCompleted: false })
62
- expect(document.body.querySelector('form')).not.toBeNull()
63
- })
64
- })
65
- })
66
-
67
- describe('markAsCompleted', () => {
68
- it('saves the form as completed in localStorage', () => {
69
- const form = new Form({ id: 1 })
70
- form.markAsCompleted()
71
- expect(localStorage.getItem('hello-form-1')).not.toBeNull()
72
- })
73
-
74
- it('emits a form:completed event', () => {
75
- const form = new Form({ id: 1 })
76
- const emit = jest.spyOn(Hellotext.eventEmitter, 'dispatch')
77
-
78
- form.markAsCompleted()
79
- expect(emit).toHaveBeenCalled()
80
- })
81
- })
82
-
83
- describe('localeAuthKey', () => {
84
- it('is email when the first step has an email input', () => {
85
- const form = new Form({
86
- steps: [
87
- {
88
- inputs: [{ kind: 'email' }]
89
- }
90
- ]
91
- })
92
-
93
- expect(form.localeAuthKey).toBe('email')
94
- })
95
-
96
- it('is phone when the first step has a phone input', () => {
97
- const form = new Form({
98
- steps: [
99
- {
100
- inputs: [{ kind: 'phone' }]
101
- }
102
- ]
103
- })
104
-
105
- expect(form.localeAuthKey).toBe('phone')
106
- })
107
-
108
- it('is phone_and_email when the first step has both email and phone inputs', () => {
109
- const form = new Form({
110
- steps: [
111
- {
112
- inputs: [{ kind: 'email' }, { kind: 'phone' }]
113
- }
114
- ]
115
- })
116
-
117
- expect(form.localeAuthKey).toBe('phone_and_email')
118
- })
119
-
120
- it('is none when the first step has neither email nor phone inputs', () => {
121
- const form = new Form({
122
- steps: [
123
- {
124
- inputs: [{ kind: 'first_name' }]
125
- }
126
- ]
127
- })
128
-
129
- expect(form.localeAuthKey).toBe('none')
130
- })
131
- })
@@ -1,97 +0,0 @@
1
- /**
2
- * @jest-environment jsdom
3
- */
4
-
5
- import { Query } from '../../src/models'
6
-
7
- describe('get', () => {
8
- beforeEach(() => {
9
- const windowMock = {
10
- location: { search: "?hello_session=session&hello_preview=1" },
11
- }
12
-
13
- jest.spyOn(global, 'window', 'get').mockImplementation(() => windowMock)
14
- })
15
-
16
- it('gets the value of a query parameter', () => {
17
- const query = new Query()
18
-
19
- expect(query.get("session")).toEqual("session")
20
- expect(query.get("preview")).toEqual("1")
21
- })
22
- })
23
-
24
- describe('has', () => {
25
- beforeEach(() => {
26
- const windowMock = {
27
- location: { search: "?hello_session=session" },
28
- }
29
-
30
- jest.spyOn(global, 'window', 'get').mockImplementation(() => windowMock)
31
- })
32
-
33
- it('is true when the query parameter is present', () => {
34
- const query = new Query()
35
- expect(query.has("session")).toEqual(true)
36
- })
37
-
38
- it('is false when the query parameter is not present', () => {
39
- const query = new Query()
40
- expect(query.has("preview")).toEqual(false)
41
- })
42
- })
43
-
44
- describe('inPreviewMode', () => {
45
- it('is true when the preview query parameter is present', () => {
46
- const windowMock = {
47
- location: { search: "?hello_preview" },
48
- }
49
-
50
- jest.spyOn(global, 'window', 'get').mockImplementation(() => windowMock)
51
-
52
- expect(Query.inPreviewMode).toEqual(true)
53
- })
54
-
55
- it('is false when the preview query parameter is not present', () => {
56
- const windowMock = {
57
- location: { search: "" },
58
- }
59
-
60
- jest.spyOn(global, 'window', 'get').mockImplementation(() => windowMock)
61
-
62
- expect(Query.inPreviewMode).toEqual(false)
63
- })
64
- })
65
-
66
- describe('session', () => {
67
- it('gets the session from the query parameter when present in query', () => {
68
- const windowMock = {
69
- location: { search: "?hello_session=session" },
70
- }
71
-
72
- jest.spyOn(global, 'window', 'get').mockImplementation(() => windowMock)
73
-
74
- const query = new Query()
75
- expect(query.session).toEqual("session")
76
- })
77
-
78
- it('gets the session from the cookie when not present in query', () => {
79
- const windowMock = {
80
- location: { search: "" },
81
- }
82
-
83
- jest.spyOn(global, 'window', 'get').mockImplementation(() => windowMock)
84
-
85
- document.cookie = "hello_session=session"
86
-
87
- const query = new Query()
88
- expect(query.session).toEqual("session")
89
- })
90
- })
91
-
92
- describe("#toHellotextParameter", () => {
93
- it("prefixes the argument with hello_", () => {
94
- const query = new Query()
95
- expect(query.toHellotextParam("preview")).toEqual("hello_preview")
96
- });
97
- })
package/docs/forms.md DELETED
@@ -1,176 +0,0 @@
1
- ## Forms
2
-
3
- Create dynamic forms based on built-in subscriber attributes such as name, email, phone or other custom properties unique to your business.
4
- Then let Hellotext handle building the form, collecting, validating and authenticating the data users submit.
5
-
6
- For more information on how to create a form from the dashboard, view this [guide](https://help.hellotext.com/forms).
7
-
8
- ### Configuration
9
-
10
- Hellotext forms have a default configuration that can be overridden by passing an object when initializing the library. It has the following attributes by default:
11
-
12
- - `autoMount`: Automatically mount forms to the DOM when a `form` element with the `data-hello-form` attribute is found. Default is `true`.
13
- - `successMessage`: Display a contextual success message when a form is submitted successfully. Default is `true`.
14
- You can turn this off by setting it to `false`, or provide your custom success message by setting it to a string.
15
-
16
- ```javascript
17
- Hellotext.initialize('HELLOTEXT_BUSINESS_ID', {
18
- forms: {
19
- autoMount: true,
20
- successMessage: 'Thank you for submitting the form'
21
- }
22
- })
23
- ```
24
-
25
- ### Collection Phase
26
-
27
- Hellotext uses the `MutationObserver` API to listen for changes in the DOM, specifically new form elements being added that have the `data-hello-form` attribute.
28
-
29
- You can access the forms object to also trigger the form collection phase manually.
30
- This is useful if you have a Single Page Application(SPA) and cannot hardcode the `data-hello-form` element on the rendered page.
31
-
32
- To manually collect forms, do the following.
33
-
34
- ```javascript
35
- Hellotext.initialize('HELLOTEXT_BUSINESS_ID')
36
- Hellotext.forms.collect()
37
- ```
38
-
39
- Once loaded, you can access the `FormCollection` object by calling `Hellotext.forms`.
40
-
41
- Make sure you have initialized with `Hellotext.initialize` otherwise an error is reported.
42
-
43
- Form collection finishes once Hellotext has fetched the data for the form elements present on the page from the Hellotext API.
44
- Afterwards, it dispatches a `forms:collected` event that you can subscribe to.
45
-
46
- ```javascript
47
- Hellotext.on('forms:collected', forms => {
48
- console.log(forms) // Instance of FormCollection
49
- })
50
- ```
51
-
52
- The `FormCollection` class is a wrapper around the forms, which providers other useful methods.
53
-
54
- - `getById(id: string): Form` - Get a form by it's id
55
- - `getByIndex(index: number): Form` - Get a form by it's index
56
- - `includes(id: string): boolean` - Check if a form is included in the collection
57
- - `excludes(id: string): boolean` - Check if a form is not included in the collection
58
- - `length` - Get the number of forms in the collection
59
- - `forEach`, `map` are also supported.
60
-
61
- ### Mounting forms
62
-
63
- Hellotext.js by default automatically mounts forms collected to the DOM. You can disable this behaviour by passing the `autoMountForms` option as `false` when initializing the library.
64
-
65
- ```javascript
66
- Hellotext.initialize('HELLOTEXT_BUSINESS_ID', { autoMountForms: false })
67
- ```
68
-
69
- If form mounting is disabled, Hellotext does not automatically mount form elements,
70
- you will have total control on when and where to mount the form elements. To mount a form object, you call the `mount` method on the form object.
71
-
72
- ```javascript
73
- Hellotext.on('forms:collected', forms => {
74
- forms.getByIndex(0).mount()
75
- })
76
- ```
77
-
78
- Mounting a form creates the form and it's components that are associated to it, and attaches it to the DOM.
79
- Hellotext looks for a `form` element with the `data-hello-form` attribute and mounts the form inside it.
80
- If this condition is not met, Hellotext creates the form manually and appends it to the body of the document.
81
- We recommend to make the criteria met to ensure the form is loaded into an expected place in your page.
82
-
83
- Hellotext provides seamless integration with your website, once forms are completed, they are stored in the `localStorage`,
84
- this ensures that the form is not displayed again to the user if they have already completed it. Of course, sometimes it may be desired to
85
- show the form to a user regardless if they have completed the form or not, in these cases, you can adjust the way you call `Form.mount()`,
86
-
87
- ```javascript
88
- Hellotext.on('forms:collected', forms => {
89
- forms.getByIndex(0).mount({ ifCompleted: false })
90
- })
91
- ```
92
-
93
- This will mount the form regardless if the user has completed the form or not.
94
-
95
- ### Validation
96
-
97
- Hellotext automatically validates the form inputs based on how they were configured on the dashboard
98
- using browser's native [checkValidity()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement/checkValidity).
99
-
100
- Once the user tries to submit the form and there are missing required fields,
101
- the submission is halted and we display default browser's error message using [HTMLObjectElement.validationMessage](https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/validationMessage) property.
102
-
103
- If the form contains a unique property and the client enters a value that is already taken,
104
- the submission will not be complete. Instead, an error will be reported to the client and the form will not be submitted.
105
-
106
- Uniqueness errors use the browser's native [setCustomValidity()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/setCustomValidity)
107
- method which are reported as normal form validation errors, they can be styled with the `input:invalid` selector.
108
-
109
- ### Understanding Verification
110
-
111
- Hellotext protects you from bot submissions and protects your customers from identity theft and impersonation.
112
-
113
- When a subscriber fills the form with an email and/or phone number, Hellotext sends a verification link to the value the submission had at the time of creation.
114
- When the customer clicks the link in their email and/or the SMS they receive, the attribute is verified.
115
- Once the email/phone number were verified, the data is considered to be _verified_.
116
-
117
- Once verification of a submission is complete, Hellotext performs an automatic merge (if needed) of all profiles
118
- that have the same email and/or phone number. After automatic merging, if a property has multiple values,
119
- it will be visible in the Audience page and the user can see the values that were merged and they can decide which one
120
- to ignore and which one to accept.
121
-
122
- ### Form Completion
123
-
124
- Once the form is considered to be complete, it will be sent to the Hellotext API to create(or update) a profile from the submission information.
125
- The library also dispatches a `form:completed` event that you can subscribe to. In addition, a Session object is set and stored on the browser's cookies.
126
- Additionally, the `Hellotext.session` is also set if no session was present already, you can listen for the session events by subscribing to `session-set` event.
127
-
128
- ```javascript
129
- Hellotext.on('form:completed', (form) => {
130
- console.log(form) // An object from FormData
131
- })
132
-
133
- {
134
- state: 'completed',
135
- completedAt: 1730114734999, // Timestamp when the form was completed
136
- id: "xxxxx", // Id of the form that has been completed
137
- data: {
138
- first_name: "Billy",
139
- last_name: "Butcher",
140
- email: "theboys@hellotext.com",
141
- phone: "+1234567890",
142
- property_by_id[xxxxx]: "value"
143
- }
144
- }
145
- ```
146
-
147
- The data in the from will differ based on the inputs you have configured on the dashboard.
148
-
149
- ### Understanding form's layout
150
-
151
- Hellotext assumes a fixed layout for forms, which are in order of Header, Inputs, Button and Notice.
152
-
153
- But you can override this layout if you want. Overriding a form's layout can be achieved
154
- by moving the placement of the form's components. For example, if you want to display the Button component after the Footer, here's how you can do that
155
-
156
- ```html
157
- <form data-hello-form=":id">
158
- <footer data-form-notice></footer>
159
-
160
- <button data-form-button></button>
161
- </form>
162
- ```
163
-
164
- Hellotext would simply load the contents inside the respective elements without creating the default layout.
165
- If these elements were not defined, Hellotext would render the button then the notice component.
166
-
167
- ### Customizing the Form's styles
168
-
169
- Generated form elements have minimum styles to make them display correctly. No colors, borders or padding are applied.
170
- You can style the form elements to match your brand guidelines. Hellotext.js ships with a few lines of CSS to apply layout on the components.
171
- See `styles/index.css` for the default styles applied to the form elements.
172
-
173
- ### White Labels
174
-
175
- As a subscriber to Hellotext, if your package does not support white labels, a `powered by Hellotext` logo is displayed at the bottom-right of the form.
176
- You are free to move the position of this element if you want, but it should be visible to your subscribers.