cozy-client 60.31.0 → 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.
@@ -5,9 +5,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
5
5
  Object.defineProperty(exports, "__esModule", {
6
6
  value: true
7
7
  });
8
- exports.dismiss = exports.getActiveBanner = exports.getActiveBanners = exports.getVisibleBanners = exports.getCta = exports.getSurface = exports.getSeverity = exports.byPriority = exports.isInWindow = exports.isDismissed = exports.isSupported = exports.isTrusted = exports.buildBannersQuery = exports.BANNERS_QUERY_NAME = exports.KNOWN_SURFACES = exports.KNOWN_SEVERITIES = exports.SUPPORTED_DOCTYPE_VERSION = exports.BANNERS_DOCTYPE = void 0;
9
-
10
- var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
8
+ exports.dismiss = exports.getActiveBanner = exports.getActiveBanners = exports.BANNERS_DOCTYPE = void 0;
11
9
 
12
10
  var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
13
11
 
@@ -15,619 +13,164 @@ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/
15
13
 
16
14
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
17
15
 
18
- var _isValid = _interopRequireDefault(require("date-fns/isValid"));
19
-
20
- var _parseISO = _interopRequireDefault(require("date-fns/parseISO"));
21
-
22
16
  var _dsl = require("../queries/dsl");
23
17
 
24
- var _utils = require("./utils");
25
-
26
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; }
27
19
 
28
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; }
29
21
 
30
- function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
31
-
32
- function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
33
-
34
- function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
35
-
36
22
  var BANNERS_DOCTYPE = 'io.cozy.banners';
37
- /**
38
- * The version of the io.cozy.banners shape this code was written against.
39
- * A document declaring a higher cozyMetadata.doctypeVersion is skipped.
40
- */
41
-
42
23
  exports.BANNERS_DOCTYPE = BANNERS_DOCTYPE;
43
- var SUPPORTED_DOCTYPE_VERSION = 1;
44
- /**
45
- * The severities the doctype defines. A value outside this list is rendered
46
- * with DEFAULT_SEVERITY. Frozen because the fallbacks read it at call time, so
47
- * a consumer mutating it would change what every other consumer renders.
48
- *
49
- * @type {readonly BannerSeverity[]}
50
- */
51
-
52
- exports.SUPPORTED_DOCTYPE_VERSION = SUPPORTED_DOCTYPE_VERSION;
53
24
  var KNOWN_SEVERITIES = ['info', 'warning', 'error'];
54
- exports.KNOWN_SEVERITIES = KNOWN_SEVERITIES;
55
- Object.freeze(KNOWN_SEVERITIES);
56
- /**
57
- * The surfaces the doctype defines. A value outside this list is rendered on
58
- * DEFAULT_SURFACE. Frozen for the same reason as KNOWN_SEVERITIES.
59
- *
60
- * @type {readonly BannerSurface[]}
61
- */
62
-
63
25
  var KNOWN_SURFACES = ['banner', 'modal'];
64
- exports.KNOWN_SURFACES = KNOWN_SURFACES;
65
- Object.freeze(KNOWN_SURFACES);
66
- /** @type {BannerSeverity} */
67
-
68
- var DEFAULT_SEVERITY = 'warning';
69
- /** @type {BannerSurface} */
70
-
71
- var DEFAULT_SURFACE = 'banner';
72
- var STACK_APP = 'stack';
73
26
  /**
74
27
  * @typedef {object} BannerCta
75
- * @property {string} label - The label, plain text, in the same language as the banner text
76
- * @property {string} url - Absolute https URL the label points to
77
- */
78
-
79
- /**
80
- * @typedef {'quota'|'billing'|'trial'|'account'|'system'} BannerCategory
81
- */
82
-
83
- /**
84
- * @typedef {'info'|'warning'|'error'} BannerSeverity
85
- */
86
-
87
- /**
88
- * @typedef {'banner'|'modal'} BannerSurface
89
- */
90
-
91
- /**
92
- * @typedef {object} VisibleBannersOptions
93
- * @property {Date} [now] - The current time, defaults to now
94
- * @property {number} [supportedDoctypeVersion] - The highest version this client handles
95
- */
96
-
97
- /**
98
- * @typedef {import("../CozyClient").default} BannerClient A CozyClient instance
99
- */
100
-
101
- /**
102
- * @typedef {object} DismissOptions
103
- * @property {string} [dismissedAt] - The timestamp to record, defaults to now
28
+ * @property {string} label - The label, plain text
29
+ * @property {string} url - Where the label points
104
30
  */
105
31
 
106
32
  /**
107
33
  * @typedef {object} Banner An io.cozy.banners document
108
- * @property {string} [_id] - Identifier of the document, minted by CouchDB
109
- * @property {string} [_rev] - Revision identifier of the document
34
+ * @property {string} [_id] - Identifier of the document
110
35
  * @property {string} bannerId - Identifier of the banner itself
111
- * @property {BannerCategory} category - What the banner is about
112
- * @property {BannerSeverity} severity - info, warning or error
113
- * @property {BannerSurface} surface - banner or modal
114
- * @property {string} text - The message, in the language given by lang
115
- * @property {string} lang - BCP 47 tag of the language text is written in
116
- * @property {BannerCta|null} [cta] - Optional call to action
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
117
44
  * @property {boolean} dismissible - Whether the client offers a dismiss control
118
- * @property {string|null} [dismissedAt] - When the user dismissed this banner
45
+ * @property {string} [dismissedAt] - When the user dismissed it
119
46
  * @property {number} priority - Sort order, highest first
120
47
  * @property {string} startsAt - Start of the validity window, inclusive
121
- * @property {string|null} [endsAt] - End of the validity window, exclusive
122
- * @property {object} [source] - What produced the document
123
- * @property {object} [cozyMetadata] - Document lifecycle metadata
48
+ * @property {string} [endsAt] - End of the validity window, exclusive
124
49
  */
