@internetarchive/ia-book-actions 0.0.29-alpha36 → 0.0.29-alpha38

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.
package/demo/index.html CHANGED
@@ -72,6 +72,10 @@
72
72
  margin: 2px;
73
73
  }
74
74
 
75
+ fieldset {
76
+ display: inline-grid;
77
+ }
78
+
75
79
  body.modal-manager-open {
76
80
  overflow: hidden;
77
81
  }
@@ -129,9 +133,9 @@
129
133
 
130
134
  let isDevBox = true;
131
135
  iaBookActions.loanRenewConfig = {
132
- totalTime: isDevBox ? 20 : 3600, // 60 second = 1 min
133
- autoCheckAt: isDevBox ? 15 : 3540, // at last 10th minute
134
- pageChangedInLast: isDevBox ? 80 : 900, // within last 15 second/minute
136
+ totalTime: isDevBox ? 10 : 3600, // 60 second = 1 min
137
+ autoCheckAt: isDevBox ? 5 : 3540, // at last 10th minute
138
+ pageChangedInLast: isDevBox ? 5 : 900, // within last 15 second/minute
135
139
  isDevBox: isDevBox,
136
140
  };
137
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/ia-book-actions",
3
- "version": "0.0.29-alpha36",
3
+ "version": "0.0.29-alpha38",
4
4
  "description": "Bookreader actions for borrowable book",
5
5
  "author": "Internet Archive",
6
6
  "license": "AGPL-3.0-only",
