@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.
- package/index.html +397 -118
- package/package.json +2 -3
- package/src/assets/data/info.js +12 -0
- package/src/core/services/actions-handler/actions-handler-service.js +5 -1
- package/src/core/services/actions-handler/actions-handler.js +4 -3
- package/src/core/services/loan-analytics.js +1 -1
- package/src/core/services/loan-renew-helper.js +2 -3
- package/src/ia-book-actions.js +222 -221
- package/test/core/services/loan-renew-helper.test.js +1 -1
- package/test/ia-book-actions.test.js +221 -7
- package/.claude/settings.local.json +0 -16
package/src/ia-book-actions.js
CHANGED
|
@@ -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
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
|
294
|
-
|
|
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 (
|
|
306
|
-
log(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
this.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
456
|
+
headline: 'Are you still there?',
|
|
384
457
|
headerColor: '#194880',
|
|
385
458
|
showCloseButton: false,
|
|
386
459
|
closeOnBackdropClick: false,
|
|
387
|
-
message:
|
|
388
|
-
|
|
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
|
-
<
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
>
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
409
|
-
</
|
|
410
|
-
</
|
|
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
|
|
479
|
+
const customModalContent = html`
|
|
438
480
|
<div
|
|
439
|
-
id="
|
|
440
|
-
style="display:flex;justify-content:center;
|
|
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
|
-
|
|
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
|
-
/**
|
|
474
|
-
|
|
475
|
-
this.
|
|
476
|
-
this.
|
|
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('
|
|
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
|
-
|
|
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 {
|
|
544
|
-
|
|
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
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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 =
|
|
717
|
+
const resyncd = this.reSyncTimerIfGoneOff(secondsLeft);
|
|
699
718
|
|
|
700
719
|
if (resyncd.hasSynced) {
|
|
701
720
|
secondsLeft = resyncd.whatShouldLeft;
|
|
702
|
-
log('startTimerCountdown
|
|
721
|
+
log('[IABookActions] startTimerCountdown: timer re-synced', {
|
|
703
722
|
secondsLeft,
|
|
704
723
|
});
|
|
705
724
|
}
|
|
706
725
|
|
|
707
|
-
log(
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
898
|
-
if (
|
|
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`
|