@hellotext/hellotext 2.5.5 → 2.5.7

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