@internetarchive/ia-book-actions 0.1.9 → 0.1.10-alpha1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/ia-book-actions",
3
- "version": "0.1.9",
3
+ "version": "0.1.10-alpha1",
4
4
  "description": "Bookreader actions for borrowable book",
5
5
  "author": "Internet Archive",
6
6
  "license": "AGPL-3.0-only",
@@ -18,9 +18,10 @@
18
18
  "test:watch": "web-test-runner --watch --playwright --browsers chromium firefox webkit"
19
19
  },
20
20
  "dependencies": {
21
+ "@internetarchive/ia-activity-indicator": "^0.0.4",
21
22
  "@internetarchive/icon-info": "^1.2.6",
22
23
  "@internetarchive/local-cache": "^0.2.1",
23
- "@internetarchive/modal-manager": "^0.2.8",
24
+ "@internetarchive/modal-manager": "^0.2.12",
24
25
  "@internetarchive/shared-resize-observer": "^0.2.0",
25
26
  "@internetarchive/toast-manager": "0.1.0",
26
27
  "lit": "^2.5.0"
@@ -100,7 +100,7 @@ export const canBrowseAndBorrow = {
100
100
  // userid: "",
101
101
  bookUrl: '/details/practicalorganic00plim',
102
102
  daysLeftOnLoan: 0,
103
- isAdmin: true,
103
+ isAdmin: false,
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: 3591,
121
+ secondsLeftOnLoan: 155,
122
122
  shouldProtectImages: true,
123
123
  totalWaitlistCount: 0,
124
124
  userHasBorrowed: false,
@@ -30,6 +30,7 @@ export class CollapsibleActionGroup extends ActionsHandler {
30
30
  returnUrl: { type: String },
31
31
  autoRenew: { type: Boolean },
32
32
  autoReturn: { type: Boolean },
33
+ returnNow: { type: Boolean },
33
34
  };
34
35
  }
35
36
 
@@ -51,6 +52,7 @@ export class CollapsibleActionGroup extends ActionsHandler {
51
52
  this.returnUrl = '';
52
53
  this.autoRenew = false;
53
54
  this.autoReturn = false;
55
+ this.returnNow = false;
54
56
  }
55
57
 
56
58
  updated(changed) {
@@ -68,18 +70,22 @@ export class CollapsibleActionGroup extends ActionsHandler {
68
70
  if (changed.has('autoReturn') && this.autoReturn === true) {
69
71
  this.dispatchLoanEvent('autoReturn');
70
72
  }
73
+
74
+ if (changed.has('returnNow') && this.returnNow === true) {
75
+ this.dispatchLoanEvent('returnNow', { borrowType: 'browse' });
76
+ }
71
77
  }
72
78
 
73
79
  /**
74
- * dispatch event when book is auto renewed / returned
80
+ * dispatch event when book is auto auto-renewed / auto-returned / returned
75
81
  * listen these events in action-handler.js to execute ajax call on petabox.
76
82
  * @see ActionsHandler
77
83
  *
78
- * @param {string} event - autoRenew|autoReturn
84
+ * @param {string} event - autoRenew|autoReturn|returnNow
79
85
  * @memberof CollapsibleActionGroup
80
86
  */
81
- dispatchLoanEvent(event) {
82
- this.dispatchEvent(new Event(event));
87
+ dispatchLoanEvent(event, detail) {
88
+ this.dispatchEvent(new CustomEvent(event, { detail }));
83
89
  }
84
90
 
85
91
  /**
@@ -1,12 +1,9 @@
1
1
  import { html, css, LitElement } from 'lit';
2
- import { sentryLogs } from '../core/config/sentry-events.js';
3
2
 
4
3
  export default class TimerCountdown extends LitElement {
5
4
  static get properties() {
6
5
  return {
7
6
  secondsLeftOnLoan: { type: Number }, // in seconds
8
- loanTotalTime: { type: Number }, // in seconds
9
- loanRenewAtLast: { type: Number }, // in seconds
10
7
  };
11
8
  }
12
9
 
@@ -18,138 +15,6 @@ export default class TimerCountdown extends LitElement {
18
15
  * @type {number}
19
16
  */
20
17
  this.secondsLeftOnLoan = 0;
21
-
22
- /**
23
- * total seconds a loan does have
24
- * @type {number}
25
- */
26
- this.loanTotalTime = 0;
27
-
28
- /**
29
- * at the remaining time, we attempt to renew current loan
30
- * @type {number}
31
- */
32
- this.loanRenewAtLast = 0;
33
-
34
- /**
35
- * store time when timer is started
36
- * @type {string}
37
- */
38
- this.timeWhenTimerStart = '';
39
-
40
- /**
41
- * delay seconds in setInterval function
42
- * @type {number}
43
- */
44
- this.timerExecutionSeconds = 60;
45
- }
46
-
47
- disconnectedCallback() {
48
- window?.IALendingIntervals?.clearTimerCountdown();
49
- }
50
-
51
- firstUpdated() {
52
- this.timerCountdown();
53
- }
54
-
55
- updated(changed) {
56
- if (changed.has('secondsLeftOnLoan') && this.secondsLeftOnLoan > 0) {
57
- this.timerCountdown();
58
- }
59
- }
60
-
61
- timerCountdown() {
62
- this.disconnectedCallback();
63
-
64
- let secondsLeft = this.secondsLeftOnLoan;
65
-
66
- // store current time
67
- this.timeWhenTimerStart = new Date();
68
-
69
- /**
70
- * set interval in window object
71
- * @see ia-lending-intervals.js
72
- */
73
- window.IALendingIntervals.timerCountdown = setInterval(async () => {
74
- secondsLeft -= this.timerExecutionSeconds;
75
- secondsLeft = Math.round(secondsLeft); // round number
76
-
77
- // re-sync timer if gone off because of background window
78
- await this.reSyncTimerIfGoneOff(secondsLeft);
79
-
80
- /**
81
- * execute from last 10th minute to 0th minute
82
- * - 10th - to check if user has viewed
83
- * - till 0th - to show warning msg with remaining time to auto expired
84
- * @see IABookActions::bindLoanRenewEvents
85
- */
86
- if (secondsLeft <= this.loanRenewAtLast) {
87
- this.dispatchLoanRenewEvent(secondsLeft);
88
- }
89
-
90
- // clear interval in secondsLeft if less
91
- if (secondsLeft <= this.timerExecutionSeconds) {
92
- this.disconnectedCallback();
93
- window?.Sentry?.captureMessage(sentryLogs.clearOneHourTimer);
94
- }
95
- }, this.timerExecutionSeconds * 1000);
96
- }
97
-
98
- /**
99
- * dispatch loan renew attempt event
100
- *
101
- * @param {number} secondsLeft
102
- * @memberof TimerCountdown
103
- */
104
- dispatchLoanRenewEvent(secondsLeft) {
105
- this.dispatchEvent(
106
- new CustomEvent('IABookActions:loanRenew', {
107
- detail: {
108
- hasPageChanged: false,
109
- secondsLeft,
110
- },
111
- bubbles: true,
112
- composed: true,
113
- })
114
- );
115
- }
116
-
117
- /**
118
- * helper function to determine if timer is not in sync properly
119
- *
120
- * @param {number} timerSecondsLeft - actual seconds left get from setInterval
121
- *
122
- * @memberof TimerCountdown
123
- */
124
- async reSyncTimerIfGoneOff(timerSecondsLeft) {
125
- // debug info
126
- console.log(
127
- new Date().toLocaleTimeString(),
128
- 'timeLeftInSec',
129
- timerSecondsLeft,
130
- ' ||| timeLeftInMin',
131
- Math.ceil(timerSecondsLeft / 60)
132
- );
133
-
134
- const currentTime = new Date();
135
-
136
- // current time - loan time
137
- const diffInSeconds =
138
- currentTime.getTime() / 1000 - this.timeWhenTimerStart.getTime() / 1000;
139
-
140
- const secondsShouldLeft = this.secondsLeftOnLoan - diffInSeconds;
141
-
142
- // convert in minutes
143
- const whatIsleft = Math.round(timerSecondsLeft);
144
- const whatShouldLeft = Math.round(secondsShouldLeft);
145
-
146
- if (whatIsleft !== whatShouldLeft) {
147
- // debug info
148
- console.log(
149
- `timer is gone off, let's re-sync, whatIsleft -> ${whatIsleft}, whatShouldLeft -> ${whatShouldLeft}`
150
- );
151
- this.secondsLeftOnLoan = secondsShouldLeft; // the seconds should left in timer again
152
- }
153
18
  }
154
19
 
155
20
  /**
@@ -40,7 +40,7 @@ import ActionsConfig from './actions-config.js';
40
40
  * @enum {string}
41
41
  */
42
42
  export const bookTitles = {
43
- available_1hr: 'Renews each hour as long as pages are turned.',
43
+ available_1hr: 'Renews automatically as long as pages are turned.',
44
44
  available_14d: 'This book can be borrowed for 14 days.',
45
45
  available_pd: 'Book available to patrons with print disabilities.',
46
46
  available_waitlist: 'A waitlist is available.',
@@ -49,7 +49,7 @@ export const bookTitles = {
49
49
  being_borrowed: 'Another patron is using this book. Please check back later.',
50
50
  eligible_pd: 'You are eligible for print-disabled access.',
51
51
  on_waitlist: 'You are on the waitlist for this book.',
52
- session_expired: '',
52
+ session_expired: 'Renews automatically as long as pages are turned.',
53
53
  unavailable: 'This book is not available at this time.',
54
54
  };
55
55
 
@@ -98,8 +98,7 @@ export class GetLendingActions {
98
98
  let isBrowsing =
99
99
  lendingStatus.user_has_browsed && !lendingStatus.browsingExpired;
100
100
  if (isBrowsing) {
101
- primaryTitleText =
102
- 'Returns automatically after 25 minutes of inactivity.';
101
+ primaryTitleText = bookTitles.available_1hr;
103
102
  } else {
104
103
  primaryTitleText = `Your loan of this book has ${lendingStatus.daysLeftOnLoan} days left.`;
105
104
  }
@@ -144,14 +144,14 @@ export default class LoanAnanlytics {
144
144
  * @memberof LoanAnanlytics
145
145
  */
146
146
  sendEvent(eventCategory, eventAction, label, extraParams) {
147
- console.log(
148
- 'eventCategory:-',
149
- eventCategory,
150
- '\t||\teventAction:-',
151
- eventAction,
152
- '\t||\tlabel:-',
153
- label
154
- );
147
+ // console.log(
148
+ // 'eventCategory:-',
149
+ // eventCategory,
150
+ // '\t||\teventAction:-',
151
+ // eventAction,
152
+ // '\t||\tlabel:-',
153
+ // label
154
+ // );
155
155
 
156
156
  window?.archive_analytics?.send_event_no_sampling(
157
157
  eventCategory,
@@ -16,7 +16,7 @@ export class LoanRenewHelper {
16
16
  this.loanRenewMessage =
17
17
  'This book has been renewed for #time #unitsOfTime.';
18
18
  this.loanReturnWarning =
19
- 'This book will be automatically returned in #time #unitsOfTime unless you turn a page.';
19
+ 'With no action, this book will be auto-returned in #time #unitsOfTime.';
20
20
 
21
21
  // private props
22
22
  this.result = {
@@ -39,7 +39,7 @@ export class LoanRenewHelper {
39
39
  }
40
40
 
41
41
  /**
42
- * Trigger this function when user has browsed a book and change the post
42
+ * Trigger this function when user has browsed a book and change the page
43
43
  * - every time user change the page, we set current time in indexedDB
44
44
  * - also check if need to auto renew current loan
45
45
  *
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable camelcase */
2
+ /* eslint-disable class-methods-use-this */
2
3
  import { html, css, LitElement, nothing } from 'lit';
3
4
 
4
5
  import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
@@ -23,6 +24,21 @@ export const events = {
23
24
  browseExpired: 'IABookReader:BrowsingHasExpired',
24
25
  };
25
26
 
27
+ /**
28
+ * custom styling for modal-manager buttons
29
+ * TODO: lets allow modal-manager to know ia-button classes
30
+ */
31
+ export const modalButtonStyle = {
32
+ iaButton:
33
+ 'min-height:3.5rem;cursor:pointer;color:white;border-radius:0.4rem;border:1px solid #c5d1df;padding:4px 8px;width:auto;user-select:none;',
34
+ renew: 'background:#194880;width:110px;',
35
+ return: 'background:#d9534f;width:120px;',
36
+ loaderIcon:
37
+ 'display:inline-block;width:20px;height:20px;margin-top:2px;color:white;--activityIndicatorLoadingRingColor:#fff;--activityIndicatorLoadingDotColor:#fff;',
38
+ refresh:
39
+ 'background:none;font-size:inherit;border:0;padding:0;color:#0000ee;cursor:pointer;text-decoration:underline',
40
+ };
41
+
26
42
  export default class IABookActions extends LitElement {
27
43
  static get properties() {
28
44
  return {
@@ -70,11 +86,17 @@ export default class IABookActions extends LitElement {
70
86
  this.primaryColor = 'primary';
71
87
  this.secondaryActions = [];
72
88
  this.lendingOptions = {};
73
- this.borrowType = ''; // (browsed|borrowed)
74
- this.consecutiveLoanCounts = 1; // consecutive loan count
75
- this.suppressToast = false;
76
- this.suppressAutoRenew = false;
89
+ this.borrowType = null; // (browsed|borrowed)
77
90
  this.browseTimer = undefined;
91
+ this.timerExecutionSeconds = 30;
92
+
93
+ /** @deprecated */
94
+ this.toast = undefined;
95
+
96
+ /**
97
+ * when user click on [return the book] button on warning modal
98
+ */
99
+ this.returnNow = false;
78
100
 
79
101
  /**
80
102
  * contains one hour auto-loan-renew time configuration
@@ -171,9 +193,6 @@ export default class IABookActions extends LitElement {
171
193
  /** End SharedObserver resize handler */
172
194
 
173
195
  async setupLendingToolbarActions() {
174
- this.suppressToast = false;
175
- this.suppressAutoRenew = false;
176
-
177
196
  this.lendingOptions = new GetLendingActions(
178
197
  this.userid,
179
198
  this.identifier,
@@ -193,7 +212,7 @@ export default class IABookActions extends LitElement {
193
212
  return action != null;
194
213
  });
195
214
 
196
- this.borrowType = actions.borrowType;
215
+ this.borrowType = actions.borrowType ? actions.borrowType : null;
197
216
 
198
217
  const hasExpired =
199
218
  'browsingExpired' in this.lendingStatus &&
@@ -218,8 +237,11 @@ export default class IABookActions extends LitElement {
218
237
  }
219
238
 
220
239
  if (this.borrowType === 'browsed') {
240
+ // start timer for loan-renew
241
+ await this.startTimerCountdown();
242
+
221
243
  // start timer for browsed.
222
- this.startBrowseTimer();
244
+ await this.startBrowseTimer();
223
245
  }
224
246
 
225
247
  // early return if not borrowed or no action-bar
@@ -248,84 +270,17 @@ export default class IABookActions extends LitElement {
248
270
  * Bind 1 hour loan auto renew event,
249
271
  * There are two events we want to use,
250
272
  * 1. BookReader:userAction - dispatched from bookreader side
251
- * 2. click event to refresh the page when book is auto expired
252
- * 3. IABookActions:loanRenew - dispatched from timer-countdown component
273
+ * 2. IABookActions:loanRenew - dispatched from timer-countdown component
253
274
  */
254
275
  bindLoanRenewEvents() {
255
276
  /**
256
277
  * dispatched this event from bookreader page changed
257
278
  */
258
279
  window.addEventListener('BookReader:userAction', () => {
259
- console.log('** BR user Action', {
260
- suppressAutoRenew: this.suppressAutoRenew,
261
- borrowType: this.borrowType,
262
- });
263
-
264
- this.suppressToast = true;
265
- this.closeToastManager();
266
-
267
- if (this.suppressAutoRenew) {
268
- return;
269
- }
270
-
271
280
  if (this.borrowType === 'browsed') {
272
281
  this.autoLoanRenewChecker(true);
273
282
  }
274
283
  });
275
-
276
- /**
277
- * detect click-event on document to close toast-template
278
- */
279
- document.addEventListener('click', e => {
280
- console.log('*** top level click handler to remove toast on click');
281
- if (this.loanRenewHelper && this.loanRenewResult.secondsLeft > 0) {
282
- this.suppressToast = true;
283
- }
284
-
285
- // reload when user click on book-page and book is EXPIRED
286
- const hasExpired =
287
- 'browsingExpired' in this.lendingStatus &&
288
- this.lendingStatus?.browsingExpired;
289
- if (hasExpired && e.target.nodeName !== 'IA-BOOK-ACTIONS') {
290
- window.location?.reload();
291
- }
292
- });
293
-
294
- /**
295
- * this event dispatched from timer-countdown component for:
296
- * 1. user turned book page after showing auto-returned warning message
297
- * 2. show warning message having remaining time
298
- * @see TimerCountdown::timerCountdown
299
- */
300
- this.addEventListener('IABookActions:loanRenew', async event => {
301
- console.log('** IABookActions:loanRenew', event.detail);
302
- await this.autoLoanRenewChecker(false);
303
-
304
- /**
305
- * auto-renew is not possible in last seconds (let say 50 second) because,
306
- * 1. less time to execute ajax call
307
- * 2. less time to write loan on datanodes
308
- * 3. less time to load images by create_token api
309
- * so if seconds left is < 50, just expire the loan
310
- */
311
- let secondsLeft = event?.detail?.secondsLeft;
312
- if (secondsLeft < 50) {
313
- this.suppressAutoRenew = true;
314
- this.browseHasExpired();
315
- }
316
-
317
- // show warning message with remaining time to auto returned it.
318
- if (this.loanRenewResult.renewNow === false) {
319
- /**
320
- * so compensate for the 50 second buffer to handle above race conditions
321
- * let's reduce 1 min from warning texts and early return the book when 1 min left.
322
- */
323
- secondsLeft -= 60;
324
-
325
- this.loanRenewResult.secondsLeft = secondsLeft;
326
- this.showToastMessage();
327
- }
328
- });
329
284
  }
330
285
 
331
286
  /**
@@ -347,34 +302,161 @@ export default class IABookActions extends LitElement {
347
302
  }
348
303
 
349
304
  /**
305
+ * create / select the modal-manager component on DOM
306
+ * @memberof IABookActions
307
+ */
308
+ async useModalManager() {
309
+ if (!this.modal) {
310
+ this.modal = document.querySelector('modal-manager');
311
+
312
+ if (!this.modal) this.modal = document.createElement('modal-manager');
313
+ }
314
+
315
+ this.modal.id = 'action-bar-modal';
316
+ await document.body.appendChild(this.modal);
317
+ }
318
+
319
+ /**
320
+ * Show the waring modal to ask user if they are still reading
321
+ */
322
+ async showWarningModal() {
323
+ console.log('****** showWarningModal ******');
324
+ await this.useModalManager();
325
+
326
+ // if secondsLeft < 60, consider it 1 minute
327
+ let { secondsLeft } = this.loanRenewResult;
328
+ if (secondsLeft === undefined) {
329
+ secondsLeft = this.lendingStatus.secondsLeftOnLoan;
330
+ } else {
331
+ secondsLeft = secondsLeft > 60 ? secondsLeft : 60;
332
+ }
333
+
334
+ const config = new ModalConfig();
335
+ config.headline = 'Are you still here?';
336
+ config.showCloseButton = false;
337
+ config.closeOnBackdropClick = false;
338
+ config.message = this.loanRenewHelper?.getMessageTexts(
339
+ this.loanRenewResult.texts,
340
+ secondsLeft
341
+ );
342
+
343
+ const customContent = html`<br />
344
+ <div id="custom-buttons" style="display:flex;justify-content:center;">
345
+ <button
346
+ style="${modalButtonStyle.iaButton} ${modalButtonStyle.renew}"
347
+ @click=${async event => {
348
+ await this.changeModalState(event);
349
+ this.loanRenewResult = { texts: '', renewNow: true };
350
+ }}
351
+ >
352
+ Keep reading
353
+ </button>
354
+ <button
355
+ style="${modalButtonStyle.iaButton} ${modalButtonStyle.return}"
356
+ @click=${event => {
357
+ this.changeModalState(event);
358
+ document.querySelector('ia-book-actions').disableActionGroup = true;
359
+ this.returnNow = true;
360
+ }}
361
+ >
362
+ Return the book
363
+ </button>
364
+ </div> `;
365
+
366
+ this.modal?.showModal({ config, customModalContent: customContent });
367
+ }
368
+
369
+ /**
370
+ * helper function to change button click states on modal-manager
371
+ * - add loader on button click
372
+ * - change button appearance
373
+ *
374
+ * @param {Event} event
375
+ */
376
+ changeModalState(event) {
377
+ if (event.target === undefined) return;
378
+
379
+ const button = event.target;
380
+ button.innerHTML = `<ia-activity-indicator
381
+ mode="processing"
382
+ style=${modalButtonStyle.loaderIcon}
383
+ ></ia-activity-indicator>`;
384
+
385
+ const parentElement = event.target.parentNode;
386
+ parentElement.style.pointerEvents = 'none';
387
+ parentElement.style.opacity = 0.8;
388
+ }
389
+
390
+ /**
391
+ * Show modal when book is auto returned
392
+ */
393
+ async showExpiredModal() {
394
+ console.log('showExpiredModal()');
395
+ await this.useModalManager();
396
+
397
+ const config = new ModalConfig();
398
+ config.headline = '';
399
+ config.showCloseButton = false;
400
+ config.closeOnBackdropClick = false;
401
+ config.message = 'This book has been returned due to inactivity.';
402
+
403
+ const customContent = html`<br />
404
+ <div style="text-align: center">
405
+ <button
406
+ style="${modalButtonStyle.iaButton} ${modalButtonStyle.renew}"
407
+ @click=${() => {
408
+ window.location?.reload();
409
+ }}
410
+ >
411
+ Okay
412
+ </button>
413
+ </div> `;
414
+
415
+ this.modal?.showModal({ config, customModalContent: customContent });
416
+ }
417
+
418
+ /**
419
+ * close/hide modal-manager
420
+ */
421
+ async closeModalManager() {
422
+ this.modal = document.querySelector('modal-manager');
423
+ this.modal?.closeModal();
424
+ }
425
+
426
+ /**
427
+ * @deprecated
428
+ * create / select the toast-template component on DOM
429
+ *
430
+ * @memberof IABookActions
431
+ */
432
+ async useToastManager() {
433
+ if (!this.toast) {
434
+ this.toast = this.shadowRoot.querySelector('toast-template');
435
+
436
+ if (!this.toast) this.toast = document.createElement('toast-template');
437
+ }
438
+
439
+ await this.shadowRoot.appendChild(this.toast);
440
+ }
441
+
442
+ /**
443
+ * @deprecated
350
444
  * close/hide toast message
351
445
  */
352
446
  async closeToastManager() {
353
- console.log('** closeToastManager');
354
- const toastTemplate = this.shadowRoot.querySelector('toast-template');
355
- if (toastTemplate) {
356
- toastTemplate?.remove();
357
- }
447
+ this.toast = this.shadowRoot.querySelector('toast-template');
448
+ this.toast?.remove();
358
449
  }
359
450
 
360
451
  /**
452
+ * @deprecated
361
453
  * Show toast messages on some specific loan renew features. e.g.
362
454
  * - show message when book is auto renewed
363
455
  * - show message when book is expired
364
456
  * - show message having remaining time when book is about to auto returned
365
457
  */
366
458
  async showToastMessage() {
367
- console.log('** showToastMessage', {
368
- suppressToast: this.suppressToast,
369
- loanRenewResult: this.loanRenewResult,
370
- });
371
- if (this.suppressToast) return;
372
-
373
- let toastTemplate = this.shadowRoot.querySelector('toast-template');
374
- if (!toastTemplate) {
375
- toastTemplate = document.createElement('toast-template');
376
- }
377
- await this.shadowRoot.appendChild(toastTemplate);
459
+ await this.useToastManager();
378
460
 
379
461
  // if secondsLeft < 60, consider it 1 minute
380
462
  let { secondsLeft } = this.loanRenewResult;
@@ -391,12 +473,14 @@ export default class IABookActions extends LitElement {
391
473
  secondsLeft
392
474
  );
393
475
 
394
- toastTemplate.showToast({
476
+ this.toast?.showToast({
395
477
  config,
396
478
  });
397
479
  }
398
480
 
399
481
  async browseHasRenew() {
482
+ window?.IALendingIntervals?.clearAll();
483
+
400
484
  const loanTime = await this.localCache.get(`${this.identifier}-loanTime`);
401
485
  const secondsLeft = (loanTime - new Date()) / 1000; // different in seconds
402
486
 
@@ -407,24 +491,6 @@ export default class IABookActions extends LitElement {
407
491
  secondsLeftOnLoan: secondsLeft,
408
492
  };
409
493
  this.lendingStatus = currStatus;
410
-
411
- this.startBrowseTimer();
412
- }
413
-
414
- startBrowseTimer() {
415
- clearTimeout(this.browseTimer);
416
- const {
417
- browsingExpired,
418
- user_has_browsed,
419
- secondsLeftOnLoan,
420
- } = this.lendingStatus;
421
- if (!user_has_browsed || browsingExpired) {
422
- return;
423
- }
424
-
425
- this.browseTimer = setTimeout(() => {
426
- this.browseHasExpired();
427
- }, secondsLeftOnLoan * 1000);
428
494
  }
429
495
 
430
496
  /**
@@ -443,14 +509,31 @@ export default class IABookActions extends LitElement {
443
509
  // show message after browsed book is expired.
444
510
  this.loanRenewResult.renewNow = false;
445
511
  this.loanRenewResult.texts =
446
- 'This book has been automatically returned due to inactivity.';
512
+ 'This book has been returned due to inactivity.';
447
513
 
448
- this.suppressToast = false;
449
- this.showToastMessage();
514
+ await this.showExpiredModal();
450
515
 
451
516
  window?.Sentry?.captureMessage(sentryLogs.browseHasExpired);
452
517
  }
453
518
 
519
+ async startBrowseTimer() {
520
+ clearTimeout(this.browseTimer);
521
+
522
+ const {
523
+ browsingExpired,
524
+ user_has_browsed,
525
+ secondsLeftOnLoan,
526
+ } = this.lendingStatus;
527
+ if (!user_has_browsed || browsingExpired) {
528
+ console.log('user not browsed', this.lendingStatus);
529
+ return;
530
+ }
531
+
532
+ this.browseTimer = setTimeout(() => {
533
+ this.browseHasExpired();
534
+ }, secondsLeftOnLoan * 1000);
535
+ }
536
+
454
537
  /**
455
538
  * Reset timer animation state after book renewed
456
539
  */
@@ -501,8 +584,6 @@ export default class IABookActions extends LitElement {
501
584
  return this.borrowType === 'browsed'
502
585
  ? html`<timer-countdown
503
586
  .secondsLeftOnLoan=${Number(this.lendingStatus.secondsLeftOnLoan)}
504
- .loanTotalTime=${this.loanRenewTimeConfig.loanTotalTime}
505
- .loanRenewAtLast=${this.loanRenewTimeConfig.loanRenewAtLast}
506
587
  ></timer-countdown>`
507
588
  : nothing;
508
589
  }
@@ -524,6 +605,7 @@ export default class IABookActions extends LitElement {
524
605
  ?disabled=${this.disableActionGroup}
525
606
  ?autoRenew=${this.loanRenewResult.renewNow}
526
607
  ?autoReturn=${this.lendingStatus.browsingExpired}
608
+ ?returnNow=${this.returnNow}
527
609
  @loanAutoRenewed=${this.handleLoanAutoRenewed}
528
610
  @lendingActionError=${this.handleLendingActionError}
529
611
  @toggleActionGroup=${this.handleToggleActionGroup}
@@ -545,19 +627,149 @@ export default class IABookActions extends LitElement {
545
627
  if (this.loanRenewResult.renewNow) {
546
628
  window?.IALendingIntervals?.clearAll();
547
629
 
548
- this.suppressToast = false;
549
- await this.browseHasRenew();
550
- await this.showToastMessage();
551
- await this.resetTimerCountState();
552
-
553
- window?.Sentry?.captureMessage(sentryLogs.bookHasRenewed);
554
-
555
630
  // testing console....
556
631
  console.log('IABookActions: AutoRenewed:- ', {
557
632
  ajaxResponse: event?.detail?.data,
558
633
  loanRenewResult: this.loanRenewResult,
559
634
  secondsLeftOnLoan: Math.round(this.lendingStatus.secondsLeftOnLoan),
560
635
  });
636
+
637
+ /**
638
+ * update the lendingStatus property
639
+ */
640
+ await this.browseHasRenew();
641
+
642
+ /**
643
+ * reset timer countdown states
644
+ */
645
+ await this.resetTimerCountState();
646
+
647
+ // close the modal
648
+ this.modal?.remove();
649
+ this.modal?.showModal({ config: {}, customModalContent: `` });
650
+
651
+ window?.Sentry?.captureMessage(sentryLogs.bookHasRenewed);
652
+ }
653
+ }
654
+
655
+ /**
656
+ * start timer countdown interval after loan auto-renewed
657
+ *
658
+ * @memberof IABookActions
659
+ */
660
+ startTimerCountdown() {
661
+ console.log('startTimerCountdown()');
662
+ window?.IALendingIntervals?.clearTimerCountdown();
663
+
664
+ let secondsLeft = Number(this.lendingStatus.secondsLeftOnLoan);
665
+ this.timeWhenTimerStart = new Date();
666
+
667
+ window.IALendingIntervals.timerCountdown = setInterval(async () => {
668
+ secondsLeft -= this.timerExecutionSeconds;
669
+ secondsLeft = Math.round(secondsLeft); // round number
670
+
671
+ // re-sync timer if gone off because of background window
672
+ await this.reSyncTimerIfGoneOff(secondsLeft);
673
+
674
+ /**
675
+ * execute from last 10th minute to 0th minute
676
+ * - 10th - to check if user has viewed
677
+ * - till 0th - to show warning msg with remaining time to auto expired
678
+ * @see IABookActions::bindLoanRenewEvents
679
+ */
680
+ if (secondsLeft <= this.loanRenewTimeConfig.loanRenewAtLast) {
681
+ await this.loanRenewAttempt(secondsLeft);
682
+ }
683
+
684
+ // clear interval in secondsLeft if less
685
+ if (secondsLeft <= this.timerExecutionSeconds) {
686
+ this.disconnectedCallback();
687
+ window?.Sentry?.captureMessage(sentryLogs.clearOneHourTimer);
688
+ }
689
+ }, this.timerExecutionSeconds * 1000);
690
+ }
691
+
692
+ /**
693
+ * helper function to determine if timer is not in sync properly
694
+ *
695
+ * @param {number} timerSecondsLeft - actual seconds left get from setInterval
696
+ *
697
+ * @memberof TimerCountdown
698
+ */
699
+ async reSyncTimerIfGoneOff(timerSecondsLeft) {
700
+ // debug info
701
+ console.log(
702
+ new Date().toLocaleTimeString(),
703
+ 'timeLeftInSec',
704
+ timerSecondsLeft,
705
+ ' ||| timeLeftInMin',
706
+ Math.ceil(timerSecondsLeft / 60)
707
+ );
708
+
709
+ const currentTime = new Date();
710
+
711
+ // current time - loan time
712
+ const diffInSeconds =
713
+ currentTime.getTime() / 1000 - this.timeWhenTimerStart.getTime() / 1000;
714
+
715
+ const secondsShouldLeft =
716
+ this.lendingStatus.secondsLeftOnLoan - diffInSeconds;
717
+
718
+ // convert in minutes
719
+ const whatIsleft = Math.round(timerSecondsLeft);
720
+ const whatShouldLeft = Math.round(secondsShouldLeft);
721
+
722
+ if (whatIsleft !== whatShouldLeft) {
723
+ // debug info
724
+ console.log(
725
+ `timer is gone off, let's re-sync, whatIsleft -> ${whatIsleft}, whatShouldLeft -> ${whatShouldLeft}`
726
+ );
727
+
728
+ // the seconds should left in timer again
729
+ const currStatus = {
730
+ ...this.lendingStatus,
731
+ secondsLeftOnLoan: secondsShouldLeft,
732
+ };
733
+ this.lendingStatus = currStatus;
734
+ }
735
+ }
736
+
737
+ /**
738
+ * attmept to loan renew from
739
+ * - timer countdown
740
+ * - onclick on [keep reading] button
741
+ *
742
+ * @param {*} secondsLeft
743
+ * @memberof IABookActions
744
+ */
745
+ async loanRenewAttempt(secondsLeft) {
746
+ console.log('loanRenewAttempt()');
747
+ let loanSecondsLeft = secondsLeft;
748
+ /**
749
+ * auto-renew is not possible in last seconds (let say 50 second) because,
750
+ * 1. less time to execute ajax call
751
+ * 2. less time to write loan on datanodes
752
+ * 3. less time to load images by create_token api
753
+ * so if seconds left is < 50, just expire the loan
754
+ */
755
+ if (loanSecondsLeft < 50) {
756
+ console.log('if (loanSecondsLeft < 50) {');
757
+ await this.browseHasExpired();
758
+ return;
759
+ }
760
+
761
+ await this.autoLoanRenewChecker(false);
762
+
763
+ // show warning modal with remaining time to auto returned it.
764
+ if (this.loanRenewResult.renewNow === false) {
765
+ /**
766
+ * so compensate for the 50 second buffer to handle above race conditions
767
+ * let's reduce 1 min from warning texts and early return the book when 1 min left.
768
+ */
769
+ loanSecondsLeft -= 60;
770
+ this.loanRenewResult.secondsLeft = loanSecondsLeft;
771
+
772
+ this.showWarningModal();
561
773
  }
562
774
  }
563
775
 
@@ -602,7 +814,7 @@ export default class IABookActions extends LitElement {
602
814
  this.disableActionGroup = !this.disableActionGroup;
603
815
  }
604
816
 
605
- /*
817
+ /**
606
818
  * handle lending errors occure during different operation like
607
819
  * browse book, borrow book, borrowed book token etc...
608
820
  *
@@ -656,16 +868,7 @@ export default class IABookActions extends LitElement {
656
868
 
657
869
  /* show error message if something went wrong */
658
870
  async showErrorModal(errorMsg, action) {
659
- // check if this.modal passed as prop
660
- if (!this.modal) {
661
- this.modal = document.querySelector('modal-manager');
662
-
663
- // check the DOM if <modal-manager> already there
664
- if (!this.modal) this.modal = document.createElement('modal-manager');
665
- }
666
-
667
- this.modal.id = 'action-bar-modal';
668
- await document.body.appendChild(this.modal);
871
+ await this.useModalManager();
669
872
 
670
873
  const modalConfig = new ModalConfig({
671
874
  title: 'Lending error',
@@ -676,7 +879,7 @@ export default class IABookActions extends LitElement {
676
879
 
677
880
  if (action === 'create_token') {
678
881
  const refreshButton = html`<button
679
- style="background:none;font-size:inherit;border:0;padding:0;color:#0000ee;cursor:pointer;text-decoration:underline"
882
+ style="${modalButtonStyle.refresh}"
680
883
  @click=${() => window.location.reload(true)}
681
884
  >
682
885
  refresh
@@ -693,7 +896,7 @@ export default class IABookActions extends LitElement {
693
896
  <code>errorLog: ${errorMsg}</code>`;
694
897
  }
695
898
 
696
- this.modal.showModal({
899
+ this.modal?.showModal({
697
900
  config: modalConfig,
698
901
  });
699
902
  }
@@ -80,7 +80,7 @@ describe('Loan Renew Determine', () => {
80
80
  );
81
81
 
82
82
  expect(toastMsg).to.be.equal(
83
- 'This book will be automatically returned in 2 minutes unless you turn a page.'
83
+ 'With no action, this book will be auto-returned in 2 minutes.'
84
84
  );
85
85
  });
86
86
  });
@@ -188,7 +188,9 @@ describe('Borrow status actions', () => {
188
188
  },
189
189
  ];
190
190
 
191
- expect(el.primaryTitle).to.equal('');
191
+ expect(el.primaryTitle).to.equal(
192
+ 'Renews automatically as long as pages are turned.'
193
+ );
192
194
  expect(el.primaryActions.length).to.equal(2);
193
195
  expect(el.primaryActions[0].text).to.equal('Log In and Borrow');
194
196
  expect(el.primaryActions[0].text).to.equal(expectedPrimaryActions[0].text);
@@ -274,7 +276,7 @@ describe('Browsing expired status', () => {
274
276
  expect(el.loanRenewResult.renewNow).to.equal(false);
275
277
  expect(el.loanRenewResult.secondsLeft).to.equal(0);
276
278
  expect(el.loanRenewResult.texts).to.equal(
277
- 'This book has been automatically returned due to inactivity.'
279
+ 'This book has been returned due to inactivity.'
278
280
  );
279
281
  });
280
282
 
@@ -347,7 +349,7 @@ describe('Auto renew one hour loan', () => {
347
349
  });
348
350
  el.dispatchEvent(myEvent);
349
351
 
350
- await aTimeout(1500); // wait for 1.5 second
352
+ await aTimeout(1900); // wait for 1.9 second
351
353
  await el.updateComplete;
352
354
 
353
355
  expect(el.loanRenewResult.texts).to.equal(