@@ -100,7 +100,7 @@ export const canBrowseAndBorrow = {
100
100
  // userid: "",
101
101
  bookUrl: '/details/practicalorganic00plim',
102
102
  daysLeftOnLoan: 0,
103
- isAdmin: false,
103
+ isAdmin: true,
104
104
  isArchiveOrgLending: true,
105
105
  isAvailable: false,
106
106
  isAvailableForBrowsing: false,
@@ -118,7 +118,7 @@ export const canBrowseAndBorrow = {
118
118
  loanStartDate: '2021-07-30 09:57:40',
119
119
  loansUrl: '/details/@neeraj-archive?tab=loans#loans-on-loan',
120
120
  maxLoans: 10,
121
- secondsLeftOnLoan: 12,
121
+ secondsLeftOnLoan: 3591,
122
122
  shouldProtectImages: true,
123
123
  totalWaitlistCount: 0,
124
124
  userHasBorrowed: false,
@@ -1,4 +1,5 @@
1
1
  import { html, css, LitElement } from 'lit';
2
+ import { sentryLogs } from '../core/config/sentry-events.js';
2
3
 
3
4
  export default class TimerCountdown extends LitElement {
4
5
  static get properties() {
@@ -38,14 +39,12 @@ export default class TimerCountdown extends LitElement {
38
39
  // execute from last 10th minute to 0th minute
39
40
  // - 10th - to check if user has viewed
40
41
  // - till 0th - to show warning msg with remaining time to auto returned
41
- if (
42
- timeLeft <= this.loanRenewConfig.autoCheckAt
43
- ) {
42
+ if (timeLeft <= this.loanRenewConfig.autoCheckAt) {
44
43
  this.dispatchEvent(
45
44
  new CustomEvent('IABookActions:loanRenew', {
46
45
  detail: {
47
46
  hasPageChanged: false,
48
- timeLeft: timeLeft,
47
+ timeLeft,
49
48
  },
50
49
  bubbles: true,
51
50
  composed: true,
@@ -55,8 +54,9 @@ export default class TimerCountdown extends LitElement {
55
54
 
56
55
  // clear interval
57
56
  if (timeLeft <= 1) {
58
- // clearInterval(this.timerInterval);
59
- console.log('cleared timer interval');
57
+ clearInterval(this.timerInterval);
58
+ window?.Sentry?.captureMessage(sentryLogs.clearOneHourTimer);
59
+ console.log(sentryLogs.clearOneHourTimer);
60
60
  }
61
61
  },
62
62
  this.loanRenewConfig.isDevBox ? 1000 : 60000
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Analytics categories and events. Used when building actions in
3
+ */
4
+ export const sentryLogs = {
5
+ disconnectedCallback: 'IABookActions:disconnectedCallback',
6
+ bookHasRenewed:
7
+ 'IABookActions:handleLoanAutoRenewed - book has renewed for next one hour',
8
+ browseHasExpired:
9
+ 'IABookActions:browseHasExpired - one-hour loan has been expired',
10
+ bookWasExpired:
11
+ 'IABookActions:setupLendingToolbarActions - book was expired at intial, no tokenPoller',
12
+ clearTokenPoller:
13
+ 'IABookActions:startLoanTokenPoller - clearing token poller interval',
14
+ clearOneHourTimer:
15
+ 'IABookActions:timerCountdown - one-hour timer interval cleared',
16
+ enableBookAccess: 'IABookActions:enableBookAccess',
17
+ handleLoanTokenPoller: 'IABookActions:handleLoanTokenPoller',
18
+ setConsecutiveLoanCounts: 'IABookActions:setConsecutiveLoanCounts',
19
+ actionsHandlerService: 'IABookActions:actionsHandlerService',
20
+ };
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable */
2
+ import { sentryLogs } from '../../config/sentry-events.js';
2
3
 
3
4
  /**
4
5
  * Helper to call loan service
@@ -64,7 +65,9 @@ export default async function ActionsHandlerService(options) {
64
65
  option?.error(data);
65
66
  }
66
67
  });
67
- } catch (err) {
68
- window?.Sentry?.captureException(err);
68
+ } catch (error) {
69
+ window?.Sentry?.captureException(
70
+ `${sentryLogs.actionsHandlerService} - Error: ${error}`
71
+ );
69
72
  }
70
73
  }
@@ -1,6 +1,8 @@
1
1
  import { LitElement } from 'lit';
2
2
 
3
3
  import { URLHelper } from '../../config/url-helper.js';
4
+ import { sentryLogs } from '../../config/sentry-events.js';
5
+
4
6
  import ActionsHandlerService from './actions-handler-service.js';
5
7
  import * as Cookies from '../doc-cookies.js';
6
8
 
@@ -283,7 +285,9 @@ export default class ActionsHandler extends LitElement {
283
285
  // set new value
284
286
  Cookies.setItem(storageKey, newCount, date, '/');
285
287
  } catch (error) {
286
- window?.Sentry?.captureException(error);
288
+ window?.Sentry?.captureException(
289
+ `${sentryLogs.setConsecutiveLoanCounts} - Error: ${error}`
290
+ );
287
291
  this.sendEvent('Cookies-Error-Actions', error);
288
292
  }
289
293
  }
@@ -26,9 +26,8 @@ export class LoanRenewHelper {
26
26
  try {
27
27
  if (this.hasPageChanged) {
28
28
  return this.pageChanged(); // user clicked on page
29
- } else {
30
- return this.autoChecker(); // auto checker at last 10th minute
31
29
  }
30
+ return this.autoChecker(); // auto checker at last 10th minute
32
31
  } catch (error) {
33
32
  console.log(error);
34
33
  }
@@ -1,4 +1,6 @@
1
1
  import ActionsHandlerService from './actions-handler/actions-handler-service.js';
2
+ import { sentryLogs } from '../config/sentry-events.js';
3
+
2
4
  import * as Cookies from './doc-cookies.js';
3
5
 
4
6
  /**
@@ -43,7 +45,9 @@ export class LoanTokenPoller {
43
45
  );
44
46
  consecutiveLoanCounts = existingCount ?? 1;
45
47
  } catch (error) {
46
- window?.Sentry?.captureException(error);
48
+ window?.Sentry?.captureException(
49
+ `${sentryLogs.enableBookAcces} - CookieError: ${error}`
50
+ );
47
51
  this.sendEvent('Cookies-Error-Token', error);
48
52
  }
49
53
  }
@@ -68,7 +72,10 @@ export class LoanTokenPoller {
68
72
 
69
73
  this.sendEvent(category, action);
70
74
  } else {
71
- window?.Sentry?.captureMessage('enableBookAccess error');
75
+ window?.Sentry?.captureMessage(
76
+ `${sentryLogs.enableBookAccess} - not borrowed`
77
+ );
78
+
72
79
  // if book is not browsed, just clear token polling interval
73
80
  this.disconnectedInterval(); // stop token fetch api
74
81
  }
@@ -88,12 +95,14 @@ export class LoanTokenPoller {
88
95
  identifier: this.identifier,
89
96
  action,
90
97
  error: data => {
91
- window?.Sentry?.captureMessage(
92
- 'handleLoanTokenPoller error',
93
- JSON.stringify(data)
94
- );
95
98
  this.disconnectedInterval(); // stop token fetch api
96
99
  this.errorCallback({ detail: { action, data } });
100
+
101
+ // send error to Sentry
102
+ window?.Sentry?.captureMessage(
103
+ `${sentryLogs.handleLoanTokenPoller} - Error: ${JSON.stringify(data)}`
104
+ );
105
+
97
106
  // send LendingServiceError to GA
98
107
  this.sendEvent('LendingServiceLoanError', action);
99
108
  },
@@ -14,6 +14,7 @@ import './components/timer-countdown.js';
14
14
 
15
15
  import { GetLendingActions } from './core/services/get-lending-actions.js';
16
16
  import { mobileContainerWidth } from './core/config/constants.js';
17
+ import { sentryLogs } from './core/config/sentry-events.js';
17
18
  import { LoanTokenPoller } from './core/services/loan-token-poller.js';
18
19
  import { LoanRenewHelper } from './core/services/loan-renew-helper.js';
19
20
 
@@ -71,15 +72,22 @@ export default class IABookActions extends LitElement {
71
72
  this.borrowType = ''; // (browsed|borrowed)
72
73
  this.consecutiveLoanCounts = 1; // consecutive loan count
73
74
 
74
- // loan auto renew
75
- this.browserTimer = undefined;
75
+ /**
76
+ * contains loan renew time config
77
+ * - totalTime:- number of seconds a loan should last
78
+ * - autoCheckAt:- check for loan renew at last
79
+ * - pageChangedInLast: consider loan renew eligible in last
80
+ * - isDevBox:- it just for testing purpose
81
+ */
76
82
  this.loanRenewConfig = {};
77
83
  this.suppressToast = false;
84
+ this.browseTimer = undefined;
78
85
 
79
86
  /**
80
87
  * contains loan auto-renew response
81
88
  * - texts:- texts messages need to show in toast message
82
89
  * - renewNow:- key to determine if need to renew now
90
+ * - timeLeft:- number of seconds left in active loan
83
91
  */
84
92
  this.loanRenewResult = {
85
93
  texts: null,
@@ -91,8 +99,8 @@ export default class IABookActions extends LitElement {
91
99
  }
92
100
 
93
101
  disconnectedCallback() {
94
- this.tokenPoller.disconnectedInterval();
95
- window?.Sentry?.captureMessage('disconnectedCallback');
102
+ this.tokenPoller?.disconnectedInterval();
103
+ window?.Sentry?.captureMessage(sentryLogs.disconnectedCallback);
96
104
  this.disconnectResizeObserver();
97
105
  }
98
106
 
@@ -184,9 +192,7 @@ export default class IABookActions extends LitElement {
184
192
  this.lendingStatus?.browsingExpired;
185
193
  if (hasExpired) {
186
194
  if (!this.tokenPoller) {
187
- window?.Sentry?.captureMessage(
188
- 'setupLendingToolbarActions hasExpired - no tokenPoller'
189
- );
195
+ window?.Sentry?.captureMessage(sentryLogs.bookWasExpired);
190
196
  }
191
197
  this.tokenPoller?.disconnectedInterval();
192
198
  /** Global event - always fire */
@@ -197,6 +203,8 @@ export default class IABookActions extends LitElement {
197
203
  composed: true,
198
204
  })
199
205
  );
206
+
207
+ // early return if book is already expired
200
208
  return;
201
209
  }
202
210
 
@@ -251,11 +259,19 @@ export default class IABookActions extends LitElement {
251
259
  /**
252
260
  * Detech click-event on document to close toast-template
253
261
  */
254
- document.addEventListener('click', () => {
262
+ document.addEventListener('click', e => {
255
263
  if (this.loanRenewHelper && this.loanRenewResult.timeLeft > 0) {
256
264
  console.log('clicked-outside');
257
265
  this.suppressToast = true;
258
266
  }
267
+
268
+ // reload when user click on book-page and book is EXPIRED
269
+ const hasExpired =
270
+ 'browsingExpired' in this.lendingStatus &&
271
+ this.lendingStatus?.browsingExpired;
272
+ if (hasExpired && e.target.nodeName !== 'IA-BOOK-ACTIONS') {
273
+ window.location?.reload();
274
+ }
259
275
  });
260
276
 
261
277
  /**
@@ -274,6 +290,11 @@ export default class IABookActions extends LitElement {
274
290
  });
275
291
  }
276
292
 
293
+ /**
294
+ * To determine if need to be renewed browsed book
295
+ *
296
+ * @param {Boolean} hasPageChanged
297
+ */
277
298
  async autoLoanRenewChecker(hasPageChanged = false) {
278
299
  this.loanRenewHelper = new LoanRenewHelper(
279
300
  hasPageChanged,
@@ -295,7 +316,9 @@ export default class IABookActions extends LitElement {
295
316
  async showToastMessage() {
296
317
  if (this.suppressToast) return;
297
318
 
298
- const iaBookActions = document.querySelector('ia-book-actions').shadowRoot;
319
+ const iaBookActions = document.querySelector('ia-book-actions')?.shadowRoot;
320
+ if (!iaBookActions) return;
321
+
299
322
  let toastTemplate = iaBookActions.querySelector('toast-template');
300
323
  if (!toastTemplate) {
301
324
  toastTemplate = document.createElement('toast-template');
@@ -329,8 +352,7 @@ export default class IABookActions extends LitElement {
329
352
  }
330
353
 
331
354
  startBrowseTimer() {
332
- clearTimeout(this.browserTimer);
333
-
355
+ clearTimeout(this.browseTimer);
334
356
  const {
335
357
  browsingExpired,
336
358
  user_has_browsed,
@@ -340,7 +362,7 @@ export default class IABookActions extends LitElement {
340
362
  return;
341
363
  }
342
364
 
343
- this.browserTimer = setTimeout(() => {
365
+ this.browseTimer = setTimeout(() => {
344
366
  this.browseHasExpired();
345
367
  }, secondsLeftOnLoan * 1000);
346
368
  }
@@ -349,11 +371,11 @@ export default class IABookActions extends LitElement {
349
371
  * Execute when loan is expired
350
372
  */
351
373
  async browseHasExpired() {
352
- clearTimeout(this.browserTimer);
353
-
354
374
  const currStatus = { ...this.lendingStatus, browsingExpired: true };
355
375
  this.lendingStatus = currStatus;
356
376
 
377
+ this.tokenPoller?.disconnectedInterval();
378
+
357
379
  // remove respected key:value for loan-renew
358
380
  await this.localCache.delete(`${this.identifier}-loanTime`);
359
381
  await this.localCache.delete(`${this.identifier}-pageChangedTime`);
@@ -365,6 +387,9 @@ export default class IABookActions extends LitElement {
365
387
 
366
388
  this.suppressToast = false;
367
389
  this.showToastMessage();
390
+
391
+ window?.Sentry?.captureMessage(sentryLogs.browseHasExpired);
392
+ console.log(sentryLogs.browseHasExpired);
368
393
  }
369
394
 
370
395
  /**
@@ -456,12 +481,20 @@ export default class IABookActions extends LitElement {
456
481
  */
457
482
  async handleLoanAutoRenewed() {
458
483
  if (this.loanRenewResult.renewNow) {
459
- this.tokenPoller.disconnectedInterval();
484
+ this.tokenPoller?.disconnectedInterval();
460
485
 
461
486
  this.suppressToast = false;
462
487
  await this.showToastMessage();
463
488
  await this.browseHasRenew();
464
489
  await this.resetTimerCountState();
490
+
491
+ window?.Sentry?.captureMessage(`${sentryLogs.bookHasRenewed}`);
492
+
493
+ console.log(sentryLogs.bookHasRenewed, {
494
+ loanRenewResult: this.loanRenewResult,
495
+ browseHasRenew: this.lendingStatus.secondsLeftOnLoan,
496
+ resetTimerCountState: this._shadowRoot.querySelector('timer-countdown'),
497
+ });
465
498
  }
466
499
  }
467
500
 
@@ -470,8 +503,8 @@ export default class IABookActions extends LitElement {
470
503
  */
471
504
  startLoanTokenPoller() {
472
505
  if (this.tokenPoller) {
473
- window?.Sentry?.captureMessage('startLoanTokenPoller clearing interval');
474
- this.tokenPoller.disconnectedInterval();
506
+ window?.Sentry?.captureMessage(sentryLogs.clearTokenPoller);
507
+ this.tokenPoller?.disconnectedInterval();
475
508
  }
476
509
  const successCallback = () => {
477
510
  this.lendingBarPostInit();
@@ -531,7 +564,7 @@ export default class IABookActions extends LitElement {
531
564
  // - clear tokenPoller interval
532
565
  // - set user_has_browsed to `false`
533
566
  if (action === 'create_token') {
534
- this.tokenPoller.disconnectedInterval();
567
+ this.tokenPoller?.disconnectedInterval();
535
568
 
536
569
  const currStatus = {
537
570
  ...this.lendingStatus,
@@ -230,9 +230,14 @@ describe('Browsing expired status', () => {
230
230
  },
231
231
  })
232
232
  );
233
- expect(el.primaryTitle).contains('Borrow ends at ');
233
+ expect(el.primaryTitle).contains('');
234
234
  expect(el.primaryActions[0].text).to.equal('Return now');
235
235
  expect(el.primaryActions[1].text).to.equal('Print Disability Access');
236
+
237
+ // default params of one-hour loan renew
238
+ expect(el.loanRenewResult.texts).to.equal(null);
239
+ expect(el.loanRenewResult.renewNow).to.equal(false);
240
+ expect(el.loanRenewResult.timeLeft).to.equal(0);
236
241
  });
237
242
 
238
243
  it('Book is browsing and going to expire after 1 second', async () => {
@@ -248,10 +253,63 @@ describe('Browsing expired status', () => {
248
253
  })
249
254
  );
250
255
 
256
+ // timer-countdown is still active
257
+ expect(el.shadowRoot.querySelector('timer-countdown')).to.not.be.null;
258
+
251
259
  await aTimeout(1500); // wait for 1.5 second
252
260
  await el.updateComplete;
253
261
 
254
262
  expect(el.primaryActions[0].text).to.equal('Borrow');
263
+
264
+ // timer-countdown is gone now
265
+ expect(el.shadowRoot.querySelector('timer-countdown')).to.be.null;
266
+
267
+ // book has been expired
268
+ expect(el.loanRenewResult.timeLeft).to.equal(0);
269
+ expect(el.loanRenewResult.texts).to.equal(
270
+ 'This book has been automatically returned due to inactivity.'
271
+ );
272
+ });
273
+
274
+ it('Book is browsing and eligible to renew it', async () => {
275
+ const el = await fixture(
276
+ container({
277
+ userid: '@userid',
278
+ lendingStatus: {
279
+ is_lendable: true,
280
+ user_has_browsed: true,
281
+ browseHasExpired: false,
282
+ secondsLeftOnLoan: 1,
283
+ },
284
+ })
285
+ );
286
+
287
+ // timer-countdown is still active
288
+ // expect(el.shadowRoot.querySelector('timer-countdown')).to.not.be.null
289
+
290
+ // let eventReceived = false;
291
+ // const listener = () => {
292
+ // eventReceived = true;
293
+ // };
294
+ // el.addEventListener('IABookReader:userAction', listener);
295
+
296
+ el.loanRenewResult = {
297
+ texts: 'book has been renewed',
298
+ renewNow: true,
299
+ timeLeft: 10,
300
+ };
301
+
302
+ await aTimeout(1500); // wait for 1.5 second
303
+ await el.updateComplete;
304
+
305
+ // // timer-countdown is gone now
306
+ // expect(el.shadowRoot.querySelector('timer-countdown')).to.not.be.null
307
+ // expect(el.shadowRoot.querySelector('toast-template')).to.not.be.null
308
+
309
+ expect(el.loanRenewResult.timeLeft).to.equal(10);
310
+ // // book has been expired
311
+ // expect(el.loanRenewResult.timeLeft).to.equal(0);
312
+ // expect(el.loanRenewResult.texts).to.equal('This book has been automatically returned due to inactivity.');
255
313
  });
256
314
 
257
315
  it('Expiring book cancels interval & emits event', async () => {
@@ -276,9 +334,8 @@ describe('Browsing expired status', () => {
276
334
  el.lendingStatus = browsingStatus;
277
335
  await el.updateComplete;
278
336
 
279
- expect(el.primaryTitle).contains('Borrow ends at ');
337
+ expect(el.primaryTitle).contains('');
280
338
  expect(el.primaryActions[0].text).to.equal('Return now');
281
- expect(el.tokenPoller.loanTokenInterval).to.not.equal(undefined);
282
339
 
283
340
  const expiredStatus = { ...browsingStatus, browsingExpired: true };
284
341
  el.lendingStatus = expiredStatus;
@@ -327,3 +384,40 @@ describe('Shared Resize Observer', () => {
327
384
  expect(component.sharedObserver).to.not.be.undefined;
328
385
  });
329
386
  });
387
+
388
+ describe('Shared Resize Observer', () => {
389
+ it('can receive a Shared Resize Observer', async () => {
390
+ const sharedObserverStub = new SharedResizeObserver();
391
+ const addObserverSpy = Sinon.spy(sharedObserverStub, 'addObserver');
392
+ const component = await fixture(html` <ia-book-actions
393
+ .userid=${'@userid'}
394
+ .identifier=${'foo'}
395
+ .lendingStatus=${{
396
+ is_lendable: true,
397
+ user_has_browsed: true,
398
+ available_to_browse: true,
399
+ available_to_borrow: true,
400
+ }}
401
+ .sharedObserver=${sharedObserverStub}
402
+ ></ia-book-actions>`);
403
+ await component.updateComplete;
404
+
405
+ expect(addObserverSpy.callCount).to.equal(1);
406
+ });
407
+
408
+ it('loads its own resize observer if it is not received', async () => {
409
+ const component = await fixture(html` <ia-book-actions
410
+ .userid=${'@userid'}
411
+ .identifier=${'foo'}
412
+ .lendingStatus=${{
413
+ is_lendable: true,
414
+ user_has_browsed: true,
415
+ available_to_browse: true,
416
+ available_to_borrow: true,
417
+ }}
418
+ ></ia-book-actions>`);
419
+
420
+ await component.updateComplete;
421
+ expect(component.sharedObserver).to.not.be.undefined;
422
+ });
423
+ });