@hellotext/hellotext 2.5.6 → 2.5.8

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 (113) hide show
  1. package/README.md +5 -26
  2. package/dist/hellotext.js +1 -1
  3. package/dist/hellotext.js.LICENSE.txt +1 -1
  4. package/index.d.ts +15 -6
  5. package/lib/api/acks.cjs +17 -31
  6. package/lib/api/acks.js +17 -42
  7. package/lib/api/businesses.cjs +14 -28
  8. package/lib/api/businesses.js +13 -36
  9. package/lib/api/events.cjs +26 -40
  10. package/lib/api/events.js +28 -52
  11. package/lib/api/forms.cjs +24 -40
  12. package/lib/api/forms.js +24 -57
  13. package/lib/api/identifications.cjs +15 -29
  14. package/lib/api/identifications.js +15 -40
  15. package/lib/api/index.cjs +26 -46
  16. package/lib/api/index.js +28 -49
  17. package/lib/api/push/identities.cjs +43 -0
  18. package/lib/api/push/identities.js +35 -0
  19. package/lib/api/response.cjs +30 -52
  20. package/lib/api/response.js +31 -60
  21. package/lib/api/submissions.cjs +18 -34
  22. package/lib/api/submissions.js +18 -48
  23. package/lib/api/webchat/messages.cjs +44 -64
  24. package/lib/api/webchat/messages.js +44 -80
  25. package/lib/api/webchats.cjs +37 -56
  26. package/lib/api/webchats.js +37 -65
  27. package/lib/api/whatsapp_widgets.cjs +49 -72
  28. package/lib/api/whatsapp_widgets.js +48 -91
  29. package/lib/builders/input_builder.cjs +36 -48
  30. package/lib/builders/input_builder.js +36 -48
  31. package/lib/builders/logo_builder.cjs +10 -29
  32. package/lib/builders/logo_builder.js +20 -28
  33. package/lib/channels/application_channel.cjs +136 -178
  34. package/lib/channels/application_channel.js +137 -180
  35. package/lib/channels/webchat_channel.cjs +125 -170
  36. package/lib/channels/webchat_channel.js +125 -170
  37. package/lib/controllers/form_controller.cjs +82 -121
  38. package/lib/controllers/form_controller.js +82 -130
  39. package/lib/controllers/message_controller.cjs +211 -282
  40. package/lib/controllers/message_controller.js +209 -284
  41. package/lib/controllers/mixins/usePopover.cjs +3 -6
  42. package/lib/controllers/mixins/usePopover.js +17 -22
  43. package/lib/controllers/webchat/emoji_picker_controller.cjs +106 -150
  44. package/lib/controllers/webchat/emoji_picker_controller.js +105 -170
  45. package/lib/controllers/webchat/useBehaviour.js +5 -5
  46. package/lib/controllers/webchat/useOpeningSequence.js +6 -6
  47. package/lib/controllers/webchat/useTeaser.js +9 -9
  48. package/lib/controllers/webchat_controller.cjs +916 -1088
  49. package/lib/controllers/webchat_controller.js +926 -1152
  50. package/lib/core/configuration/forms.cjs +21 -35
  51. package/lib/core/configuration/forms.js +20 -35
  52. package/lib/core/configuration/locale.cjs +36 -71
  53. package/lib/core/configuration/locale.js +36 -70
  54. package/lib/core/configuration/push.cjs +16 -0
  55. package/lib/core/configuration/push.js +10 -0
  56. package/lib/core/configuration/webchat.cjs +142 -182
  57. package/lib/core/configuration/webchat.js +145 -189
  58. package/lib/core/configuration/whatsapp.cjs +76 -102
  59. package/lib/core/configuration/whatsapp.js +76 -105
  60. package/lib/core/configuration.cjs +62 -75
  61. package/lib/core/configuration.js +61 -75
  62. package/lib/core/event.cjs +36 -59
  63. package/lib/core/event.js +35 -61
  64. package/lib/core/sanitize_html.js +4 -4
  65. package/lib/errors/invalid_event.cjs +5 -26
  66. package/lib/errors/invalid_event.js +5 -26
  67. package/lib/errors/not_initialized_error.cjs +5 -26
  68. package/lib/errors/not_initialized_error.js +5 -26
  69. package/lib/hellotext.cjs +186 -211
  70. package/lib/hellotext.js +189 -236
  71. package/lib/index.js +1 -1
  72. package/lib/models/business.cjs +115 -152
  73. package/lib/models/business.js +118 -162
  74. package/lib/models/cookies.cjs +34 -51
  75. package/lib/models/cookies.js +34 -51
  76. package/lib/models/fingerprint.cjs +8 -23
  77. package/lib/models/fingerprint.js +25 -58
  78. package/lib/models/form.cjs +106 -142
  79. package/lib/models/form.js +105 -153
  80. package/lib/models/form_collection.cjs +63 -101
  81. package/lib/models/form_collection.js +61 -107
  82. package/lib/models/index.cjs +7 -0
  83. package/lib/models/index.js +1 -0
  84. package/lib/models/page.cjs +106 -129
  85. package/lib/models/page.js +106 -131
  86. package/lib/models/push.cjs +345 -0
  87. package/lib/models/push.js +339 -0
  88. package/lib/models/query.cjs +21 -41
  89. package/lib/models/query.js +21 -41
  90. package/lib/models/session.cjs +33 -65
  91. package/lib/models/session.js +30 -63
  92. package/lib/models/user.cjs +29 -51
  93. package/lib/models/user.js +29 -51
  94. package/lib/models/utm.cjs +15 -27
  95. package/lib/models/utm.js +17 -32
  96. package/lib/models/webchat.cjs +52 -76
  97. package/lib/models/webchat.js +52 -90
  98. package/lib/models/whatsapp_widget.cjs +41 -59
  99. package/lib/models/whatsapp_widget.js +41 -73
  100. package/lib/vanilla.cjs +4 -7
  101. package/lib/vanilla.js +2 -1
  102. package/package.json +2 -10
  103. package/src/api/index.js +5 -0
  104. package/src/api/push/identities.js +42 -0
  105. package/src/core/configuration/push.js +12 -0
  106. package/src/core/configuration.js +5 -0
  107. package/src/hellotext.js +15 -1
  108. package/src/index.bundle.js +0 -1
  109. package/src/models/business.js +1 -0
  110. package/src/models/index.js +1 -0
  111. package/src/models/push.js +397 -0
  112. package/src/vanilla.js +2 -1
  113. package/styles/index.css +0 -30
