cozy-client 60.30.6 → 60.32.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,399 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.dismiss = exports.getActiveBanner = exports.getActiveBanners = exports.BANNERS_DOCTYPE = void 0;
9
+
10
+ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11
+
12
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
13
+
14
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
15
+
16
+ var _dsl = require("../queries/dsl");
17
+
18
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
19
+
20
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
21
+
22
+ var BANNERS_DOCTYPE = 'io.cozy.banners';
23
+ exports.BANNERS_DOCTYPE = BANNERS_DOCTYPE;
24
+ var KNOWN_SEVERITIES = ['info', 'warning', 'error'];
25
+ var KNOWN_SURFACES = ['banner', 'modal'];
26
+ /**
27
+ * @typedef {object} BannerCta
28
+ * @property {string} label - The label, plain text
29
+ * @property {string} url - Where the label points
30
+ */
31
+
32
+ /**
33
+ * @typedef {object} Banner An io.cozy.banners document
34
+ * @property {string} [_id] - Identifier of the document
35
+ * @property {string} bannerId - Identifier of the banner itself
36
+ * @property {string} category - What the banner is about
37
+ * @property {'info'|'warning'|'error'} severity - How loudly to render it
38
+ * @property {'banner'|'modal'} surface - Where to render it
39
+ * @property {string} [title] - Optional heading, used on the modal surface
40
+ * @property {string} text - The message, already localized
41
+ * @property {string} lang - BCP 47 tag of the language `text` and `cta.label` are in
42
+ * @property {BannerCta} [cta] - Optional call to action
43
+ * @property {BannerCta} [secondaryCta] - Optional lesser action, needs a cta
44
+ * @property {boolean} dismissible - Whether the client offers a dismiss control
45
+ * @property {string} [dismissedAt] - When the user dismissed it
46
+ * @property {number} priority - Sort order, highest first
47
+ * @property {string} startsAt - Start of the validity window, inclusive
48
+ * @property {string} [endsAt] - End of the validity window, exclusive
49
+ */
50
+
51
+ /**
52
+ * The doctype writes an ISO 8601 timestamp with an explicit offset, and nothing
53
+ * else parses. `Date.parse` on its own falls back to the engine's legacy parser
54
+ * for anything it does not recognise, so a malformed bound would resolve to a
55
+ * real instant in one browser and to NaN in another, and the shared contract
56
+ * vectors could no longer say what every client displays.
57
+ */
58
+
59
+ var ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
60
+ /** A bound that is absent leaves that end of the window open, unlike an unparseable one. */
61
+
62
+ var toTime = function toTime(value) {
63
+ if (value == null) return null;
64
+ if (typeof value !== 'string' || !ISO_TIMESTAMP.test(value)) return NaN;
65
+ return Date.parse(value);
66
+ };
67
+
68
+ var isDismissed = function isDismissed(banner) {
69
+ return (banner === null || banner === void 0 ? void 0 : banner.dismissedAt) != null;
70
+ };
71
+ /** The window is inclusive on startsAt, exclusive on endsAt, open when absent. */
72
+
73
+
74
+ var isInWindow = function isInWindow(banner, now) {
75
+ var startsAt = toTime(banner === null || banner === void 0 ? void 0 : banner.startsAt);
76
+ var endsAt = toTime(banner === null || banner === void 0 ? void 0 : banner.endsAt);
77
+
78
+ if (startsAt === null || Number.isNaN(startsAt) || Number.isNaN(endsAt)) {
79
+ return false;
80
+ }
81
+
82
+ var at = now.getTime();
83
+ if (at < startsAt) return false;
84
+ return endsAt === null || at < endsAt;
85
+ };
86
+ /**
87
+ * Highest priority first, then bannerId, compared code unit by code unit so
88
+ * that every client orders equal priorities the same way. `localeCompare`
89
+ * would depend on the runtime locale.
90
+ */
91
+
92
+
93
+ var rankOf = function rankOf(banner) {
94
+ return Number.isFinite(banner === null || banner === void 0 ? void 0 : banner.priority) ? banner.priority : 0;
95
+ };
96
+
97
+ var byPriority = function byPriority(a, b) {
98
+ var byRank = rankOf(b) - rankOf(a);
99
+ if (byRank !== 0) return byRank;
100
+ if (a.bannerId < b.bannerId) return -1;
101
+ if (a.bannerId > b.bannerId) return 1;
102
+ return 0;
103
+ };
104
+ /**
105
+ * The doctype specifies an absolute `https://` URL, so anything else is dropped
106
+ * rather than handed to a client that would put it in an `href`: the documents
107
+ * live in a database the applications can write, and a `javascript:` URL there
108
+ * would run in the origin of whichever application rendered the banner.
109
+ */
110
+
111
+
112
+ var isSafeCta = function isSafeCta(cta) {
113
+ return typeof (cta === null || cta === void 0 ? void 0 : cta.url) === 'string' && /^https:\/\//i.test(cta.url);
114
+ };
115
+ /**
116
+ * The doctype allows a new severity or surface without a version bump, so a
117
+ * value this client does not know is rendered rather than dropped.
118
+ */
119
+
120
+
121
+ var withFallbacks = function withFallbacks(banner) {
122
+ var cta = isSafeCta(banner.cta) ? banner.cta : undefined;
123
+ return _objectSpread(_objectSpread({}, banner), {}, {
124
+ severity: KNOWN_SEVERITIES.includes(banner.severity) ? banner.severity : 'warning',
125
+ surface: KNOWN_SURFACES.includes(banner.surface) ? banner.surface : 'banner',
126
+ cta: cta,
127
+ // The secondary action is the lesser one, so it is never promoted to the
128
+ // only thing on offer: without a primary it is dropped rather than shown.
129
+ secondaryCta: cta && isSafeCta(banner.secondaryCta) ? banner.secondaryCta : undefined
130
+ });
131
+ };
132
+ /**
133
+ * `client.query` resolves undefined instead of rejecting when the client has an
134
+ * `onError` callback, which would otherwise read as "no banners apply".
135
+ */
136
+
137
+
138
+ var readData = function readData(response) {
139
+ if (response === null || response === undefined) {
140
+ throw new Error("The ".concat(BANNERS_DOCTYPE, " query resolved nothing"));
141
+ }
142
+
143
+ return response.data;
144
+ };
145
+ /** The stack link answers a missing document with null, the pouch link with []. */
146
+
147
+
148
+ var readOne = function readOne(response) {
149
+ var data = readData(response);
150
+ var doc = Array.isArray(data) ? data[0] : data;
151
+ return doc !== null && doc !== void 0 && doc._id ? doc : null;
152
+ };
153
+
154
+ var isConflict = function isConflict(error) {
155
+ var _error$message;
156
+
157
+ return (error === null || error === void 0 ? void 0 : error.status) === 409 || /Document update conflict/.test((_error$message = error === null || error === void 0 ? void 0 : error.message) !== null && _error$message !== void 0 ? _error$message : '');
158
+ };
159
+ /**
160
+ * The banners to display, in order.
161
+ *
162
+ * @param {object} client - A CozyClient instance
163
+ * @param {object} [options] - Options
164
+ * @param {Date} [options.now] - The current time
165
+ * @returns {Promise<Banner[]>} The banners to render
166
+ */
167
+
168
+
169
+ var getActiveBanners = /*#__PURE__*/function () {
170
+ var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(client) {
171
+ var _ref2,
172
+ _ref2$now,
173
+ now,
174
+ data,
175
+ _args = arguments;
176
+
177
+ return _regenerator.default.wrap(function _callee$(_context) {
178
+ while (1) {
179
+ switch (_context.prev = _context.next) {
180
+ case 0:
181
+ _ref2 = _args.length > 1 && _args[1] !== undefined ? _args[1] : {}, _ref2$now = _ref2.now, now = _ref2$now === void 0 ? new Date() : _ref2$now;
182
+ _context.t0 = readData;
183
+ _context.next = 4;
184
+ return client.query((0, _dsl.Q)(BANNERS_DOCTYPE).UNSAFE_noLimit(), {
185
+ as: "".concat(BANNERS_DOCTYPE, "/all")
186
+ });
187
+
188
+ case 4:
189
+ _context.t1 = _context.sent;
190
+ data = (0, _context.t0)(_context.t1);
191
+ return _context.abrupt("return", (Array.isArray(data) ? data : []).filter(function (banner) {
192
+ return !isDismissed(banner) && isInWindow(banner, now);
193
+ }).sort(byPriority).map(withFallbacks));
194
+
195
+ case 7:
196
+ case "end":
197
+ return _context.stop();
198
+ }
199
+ }
200
+ }, _callee);
201
+ }));
202
+
203
+ return function getActiveBanners(_x) {
204
+ return _ref.apply(this, arguments);
205
+ };
206
+ }();
207
+ /**
208
+ * The single banner to display, for a surface that shows one at a time.
209
+ *
210
+ * A native client rendering one slot reads this rather than taking the head of
211
+ * the list itself, so the ordering rules stay in one place.
212
+ *
213
+ * @param {object} client - A CozyClient instance
214
+ * @param {object} [options] - Options
215
+ * @param {Date} [options.now] - The current time
216
+ * @returns {Promise<Banner|null>} The highest priority banner, or null
217
+ */
218
+
219
+
220
+ exports.getActiveBanners = getActiveBanners;
221
+
222
+ var getActiveBanner = /*#__PURE__*/function () {
223
+ var _ref3 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(client, options) {
224
+ var _yield$getActiveBanne;
225
+
226
+ return _regenerator.default.wrap(function _callee2$(_context2) {
227
+ while (1) {
228
+ switch (_context2.prev = _context2.next) {
229
+ case 0:
230
+ _context2.next = 2;
231
+ return getActiveBanners(client, options);
232
+
233
+ case 2:
234
+ _context2.t1 = _yield$getActiveBanne = _context2.sent[0];
235
+ _context2.t0 = _context2.t1 !== null;
236
+
237
+ if (!_context2.t0) {
238
+ _context2.next = 6;
239
+ break;
240
+ }
241
+
242
+ _context2.t0 = _yield$getActiveBanne !== void 0;
243
+
244
+ case 6:
245
+ if (!_context2.t0) {
246
+ _context2.next = 10;
247
+ break;
248
+ }
249
+
250
+ _context2.t2 = _yield$getActiveBanne;
251
+ _context2.next = 11;
252
+ break;
253
+
254
+ case 10:
255
+ _context2.t2 = null;
256
+
257
+ case 11:
258
+ return _context2.abrupt("return", _context2.t2);
259
+
260
+ case 12:
261
+ case "end":
262
+ return _context2.stop();
263
+ }
264
+ }
265
+ }, _callee2);
266
+ }));
267
+
268
+ return function getActiveBanner(_x2, _x3) {
269
+ return _ref3.apply(this, arguments);
270
+ };
271
+ }();
272
+ /**
273
+ * Records a dismissal on the stored document, retrying once on a conflict.
274
+ *
275
+ * The stored document is written back rather than the one the caller holds,
276
+ * whose severity and surface have had the fallbacks applied.
277
+ *
278
+ * @param {object} client - A CozyClient instance
279
+ * @param {Banner} banner - The banner to dismiss
280
+ * @param {object} [options] - Options
281
+ * @param {string} [options.dismissedAt] - The timestamp to record
282
+ * @returns {Promise<{data: Banner|null}>} The dismissed document, null when gone
283
+ */
284
+
285
+
286
+ exports.getActiveBanner = getActiveBanner;
287
+
288
+ var dismiss = /*#__PURE__*/function () {
289
+ var _ref4 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4(client, banner) {
290
+ var _ref5,
291
+ dismissedAt,
292
+ at,
293
+ write,
294
+ _args4 = arguments;
295
+
296
+ return _regenerator.default.wrap(function _callee4$(_context4) {
297
+ while (1) {
298
+ switch (_context4.prev = _context4.next) {
299
+ case 0:
300
+ _ref5 = _args4.length > 2 && _args4[2] !== undefined ? _args4[2] : {}, dismissedAt = _ref5.dismissedAt;
301
+
302
+ if (banner !== null && banner !== void 0 && banner._id) {
303
+ _context4.next = 3;
304
+ break;
305
+ }
306
+
307
+ throw new Error('A banner needs an _id to be dismissed');
308
+
309
+ case 3:
310
+ at = dismissedAt !== null && dismissedAt !== void 0 ? dismissedAt : new Date().toISOString();
311
+
312
+ write = /*#__PURE__*/function () {
313
+ var _ref6 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3() {
314
+ var stored;
315
+ return _regenerator.default.wrap(function _callee3$(_context3) {
316
+ while (1) {
317
+ switch (_context3.prev = _context3.next) {
318
+ case 0:
319
+ _context3.t0 = readOne;
320
+ _context3.next = 3;
321
+ return client.query((0, _dsl.Q)(BANNERS_DOCTYPE).getById(banner._id));
322
+
323
+ case 3:
324
+ _context3.t1 = _context3.sent;
325
+ stored = (0, _context3.t0)(_context3.t1);
326
+
327
+ if (stored) {
328
+ _context3.next = 7;
329
+ break;
330
+ }
331
+
332
+ return _context3.abrupt("return", {
333
+ data: null
334
+ });
335
+
336
+ case 7:
337
+ if (!isDismissed(stored)) {
338
+ _context3.next = 9;
339
+ break;
340
+ }
341
+
342
+ return _context3.abrupt("return", {
343
+ data: stored
344
+ });
345
+
346
+ case 9:
347
+ return _context3.abrupt("return", client.save(_objectSpread(_objectSpread({}, stored), {}, {
348
+ _type: BANNERS_DOCTYPE,
349
+ dismissedAt: at
350
+ })));
351
+
352
+ case 10:
353
+ case "end":
354
+ return _context3.stop();
355
+ }
356
+ }
357
+ }, _callee3);
358
+ }));
359
+
360
+ return function write() {
361
+ return _ref6.apply(this, arguments);
362
+ };
363
+ }();
364
+
365
+ _context4.prev = 5;
366
+ _context4.next = 8;
367
+ return write();
368
+
369
+ case 8:
370
+ return _context4.abrupt("return", _context4.sent);
371
+
372
+ case 11:
373
+ _context4.prev = 11;
374
+ _context4.t0 = _context4["catch"](5);
375
+
376
+ if (isConflict(_context4.t0)) {
377
+ _context4.next = 15;
378
+ break;
379
+ }
380
+
381
+ throw _context4.t0;
382
+
383
+ case 15:
384
+ return _context4.abrupt("return", write());
385
+
386
+ case 16:
387
+ case "end":
388
+ return _context4.stop();
389
+ }
390
+ }
391
+ }, _callee4, null, [[5, 11]]);
392
+ }));
393
+
394
+ return function dismiss(_x4, _x5) {
395
+ return _ref4.apply(this, arguments);
396
+ };
397
+ }();
398
+
399
+ exports.dismiss = dismiss;