125
50
 
126
51
  /**
127
- * The name the banners query is stored under. It is namespaced rather than the
128
- * bare doctype: `client.query` keys the store entry on this name but keys the
129
- * in-flight promise on the definition, so an unrelated consumer naming its own
130
- * `io.cozy.banners` query would make this one resolve nothing while that query
131
- * is loading.
132
- */
133
-
134
- var BANNERS_QUERY_NAME = "".concat(BANNERS_DOCTYPE, "/all");
135
- /**
136
- * Query for the banners of the current instance.
137
- *
138
- * The query is named so every consumer shares one store entry rather than
139
- * minting a new one per call, and the limit is lifted because the whole
140
- * collection is read in full.
141
- *
142
- * @returns {import('../types').Query} A named query
143
- */
144
-
145
- exports.BANNERS_QUERY_NAME = BANNERS_QUERY_NAME;
146
-
147
- var buildBannersQuery = function buildBannersQuery() {
148
- return {
149
- definition: (0, _dsl.Q)(BANNERS_DOCTYPE).UNSAFE_noLimit(),
150
- options: {
151
- as: BANNERS_QUERY_NAME
152
- }
153
- };
154
- };
155
- /**
156
- * Documents are only trusted when the stack wrote them. Permissions scope by
157
- * doctype and verb, never by field, so any application allowed to record a
158
- * dismissal is also able to author a document that looks like a platform
159
- * message.
160
- *
161
- * This is not a security boundary, and should not be read as one. The stack
162
- * does not own this field: cozy-client keeps whatever `cozyMetadata` the caller
163
- * passes (CozyClient.js, ensureCozyMetadata spreads it last), so an application
164
- * can set `createdByApp: 'stack'` on a document it authored itself and pass
165
- * this check. It catches an application writing under its own name, which is
166
- * the ordinary mistake, not one deliberately impersonating the stack. Enforcing
167
- * that needs the doctype to be stack write only on the server side.
168
- *
169
- * It also only speaks to who created the document. An application holding write
170
- * permission can rewrite the fields of a stack authored one, which the platform
171
- * cannot prevent, so a client escapes text and never interprets markup in it.
172
- *
173
- * @param {Banner} banner - The document to check
174
- * @returns {boolean} True when the stack is the author
175
- */
176
-
177
-
178
- exports.buildBannersQuery = buildBannersQuery;
179
-
180
- var isTrusted = function isTrusted(banner) {
181
- return (0, _utils.getCreatedByApp)(banner) === STACK_APP;
182
- };
183
- /**
184
- * cozy-stack writes numbers as strings in places, `cozyMetadata` declares
185
- * `doctypeVersion` that way platform wide, so a value is parsed rather than
186
- * compared as it comes. `Number()` alone is not enough: it maps null, "", false
187
- * and [] to a finite 0, which would read as a real version or a real priority
188
- * rather than as an unusable value, so only a number or a non blank string is
189
- * parsed at all.
190
- *
191
- * @param {any} value - The value to read
192
- * @param {number} fallback - What an absent or unusable value means
193
- * @returns {number} The parsed number, or the fallback
194
- */
195
-
196
-
197
- exports.isTrusted = isTrusted;
198
-
199
- var toNumber = function toNumber(value, fallback) {
200
- var parsed = typeof value === 'number' || typeof value === 'string' && value.trim() !== '' ? Number(value) : NaN;
201
- return Number.isFinite(parsed) ? parsed : fallback;
202
- };
203
- /**
204
- * @param {Banner} banner - The document to read
205
- * @returns {number} The version, defaulting to 1 for a missing or unusable one
206
- */
207
-
208
-
209
- var doctypeVersion = function doctypeVersion(banner) {
210
- var _banner$cozyMetadata;
211
-
212
- return toNumber(banner === null || banner === void 0 ? void 0 : (_banner$cozyMetadata = banner.cozyMetadata) === null || _banner$cozyMetadata === void 0 ? void 0 : _banner$cozyMetadata.doctypeVersion, 1);
213
- };
214
- /**
215
- * @param {Banner} banner - The document to check
216
- * @param {number} [supportedVersion] - The highest version this client handles
217
- * @returns {boolean} True when the shape is one this client can read
218
- */
219
-
220
-
221
- var isSupported = function isSupported(banner) {
222
- var supportedVersion = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : SUPPORTED_DOCTYPE_VERSION;
223
- return doctypeVersion(banner) <= supportedVersion;
224
- };
225
- /**
226
- * A null value and an absent key mean the same thing for dismissedAt and
227
- * endsAt, and `new Date(null)` is the Unix epoch rather than "no date", so
228
- * every date is null checked before being parsed.
229
- *
230
- * Returns a timestamp rather than a Date so the comparisons stay primitive,
231
- * and NaN when the value is present but unparseable, which the callers reject
232
- * rather than silently treating as "no bound".
233
- *
234
- * This is the only date parser in the file. `Date.parse` is deliberately not
235
- * used anywhere: it accepts implementation defined formats that `parseISO`
236
- * rejects, and the two disagree by the local UTC offset on a date only value,
237
- * so mixing them would give one document two different instants.
238
- *
239
- * @param {string|null|undefined} value - An ISO 8601 timestamp
240
- * @returns {number|null} The timestamp, NaN when malformed, null when absent
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.
241
57
  */
