@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.
- package/index.html +416 -118
- package/package.json +2 -2
- 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 +24 -18
- 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 +243 -221
- package/test/core/services/actions-handler.test.js +110 -1
- package/test/core/services/loan-renew-helper.test.js +1 -1
- package/test/ia-book-actions.test.js +342 -7
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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 (
|
|
306
|
-
log(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
this.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
473
|
+
headline: 'Are you still there?',
|
|
384
474
|
headerColor: '#194880',
|
|
385
475
|
showCloseButton: false,
|
|
386
476
|
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;"
|
|
477
|
+
message: html`<span
|
|
478
|
+
style="display:inline-flex;align-items:center;flex-wrap:nowrap;gap:4px;"
|
|
397
479
|
>
|
|
398
|
-
<
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
>
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
),
|
|
491
|
+
${infoIcon}
|
|
492
|
+
</a>
|
|
493
|
+
</span>`,
|
|
435
494
|
});
|
|
436
495
|
|
|
437
|
-
const customModalContent = html
|
|
496
|
+
const customModalContent = html`
|
|
438
497
|
<div
|
|
439
|
-
id="
|
|
440
|
-
style="display:flex;justify-content:center;
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 });
|
|
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('
|
|
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
|
-
|
|
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 {
|
|
544
|
-
|
|
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
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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 =
|
|
734
|
+
const resyncd = this.reSyncTimerIfGoneOff(secondsLeft);
|
|
699
735
|
|
|
700
736
|
if (resyncd.hasSynced) {
|
|
701
737
|
secondsLeft = resyncd.whatShouldLeft;
|
|
702
|
-
log('
|
|
738
|
+
log('[IABookActions] timer: timer re-synced', {
|
|
703
739
|
secondsLeft,
|
|
704
740
|
});
|
|
705
741
|
}
|
|
706
742
|
|
|
707
|
-
log(
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
898
|
-
|
|
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`
|