@internetarchive/ia-book-actions 1.4.2 → 1.4.3-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.
@@ -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
 
@@ -208,32 +217,16 @@ export default class IABookActions extends LitElement {
208
217
  /** End SharedObserver resize handler */
209
218
 
210
219
  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
220
  const hasExpired =
233
221
  'browsingExpired' in this.lendingStatus &&
234
222
  this.lendingStatus?.browsingExpired;
235
223
  if (hasExpired) {
236
- log('setupLendingToolbarActions > hasExpired --- ');
224
+ // Intentionally do NOT recompute primaryActions/primaryColor/borrowType
225
+ // here — per the ticket, auto-return must not visibly change anything.
226
+ // The action bar stays exactly as it looked while reading; only a
227
+ // successful silent renewal or the "unavailable" error modal are
228
+ // allowed to change what's shown next.
229
+ log('[IABookActions] browsing expired — leaving action bar untouched');
237
230
 
238
231
  if (!this.tokenPoller) {
239
232
  this.sentryCaptureMsg(sentryLogs.bookWasExpired);
@@ -253,6 +246,27 @@ export default class IABookActions extends LitElement {
253
246
  return;
254
247
  }
255
248
 
249
+ this.lendingOptions = new GetLendingActions(
250
+ this.userid,
251
+ this.identifier,
252
+ this.lendingStatus,
253
+ this.bwbPurchaseUrl
254
+ );
255
+ const actions = this.lendingOptions.getCurrentLendingActions();
256
+
257
+ if (!actions) return;
258
+
259
+ this.primaryTitle = actions.primaryTitle;
260
+ this.primaryActions = actions.primaryActions?.filter(action => {
261
+ return action != null;
262
+ });
263
+ this.primaryColor = actions.primaryColor;
264
+ this.secondaryActions = actions.secondaryActions?.filter(action => {
265
+ return action != null;
266
+ });
267
+
268
+ this.borrowType = actions.borrowType ? actions.borrowType : null;
269
+
256
270
  if (this.borrowType === 'browsed') {
257
271
  // start timer for loan-renew
258
272
  await this.startTimerCountdown();
@@ -290,8 +304,17 @@ export default class IABookActions extends LitElement {
290
304
  * dispatched this event from bookreader page changed
291
305
  */
292
306
  window.addEventListener('BookReader:userAction', () => {
293
- log('IABookActions:BookReader:userAction');
294
- if (this.borrowType === 'browsed') {
307
+ log('[IABookActions] BookReader:userAction received', {
308
+ borrowType: this.borrowType,
309
+ browsingExpired: this.lendingStatus.browsingExpired,
310
+ });
311
+
312
+ // If the loan expired while the tab stayed visible, auto-renew on page turn
313
+ if (this.lendingStatus.browsingExpired) {
314
+ this.autoRenewExpiredLoan();
315
+ }
316
+
317
+ if (this.borrowType === 'browsed' && !this.lendingStatus.browsingExpired) {
295
318
  this.autoLoanRenewChecker(true);
296
319
  }
297
320
  });
@@ -302,18 +325,23 @@ export default class IABookActions extends LitElement {
302
325
  * the [visibilitychange] event can trigger timer to show relavent messages
303
326
  */
304
327
  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
- ) {
328
+ if (document.hidden) {
329
+ log('[IABookActions] visibilitychange: tab backgrounded');
330
+ return;
331
+ }
332
+
333
+ log('[IABookActions] visibilitychange: tab foregrounded', this.borrowType);
334
+
335
+ try {
336
+ // Loan already expired while tab was hidden — try to silently renew
337
+ if (this.lendingStatus.browsingExpired === true) {
338
+ this.autoRenewExpiredLoan();
339
+ return;
340
+ }
341
+
342
+ if (this.borrowType !== 'browsed') return;
343
+
344
+ if (this.lendingStatus.browsingExpired === false) {
317
345
  const loanTime = await this.localCache.get(
318
346
  `${this.identifier}-loanTime`
319
347
  );
@@ -324,10 +352,15 @@ export default class IABookActions extends LitElement {
324
352
  if (secondsLeft >= this.timerExecutionSeconds) {
325
353
  this.loanStatusCheckInterval(Number(secondsLeft));
326
354
  } else {
327
- this.browseHasExpired();
355
+ // Loan expired while user was away — try to silently renew
356
+ this.autoRenewExpiredLoan();
328
357
  this.disconnectedCallback();
329
358
  }
330
359
  }
360
+ } catch (error) {
361
+ // Surface localCache failures instead of an unhandled rejection.
362
+ log('[IABookActions] visibilitychange handler failed', error);
363
+ this.sentryCaptureMsg(`visibilitychange handler failed: ${error}`);
331
364
  }
332
365
  });
333
366
  }
@@ -350,6 +383,36 @@ export default class IABookActions extends LitElement {
350
383
  this.loanRenewResult = this.loanRenewHelper.result;
351
384
  }
352
385
 
386
+ /**
387
+ * Attempt to automatically renew a browse loan that has expired.
388
+ * Called on visibilitychange (user returns to tab) or on page turn when
389
+ * the loan has expired but the book may still be available.
390
+ *
391
+ * Reuses the same renew_loan path as the automatic loan-renew checker.
392
+ * On success: handleLoanAutoRenewed() resets the timer seamlessly.
393
+ * On error: handleLendingActionError() shows showLoanUnavailableModal().
394
+ */
395
+ autoRenewExpiredLoan() {
396
+ if (this.loanRenewInProgress) return;
397
+ this.loanRenewInProgress = true;
398
+
399
+ this.modal?.closeModal();
400
+
401
+ // Optimistically flip browsingExpired back to false so the action bar
402
+ // stays red (patronIsReadingAction) while the renew_loan request is in
403
+ // flight, instead of showing the blue "Borrow" state.
404
+ this.lendingStatus = {
405
+ ...this.lendingStatus,
406
+ browsingExpired: false,
407
+ secondsLeftOnLoan: this.loanRenewTimeConfig.loanTotalTime,
408
+ };
409
+
410
+ // Defer renewNow until after lendingStatus timer setup completes.
411
+ setTimeout(() => {
412
+ this.loanRenewResult = { texts: '', renewNow: true, renewType: 'auto' };
413
+ }, 0);
414
+ }
415
+
353
416
  /**
354
417
  * Required as sibling on page
355
418
  * @returns HTMLElement
@@ -362,158 +425,86 @@ export default class IABookActions extends LitElement {
362
425
  }
363
426
 
364
427
  /**
365
- * Show the waring modal to ask user if they are still reading
428
+ * Show the informational modal warning the patron their loan will expire
429
+ * soon. This is acknowledgement-only — closing it does not renew the loan.
430
+ * Only interacting with the book itself (e.g. turning a page) renews it.
366
431
  */
367
432
  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) {
433
+ if (this.warningModalOpen) return;
434
+ this.warningModalOpen = true;
435
+
436
+ log('[IABookActions] showWarningModal');
437
+
438
+ // Capture texts and secondsLeft before resetting loanRenewResult
439
+ const {
440
+ texts: warningTexts,
441
+ secondsLeft: rawSecondsLeft,
442
+ } = this.loanRenewResult;
443
+ let secondsLeft = rawSecondsLeft;
444
+ if (secondsLeft === undefined || secondsLeft <= 0) {
377
445
  secondsLeft = this.lendingStatus.secondsLeftOnLoan;
378
446
  } else {
379
447
  secondsLeft = secondsLeft > 60 ? secondsLeft : 60;
380
448
  }
381
449
 
450
+ // clear modal and reset renew state
451
+ this.modal.customModalContent = nothing;
452
+ this.modal?.closeModal();
453
+ this.loanRenewResult = { texts: '', renewNow: false };
454
+
382
455
  const config = new ModalConfig({
383
- headline: 'Are you still reading?',
456
+ headline: 'Are you still there?',
384
457
  headerColor: '#194880',
385
458
  showCloseButton: false,
386
459
  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;"
460
+ message: html`<span
461
+ style="display:inline-flex;align-items:center;flex-wrap:nowrap;gap:4px;"
397
462
  >
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()}
463
+ <span>${this.loanRenewHelper?.getMessageTexts(
464
+ warningTexts,
465
+ secondsLeft
466
+ )}</span>
467
+ <a
468
+ href="https://help.archive.org/help/borrowing-from-the-lending-library"
469
+ target="_blank"
470
+ title="Get more info on borrowing from The Lending Library"
471
+ data-event-click-tracking="BookReader|BrowsableMoreInfo"
472
+ style="display:inline-flex;flex-shrink:0;line-height:0;"
407
473
  >
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
- ),
474
+ ${infoIcon}
475
+ </a>
476
+ </span>`,
435
477
  });
436
478
 
437
- const customModalContent = html`<br />
479
+ const customModalContent = html`
438
480
  <div
439
- id="disabled-book-action-bar-custom-buttons"
440
- style="display:flex;justify-content:center; opacity:0.8; pointer-events:none;"
481
+ id="book-action-bar-custom-buttons"
482
+ style="display:flex;flex-direction:column;justify-content:center;align-items:center;gap:8px;margin-top:10px;"
441
483
  >
442
484
  <button
443
- disabled
444
485
  style="${modalButtonStyle.iaButton} ${modalButtonStyle.renew}"
486
+ @click=${() => this.dismissWarningModal()}
445
487
  >
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'}
488
+ Okay
467
489
  </button>
468
490
  </div> `;
469
491
 
492
+ this.modal.setAttribute('aria-live', 'assertive');
470
493
  await this.modal?.showModal({ config, customModalContent });
471
494
  }
472
495
 
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 });
496
+ /** Acknowledges the warning modal — closes it without renewing the loan */
497
+ dismissWarningModal() {
498
+ this.modal?.closeModal();
499
+ this.warningModalOpen = false;
500
+ this.warningModalDismissed = true;
510
501
  }
511
502
 
512
503
  /**
513
504
  * Execute when loan is expired
514
505
  */
515
506
  async browseHasExpired() {
516
- log('BrowseHasExpired ---');
507
+ log('[IABookActions] browseHasExpired');
517
508
  window?.IALendingIntervals?.clearAll();
518
509
 
519
510
  const currStatus = {
@@ -532,31 +523,52 @@ export default class IABookActions extends LitElement {
532
523
  this.loanRenewResult.texts =
533
524
  'This book has been returned due to inactivity.';
534
525
 
535
- await this.showExpiredModal();
526
+ this.modal?.closeModal();
536
527
 
537
528
  this.sentryCaptureMsg(sentryLogs.browseHasExpired);
538
529
  }
539
530
 
531
+ /**
532
+ * Show modal when the book can no longer be automatically renewed
533
+ * because another patron has checked it out.
534
+ */
535
+ async showLoanUnavailableModal() {
536
+ const config = new ModalConfig({
537
+ headline: '',
538
+ showCloseButton: false,
539
+ closeOnBackdropClick: false,
540
+ headerColor: '#194880',
541
+ message:
542
+ 'Due to inactivity, this book was returned, and someone else has now borrowed it. Please try again later.',
543
+ });
544
+
545
+ const customModalContent = html`<br />
546
+ <div style="text-align: center">
547
+ <button
548
+ style="${modalButtonStyle.iaButton} ${modalButtonStyle.renew}"
549
+ @click=${() => URLHelper.goToUrl(this.returnUrl, true)}
550
+ >
551
+ Okay
552
+ </button>
553
+ </div>`;
554
+
555
+ await this.modal?.showModal({ config, customModalContent });
556
+ }
557
+
540
558
  async startBrowseTimer() {
541
559
  window?.IALendingIntervals?.clearBrowseExpireTimeout();
542
560
 
543
- const { browsingExpired, user_has_browsed, secondsLeftOnLoan } =
544
- this.lendingStatus;
561
+ const {
562
+ browsingExpired,
563
+ user_has_browsed,
564
+ secondsLeftOnLoan,
565
+ } = this.lendingStatus;
545
566
 
546
567
  if (!user_has_browsed || browsingExpired) {
547
- log('startBrowseTimer --- !user_has_browsed || browsingExpired', {
548
- user_has_browsed,
549
- browsingExpired,
550
- secondsLeftOnLoan,
551
- });
552
568
  return;
553
569
  }
554
570
 
555
571
  window.IALendingIntervals.browseExpireTimeout = setTimeout(() => {
556
- log(
557
- 'startBrowseTimer > browseExpireTimeout --- will expire loan',
558
- secondsLeftOnLoan
559
- );
560
572
  this.browseHasExpired();
561
573
  }, secondsLeftOnLoan * 1000);
562
574
  }
@@ -602,7 +614,6 @@ export default class IABookActions extends LitElement {
602
614
  ?disabled=${this.disableActionGroup}
603
615
  ?autoRenew=${this.loanRenewResult.renewNow}
604
616
  ?autoReturn=${this.lendingStatus.browsingExpired}
605
- ?returnNow=${this.returnNow}
606
617
  @loanAutoRenewed=${this.handleLoanAutoRenewed}
607
618
  @lendingActionError=${this.handleLendingActionError}
608
619
  @toggleActionGroup=${this.handleToggleActionGroup}
@@ -637,13 +648,17 @@ export default class IABookActions extends LitElement {
637
648
  const loanTime = await this.localCache.get(`${this.identifier}-loanTime`);
638
649
 
639
650
  // 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 --- ', {
651
+ const rawSecondsLeft = Math.round((loanTime - new Date()) / 1000);
652
+ // Guard against a stale/missing loanTime read producing NaN, which
653
+ // would get the loan stuck "active" with a countdown that never moves.
654
+ const secondsLeft =
655
+ Number.isFinite(rawSecondsLeft) && rawSecondsLeft > 0
656
+ ? rawSecondsLeft
657
+ : this.loanRenewTimeConfig.loanTotalTime;
658
+ log('[IABookActions] handleLoanAutoRenewed', {
659
+ secondsLeft,
660
+ rawSecondsLeft,
644
661
  ajaxResponse: detail?.data,
645
- loanRenewResult: this.loanRenewResult,
646
- secondsLeftOnLoan: secondsLeft,
647
662
  });
648
663
 
649
664
  const currStatus = {
@@ -659,7 +674,11 @@ export default class IABookActions extends LitElement {
659
674
  this.modal.removeAttribute('id');
660
675
  this.modal.customModalContent = nothing;
661
676
  this.sentryCaptureMsg(sentryLogs.bookHasRenewed);
677
+ this.warningModalDismissed = false;
662
678
  }
679
+
680
+ this.warningModalOpen = false;
681
+ this.loanRenewInProgress = false;
663
682
  }
664
683
 
665
684
  /**
@@ -695,29 +714,19 @@ export default class IABookActions extends LitElement {
695
714
  // re-sync timer if gone off because of background window
696
715
  // side effect: updates this.lendingStatus & kicks off lifecycle,
697
716
  // if really updated, escape from here
698
- const resyncd = await this.reSyncTimerIfGoneOff(secondsLeft);
717
+ const resyncd = this.reSyncTimerIfGoneOff(secondsLeft);
699
718
 
700
719
  if (resyncd.hasSynced) {
701
720
  secondsLeft = resyncd.whatShouldLeft;
702
- log('startTimerCountdown --- stale, timer has resyncd', {
721
+ log('[IABookActions] startTimerCountdown: timer re-synced', {
703
722
  secondsLeft,
704
723
  });
705
724
  }
706
725
 
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
- );
726
+ log('[IABookActions] startTimerCountdown', {
727
+ secondsLeft,
728
+ loanRenewAtLast: this.loanRenewTimeConfig.loanRenewAtLast,
729
+ });
721
730
 
722
731
  /**
723
732
  * execute from last 10th minutes to 0th minute
@@ -742,7 +751,7 @@ export default class IABookActions extends LitElement {
742
751
  * @param {number} timerSecondsLeft - actual seconds left get from setInterval
743
752
  * @returns {Object} { hasSynced: [boolean], whatShouldLeft: [number] }
744
753
  */
745
- async reSyncTimerIfGoneOff(timerSecondsLeft) {
754
+ reSyncTimerIfGoneOff(timerSecondsLeft) {
746
755
  const currentTime = new Date();
747
756
 
748
757
  // current time - loan time
@@ -752,27 +761,11 @@ export default class IABookActions extends LitElement {
752
761
  const secondsShouldLeft =
753
762
  this.lendingStatus.secondsLeftOnLoan - diffInSeconds;
754
763
 
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
764
  // convert in minutes
765
765
  const whatIsleft = Math.round(timerSecondsLeft);
766
766
  const whatShouldLeft = Math.round(secondsShouldLeft);
767
767
  const timerElSeconds = this.timerCountdownEl.secondsLeftOnLoan || 0;
768
768
 
769
- log(`reSyncTimerIfGoneOff?`, {
770
- whatIsleft,
771
- whatShouldLeft,
772
- timerElSeconds,
773
- timeLeftInMin: Math.ceil(timerSecondsLeft / 60),
774
- });
775
-
776
769
  if (timerElSeconds !== whatShouldLeft || whatIsleft !== whatShouldLeft) {
777
770
  // set lending status with new time to update decrementor
778
771
  const currStatus = {
@@ -783,7 +776,9 @@ export default class IABookActions extends LitElement {
783
776
  }
784
777
 
785
778
  if (whatIsleft !== whatShouldLeft) {
786
- log(`reSyncTimerIfGoneOff --- let's re-sync.`);
779
+ log(
780
+ `[IABookActions] reSyncTimerIfGoneOff ${whatIsleft} - ${whatShouldLeft}: re-syncing timer`
781
+ );
787
782
  return { hasSynced: true, whatShouldLeft };
788
783
  }
789
784
 
@@ -799,10 +794,6 @@ export default class IABookActions extends LitElement {
799
794
  * @memberof IABookActions
800
795
  */
801
796
  async loanRenewAttempt(secondsLeft) {
802
- log('loanRenewAttempt ---', {
803
- secondsLeft,
804
- loanRenewResult: this.loanRenewResult,
805
- });
806
797
  let loanSecondsLeft = secondsLeft;
807
798
  /**
808
799
  * auto-renew is not possible in last seconds (let say 50 second) because,
@@ -812,7 +803,7 @@ export default class IABookActions extends LitElement {
812
803
  * so if seconds left is < 50, just expire the loan
813
804
  */
814
805
  if (loanSecondsLeft < 50) {
815
- log('loanRenewAttempt --- loanSecondsLeft < 50, will expire');
806
+ log('[IABookActions] loanRenewAttempt: < 50s left, expiring loan');
816
807
  await this.browseHasExpired();
817
808
  return;
818
809
  }
@@ -820,7 +811,9 @@ export default class IABookActions extends LitElement {
820
811
  await this.autoLoanRenewChecker(false);
821
812
 
822
813
  // show warning modal with remaining time to auto returned it.
823
- if (this.loanRenewResult.renewNow === false) {
814
+ // once the patron has dismissed it, don't re-show it on every subsequent
815
+ // tick — only an actual renewal (see handleLoanAutoRenewed) re-arms it.
816
+ if (this.loanRenewResult.renewNow === false && !this.warningModalDismissed) {
824
817
  /**
825
818
  * so compensate for the 50 second buffer to handle above race conditions
826
819
  * let's reduce 1 min from warning texts and early return the book when 1 min left.
@@ -894,9 +887,17 @@ export default class IABookActions extends LitElement {
894
887
  const action = event?.detail?.action;
895
888
  const errorMsg = event?.detail?.data?.error;
896
889
 
897
- // template not show create_token errors
898
- if (errorMsg && action !== 'create_token')
890
+ // For renew_loan failures after auto-renew, show the patron-facing message
891
+ if (
892
+ errorMsg &&
893
+ action === 'renew_loan' &&
894
+ errorMsg.match(/not available/)
895
+ ) {
896
+ this.loanRenewInProgress = false;
897
+ this.showLoanUnavailableModal();
898
+ } else if (errorMsg && action !== 'create_token') {
899
899
  this.showErrorModal(errorMsg, action);
900
+ }
900
901
 
901
902
  // if error related to loan token
902
903
  // - set user_has_browsed to `false`
@@ -80,7 +80,7 @@ describe('Loan Renew Determine', () => {
80
80
  );
81
81
 
82
82
  expect(toastMsg).to.be.equal(
83
- 'With no action, this book will be auto-returned in 2 minutes.'
83
+ 'Go to any other page to keep your loan active.'
84
84
  );
85
85
  });
86
86
  });