242
58
 
243
-
244
- exports.isSupported = isSupported;
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. */
245
61
 
246
62
  var toTime = function toTime(value) {
247
- if (value === null || value === undefined) return null;
248
- var parsed = (0, _parseISO.default)(value);
249
- return (0, _isValid.default)(parsed) ? parsed.getTime() : NaN;
63
+ if (value == null) return null;
64
+ if (typeof value !== 'string' || !ISO_TIMESTAMP.test(value)) return NaN;
65
+ return Date.parse(value);
250
66
  };
251
- /**
252
- * @param {Banner} banner - The document to check
253
- * @returns {boolean} True when the user dismissed it
254
- */
255
-
256
67
 
257
68
  var isDismissed = function isDismissed(banner) {
258
69
  return (banner === null || banner === void 0 ? void 0 : banner.dismissedAt) != null;
259
70
  };
260
- /**
261
- * The window is inclusive on startsAt and exclusive on endsAt, and an absent
262
- * endsAt is open ended. A malformed bound hides the banner: a document whose
263
- * window cannot be read is not one a client can decide to show.
264
- *
265
- * An absent startsAt hides it too. The doctype makes cta, dismissedAt and
266
- * endsAt the only fields that may be missing, so a document with no lower bound
267
- * is not open ended, it is unreadable. Treating it as "no bound" would make the
268
- * missing field more permissive than a malformed one, and would let anything
269
- * able to rewrite the document turn a scheduled banner into a permanent one by
270
- * deleting both bounds.
271
- *
272
- * @param {Banner} banner - The document to check
273
- * @param {Date} [now] - The current time
274
- * @returns {boolean} True when the banner applies at that time
275
- */
276
-
71
+ /** The window is inclusive on startsAt, exclusive on endsAt, open when absent. */
277
72
 
278
- exports.isDismissed = isDismissed;
279
73
 
