@internetarchive/ia-book-actions 1.4.3 → 1.4.4-alpha2

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.
@@ -20,6 +20,7 @@ import { LoanTokenPoller } from './core/services/loan-token-poller.js';
20
20
  import { LoanRenewHelper } from './core/services/loan-renew-helper.js';
21
21
  import log from './core/services/log.js';
22
22
  import { URLHelper } from './core/config/url-helper.js';
23
+ import { infoIcon } from './assets/data/info.js';
23
24
 
24
25
  export const events = {
25
26
  browseExpired: 'IABookReader:BrowsingHasExpired',
@@ -93,10 +94,19 @@ export default class IABookActions extends LitElement {
93
94
  this.browseTimer = undefined; // timeout
94
95
  this.timeWhenTimerStart = undefined;
95
96
 
97
+ this.loanRenewInProgress = false;
98
+
99
+ this.warningModalOpen = false;
100
+
96
101
  /**
97
- * when user click on [return the book] button on warning modal
102
+ * Once the patron has seen & dismissed the informational warning modal,
103
+ * don't nag them again with the same modal every timer tick — only a
104
+ * real renewal (via interacting with the book) clears this, giving the
105
+ * next pre-expiry window its own single warning.
106
+ * @see loanRenewAttempt
107
+ * @see handleLoanAutoRenewed
98
108
  */
99
- this.returnNow = false;
109
+ this.warningModalDismissed = false;
100
110
 
101
111
  /**
102
112
  * contains one hour auto-loan-renew time configuration
@@ -141,7 +151,6 @@ export default class IABookActions extends LitElement {
141
151
  * @param {string} msg
142
152
  */
143
153
  sentryCaptureMsg(msg) {
144
- log(window?.Sentry);
145
154
  window?.Sentry?.captureMessage(msg);
146
155
  }
147
156
 
@@ -171,6 +180,13 @@ export default class IABookActions extends LitElement {
171
180
  }
172
181
 
173
182
  if (changed.has('loanRenewResult') && this.loanRenewResult.renewNow) {
183
+ // Any renewal — not just the auto-return recovery path — pauses the
184
+ // countdown for the renew_loan round-trip. Flag it here, centrally,
185
+ // so every path (interaction during active reading, periodic
186
+ // checker, auto-return recovery) gets the same "renewing" signal
187
+ // instead of just looking frozen until handleLoanAutoRenewed() clears
188
+ // this and restarts the timer.
189
+ this.loanRenewInProgress = true;
174
190
  window.IALendingIntervals.clearAll();
175
191
  }
176
192
  }
@@ -208,32 +224,16 @@ export default class IABookActions extends LitElement {
208
224
  /** End SharedObserver resize handler */
209
225
 
210
226
  async setupLendingToolbarActions() {
211
- this.lendingOptions = new GetLendingActions(
212
- this.userid,
213
- this.identifier,
214
- this.lendingStatus,
215
- this.bwbPurchaseUrl
216
- );
217
- const actions = this.lendingOptions.getCurrentLendingActions();
218
-
219
- if (!actions) return;
220
-
221
- this.primaryTitle = actions.primaryTitle;
222
- this.primaryActions = actions.primaryActions?.filter(action => {
223
- return action != null;
224
- });
225
- this.primaryColor = actions.primaryColor;
226
- this.secondaryActions = actions.secondaryActions?.filter(action => {
227
- return action != null;
228
- });
229
-
230
- this.borrowType = actions.borrowType ? actions.borrowType : null;
231
-
232
227
  const hasExpired =
233
228
  'browsingExpired' in this.lendingStatus &&
234
229
  this.lendingStatus?.browsingExpired;
235
230
  if (hasExpired) {
236
- log('setupLendingToolbarActions > hasExpired --- ');
231
+ // Intentionally do NOT recompute primaryActions/primaryColor/borrowType
232
+ // here — per the ticket, auto-return must not visibly change anything.
233
+ // The action bar stays exactly as it looked while reading; only a
234
+ // successful silent renewal or the "unavailable" error modal are
235
+ // allowed to change what's shown next.
236
+ log('[IABookActions] browsing expired — leaving action bar untouched');
237
237
 
238
238
  if (!this.tokenPoller) {
239
239
  this.sentryCaptureMsg(sentryLogs.bookWasExpired);
@@ -253,6 +253,27 @@ export default class IABookActions extends LitElement {
253
253
  return;
254
254
  }
255
255
 
256
+ this.lendingOptions = new GetLendingActions(
257
+ this.userid,
258
+ this.identifier,
259
+ this.lendingStatus,
260
+ this.bwbPurchaseUrl
261
+ );
262
+ const actions = this.lendingOptions.getCurrentLendingActions();
263
+
264
+ if (!actions) return;
265
+
266
+ this.primaryTitle = actions.primaryTitle;
267
+ this.primaryActions = actions.primaryActions?.filter(action => {
268
+ return action != null;
269
+ });
270
+ this.primaryColor = actions.primaryColor;
271
+ this.secondaryActions = actions.secondaryActions?.filter(action => {
272
+ return action != null;
273
+ });
274
+
275
+ this.borrowType = actions.borrowType ? actions.borrowType : null;
276
+
256
277
  if (this.borrowType === 'browsed') {
257
278
  // start timer for loan-renew
258
279
  await this.startTimerCountdown();
@@ -290,8 +311,27 @@ export default class IABookActions extends LitElement {
290
311
  * dispatched this event from bookreader page changed
291
312
  */
292
313
  window.addEventListener('BookReader:userAction', () => {
293
- log('IABookActions:BookReader:userAction');
294
- if (this.borrowType === 'browsed') {
314
+ // Capture before autoRenewExpiredLoan() runs — it synchronously flips
315
+ // lendingStatus.browsingExpired to false as an optimistic UI update,
316
+ // so checking the live property afterward would always see it as
317
+ // false and wrongly let autoLoanRenewChecker() run too. That second
318
+ // call would then overwrite loanRenewResult.renewNow (set true by
319
+ // autoRenewExpiredLoan) back to false, since the loanTime cache entry
320
+ // was already deleted by browseHasExpired() — silently killing the
321
+ // in-flight renewal and leaving the countdown stuck.
322
+ const wasExpired = this.lendingStatus.browsingExpired;
323
+
324
+ log('[IABookActions] BookReader:userAction received', {
325
+ borrowType: this.borrowType,
326
+ browsingExpired: wasExpired,
327
+ });
328
+
329
+ // If the loan expired while the tab stayed visible, auto-renew on page turn
330
+ if (wasExpired) {
331
+ this.autoRenewExpiredLoan();
332
+ }
333
+
334
+ if (this.borrowType === 'browsed' && !wasExpired) {
295
335
  this.autoLoanRenewChecker(true);
296
336
  }
297
337
  });
@@ -302,18 +342,23 @@ export default class IABookActions extends LitElement {
302
342
  * the [visibilitychange] event can trigger timer to show relavent messages
303
343
  */
304
344
  document.addEventListener('visibilitychange', async () => {
305
- if (!document.hidden) {
306
- log(
307
- 'visibilitychange event execute:------------------ ',
308
- new Date().getMinutes(),
309
- new Date().getSeconds(),
310
- this.borrowType
311
- );
312
-
313
- if (
314
- this.borrowType === 'browsed' &&
315
- this.lendingStatus.browsingExpired === false
316
- ) {
345
+ if (document.hidden) {
346
+ log('[IABookActions] visibilitychange: tab backgrounded');
347
+ return;
348
+ }
349
+
350
+ log('[IABookActions] visibilitychange: tab foregrounded', this.borrowType);
351
+
352
+ try {
353
+ // Loan already expired while tab was hidden — try to silently renew
354
+ if (this.lendingStatus.browsingExpired === true) {
355
+ this.autoRenewExpiredLoan();
356
+ return;
357
+ }
358
+
359
+ if (this.borrowType !== 'browsed') return;
360
+
361
+ if (this.lendingStatus.browsingExpired === false) {
317
362
  const loanTime = await this.localCache.get(
318
363
  `${this.identifier}-loanTime`
319
364
  );
@@ -324,10 +369,15 @@ export default class IABookActions extends LitElement {
324
369
  if (secondsLeft >= this.timerExecutionSeconds) {
325
370
  this.loanStatusCheckInterval(Number(secondsLeft));
326
371
  } else {
327
- this.browseHasExpired();
372
+ // Loan expired while user was away — try to silently renew
373
+ this.autoRenewExpiredLoan();
328
374
  this.disconnectedCallback();
329
375
  }
330
376
  }
377
+ } catch (error) {
378
+ // Surface localCache failures instead of an unhandled rejection.
379
+ log('[IABookActions] visibilitychange handler failed', error);
380
+ this.sentryCaptureMsg(`visibilitychange handler failed: ${error}`);
331
381
  }
332
382
  });
333
383
  }
@@ -350,6 +400,36 @@ export default class IABookActions extends LitElement {
350
400
  this.loanRenewResult = this.loanRenewHelper.result;
351
401
  }
352
402
 
403
+ /**
404
+ * Attempt to automatically renew a browse loan that has expired.
405
+ * Called on visibilitychange (user returns to tab) or on page turn when
406
+ * the loan has expired but the book may still be available.
407
+ *
408
+ * Reuses the same renew_loan path as the automatic loan-renew checker.
409
+ * On success: handleLoanAutoRenewed() resets the timer seamlessly.
410
+ * On error: handleLendingActionError() shows showLoanUnavailableModal().
411
+ */
412
+ autoRenewExpiredLoan() {
413
+ if (this.loanRenewInProgress) return;
414
+ this.loanRenewInProgress = true;
415
+
416
+ this.modal?.closeModal();
417
+
418
+ // Optimistically flip browsingExpired back to false so the action bar
419
+ // stays red (patronIsReadingAction) while the renew_loan request is in
420
+ // flight, instead of showing the blue "Borrow" state.
421
+ this.lendingStatus = {
422
+ ...this.lendingStatus,
423
+ browsingExpired: false,
424
+ secondsLeftOnLoan: this.loanRenewTimeConfig.loanTotalTime,
425
+ };
426
+
427
+ // Defer renewNow until after lendingStatus timer setup completes.
428
+ setTimeout(() => {
429
+ this.loanRenewResult = { texts: '', renewNow: true, renewType: 'auto' };
430
+ }, 0);
431
+ }
432
+
353
433
  /**
354
434
  * Required as sibling on page
355
435
  * @returns HTMLElement
@@ -362,158 +442,86 @@ export default class IABookActions extends LitElement {
362
442
  }
363
443
 
364
444
  /**
365
- * Show the waring modal to ask user if they are still reading
445
+ * Show the informational modal warning the patron their loan will expire
446
+ * soon. This is acknowledgement-only — closing it does not renew the loan.
447
+ * Only interacting with the book itself (e.g. turning a page) renews it.
366
448
  */
367
449
  async showWarningModal() {
368
- log('****** showWarningModal ******');
369
- // clear modal
370
- this.modal.customModalContent = nothing;
371
- this.modal?.closeModal();
372
- this.loanRenewResult = { texts: '', renewNow: false };
373
-
374
- // if secondsLeft < 60, consider it 1 minute
375
- let { secondsLeft } = this.loanRenewResult;
376
- if (secondsLeft === undefined) {
450
+ if (this.warningModalOpen) return;
451
+ this.warningModalOpen = true;
452
+
453
+ log('[IABookActions] showWarningModal');
454
+
455
+ // Capture texts and secondsLeft before resetting loanRenewResult
456
+ const {
457
+ texts: warningTexts,
458
+ secondsLeft: rawSecondsLeft,
459
+ } = this.loanRenewResult;
460
+ let secondsLeft = rawSecondsLeft;
461
+ if (secondsLeft === undefined || secondsLeft <= 0) {
377
462
  secondsLeft = this.lendingStatus.secondsLeftOnLoan;
378
463
  } else {
379
464
  secondsLeft = secondsLeft > 60 ? secondsLeft : 60;
380
465
  }
381
466
 
467
+ // clear modal and reset renew state
468
+ this.modal.customModalContent = nothing;
469
+ this.modal?.closeModal();
470
+ this.loanRenewResult = { texts: '', renewNow: false };
471
+
382
472
  const config = new ModalConfig({
383
- headline: 'Are you still reading?',
473
+ headline: 'Are you still there?',
384
474
  headerColor: '#194880',
385
475
  showCloseButton: false,
386
476
  closeOnBackdropClick: false,
387
- message: this.loanRenewHelper?.getMessageTexts(
388
- this.loanRenewResult.texts,
389
- secondsLeft
390
- ),
391
- });
392
-
393
- const customModalContent = html`<br />
394
- <div
395
- id="book-action-bar-custom-buttons"
396
- style="display:flex;justify-content:center;"
477
+ message: html`<span
478
+ style="display:inline-flex;align-items:center;flex-wrap:nowrap;gap:4px;"
397
479
  >
398
- <button
399
- style="${modalButtonStyle.iaButton} ${modalButtonStyle.renew}"
400
- @click=${() => this.patronWantsToRenewBook()}
401
- >
402
- Keep reading
403
- </button>
404
- <button
405
- style="${modalButtonStyle.iaButton} ${modalButtonStyle.return}"
406
- @click=${() => this.patronWantsToReturnBook()}
480
+ <span>${this.loanRenewHelper?.getMessageTexts(
481
+ warningTexts,
482
+ secondsLeft
483
+ )}</span>
484
+ <a
485
+ href="https://help.archive.org/help/borrowing-from-the-lending-library"
486
+ target="_blank"
487
+ title="Get more info on borrowing from The Lending Library"
488
+ data-event-click-tracking="BookReader|BrowsableMoreInfo"
489
+ style="display:inline-flex;flex-shrink:0;line-height:0;"
407
490
  >
408
- Return the book
409
- </button>
410
- </div> `;
411
-
412
- this.modal.setAttribute('aria-live', 'assertive');
413
- await this.modal?.showModal({ config, customModalContent });
414
- }
415
-
416
- /** @param { 'renewBook' | 'returnBook' } buttonToDisable */
417
- async showWarningDisabledModal(buttonToDisable = 'renewBook') {
418
- // if secondsLeft < 60, consider it 1 minute
419
- let { secondsLeft } = this.loanRenewResult;
420
- if (secondsLeft === undefined) {
421
- secondsLeft = this.lendingStatus.secondsLeftOnLoan;
422
- } else {
423
- secondsLeft = secondsLeft > 60 ? secondsLeft : 60;
424
- }
425
-
426
- const config = new ModalConfig({
427
- headline: 'Are you still reading?',
428
- headerColor: '#194880',
429
- showCloseButton: false,
430
- closeOnBackdropClick: false,
431
- message: this.loanRenewHelper?.getMessageTexts(
432
- this.loanRenewResult.texts,
433
- secondsLeft
434
- ),
491
+ ${infoIcon}
492
+ </a>
493
+ </span>`,
435
494
  });
436
495
 
437
- const customModalContent = html`<br />
496
+ const customModalContent = html`
438
497
  <div
439
- id="disabled-book-action-bar-custom-buttons"
440
- style="display:flex;justify-content:center; opacity:0.8; pointer-events:none;"
498
+ id="book-action-bar-custom-buttons"
499
+ style="display:flex;flex-direction:column;justify-content:center;align-items:center;gap:8px;margin-top:10px;"
441
500
  >
442
501
  <button
443
- disabled
444
502
  style="${modalButtonStyle.iaButton} ${modalButtonStyle.renew}"
503
+ @click=${() => this.dismissWarningModal()}
445
504
  >
446
- ${buttonToDisable === 'renewBook'
447
- ? html`<ia-activity-indicator
448
- mode="processing"
449
- style=${modalButtonStyle.loaderIcon}
450
- ></ia-activity-indicator>`
451
- : 'Keep reading'}
452
- </button>
453
- <span
454
- style="position: absolute; visibility: none; height: 1px; width: 1px; overflow: hidden;"
455
- >Renewing loan, one moment please.</span
456
- >
457
- <button
458
- disabled
459
- style="${modalButtonStyle.iaButton} ${modalButtonStyle.return}"
460
- >
461
- ${buttonToDisable === 'returnBook'
462
- ? html`<ia-activity-indicator
463
- mode="processing"
464
- style=${modalButtonStyle.loaderIcon}
465
- ></ia-activity-indicator>`
466
- : 'Return the book'}
505
+ Okay
467
506
  </button>
468
507
  </div> `;
469
508
 
509
+ this.modal.setAttribute('aria-live', 'assertive');
470
510
  await this.modal?.showModal({ config, customModalContent });
471
511
  }
472
512
 
473
- /** Handles Renew action in warning modal */
474
- async patronWantsToRenewBook() {
475
- this.showWarningDisabledModal();
476
- this.loanRenewResult = { texts: '', renewNow: true, renewType: 'manual' };
477
- }
478
-
479
- async patronWantsToReturnBook() {
480
- this.showWarningDisabledModal('returnBook');
481
- document.querySelector('ia-book-actions').disableActionGroup = true;
482
- this.returnNow = true;
483
- }
484
-
485
- /**
486
- * Show modal when book is auto returned
487
- */
488
- async showExpiredModal() {
489
- const config = new ModalConfig({
490
- headline: '',
491
- showCloseButton: false,
492
- closeOnBackdropClick: false,
493
- headerColor: '#194880',
494
- message: 'This book has been returned due to inactivity.',
495
- });
496
-
497
- const customModalContent = html`<br />
498
- <div style="text-align: center">
499
- <button
500
- style="${modalButtonStyle.iaButton} ${modalButtonStyle.renew}"
501
- @click=${() => {
502
- URLHelper.goToUrl(this.returnUrl, true);
503
- }}
504
- >
505
- Okay
506
- </button>
507
- </div> `;
508
-
509
- await this.modal?.showModal({ config, customModalContent });
513
+ /** Acknowledges the warning modal — closes it without renewing the loan */
514
+ dismissWarningModal() {
515
+ this.modal?.closeModal();
516
+ this.warningModalOpen = false;
517
+ this.warningModalDismissed = true;
510
518
  }
511
519
 
512
520
  /**
513
521
  * Execute when loan is expired
514
522
  */
515
523
  async browseHasExpired() {
516
- log('BrowseHasExpired ---');
524
+ log('[IABookActions] browseHasExpired');
517
525
  window?.IALendingIntervals?.clearAll();
518
526
 
519
527
  const currStatus = {
@@ -532,31 +540,52 @@ export default class IABookActions extends LitElement {
532
540
  this.loanRenewResult.texts =
533
541
  'This book has been returned due to inactivity.';
534
542
 
535
- await this.showExpiredModal();
543
+ this.modal?.closeModal();
536
544
 
537
545
  this.sentryCaptureMsg(sentryLogs.browseHasExpired);
538
546
  }
539
547
 
548
+ /**
549
+ * Show modal when the book can no longer be automatically renewed
550
+ * because another patron has checked it out.
551
+ */
552
+ async showLoanUnavailableModal() {
553
+ const config = new ModalConfig({
554
+ headline: '',
555
+ showCloseButton: false,
556
+ closeOnBackdropClick: false,
557
+ headerColor: '#d9534f',
558
+ message:
559
+ 'Due to inactivity, this book was returned, and someone else has now borrowed it. Please try again later.',
560
+ });
561
+
562
+ const customModalContent = html`<br />
563
+ <div style="text-align: center">
564
+ <button
565
+ style="${modalButtonStyle.iaButton} ${modalButtonStyle.return}"
566
+ @click=${() => URLHelper.goToUrl(this.returnUrl, true)}
567
+ >
568
+ Okay
569
+ </button>
570
+ </div>`;
571
+
572
+ await this.modal?.showModal({ config, customModalContent });
573
+ }
574
+
540
575
  async startBrowseTimer() {
541
576
  window?.IALendingIntervals?.clearBrowseExpireTimeout();
542
577
 
543
- const { browsingExpired, user_has_browsed, secondsLeftOnLoan } =
544
- this.lendingStatus;
578
+ const {
579
+ browsingExpired,
580
+ user_has_browsed,
581
+ secondsLeftOnLoan,
582
+ } = this.lendingStatus;
545
583
 
546
584
  if (!user_has_browsed || browsingExpired) {
547
- log('startBrowseTimer --- !user_has_browsed || browsingExpired', {
548
- user_has_browsed,
549
- browsingExpired,
550
- secondsLeftOnLoan,
551
- });
552
585
  return;
553
586
  }
554
587
 
555
588
  window.IALendingIntervals.browseExpireTimeout = setTimeout(() => {
556
- log(
557
- 'startBrowseTimer > browseExpireTimeout --- will expire loan',
558
- secondsLeftOnLoan
559
- );
560
589
  this.browseHasExpired();
561
590
  }, secondsLeftOnLoan * 1000);
562
591
  }
@@ -602,7 +631,6 @@ export default class IABookActions extends LitElement {
602
631
  ?disabled=${this.disableActionGroup}
603
632
  ?autoRenew=${this.loanRenewResult.renewNow}
604
633
  ?autoReturn=${this.lendingStatus.browsingExpired}
605
- ?returnNow=${this.returnNow}
606
634
  @loanAutoRenewed=${this.handleLoanAutoRenewed}
607
635
  @lendingActionError=${this.handleLendingActionError}
608
636
  @toggleActionGroup=${this.handleToggleActionGroup}
@@ -637,13 +665,17 @@ export default class IABookActions extends LitElement {
637
665
  const loanTime = await this.localCache.get(`${this.identifier}-loanTime`);
638
666
 
639
667
  // number of seconds left in current loan
640
- const secondsLeft = Math.round((loanTime - new Date()) / 1000);
641
- log(loanTime, secondsLeft);
642
-
643
- log('IABookActions: handleLoanAutoRenewed --- ', {
668
+ const rawSecondsLeft = Math.round((loanTime - new Date()) / 1000);
669
+ // Guard against a stale/missing loanTime read producing NaN, which
670
+ // would get the loan stuck "active" with a countdown that never moves.
671
+ const secondsLeft =
672
+ Number.isFinite(rawSecondsLeft) && rawSecondsLeft > 0
673
+ ? rawSecondsLeft
674
+ : this.loanRenewTimeConfig.loanTotalTime;
675
+ log('[IABookActions] handleLoanAutoRenewed', {
676
+ secondsLeft,
677
+ rawSecondsLeft,
644
678
  ajaxResponse: detail?.data,
645
- loanRenewResult: this.loanRenewResult,
646
- secondsLeftOnLoan: secondsLeft,
647
679
  });
648
680
 
649
681
  const currStatus = {
@@ -659,7 +691,11 @@ export default class IABookActions extends LitElement {
659
691
  this.modal.removeAttribute('id');
660
692
  this.modal.customModalContent = nothing;
661
693
  this.sentryCaptureMsg(sentryLogs.bookHasRenewed);
694
+ this.warningModalDismissed = false;
662
695
  }
696
+
697
+ this.warningModalOpen = false;
698
+ this.loanRenewInProgress = false;
663
699
  }
664
700
 
665
701
  /**
@@ -695,29 +731,20 @@ export default class IABookActions extends LitElement {
695
731
  // re-sync timer if gone off because of background window
696
732
  // side effect: updates this.lendingStatus & kicks off lifecycle,
697
733
  // if really updated, escape from here
698
- const resyncd = await this.reSyncTimerIfGoneOff(secondsLeft);
734
+ const resyncd = this.reSyncTimerIfGoneOff(secondsLeft);
699
735
 
700
736
  if (resyncd.hasSynced) {
701
737
  secondsLeft = resyncd.whatShouldLeft;
702
- log('startTimerCountdown --- stale, timer has resyncd', {
738
+ log('[IABookActions] timer: timer re-synced', {
703
739
  secondsLeft,
704
740
  });
705
741
  }
706
742
 
707
- log(
708
- 'startTimerCountdown --- countdown still valid. continue...',
709
- {
710
- resyncd,
711
- secondsLeft,
712
- loanRenewAtLast: this.loanRenewTimeConfig.loanRenewAtLast,
713
- },
714
- 'time: ',
715
- new Date().getMinutes(),
716
- ':',
717
- new Date().getSeconds(),
718
- ', timerDelay: ',
719
- this.timerExecutionSeconds
720
- );
743
+ log('[IABookActions] timer', {
744
+ secondsLeft,
745
+ loanRenewAtLast: this.loanRenewTimeConfig.loanRenewAtLast,
746
+ pageChangedInLast: this.loanRenewTimeConfig.pageChangedInLast,
747
+ });
721
748
 
722
749
  /**
723
750
  * execute from last 10th minutes to 0th minute
@@ -742,7 +769,7 @@ export default class IABookActions extends LitElement {
742
769
  * @param {number} timerSecondsLeft - actual seconds left get from setInterval
743
770
  * @returns {Object} { hasSynced: [boolean], whatShouldLeft: [number] }
744
771
  */
745
- async reSyncTimerIfGoneOff(timerSecondsLeft) {
772
+ reSyncTimerIfGoneOff(timerSecondsLeft) {
746
773
  const currentTime = new Date();
747
774
 
748
775
  // current time - loan time
@@ -752,27 +779,11 @@ export default class IABookActions extends LitElement {
752
779
  const secondsShouldLeft =
753
780
  this.lendingStatus.secondsLeftOnLoan - diffInSeconds;
754
781
 
755
- log('currentTime: ', currentTime);
756
- log('timeWhenTimerStart: ', this.timeWhenTimerStart);
757
- log('diffInSeconds: ', diffInSeconds);
758
- log('secondsShouldLeft: ', secondsShouldLeft);
759
- log(
760
- 'this.lendingStatus.secondsLeftOnLoan: ',
761
- this.lendingStatus.secondsLeftOnLoan
762
- );
763
-
764
782
  // convert in minutes
765
783
  const whatIsleft = Math.round(timerSecondsLeft);
766
784
  const whatShouldLeft = Math.round(secondsShouldLeft);
767
785
  const timerElSeconds = this.timerCountdownEl.secondsLeftOnLoan || 0;
768
786
 
769
- log(`reSyncTimerIfGoneOff?`, {
770
- whatIsleft,
771
- whatShouldLeft,
772
- timerElSeconds,
773
- timeLeftInMin: Math.ceil(timerSecondsLeft / 60),
774
- });
775
-
776
787
  if (timerElSeconds !== whatShouldLeft || whatIsleft !== whatShouldLeft) {
777
788
  // set lending status with new time to update decrementor
778
789
  const currStatus = {
@@ -783,7 +794,9 @@ export default class IABookActions extends LitElement {
783
794
  }
784
795
 
785
796
  if (whatIsleft !== whatShouldLeft) {
786
- log(`reSyncTimerIfGoneOff --- let's re-sync.`);
797
+ log(
798
+ `[IABookActions] reSyncTimerIfGoneOff ${whatIsleft} - ${whatShouldLeft}: re-syncing timer`
799
+ );
787
800
  return { hasSynced: true, whatShouldLeft };
788
801
  }
789
802
 
@@ -799,10 +812,6 @@ export default class IABookActions extends LitElement {
799
812
  * @memberof IABookActions
800
813
  */
801
814
  async loanRenewAttempt(secondsLeft) {
802
- log('loanRenewAttempt ---', {
803
- secondsLeft,
804
- loanRenewResult: this.loanRenewResult,
805
- });
806
815
  let loanSecondsLeft = secondsLeft;
807
816
  /**
808
817
  * auto-renew is not possible in last seconds (let say 50 second) because,
@@ -812,7 +821,7 @@ export default class IABookActions extends LitElement {
812
821
  * so if seconds left is < 50, just expire the loan
813
822
  */
814
823
  if (loanSecondsLeft < 50) {
815
- log('loanRenewAttempt --- loanSecondsLeft < 50, will expire');
824
+ log('[IABookActions] loanRenewAttempt: < 50s left, expiring loan');
816
825
  await this.browseHasExpired();
817
826
  return;
818
827
  }
@@ -820,7 +829,9 @@ export default class IABookActions extends LitElement {
820
829
  await this.autoLoanRenewChecker(false);
821
830
 
822
831
  // show warning modal with remaining time to auto returned it.
823
- if (this.loanRenewResult.renewNow === false) {
832
+ // once the patron has dismissed it, don't re-show it on every subsequent
833
+ // tick — only an actual renewal (see handleLoanAutoRenewed) re-arms it.
834
+ if (this.loanRenewResult.renewNow === false && !this.warningModalDismissed) {
824
835
  /**
825
836
  * so compensate for the 50 second buffer to handle above race conditions
826
837
  * let's reduce 1 min from warning texts and early return the book when 1 min left.
@@ -894,9 +905,20 @@ export default class IABookActions extends LitElement {
894
905
  const action = event?.detail?.action;
895
906
  const errorMsg = event?.detail?.data?.error;
896
907
 
897
- // template not show create_token errors
898
- if (errorMsg && action !== 'create_token')
908
+ if (action === 'renew_loan') {
909
+ this.loanRenewInProgress = false;
910
+ }
911
+
912
+ // For renew_loan failures after auto-renew, show the patron-facing message
913
+ if (
914
+ errorMsg &&
915
+ action === 'renew_loan' &&
916
+ errorMsg.match(/not available/)
917
+ ) {
918
+ this.showLoanUnavailableModal();
919
+ } else if (errorMsg && action !== 'create_token') {
899
920
  this.showErrorModal(errorMsg, action);
921
+ }
900
922
 
901
923
  // if error related to loan token
902
924
  // - set user_has_browsed to `false`