@@ -0,0 +1,345 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.Push = void 0;
7
+ var _api = _interopRequireDefault(require("../api"));
8
+ var _core = require("../core");
9
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
10
+ /**
11
+ * Manages browser Push subscriptions for Hellotext.
12
+ *
13
+ * @property {Promise<void>|null} ready - Initialization promise, available after initialize().
14
+ */
15
+ class Push {
16
+ /**
17
+ * @param {Object} data - Push configuration from the business response.
18
+ * @param {String} data.public_key - Base64url-encoded VAPID public key.
19
+ */
20
+ constructor(data) {
21
+ this.publicKey = data.public_key;
22
+ this.serviceWorkerUrl = _core.Configuration.push.serviceWorkerUrl;
23
+ this.channelId = _core.Configuration.push.channelId;
24
+ this.ready = null;
25
+ this.registrationPromise = null;
26
+ this.subscribePromise = null;
27
+ this.unsubscribePromise = null;
28
+ this.syncPromise = null;
29
+ this.subscription = null;
30
+ this.retryTimeout = null;
31
+ this.retryAttempts = 0;
32
+ this.disposed = false;
33
+ }
34
+
35
+ /**
36
+ * Prepares the service worker and restores an existing subscription.
37
+ *
38
+ * @returns {Promise<void>}
39
+ */
40
+ initialize() {
41
+ this.ready = this.restoreSubscription();
42
+ return this.ready;
43
+ }
44
+
45
+ /**
46
+ * Registers an existing Hellotext subscription with the server without prompting.
47
+ *
48
+ * @private
49
+ * @returns {Promise<void>}
50
+ */
51
+ async restoreSubscription() {
52
+ const registration = await this.getRegistration();
53
+ const subscription = await registration.pushManager.getSubscription();
54
+ if (this.disposed || this.unsubscribePromise || !subscription || !this.owns(subscription)) {
55
+ return;
56
+ }
57
+ this.subscription = subscription;
58
+ await this.sync();
59
+ }
60
+
61
+ /**
62
+ * Creates or reuses a Push subscription. Call from a user click handler.
63
+ * Concurrent calls share the same pending request.
64
+ *
65
+ * @returns {Promise<import('../api/response').Response|void>} Server registration response,
66
+ * or no value when disposed.
67
+ * Rejects when permission is denied or a browser or network operation fails.
68
+ */
69
+ subscribe() {
70
+ if (this.disposed) return Promise.resolve();
71
+ if (this.unsubscribePromise) return Promise.reject(new Error('Push unsubscribe is in progress'));
72
+ if (this.subscribePromise) return this.subscribePromise;
73
+
74
+ // Request permission before awaiting worker readiness to retain the click's user activation.
75
+ const permission = Notification.permission === 'default' ? Notification.requestPermission() : Promise.resolve(Notification.permission);
76
+ this.subscribePromise = this.createSubscription(permission).finally(() => {
77
+ this.subscribePromise = null;
78
+ });
79
+ return this.subscribePromise;
80
+ }
81
+
82
+ /**
83
+ * Waits for permission and worker readiness, then subscribes and registers with Hellotext.
84
+ *
85
+ * @private
86
+ * @param {Promise<NotificationPermission>} permission - Pending notification permission result.
87
+ * @returns {Promise<import('../api/response').Response|void>}
88
+ */
89
+ async createSubscription(permission) {
90
+ if ((await permission) !== 'granted') throw new Error('Push permission was not granted');
91
+ const registration = await this.getRegistration();
92
+ if (this.disposed) return;
93
+ let subscription = await registration.pushManager.getSubscription();
94
+ if (this.disposed) return;
95
+ if (subscription && !this.owns(subscription)) {
96
+ throw new Error('The existing Push subscription belongs to a different application');
97
+ }
98
+ subscription ||= await registration.pushManager.subscribe({
99
+ userVisibleOnly: true,
100
+ applicationServerKey: this.applicationServerKey
101
+ });
102
+ if (this.disposed) return;
103
+ this.subscription = subscription;
104
+ return this.sync();
105
+ }
106
+
107
+ /**
108
+ * Disables the server identity and removes the browser subscription.
109
+ * Keeps the subscription when the server request fails so the caller can retry.
110
+ *
111
+ * @returns {Promise<import('../api/response').Response|null|void>} Server response, null when
112
+ * no subscription exists, or no value when disposed. Rejects when a browser or network
113
+ * operation fails.
114
+ */
115
+ unsubscribe() {
116
+ if (this.disposed) return Promise.resolve();
117
+ if (this.unsubscribePromise) return this.unsubscribePromise;
118
+ this.clearRetryTimeout();
119
+ this.unsubscribePromise = this.removeSubscription().finally(() => {
120
+ this.unsubscribePromise = null;
121
+ });
122
+ return this.unsubscribePromise;
123
+ }
124
+
125
+ /**
126
+ * Finishes pending registration before disabling and removing the subscription.
127
+ *
128
+ * @private
129
+ * @returns {Promise<import('../api/response').Response|null|void>}
130
+ */
131
+ async removeSubscription() {
132
+ await this.ready?.catch(() => {});
133
+ await this.subscribePromise?.catch(() => {});
134
+ await this.syncPromise?.catch(() => {});
135
+ const registration = await this.getRegistration();
136
+ const subscription = (await registration.pushManager.getSubscription()) || this.subscription;
137
+ if (this.disposed) return;
138
+ if (!subscription) return null;
139
+ if (!this.owns(subscription)) {
140
+ throw new Error('The existing Push subscription belongs to a different application');
141
+ }
142
+ this.subscription = subscription;
143
+ const response = await _api.default.pushIdentities.destroy({
144
+ subscription: subscription.toJSON()
145
+ });
146
+ if (response.failed) return response;
147
+ if (this.disposed) return;
148
+ await subscription.unsubscribe();
149
+ this.subscription = null;
150
+ this.clearRetryTimeout();
151
+ return response;
152
+ }
153
+
154
+ /**
155
+ * Registers the current subscription, sharing any request already in progress.
156
+ *
157
+ * @private
158
+ * @returns {Promise<import('../api/response').Response|void>}
159
+ */
160
+ sync() {
161
+ if (this.disposed) return Promise.resolve();
162
+ if (this.syncPromise) return this.syncPromise;
163
+ this.clearRetryTimeout();
164
+ this.syncPromise = this.registerIdentity().finally(() => {
165
+ this.syncPromise = null;
166
+ });
167
+ return this.syncPromise;
168
+ }
169
+
170
+ /**
171
+ * Sends the subscription to Hellotext and schedules a retry on failure.
172
+ *
173
+ * @private
174
+ * @returns {Promise<import('../api/response').Response>}
175
+ */
176
+ async registerIdentity() {
177
+ try {
178
+ const response = await _api.default.pushIdentities.create({
179
+ subscription: this.subscription.toJSON(),
180
+ ...(this.channelId ? {
181
+ channel_id: this.channelId
182
+ } : {})
183
+ });
184
+ if (response.succeeded) {
185
+ this.retryAttempts = 0;
186
+ } else {
187
+ this.scheduleRetry();
188
+ }
189
+ return response;
190
+ } catch (error) {
191
+ this.scheduleRetry();
192
+ throw error;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Schedules up to three registration retries with increasing delays.
198
+ *
199
+ * @private
200
+ * @returns {void}
201
+ */
202
+ scheduleRetry() {
203
+ if (this.disposed || this.unsubscribePromise || this.retryTimeout || this.retryAttempts >= 3) return;
204
+ this.retryTimeout = setTimeout(() => {
205
+ this.retryTimeout = null;
206
+ this.sync().catch(() => {});
207
+ }, 1000 * 2 ** this.retryAttempts);
208
+ this.retryAttempts += 1;
209
+ }
210
+
211
+ /**
212
+ * Cancels a pending registration retry.
213
+ *
214
+ * @private
215
+ * @returns {void}
216
+ */
217
+ clearRetryTimeout() {
218
+ clearTimeout(this.retryTimeout);
219
+ this.retryTimeout = null;
220
+ }
221
+
222
+ /**
223
+ * Stops registration retries and prevents further use of this instance.
224
+ *
225
+ * @returns {void}
226
+ */
227
+ dispose() {
228
+ this.disposed = true;
229
+ this.clearRetryTimeout();
230
+ }
231
+
232
+ /**
233
+ * Whether this instance has a browser subscription, regardless of server registration.
234
+ *
235
+ * @returns {Boolean}
236
+ */
237
+ get subscribed() {
238
+ return !!this.subscription;
239
+ }
240
+
241
+ /**
242
+ * Decodes the public key into the bytes expected by PushManager.subscribe().
243
+ *
244
+ * @private
245
+ * @returns {Uint8Array}
246
+ */
247
+ get applicationServerKey() {
248
+ const base64 = this.publicKey.replace(/-/g, '+').replace(/_/g, '/');
249
+ return Uint8Array.from(atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, '=')), char => char.charCodeAt(0));
250
+ }
251
+
252
+ /**
253
+ * Checks whether a subscription uses this instance's public key.
254
+ *
255
+ * @private
256
+ * @param {PushSubscription} subscription - Browser subscription to check.
257
+ * @returns {Boolean}
258
+ */
259
+ owns(subscription) {
260
+ const key = subscription.options?.applicationServerKey;
261
+ if (!key) return false;
262
+ const actual = new Uint8Array(key);
263
+ const expected = this.applicationServerKey;
264
+ return actual.length === expected.length && actual.every((value, index) => value === expected[index]);
265
+ }
266
+
267
+ /**
268
+ * Gets the worker registration, sharing any pending lookup.
269
+ * Failed lookups can be retried by a later call.
270
+ *
271
+ * @private
272
+ * @returns {Promise<ServiceWorkerRegistration>}
273
+ */
274
+ getRegistration() {
275
+ if (!this.registrationPromise) {
276
+ this.registrationPromise = this.loadRegistration().catch(error => {
277
+ this.registrationPromise = null;
278
+ throw error;
279
+ });
280
+ }
281
+ return this.registrationPromise;
282
+ }
283
+
284
+ /**
285
+ * Registers the configured worker or waits for the page's existing registration.
286
+ *
287
+ * @private
288
+ * @returns {Promise<ServiceWorkerRegistration>} An active registration.
289
+ * Rejects if registration fails or the worker readiness wait times out.
290
+ */
291
+ async loadRegistration() {
292
+ if (this.serviceWorkerUrl) {
293
+ const registration = await navigator.serviceWorker.register(this.serviceWorkerUrl);
294
+ if (registration.active && !registration.installing && !registration.waiting) {
295
+ // A returning visitor may have an older worker that does not include our Push handlers.
296
+ // Calling register() with the same URL can return that existing registration without
297
+ // checking whether the script served at that URL has changed since their last visit.
298
+ //
299
+ // If no replacement is already installing or waiting, explicitly check for an update
300
+ // before treating the active worker as ready. Awaiting update() completes the update
301
+ // check, but does not wait for a replacement worker to activate. The code below selects
302
+ // that replacement, if one was found, and waits for its activation before subscribing.
303
+ await registration.update();
304
+ }
305
+ const worker = registration.installing || registration.waiting || registration.active;
306
+ if (worker?.state === 'activated') return registration;
307
+ return new Promise((resolve, reject) => {
308
+ const timeout = setTimeout(() => finish(new Error('Push service worker did not become active')), 10000);
309
+ const finish = error => {
310
+ clearTimeout(timeout);
311
+ worker?.removeEventListener('statechange', changed);
312
+ error ? reject(error) : resolve(registration);
313
+ };
314
+ const changed = () => {
315
+ if (worker?.state === 'activated') finish();
316
+ if (worker?.state === 'redundant') finish(new Error('Push service worker installation failed'));
317
+ };
318
+ worker?.addEventListener('statechange', changed);
319
+ changed();
320
+ });
321
+ }
322
+
323
+ // Reuse the service worker already registered for this page.
324
+ return new Promise((resolve, reject) => {
325
+ const timeout = setTimeout(() => reject(new Error('Push service worker is not available')), 10000);
326
+ navigator.serviceWorker.ready.then(registration => {
327
+ clearTimeout(timeout);
328
+ resolve(registration);
329
+ }, error => {
330
+ clearTimeout(timeout);
331
+ reject(error);
332
+ });
333
+ });
334
+ }
335
+
336
+ /**
337
+ * Whether the current page provides the browser APIs required for Push.
338
+ *
339
+ * @returns {Boolean}
340
+ */
341
+ static get supported() {
342
+ return typeof window !== 'undefined' && window.isSecureContext === true && typeof navigator !== 'undefined' && 'serviceWorker' in navigator && typeof PushManager !== 'undefined' && typeof Notification !== 'undefined';
343
+ }
344
+ }
345
+ exports.Push = Push;
@@ -0,0 +1,339 @@
1
+ import API from '../api';
2
+ import { Configuration } from '../core';
3
+
4
+ /**
5
+ * Manages browser Push subscriptions for Hellotext.
6
+ *
7
+ * @property {Promise<void>|null} ready - Initialization promise, available after initialize().
8
+ */
9
+ class Push {
10
+ /**
11
+ * @param {Object} data - Push configuration from the business response.
12
+ * @param {String} data.public_key - Base64url-encoded VAPID public key.
13
+ */
14
+ constructor(data) {
15
+ this.publicKey = data.public_key;
16
+ this.serviceWorkerUrl = Configuration.push.serviceWorkerUrl;
17
+ this.channelId = Configuration.push.channelId;
18
+ this.ready = null;
19
+ this.registrationPromise = null;
20
+ this.subscribePromise = null;
21
+ this.unsubscribePromise = null;
22
+ this.syncPromise = null;
23
+ this.subscription = null;
24
+ this.retryTimeout = null;
25
+ this.retryAttempts = 0;
26
+ this.disposed = false;
27
+ }
28
+
29
+ /**
30
+ * Prepares the service worker and restores an existing subscription.
31
+ *
32
+ * @returns {Promise<void>}
33
+ */
34
+ initialize() {
35
+ this.ready = this.restoreSubscription();
36
+ return this.ready;
37
+ }
38
+
39
+ /**
40
+ * Registers an existing Hellotext subscription with the server without prompting.
41
+ *
42
+ * @private
43
+ * @returns {Promise<void>}
44
+ */
45
+ async restoreSubscription() {
46
+ const registration = await this.getRegistration();
47
+ const subscription = await registration.pushManager.getSubscription();
48
+ if (this.disposed || this.unsubscribePromise || !subscription || !this.owns(subscription)) {
49
+ return;
50
+ }
51
+ this.subscription = subscription;
52
+ await this.sync();
53
+ }
54
+
55
+ /**
56
+ * Creates or reuses a Push subscription. Call from a user click handler.
57
+ * Concurrent calls share the same pending request.
58
+ *
59
+ * @returns {Promise<import('../api/response').Response|void>} Server registration response,
60
+ * or no value when disposed.
61
+ * Rejects when permission is denied or a browser or network operation fails.
62
+ */
63
+ subscribe() {
64
+ if (this.disposed) return Promise.resolve();
65
+ if (this.unsubscribePromise) return Promise.reject(new Error('Push unsubscribe is in progress'));
66
+ if (this.subscribePromise) return this.subscribePromise;
67
+
68
+ // Request permission before awaiting worker readiness to retain the click's user activation.
69
+ const permission = Notification.permission === 'default' ? Notification.requestPermission() : Promise.resolve(Notification.permission);
70
+ this.subscribePromise = this.createSubscription(permission).finally(() => {
71
+ this.subscribePromise = null;
72
+ });
73
+ return this.subscribePromise;
74
+ }
75
+
76
+ /**
77
+ * Waits for permission and worker readiness, then subscribes and registers with Hellotext.
78
+ *
79
+ * @private
80
+ * @param {Promise<NotificationPermission>} permission - Pending notification permission result.
81
+ * @returns {Promise<import('../api/response').Response|void>}
82
+ */
83
+ async createSubscription(permission) {
84
+ if ((await permission) !== 'granted') throw new Error('Push permission was not granted');
85
+ const registration = await this.getRegistration();
86
+ if (this.disposed) return;
87
+ let subscription = await registration.pushManager.getSubscription();
88
+ if (this.disposed) return;
89
+ if (subscription && !this.owns(subscription)) {
90
+ throw new Error('The existing Push subscription belongs to a different application');
91
+ }
92
+ subscription ||= await registration.pushManager.subscribe({
93
+ userVisibleOnly: true,
94
+ applicationServerKey: this.applicationServerKey
95
+ });
96
+ if (this.disposed) return;
97
+ this.subscription = subscription;
98
+ return this.sync();
99
+ }
100
+
101
+ /**
102
+ * Disables the server identity and removes the browser subscription.
103
+ * Keeps the subscription when the server request fails so the caller can retry.
104
+ *
105
+ * @returns {Promise<import('../api/response').Response|null|void>} Server response, null when
106
+ * no subscription exists, or no value when disposed. Rejects when a browser or network
107
+ * operation fails.
108
+ */
109
+ unsubscribe() {
110
+ if (this.disposed) return Promise.resolve();
111
+ if (this.unsubscribePromise) return this.unsubscribePromise;
112
+ this.clearRetryTimeout();
113
+ this.unsubscribePromise = this.removeSubscription().finally(() => {
114
+ this.unsubscribePromise = null;
115
+ });
116
+ return this.unsubscribePromise;
117
+ }
118
+
119
+ /**
120
+ * Finishes pending registration before disabling and removing the subscription.
121
+ *
122
+ * @private
123
+ * @returns {Promise<import('../api/response').Response|null|void>}
124
+ */
125
+ async removeSubscription() {
126
+ await this.ready?.catch(() => {});
127
+ await this.subscribePromise?.catch(() => {});
128
+ await this.syncPromise?.catch(() => {});
129
+ const registration = await this.getRegistration();
130
+ const subscription = (await registration.pushManager.getSubscription()) || this.subscription;
131
+ if (this.disposed) return;
132
+ if (!subscription) return null;
133
+ if (!this.owns(subscription)) {
134
+ throw new Error('The existing Push subscription belongs to a different application');
135
+ }
136
+ this.subscription = subscription;
137
+ const response = await API.pushIdentities.destroy({
138
+ subscription: subscription.toJSON()
139
+ });
140
+ if (response.failed) return response;
141
+ if (this.disposed) return;
142
+ await subscription.unsubscribe();
143
+ this.subscription = null;
144
+ this.clearRetryTimeout();
145
+ return response;
146
+ }
147
+
148
+ /**
149
+ * Registers the current subscription, sharing any request already in progress.
150
+ *
151
+ * @private
152
+ * @returns {Promise<import('../api/response').Response|void>}
153
+ */
154
+ sync() {
155
+ if (this.disposed) return Promise.resolve();
156
+ if (this.syncPromise) return this.syncPromise;
157
+ this.clearRetryTimeout();
158
+ this.syncPromise = this.registerIdentity().finally(() => {
159
+ this.syncPromise = null;
160
+ });
161
+ return this.syncPromise;
162
+ }
163
+
164
+ /**
165
+ * Sends the subscription to Hellotext and schedules a retry on failure.
166
+ *
167
+ * @private
168
+ * @returns {Promise<import('../api/response').Response>}
169
+ */
170
+ async registerIdentity() {
171
+ try {
172
+ const response = await API.pushIdentities.create({
173
+ subscription: this.subscription.toJSON(),
174
+ ...(this.channelId ? {
175
+ channel_id: this.channelId
176
+ } : {})
177
+ });
178
+ if (response.succeeded) {
179
+ this.retryAttempts = 0;
180
+ } else {
181
+ this.scheduleRetry();
182
+ }
183
+ return response;
184
+ } catch (error) {
185
+ this.scheduleRetry();
186
+ throw error;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Schedules up to three registration retries with increasing delays.
192
+ *
193
+ * @private
194
+ * @returns {void}
195
+ */
196
+ scheduleRetry() {
197
+ if (this.disposed || this.unsubscribePromise || this.retryTimeout || this.retryAttempts >= 3) return;
198
+ this.retryTimeout = setTimeout(() => {
199
+ this.retryTimeout = null;
200
+ this.sync().catch(() => {});
201
+ }, 1000 * 2 ** this.retryAttempts);
202
+ this.retryAttempts += 1;
203
+ }
204
+
205
+ /**
206
+ * Cancels a pending registration retry.
207
+ *
208
+ * @private
209
+ * @returns {void}
210
+ */
211
+ clearRetryTimeout() {
212
+ clearTimeout(this.retryTimeout);
213
+ this.retryTimeout = null;
214
+ }
215
+
216
+ /**
217
+ * Stops registration retries and prevents further use of this instance.
218
+ *
219
+ * @returns {void}
220
+ */
221
+ dispose() {
222
+ this.disposed = true;
223
+ this.clearRetryTimeout();
224
+ }
225
+
226
+ /**
227
+ * Whether this instance has a browser subscription, regardless of server registration.
228
+ *
229
+ * @returns {Boolean}
230
+ */
231
+ get subscribed() {
232
+ return !!this.subscription;
233
+ }
234
+
235
+ /**
236
+ * Decodes the public key into the bytes expected by PushManager.subscribe().
237
+ *
238
+ * @private
239
+ * @returns {Uint8Array}
240
+ */
241
+ get applicationServerKey() {
242
+ const base64 = this.publicKey.replace(/-/g, '+').replace(/_/g, '/');
243
+ return Uint8Array.from(atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, '=')), char => char.charCodeAt(0));
244
+ }
245
+
246
+ /**
247
+ * Checks whether a subscription uses this instance's public key.
248
+ *
249
+ * @private
250
+ * @param {PushSubscription} subscription - Browser subscription to check.
251
+ * @returns {Boolean}
252
+ */
253
+ owns(subscription) {
254
+ const key = subscription.options?.applicationServerKey;
255
+ if (!key) return false;
256
+ const actual = new Uint8Array(key);
257
+ const expected = this.applicationServerKey;
258
+ return actual.length === expected.length && actual.every((value, index) => value === expected[index]);
259
+ }
260
+
261
+ /**
262
+ * Gets the worker registration, sharing any pending lookup.
263
+ * Failed lookups can be retried by a later call.
264
+ *
265
+ * @private
266
+ * @returns {Promise<ServiceWorkerRegistration>}
267
+ */
268
+ getRegistration() {
269
+ if (!this.registrationPromise) {
270
+ this.registrationPromise = this.loadRegistration().catch(error => {
271
+ this.registrationPromise = null;
272
+ throw error;
273
+ });
274
+ }
275
+ return this.registrationPromise;
276
+ }
277
+
278
+ /**
279
+ * Registers the configured worker or waits for the page's existing registration.
280
+ *
281
+ * @private
282
+ * @returns {Promise<ServiceWorkerRegistration>} An active registration.
283
+ * Rejects if registration fails or the worker readiness wait times out.
284
+ */
285
+ async loadRegistration() {
286
+ if (this.serviceWorkerUrl) {
287
+ const registration = await navigator.serviceWorker.register(this.serviceWorkerUrl);
288
+ if (registration.active && !registration.installing && !registration.waiting) {
289
+ // A returning visitor may have an older worker that does not include our Push handlers.
290
+ // Calling register() with the same URL can return that existing registration without
291
+ // checking whether the script served at that URL has changed since their last visit.
292
+ //
293
+ // If no replacement is already installing or waiting, explicitly check for an update
294
+ // before treating the active worker as ready. Awaiting update() completes the update
295
+ // check, but does not wait for a replacement worker to activate. The code below selects
296
+ // that replacement, if one was found, and waits for its activation before subscribing.
297
+ await registration.update();
298
+ }
299
+ const worker = registration.installing || registration.waiting || registration.active;
300
+ if (worker?.state === 'activated') return registration;
301
+ return new Promise((resolve, reject) => {
302
+ const timeout = setTimeout(() => finish(new Error('Push service worker did not become active')), 10000);
303
+ const finish = error => {
304
+ clearTimeout(timeout);
305
+ worker?.removeEventListener('statechange', changed);
306
+ error ? reject(error) : resolve(registration);
307
+ };
308
+ const changed = () => {
309
+ if (worker?.state === 'activated') finish();
310
+ if (worker?.state === 'redundant') finish(new Error('Push service worker installation failed'));
311
+ };
312
+ worker?.addEventListener('statechange', changed);
313
+ changed();
314
+ });
315
+ }
316
+
317
+ // Reuse the service worker already registered for this page.
318
+ return new Promise((resolve, reject) => {
319
+ const timeout = setTimeout(() => reject(new Error('Push service worker is not available')), 10000);
320
+ navigator.serviceWorker.ready.then(registration => {
321
+ clearTimeout(timeout);
322
+ resolve(registration);
323
+ }, error => {
324
+ clearTimeout(timeout);
325
+ reject(error);
326
+ });
327
+ });
328
+ }
329
+
330
+ /**
331
+ * Whether the current page provides the browser APIs required for Push.
332
+ *
333
+ * @returns {Boolean}
334
+ */
335
+ static get supported() {
336
+ return typeof window !== 'undefined' && window.isSecureContext === true && typeof navigator !== 'undefined' && 'serviceWorker' in navigator && typeof PushManager !== 'undefined' && typeof Notification !== 'undefined';
337
+ }
338
+ }
339
+ export { Push };