280
- var isInWindow = function isInWindow(banner) {
281
- var now = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : new Date();
74
+ var isInWindow = function isInWindow(banner, now) {
282
75
  var startsAt = toTime(banner === null || banner === void 0 ? void 0 : banner.startsAt);
283
76
  var endsAt = toTime(banner === null || banner === void 0 ? void 0 : banner.endsAt);
284
- if (startsAt === null || Number.isNaN(startsAt)) return false;
285
- if (Number.isNaN(endsAt)) return false;
286
- var at = now.getTime();
287
- if (Number.isNaN(at)) return false;
288
- if (at < startsAt) return false;
289
- if (endsAt !== null && at >= endsAt) return false;
290
- return true;
291
- };
292
-
293
- exports.isInWindow = isInWindow;
294
-
295
- var priorityOf = function priorityOf(banner) {
296
- return toNumber(banner === null || banner === void 0 ? void 0 : banner.priority, 0);
297
- };
298
- /**
299
- * Highest priority first, then bannerId ascending so every client orders
300
- * identically when priorities are equal.
301
- *
302
- * The tie break compares code units rather than calling localeCompare, whose
303
- * result depends on the runtime's locale and ICU build. A missing or non
304
- * numeric priority sorts as 0 instead of producing NaN, which would make the
305
- * comparator intransitive and the sort order engine defined.
306
- *
307
- * Documents that lost their bannerId fall back on _id, as the deduplication
308
- * does, so two of them do not compare equal and land in whatever order the
309
- * database returned them in.
310
- *
311
- * @param {Banner} a - A banner
312
- * @param {Banner} b - Another banner
313
- * @returns {number} The comparison result
314
- */
315
77
 
316
-
317
- var byPriority = function byPriority(a, b) {
318
- var _ref, _a$bannerId, _ref2, _b$bannerId;
319
-
320
- var byRank = priorityOf(b) - priorityOf(a);
321
- if (byRank !== 0) return byRank;
322
- var idA = String((_ref = (_a$bannerId = a === null || a === void 0 ? void 0 : a.bannerId) !== null && _a$bannerId !== void 0 ? _a$bannerId : a === null || a === void 0 ? void 0 : a._id) !== null && _ref !== void 0 ? _ref : '');
323
- var idB = String((_ref2 = (_b$bannerId = b === null || b === void 0 ? void 0 : b.bannerId) !== null && _b$bannerId !== void 0 ? _b$bannerId : b === null || b === void 0 ? void 0 : b._id) !== null && _ref2 !== void 0 ? _ref2 : '');
324
- if (idA < idB) return -1;
325
- if (idA > idB) return 1;
326
- return 0;
327
- };
328
- /**
329
- * @param {Banner} banner - The document to read
330
- * @returns {BannerSeverity} The severity to render, falling back when unknown
331
- */
332
-
333
-
334
- exports.byPriority = byPriority;
335
-
336
- var getSeverity = function getSeverity(banner) {
337
- return KNOWN_SEVERITIES.includes(banner === null || banner === void 0 ? void 0 : banner.severity) ? banner.severity : DEFAULT_SEVERITY;
338
- };
339
- /**
340
- * @param {Banner} banner - The document to read
341
- * @returns {BannerSurface} The surface to render on, falling back when unknown
342
- */
343
-
344
-
345
- exports.getSeverity = getSeverity;
346
-
347
- var getSurface = function getSurface(banner) {
348
- return KNOWN_SURFACES.includes(banner === null || banner === void 0 ? void 0 : banner.surface) ? banner.surface : DEFAULT_SURFACE;
349
- };
350
- /**
351
- * Whitespace plus the zero width characters `trim` leaves behind. A label made
352
- * only of these renders as a button with nothing on it. They are stripped to
353
- * decide whether the label is empty, never from the label itself, so a script
354
- * that uses a zero width joiner between glyphs keeps it.
355
- */
356
-
357
-
358
- exports.getSurface = getSurface;
359
- var BLANK = /[\s\u200B-\u200D\uFEFF]/g;
360
- /**
361
- * A call to action is only rendered when it carries a label and points at
362
- * https, so a locally authored document cannot turn into a platform styled
363
- * link on any scheme, and no banner renders a button the user cannot read.
364
- * The scheme is matched case insensitively since URL schemes are.
365
- *
366
- * @param {Banner} banner - The document to read
367
- * @returns {BannerCta|null} The call to action, or null when there is none to show
368
- */
369
-
370
- var getCta = function getCta(banner) {
371
- var cta = banner === null || banner === void 0 ? void 0 : banner.cta;
372
- if (!cta || typeof cta.url !== 'string') return null;
373
-
374
- if (typeof cta.label !== 'string' || cta.label.replace(BLANK, '') === '') {
375
- return null;
376
- }
377
-
378
- return isHttpsUrl(cta.url) ? cta : null;
379
- };
380
- /**
381
- * A prefix check accepts "https://" and "https:example.org", which do not point
382
- * where they look like they point, so the value is parsed instead. The parser
383
- * rejects an https URL with no host outright, so reaching the scheme comparison
384
- * already means there is one.
385
- *
386
- * @param {string} raw - The candidate URL
387
- * @returns {boolean} True when it is an absolute https URL
388
- */
389
-
390
-
391
- exports.getCta = getCta;
392
-
393
- var isHttpsUrl = function isHttpsUrl(raw) {
394
- try {
395
- return new URL(raw).protocol === 'https:';
396
- } catch (error) {
78
+ if (startsAt === null || Number.isNaN(startsAt) || Number.isNaN(endsAt)) {
397
79
  return false;
398
80
  }
399
- };
400
-
401
- var updatedAtOf = function updatedAtOf(banner) {
402
- var _banner$cozyMetadata2;
403
-
404
- var at = toTime(banner === null || banner === void 0 ? void 0 : (_banner$cozyMetadata2 = banner.cozyMetadata) === null || _banner$cozyMetadata2 === void 0 ? void 0 : _banner$cozyMetadata2.updatedAt);
405
- return Number.isFinite(at) ? at : 0;
406
- };
407
- /**
408
- * Orders two documents carrying the same bannerId, most relevant first. The
409
- * highest supported doctypeVersion wins; equal versions fall back on the most
410
- * recently written document and then on _id, so the winner never depends on
411
- * the order the rows came back in.
412
- *
413
- * @param {Banner} candidate - The document being considered
414
- * @param {Banner} current - The document currently held
415
- * @returns {boolean} True when candidate should replace current
416
- */
417
-
418
-
419
- var supersedes = function supersedes(candidate, current) {
420
- var _candidate$_id, _current$_id;
421
81
 
422
- var byVersion = doctypeVersion(candidate) - doctypeVersion(current);
423
- if (byVersion !== 0) return byVersion > 0;
424
- var byUpdate = updatedAtOf(candidate) - updatedAtOf(current);
425
- if (byUpdate !== 0) return byUpdate > 0;
426
- return String((_candidate$_id = candidate === null || candidate === void 0 ? void 0 : candidate._id) !== null && _candidate$_id !== void 0 ? _candidate$_id : '') < String((_current$_id = current === null || current === void 0 ? void 0 : current._id) !== null && _current$_id !== void 0 ? _current$_id : '');
82
+ var at = now.getTime();
83
+ if (at < startsAt) return false;
84
+ return endsAt === null || at < endsAt;
427
85
  };
428
86
  /**
429
- * The identity two documents are grouped and dismissed under. bannerId and _id
430
- * are kept in separate namespaces: sharing one would let a document whose _id
431
- * happens to equal another document's bannerId dismiss or supersede it. A
432
- * document carrying neither keys on itself, so unrelated ones do not collapse
433
- * onto a single `undefined` key.
434
- *
435
- * @param {Banner} banner - The document to key
436
- * @returns {string|Banner} The grouping key
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.
437
90
  */
438
91
 
439
92
 
440
- var groupKey = function groupKey(banner) {
441
- if ((banner === null || banner === void 0 ? void 0 : banner.bannerId) != null) return "bannerId:".concat(banner.bannerId);
442
- if ((banner === null || banner === void 0 ? void 0 : banner._id) != null) return "_id:".concat(banner._id);
443
- return banner;
93
+ var rankOf = function rankOf(banner) {
94
+ return Number.isFinite(banner === null || banner === void 0 ? void 0 : banner.priority) ? banner.priority : 0;
444
95
  };
445
- /**
446
- * Keeps the highest supported version of each bannerId. Without this the
447
- * documents materialized on both sides of a version bump render twice.
448
- *
449
- * @param {Banner[]} banners - The documents to reduce
450
- * @returns {Banner[]} One document per bannerId
451
- */
452
96
 
453
-
454
- var keepHighestVersion = function keepHighestVersion(banners) {
455
- var best = new Map();
456
-
457
- var _iterator = _createForOfIteratorHelper(banners),
458
- _step;
459
-
460
- try {
461
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
462
- var banner = _step.value;
463
- var key = groupKey(banner);
464
- var current = best.get(key);
465
-
466
- if (current === undefined || supersedes(banner, current)) {
467
- best.set(key, banner);
468
- }
469
- }
470
- } catch (err) {
471
- _iterator.e(err);
472
- } finally {
473
- _iterator.f();
474
- }
475
-
476
- return Array.from(best.values());
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;
477
103
  };
478
104
  /**
479
- * The banners a client displays, in the order it displays them.
480
- *
481
- * A dismissal applies to the bannerId rather than to the single document
482
- * carrying it, so a banner dismissed against one version stays hidden once the
483
- * client moves to another. Deduplication runs last, on the documents that are
484
- * actually renderable, so a newer version sitting outside its validity window
485
- * never suppresses the older one that is still live.
486
- *
487
- * @param {Banner[]} banners - The documents read from the doctype
488
- * @param {VisibleBannersOptions} [options] - Options
489
- * @returns {Banner[]} The visible banners, ordered
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.
490
109
  */
491
110
 
492
111
 
493
- var getVisibleBanners = function getVisibleBanners(banners) {
494
- var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
495
- var now = options.now instanceof Date && !Number.isNaN(options.now.getTime()) ? options.now : new Date(); // Validated rather than defaulted with ??, which only guards null and
496
- // undefined: a NaN reaching isSupported makes every comparison false and
497
- // hides every banner with no error.
498
-
499
- var supportedVersion = toNumber(options.supportedDoctypeVersion, SUPPORTED_DOCTYPE_VERSION); // Only documents the stack wrote are read at all, dismissals included: a
500
- // dismissal on a document an application authored must not hide a platform
501
- // message.
502
-
503
- var trusted = (banners !== null && banners !== void 0 ? banners : []).filter(isTrusted); // Collected before the version filter: a dismissal recorded by a client on a
504
- // newer version of the document still applies to a client that only reads the
505
- // older one, which would otherwise render a banner the user has closed.
506
-
507
- var dismissed = new Set(trusted.filter(isDismissed).map(groupKey));
508
- return keepHighestVersion(trusted.filter(function (banner) {
509
- return isSupported(banner, supportedVersion) && !dismissed.has(groupKey(banner)) && isInWindow(banner, now);
510
- })).sort(byPriority).map(resolve);
112
+ var isSafeCta = function isSafeCta(cta) {
113
+ return typeof (cta === null || cta === void 0 ? void 0 : cta.url) === 'string' && /^https:\/\//i.test(cta.url);
511
114
  };
512
115
  /**
513
- * Applies every fallback the doctype defines, so a caller renders what it is
514
- * given instead of reaching for the helpers itself: an unknown severity or
515
- * surface is already replaced, and a call to action that is not an absolute
516
- * https URL is already dropped.
517
- *
518
- * The doctype guarantees that a non dismissible banner on the modal surface
519
- * carries a call to action, so the user is never left without a way out.
520
- * Dropping an unusable one would break that guarantee and paint a blocking
521
- * dialog with no dismiss control and no button, so the banner falls back to the
522
- * surface that is never blocking.
523
- *
524
- * The document is returned unchanged when no fallback applied. The store relies
525
- * on referential equality, and copying every banner on every read would make a
526
- * memoized renderer repaint on every poll tick. An absent cta is not left
527
- * absent though: the resolved shape always carries the key, so a caller reads
528
- * one value rather than checking for both null and undefined.
529
- *
530
- * @param {Banner} banner - A visible banner
531
- * @returns {Banner} The same banner, ready to render
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.
532
118
  */
533
119
 
534
120
 
535
- exports.getVisibleBanners = getVisibleBanners;
536
-
537
- var resolve = function resolve(banner) {
538
- var severity = getSeverity(banner);
539
- var cta = getCta(banner);
540
- var surface = getSurface(banner) === 'modal' && cta === null && !(banner !== null && banner !== void 0 && banner.dismissible) ? DEFAULT_SURFACE : getSurface(banner);
541
-
542
- if (severity === (banner === null || banner === void 0 ? void 0 : banner.severity) && surface === (banner === null || banner === void 0 ? void 0 : banner.surface) && cta === (banner === null || banner === void 0 ? void 0 : banner.cta)) {
543
- return banner;
544
- }
545
-
121
+ var withFallbacks = function withFallbacks(banner) {
122
+ var cta = isSafeCta(banner.cta) ? banner.cta : undefined;
546
123
  return _objectSpread(_objectSpread({}, banner), {}, {
547
- severity: severity,
548
- surface: surface,
549
- cta: cta
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
550
130
  });
551
131
  };
552
132
  /**
553
- * A conflict reaches this code in two shapes: a FetchError from the stack,
554
- * which carries the HTTP status, and a PouchDB conflict, which the pouch link
555
- * rewraps into a plain Error that only keeps the message.
556
- *
557
- * @param {Error & {status?: number}} error - The error to classify
558
- * @returns {boolean} True when the write lost a revision race
133
+ * `client.query` resolves undefined instead of rejecting when the client has an
134
+ * `onError` callback, which would otherwise read as "no banners apply".
559
135
  */
560
136
 
561
137
 
562
- var isConflict = function isConflict(error) {
563
- var _error$message;
564
-
565
- 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 : '');
566
- };
567
- /**
568
- * `client.query` does not always resolve a response. It resolves undefined when
569
- * the client was built with an `onError` callback, which swallows the rejection,
570
- * and when a query named the same as this one is already in flight with a
571
- * different definition. Both are indistinguishable from an empty collection
572
- * once the response is read, so they are rejected here instead: reporting a
573
- * failed read as "there is nothing" hides a banner the user should see, and
574
- * reports a dismissal that never happened as done.
575
- *
576
- * @param {object} response - The response of a query
577
- * @returns {object} The same response
578
- * @throws {Error} When the query resolved nothing
579
- */
580
-
581
-
582
- var requireResponse = function requireResponse(response) {
138
+ var readData = function readData(response) {
583
139
  if (response === null || response === undefined) {
584
- throw new Error("The ".concat(BANNERS_DOCTYPE, " query resolved nothing. Either the client swallowed the error through its onError option, or another query is registered under the name ").concat(BANNERS_QUERY_NAME, "."));
140
+ throw new Error("The ".concat(BANNERS_DOCTYPE, " query resolved nothing"));
585
141
  }
586
142
 
587
- return response;
143
+ return response.data;
588
144
  };
589
- /**
590
- * The links disagree on how a missing document comes back: the stack link
591
- * resolves `{data: null}` while the pouch link resolves `{data: []}`, and an
592
- * empty array is truthy. Both mean there is nothing to write to.
593
- *
594
- * @param {object} response - The response of a getById query
595
- * @returns {Banner|null} The document, or null when there is none
596
- */
145
+ /** The stack link answers a missing document with null, the pouch link with []. */
597
146
 
598
147
 
599
148
  var readOne = function readOne(response) {
600
- var _requireResponse;
601
-
602
- var data = (_requireResponse = requireResponse(response)) === null || _requireResponse === void 0 ? void 0 : _requireResponse.data;
149
+ var data = readData(response);
603
150
  var doc = Array.isArray(data) ? data[0] : data;
604
151
  return doc !== null && doc !== void 0 && doc._id ? doc : null;
605
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
+ };
606
159
  /**
607
- * The banners to display, in the order to display them. This is the entry
608
- * point: it queries the doctype and applies every rule the contract defines,
609
- * so a caller does not compose the helpers itself.
160
+ * The banners to display, in order.
610
161
  *
611
- * The whole collection is read rather than filtered by a Mango selector. The
612
- * validity window, the dismissal and the version filter would each need their
613
- * own index, and the doctype keeps the collection small on purpose: the stack
614
- * deletes a banner when its condition clears.
615
- *
616
- * @param {BannerClient} client - A CozyClient instance
617
- * @param {VisibleBannersOptions} [options] - Options
618
- * @returns {Promise<Banner[]>} The banners to display, ready to render
619
- * @throws {Error} When the doctype cannot be read, a missing permission included
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
620
166
  */
621
167
 
622
168
 
623
169
  var getActiveBanners = /*#__PURE__*/function () {
624
- var _ref3 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(client) {
625
- var _requireResponse2;
626
-
627
- var options,
628
- _buildBannersQuery,
629
- definition,
630
- queryOptions,
170
+ var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(client) {
171
+ var _ref2,
172
+ _ref2$now,
173
+ now,
631
174
  data,
632
175
  _args = arguments;
633
176
 
@@ -635,42 +178,21 @@ var getActiveBanners = /*#__PURE__*/function () {
635
178
  while (1) {
636
179
  switch (_context.prev = _context.next) {
637
180
  case 0:
638
- options = _args.length > 1 && _args[1] !== undefined ? _args[1] : {};
639
- _buildBannersQuery = buildBannersQuery(), definition = _buildBannersQuery.definition, queryOptions = _buildBannersQuery.options;
640
- _context.t1 = requireResponse;
641
- _context.next = 5;
642
- return client.query(definition, queryOptions);
643
-
644
- case 5:
645
- _context.t2 = _context.sent;
646
- _context.t3 = _requireResponse2 = (0, _context.t1)(_context.t2);
647
- _context.t0 = _context.t3 === null;
648
-
649
- if (_context.t0) {
650
- _context.next = 10;
651
- break;
652
- }
653
-
654
- _context.t0 = _requireResponse2 === void 0;
655
-
656
- case 10:
657
- if (!_context.t0) {
658
- _context.next = 14;
659
- break;
660
- }
661
-
662
- _context.t4 = void 0;
663
- _context.next = 15;
664
- break;
665
-
666
- case 14:
667
- _context.t4 = _requireResponse2.data;
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
+ });
668
187
 
669
- case 15:
670
- data = _context.t4;
671
- return _context.abrupt("return", getVisibleBanners(Array.isArray(data) ? data : [], options));
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));
672
194
 
673
- case 17:
195
+ case 7:
674
196
  case "end":
675
197
  return _context.stop();
676
198
  }
@@ -679,300 +201,199 @@ var getActiveBanners = /*#__PURE__*/function () {
679
201
  }));
680
202
 
681
203
  return function getActiveBanners(_x) {
682
- return _ref3.apply(this, arguments);
204
+ return _ref.apply(this, arguments);
683
205
  };
684
206
  }();
685
207
  /**
686
208
  * The single banner to display, for a surface that shows one at a time.
687
209
  *
688
- * @param {BannerClient} client - A CozyClient instance
689
- * @param {VisibleBannersOptions} [options] - Options
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
690
216
  * @returns {Promise<Banner|null>} The highest priority banner, or null
691
- * @throws {Error} When the doctype cannot be read, a missing permission included
692
217
  */
693
218
 
694
219
 
695
220
  exports.getActiveBanners = getActiveBanners;
696
221
 
697
222
  var getActiveBanner = /*#__PURE__*/function () {
698
- var _ref4 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(client) {
699
- var options,
700
- _yield$getActiveBanne,
701
- _yield$getActiveBanne2,
702
- first,
703
- _args2 = arguments;
223
+ var _ref3 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(client, options) {
224
+ var _yield$getActiveBanne;
704
225
 
705
226
  return _regenerator.default.wrap(function _callee2$(_context2) {
706
227
  while (1) {
707
228
  switch (_context2.prev = _context2.next) {
708
229
  case 0:
709
- options = _args2.length > 1 && _args2[1] !== undefined ? _args2[1] : {};
710
- _context2.next = 3;
230
+ _context2.next = 2;
711
231
  return getActiveBanners(client, options);
712
232
 
713
- case 3:
714
- _yield$getActiveBanne = _context2.sent;
715
- _yield$getActiveBanne2 = (0, _slicedToArray2.default)(_yield$getActiveBanne, 1);
716
- first = _yield$getActiveBanne2[0];
717
- return _context2.abrupt("return", first !== null && first !== void 0 ? first : null);
718
-
719
- case 7:
720
- case "end":
721
- return _context2.stop();
722
- }
723
- }
724
- }, _callee2);
725
- }));
726
-
727
- return function getActiveBanner(_x2) {
728
- return _ref4.apply(this, arguments);
729
- };
730
- }();
731
- /**
732
- * Records a dismissal. The field is idempotent, so a conflict is resolved by
733
- * reading the document again and writing the value on the fresh revision.
734
- *
735
- * The dismissal is written to the document it is given. During a doctypeVersion
736
- * migration the stack materializes the same bannerId under several versions,
737
- * and the sibling documents are not reached from here.
738
- *
739
- * @param {BannerClient} client - A CozyClient instance
740
- * @param {Banner & {_id: string}} banner - The banner to dismiss, as stored
741
- * @param {DismissOptions} [options] - Options
742
- * @returns {Promise<{data: Banner|null}>} The dismissed document, null when it is gone
743
- */
744
-
745
-
746
- exports.getActiveBanner = getActiveBanner;
747
-
748
- var dismiss = /*#__PURE__*/function () {
749
- var _ref5 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(client, banner) {
750
- var _options$dismissedAt;
233
+ case 2:
234
+ _context2.t1 = _yield$getActiveBanne = _context2.sent[0];
235
+ _context2.t0 = _context2.t1 !== null;
751
236
 
752
- var options,
753
- dismissedAt,
754
- id,
755
- siblings,
756
- result,
757
- _iterator2,
758
- _step2,
759
- sibling,
760
- written,
761
- _args3 = arguments;
762
-
763
- return _regenerator.default.wrap(function _callee3$(_context3) {
764
- while (1) {
765
- switch (_context3.prev = _context3.next) {
766
- case 0:
767
- options = _args3.length > 2 && _args3[2] !== undefined ? _args3[2] : {};
768
- dismissedAt = (_options$dismissedAt = options.dismissedAt) !== null && _options$dismissedAt !== void 0 ? _options$dismissedAt : new Date().toISOString();
769
- id = banner === null || banner === void 0 ? void 0 : banner._id;
770
-
771
- if (id) {
772
- _context3.next = 5;
237
+ if (!_context2.t0) {
238
+ _context2.next = 6;
773
239
  break;
774
240
  }
775
241
 
776
- throw new Error('A banner needs an _id to be dismissed');
242
+ _context2.t0 = _yield$getActiveBanne !== void 0;
777
243
 
778
- case 5:
779
- _context3.next = 7;
780
- return findSiblings(client, banner);
781
-
782
- case 7:
783
- siblings = _context3.sent;
784
- result = {
785
- data: null
786
- };
787
- _iterator2 = _createForOfIteratorHelper(siblings);
788
- _context3.prev = 10;
789
-
790
- _iterator2.s();
791
-
792
- case 12:
793
- if ((_step2 = _iterator2.n()).done) {
794
- _context3.next = 20;
244
+ case 6:
245
+ if (!_context2.t0) {
246
+ _context2.next = 10;
795
247
  break;
796
248
  }
797
249
 
798
- sibling = _step2.value;
799
- _context3.next = 16;
800
- return writeDismissal(client, sibling._id, dismissedAt);
801
-
802
- case 16:
803
- written = _context3.sent;
804
- // The caller is answered with the document it asked about.
805
- if (sibling._id === id) result = written;
806
-
807
- case 18:
808
- _context3.next = 12;
250
+ _context2.t2 = _yield$getActiveBanne;
251
+ _context2.next = 11;
809
252
  break;
810
253
 
811
- case 20:
812
- _context3.next = 25;
813
- break;
814
-
815
- case 22:
816
- _context3.prev = 22;
817
- _context3.t0 = _context3["catch"](10);
818
-
819
- _iterator2.e(_context3.t0);
820
-
821
- case 25:
822
- _context3.prev = 25;
823
-
824
- _iterator2.f();
825
-
826
- return _context3.finish(25);
254
+ case 10:
255
+ _context2.t2 = null;
827
256
 
828
- case 28:
829
- return _context3.abrupt("return", result);
257
+ case 11:
258
+ return _context2.abrupt("return", _context2.t2);
830
259
 
831
- case 29:
260
+ case 12:
832
261
  case "end":
833
- return _context3.stop();
262
+ return _context2.stop();
834
263
  }
835
264
  }
836
- }, _callee3, null, [[10, 22, 25, 28]]);
265
+ }, _callee2);
837
266
  }));
838
267
 
839
- return function dismiss(_x3, _x4) {
840
- return _ref5.apply(this, arguments);
268
+ return function getActiveBanner(_x2, _x3) {
269
+ return _ref3.apply(this, arguments);
841
270
  };
842
271
  }();
843
272
  /**
844
- * Every stored document sharing the banner's identity, the one the caller holds
845
- * included. The whole collection is read because the versions of a bannerId are
846
- * not reachable by id, and the doctype keeps the collection small on purpose.
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.
847
277
  *
848
- * @param {BannerClient} client - A CozyClient instance
849
- * @param {Banner} banner - The banner being dismissed
850
- * @returns {Promise<Banner[]>} The documents to write the dismissal to
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
851
283
  */
852
284
 
853
285
 
854
- exports.dismiss = dismiss;
286
+ exports.getActiveBanner = getActiveBanner;
855
287
 
856
- var findSiblings = /*#__PURE__*/function () {
857
- var _ref6 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4(client, banner) {
858
- var _buildBannersQuery2, definition, queryOptions, response, data, key, siblings;
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;
859
295
 
860
296
  return _regenerator.default.wrap(function _callee4$(_context4) {
861
297
  while (1) {
862
298
  switch (_context4.prev = _context4.next) {
863
299
  case 0:
864
- _buildBannersQuery2 = buildBannersQuery(), definition = _buildBannersQuery2.definition, queryOptions = _buildBannersQuery2.options;
865
- _context4.t0 = requireResponse;
866
- _context4.next = 4;
867
- return client.query(definition, queryOptions);
300
+ _ref5 = _args4.length > 2 && _args4[2] !== undefined ? _args4[2] : {}, dismissedAt = _ref5.dismissedAt;
868
301
 
869
- case 4:
870
- _context4.t1 = _context4.sent;
871
- response = (0, _context4.t0)(_context4.t1);
872
- data = Array.isArray(response.data) ? response.data : [];
873
- key = groupKey(banner);
874
- siblings = data.filter(function (doc) {
875
- return (doc === null || doc === void 0 ? void 0 : doc._id) && groupKey(doc) === key;
876
- }); // The document may have been deleted between the read and the click, in which
877
- // case there is nothing left to write to.
878
-
879
- return _context4.abrupt("return", siblings.length > 0 ? siblings : []);
880
-
881
- case 10:
882
- case "end":
883
- return _context4.stop();
884
- }
885
- }
886
- }, _callee4);
887
- }));
888
-
889
- return function findSiblings(_x5, _x6) {
890
- return _ref6.apply(this, arguments);
891
- };
892
- }();
893
- /** How many times the write is attempted before a conflict reaches the caller. */
894
-
895
-
896
- var DISMISS_ATTEMPTS = 2;
897
-
898
- var writeDismissal = /*#__PURE__*/function () {
899
- var _ref7 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee5(client, id, dismissedAt) {
900
- var attempt, stored;
901
- return _regenerator.default.wrap(function _callee5$(_context5) {
902
- while (1) {
903
- switch (_context5.prev = _context5.next) {
904
- case 0:
905
- attempt = 1;
906
-
907
- case 1:
908
- if (!(attempt <= DISMISS_ATTEMPTS)) {
909
- _context5.next = 24;
302
+ if (banner !== null && banner !== void 0 && banner._id) {
303
+ _context4.next = 3;
910
304
  break;
911
305
  }
912
306
 
913
- _context5.t0 = readOne;
914
- _context5.next = 5;
915
- return client.query((0, _dsl.Q)(BANNERS_DOCTYPE).getById(id));
916
-
917
- case 5:
918
- _context5.t1 = _context5.sent;
919
- stored = (0, _context5.t0)(_context5.t1);
920
-
921
- if (stored) {
922
- _context5.next = 9;
923
- break;
924
- }
925
-
926
- return _context5.abrupt("return", {
927
- data: null
928
- });
929
-
930
- case 9:
931
- if (!isDismissed(stored)) {
932
- _context5.next = 11;
933
- break;
934
- }
307
+ throw new Error('A banner needs an _id to be dismissed');
935
308
 
936
- return _context5.abrupt("return", {
937
- data: stored
938
- });
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);
939
371
 
940
372
  case 11:
941
- _context5.prev = 11;
942
- _context5.next = 14;
943
- return client.save(_objectSpread(_objectSpread({}, stored), {}, {
944
- _type: BANNERS_DOCTYPE,
945
- dismissedAt: dismissedAt
946
- }));
947
-
948
- case 14:
949
- return _context5.abrupt("return", _context5.sent);
950
-
951
- case 17:
952
- _context5.prev = 17;
953
- _context5.t2 = _context5["catch"](11);
954
-
955
- if (!(!isConflict(_context5.t2) || attempt === DISMISS_ATTEMPTS)) {
956
- _context5.next = 21;
373
+ _context4.prev = 11;
374
+ _context4.t0 = _context4["catch"](5);
375
+
376
+ if (isConflict(_context4.t0)) {
377
+ _context4.next = 15;
957
378
  break;
958
379
  }
959
380
 
960
- throw _context5.t2;
381
+ throw _context4.t0;
961
382
 
962
- case 21:
963
- attempt++;
964
- _context5.next = 1;
965
- break;
383
+ case 15:
384
+ return _context4.abrupt("return", write());
966
385
 
967
- case 24:
386
+ case 16:
968
387
  case "end":
969
- return _context5.stop();
388
+ return _context4.stop();
970
389
  }
971
390
  }
972
- }, _callee5, null, [[11, 17]]);
391
+ }, _callee4, null, [[5, 11]]);
973
392
  }));
974
393
 
975
- return function writeDismissal(_x7, _x8, _x9) {
976
- return _ref7.apply(this, arguments);
394
+ return function dismiss(_x4, _x5) {
395
+ return _ref4.apply(this, arguments);
977
396
  };
978
- }();
397
+ }();
398
+
399
+ exports.dismiss = dismiss;