@internetarchive/ia-book-actions 1.4.3 → 1.4.4-alpha10
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 +25 -19
- package/src/core/services/loan-analytics.js +1 -1
- package/src/core/services/loan-renew-helper.js +2 -3
- package/src/core/services/loan-token-poller.js +63 -16
- package/src/ia-book-actions.js +337 -247
- package/test/core/services/actions-handler.test.js +110 -1
- package/test/core/services/loan-renew-helper.test.js +1 -1
- package/test/core/services/loan-token-poller.test.js +76 -0
- package/test/ia-book-actions.test.js +716 -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,28 @@ export default class IABookActions extends LitElement {
|
|
|
93
94
|
this.browseTimer = undefined; // timeout
|
|
94
95
|
this.timeWhenTimerStart = undefined;
|
|
95
96
|
|
|
97
|
+
this.loanRenewInProgress = false;
|
|
98
|
+
|
|
96
99
|
/**
|
|
97
|
-
*
|
|
100
|
+
* True while a renewal in flight is recovering from a loan that
|
|
101
|
+
* genuinely lapsed (set by autoRenewExpiredLoan), as opposed to a
|
|
102
|
+
* routine pre-expiry top-up. Only the recovery case needs BookReader
|
|
103
|
+
* re-initialized once the renewal is confirmed — see
|
|
104
|
+
* handleLoanAutoRenewed.
|
|
98
105
|
*/
|
|
99
|
-
this.
|
|
106
|
+
this.recoveringFromLoanExpiry = false;
|
|
107
|
+
|
|
108
|
+
this.warningModalOpen = false;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Once the patron has seen & dismissed the informational warning modal,
|
|
112
|
+
* don't nag them again with the same modal every timer tick — only a
|
|
113
|
+
* real renewal (via interacting with the book) clears this, giving the
|
|
114
|
+
* next pre-expiry window its own single warning.
|
|
115
|
+
* @see loanRenewAttempt
|
|
116
|
+
* @see handleLoanAutoRenewed
|
|
117
|
+
*/
|
|
118
|
+
this.warningModalDismissed = false;
|
|
100
119
|
|
|
101
120
|
/**
|
|
102
121
|
* contains one hour auto-loan-renew time configuration
|
|
@@ -141,7 +160,6 @@ export default class IABookActions extends LitElement {
|
|
|
141
160
|
* @param {string} msg
|
|
142
161
|
*/
|
|
143
162
|
sentryCaptureMsg(msg) {
|
|
144
|
-
log(window?.Sentry);
|
|
145
163
|
window?.Sentry?.captureMessage(msg);
|
|
146
164
|
}
|
|
147
165
|
|
|
@@ -171,6 +189,13 @@ export default class IABookActions extends LitElement {
|
|
|
171
189
|
}
|
|
172
190
|
|
|
173
191
|
if (changed.has('loanRenewResult') && this.loanRenewResult.renewNow) {
|
|
192
|
+
// Any renewal — not just the auto-return recovery path — pauses the
|
|
193
|
+
// countdown for the renew_loan round-trip. Flag it here, centrally,
|
|
194
|
+
// so every path (interaction during active reading, periodic
|
|
195
|
+
// checker, auto-return recovery) gets the same "renewing" signal
|
|
196
|
+
// instead of just looking frozen until handleLoanAutoRenewed() clears
|
|
197
|
+
// this and restarts the timer.
|
|
198
|
+
this.loanRenewInProgress = true;
|
|
174
199
|
window.IALendingIntervals.clearAll();
|
|
175
200
|
}
|
|
176
201
|
}
|
|
@@ -208,32 +233,16 @@ export default class IABookActions extends LitElement {
|
|
|
208
233
|
/** End SharedObserver resize handler */
|
|
209
234
|
|
|
210
235
|
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
236
|
const hasExpired =
|
|
233
237
|
'browsingExpired' in this.lendingStatus &&
|
|
234
238
|
this.lendingStatus?.browsingExpired;
|
|
235
239
|
if (hasExpired) {
|
|
236
|
-
|
|
240
|
+
// Intentionally do NOT recompute primaryActions/primaryColor/borrowType
|
|
241
|
+
// here — per the ticket, auto-return must not visibly change anything.
|
|
242
|
+
// The action bar stays exactly as it looked while reading; only a
|
|
243
|
+
// successful silent renewal or the "unavailable" error modal are
|
|
244
|
+
// allowed to change what's shown next.
|
|
245
|
+
log('[IABookActions] browsing expired — leaving action bar untouched');
|
|
237
246
|
|
|
238
247
|
if (!this.tokenPoller) {
|
|
239
248
|
this.sentryCaptureMsg(sentryLogs.bookWasExpired);
|
|
@@ -253,6 +262,27 @@ export default class IABookActions extends LitElement {
|
|
|
253
262
|
return;
|
|
254
263
|
}
|
|
255
264
|
|
|
265
|
+
this.lendingOptions = new GetLendingActions(
|
|
266
|
+
this.userid,
|
|
267
|
+
this.identifier,
|
|
268
|
+
this.lendingStatus,
|
|
269
|
+
this.bwbPurchaseUrl
|
|
270
|
+
);
|
|
271
|
+
const actions = this.lendingOptions.getCurrentLendingActions();
|
|
272
|
+
|
|
273
|
+
if (!actions) return;
|
|
274
|
+
|
|
275
|
+
this.primaryTitle = actions.primaryTitle;
|
|
276
|
+
this.primaryActions = actions.primaryActions?.filter(action => {
|
|
277
|
+
return action != null;
|
|
278
|
+
});
|
|
279
|
+
this.primaryColor = actions.primaryColor;
|
|
280
|
+
this.secondaryActions = actions.secondaryActions?.filter(action => {
|
|
281
|
+
return action != null;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
this.borrowType = actions.borrowType ? actions.borrowType : null;
|
|
285
|
+
|
|
256
286
|
if (this.borrowType === 'browsed') {
|
|
257
287
|
// start timer for loan-renew
|
|
258
288
|
await this.startTimerCountdown();
|
|
@@ -269,11 +299,22 @@ export default class IABookActions extends LitElement {
|
|
|
269
299
|
|
|
270
300
|
/**
|
|
271
301
|
* tokenPoller determines if user has loan token for this book
|
|
272
|
-
* - if book is going to renew, need to wait until renew is completed
|
|
302
|
+
* - if book is going to renew, need to wait until renew is completed:
|
|
303
|
+
* loanRenewInProgress is set before renew_loan is even dispatched
|
|
304
|
+
* (including the optimistic pre-confirmation window) and only
|
|
305
|
+
* clears once the renewal is confirmed, so starting the poller here
|
|
306
|
+
* mid-renewal would call create_token against the loan record
|
|
307
|
+
* before it's actually renewed.
|
|
273
308
|
*/
|
|
274
309
|
setTimeout(() => {
|
|
275
|
-
if (
|
|
310
|
+
if (
|
|
311
|
+
!hasExpired &&
|
|
312
|
+
!this.loanRenewInProgress &&
|
|
313
|
+
!window.IALendingIntervals.tokenPoller
|
|
314
|
+
) {
|
|
315
|
+
this.recoveringFromLoanExpiry = false;
|
|
276
316
|
this.startLoanTokenPoller();
|
|
317
|
+
}
|
|
277
318
|
}, 100);
|
|
278
319
|
|
|
279
320
|
this.requestUpdate();
|
|
@@ -290,8 +331,27 @@ export default class IABookActions extends LitElement {
|
|
|
290
331
|
* dispatched this event from bookreader page changed
|
|
291
332
|
*/
|
|
292
333
|
window.addEventListener('BookReader:userAction', () => {
|
|
293
|
-
|
|
294
|
-
|
|
334
|
+
// Capture before autoRenewExpiredLoan() runs — it synchronously flips
|
|
335
|
+
// lendingStatus.browsingExpired to false as an optimistic UI update,
|
|
336
|
+
// so checking the live property afterward would always see it as
|
|
337
|
+
// false and wrongly let autoLoanRenewChecker() run too. That second
|
|
338
|
+
// call would then overwrite loanRenewResult.renewNow (set true by
|
|
339
|
+
// autoRenewExpiredLoan) back to false, since the loanTime cache entry
|
|
340
|
+
// was already deleted by browseHasExpired() — silently killing the
|
|
341
|
+
// in-flight renewal and leaving the countdown stuck.
|
|
342
|
+
const wasExpired = this.lendingStatus.browsingExpired;
|
|
343
|
+
|
|
344
|
+
log('[IABookActions] BookReader:userAction received', {
|
|
345
|
+
borrowType: this.borrowType,
|
|
346
|
+
browsingExpired: wasExpired,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// If the loan expired while the tab stayed visible, auto-renew on page turn
|
|
350
|
+
if (wasExpired) {
|
|
351
|
+
this.autoRenewExpiredLoan();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (this.borrowType === 'browsed' && !wasExpired) {
|
|
295
355
|
this.autoLoanRenewChecker(true);
|
|
296
356
|
}
|
|
297
357
|
});
|
|
@@ -302,18 +362,23 @@ export default class IABookActions extends LitElement {
|
|
|
302
362
|
* the [visibilitychange] event can trigger timer to show relavent messages
|
|
303
363
|
*/
|
|
304
364
|
document.addEventListener('visibilitychange', async () => {
|
|
305
|
-
if (
|
|
306
|
-
log(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
this.
|
|
316
|
-
|
|
365
|
+
if (document.hidden) {
|
|
366
|
+
log('[IABookActions] visibilitychange: tab backgrounded');
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
log('[IABookActions] visibilitychange: tab foregrounded', this.borrowType);
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
// Loan already expired while tab was hidden — try to silently renew
|
|
374
|
+
if (this.lendingStatus.browsingExpired === true) {
|
|
375
|
+
this.autoRenewExpiredLoan();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (this.borrowType !== 'browsed') return;
|
|
380
|
+
|
|
381
|
+
if (this.lendingStatus.browsingExpired === false) {
|
|
317
382
|
const loanTime = await this.localCache.get(
|
|
318
383
|
`${this.identifier}-loanTime`
|
|
319
384
|
);
|
|
@@ -324,10 +389,15 @@ export default class IABookActions extends LitElement {
|
|
|
324
389
|
if (secondsLeft >= this.timerExecutionSeconds) {
|
|
325
390
|
this.loanStatusCheckInterval(Number(secondsLeft));
|
|
326
391
|
} else {
|
|
327
|
-
|
|
392
|
+
// Loan expired while user was away — try to silently renew
|
|
393
|
+
this.autoRenewExpiredLoan();
|
|
328
394
|
this.disconnectedCallback();
|
|
329
395
|
}
|
|
330
396
|
}
|
|
397
|
+
} catch (error) {
|
|
398
|
+
// Surface localCache failures instead of an unhandled rejection.
|
|
399
|
+
log('[IABookActions] visibilitychange handler failed', error);
|
|
400
|
+
this.sentryCaptureMsg(`visibilitychange handler failed: ${error}`);
|
|
331
401
|
}
|
|
332
402
|
});
|
|
333
403
|
}
|
|
@@ -339,6 +409,16 @@ export default class IABookActions extends LitElement {
|
|
|
339
409
|
* @param {Boolean} hasPageChanged
|
|
340
410
|
*/
|
|
341
411
|
async autoLoanRenewChecker(hasPageChanged = false) {
|
|
412
|
+
// Guards against re-entrancy the same way autoRenewExpiredLoan() does.
|
|
413
|
+
// BookReader:userAction can fire several times in quick succession
|
|
414
|
+
// (e.g. a single scroll gesture), and this method has no debouncing of
|
|
415
|
+
// its own — without this guard, each event would spin up its own
|
|
416
|
+
// LoanRenewHelper concurrently, and whichever's async localCache reads
|
|
417
|
+
// resolve last would clobber this.loanRenewResult, potentially
|
|
418
|
+
// re-triggering the whole renew_loan/create_token flow while a
|
|
419
|
+
// renewal from an earlier event is already in flight or just landed.
|
|
420
|
+
if (this.loanRenewInProgress) return;
|
|
421
|
+
|
|
342
422
|
this.loanRenewHelper = new LoanRenewHelper(
|
|
343
423
|
hasPageChanged,
|
|
344
424
|
this.identifier,
|
|
@@ -350,6 +430,37 @@ export default class IABookActions extends LitElement {
|
|
|
350
430
|
this.loanRenewResult = this.loanRenewHelper.result;
|
|
351
431
|
}
|
|
352
432
|
|
|
433
|
+
/**
|
|
434
|
+
* Attempt to automatically renew a browse loan that has expired.
|
|
435
|
+
* Called on visibilitychange (user returns to tab) or on page turn when
|
|
436
|
+
* the loan has expired but the book may still be available.
|
|
437
|
+
*
|
|
438
|
+
* Reuses the same renew_loan path as the automatic loan-renew checker.
|
|
439
|
+
* On success: handleLoanAutoRenewed() resets the timer seamlessly.
|
|
440
|
+
* On error: handleLendingActionError() shows showLoanUnavailableModal().
|
|
441
|
+
*/
|
|
442
|
+
autoRenewExpiredLoan() {
|
|
443
|
+
if (this.loanRenewInProgress) return;
|
|
444
|
+
this.loanRenewInProgress = true;
|
|
445
|
+
this.recoveringFromLoanExpiry = true;
|
|
446
|
+
|
|
447
|
+
this.modal?.closeModal();
|
|
448
|
+
|
|
449
|
+
// Optimistically flip browsingExpired back to false so the action bar
|
|
450
|
+
// stays red (patronIsReadingAction) while the renew_loan request is in
|
|
451
|
+
// flight, instead of showing the blue "Borrow" state.
|
|
452
|
+
this.lendingStatus = {
|
|
453
|
+
...this.lendingStatus,
|
|
454
|
+
browsingExpired: false,
|
|
455
|
+
secondsLeftOnLoan: this.loanRenewTimeConfig.loanTotalTime,
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// Defer renewNow until after lendingStatus timer setup completes.
|
|
459
|
+
setTimeout(() => {
|
|
460
|
+
this.loanRenewResult = { texts: '', renewNow: true, renewType: 'auto' };
|
|
461
|
+
}, 0);
|
|
462
|
+
}
|
|
463
|
+
|
|
353
464
|
/**
|
|
354
465
|
* Required as sibling on page
|
|
355
466
|
* @returns HTMLElement
|
|
@@ -362,158 +473,86 @@ export default class IABookActions extends LitElement {
|
|
|
362
473
|
}
|
|
363
474
|
|
|
364
475
|
/**
|
|
365
|
-
* Show the
|
|
476
|
+
* Show the informational modal warning the patron their loan will expire
|
|
477
|
+
* soon. This is acknowledgement-only — closing it does not renew the loan.
|
|
478
|
+
* Only interacting with the book itself (e.g. turning a page) renews it.
|
|
366
479
|
*/
|
|
367
480
|
async showWarningModal() {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
481
|
+
if (this.warningModalOpen) return;
|
|
482
|
+
this.warningModalOpen = true;
|
|
483
|
+
|
|
484
|
+
log('[IABookActions] showWarningModal');
|
|
485
|
+
|
|
486
|
+
// Capture texts and secondsLeft before resetting loanRenewResult
|
|
487
|
+
const {
|
|
488
|
+
texts: warningTexts,
|
|
489
|
+
secondsLeft: rawSecondsLeft,
|
|
490
|
+
} = this.loanRenewResult;
|
|
491
|
+
let secondsLeft = rawSecondsLeft;
|
|
492
|
+
if (secondsLeft === undefined || secondsLeft <= 0) {
|
|
377
493
|
secondsLeft = this.lendingStatus.secondsLeftOnLoan;
|
|
378
494
|
} else {
|
|
379
495
|
secondsLeft = secondsLeft > 60 ? secondsLeft : 60;
|
|
380
496
|
}
|
|
381
497
|
|
|
498
|
+
// clear modal and reset renew state
|
|
499
|
+
this.modal.customModalContent = nothing;
|
|
500
|
+
this.modal?.closeModal();
|
|
501
|
+
this.loanRenewResult = { texts: '', renewNow: false };
|
|
502
|
+
|
|
382
503
|
const config = new ModalConfig({
|
|
383
|
-
headline: 'Are you still
|
|
504
|
+
headline: 'Are you still there?',
|
|
384
505
|
headerColor: '#194880',
|
|
385
506
|
showCloseButton: false,
|
|
386
507
|
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;"
|
|
508
|
+
message: html`<span
|
|
509
|
+
style="display:inline-flex;align-items:center;flex-wrap:nowrap;gap:4px;"
|
|
397
510
|
>
|
|
398
|
-
<
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
>
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
511
|
+
<span>${this.loanRenewHelper?.getMessageTexts(
|
|
512
|
+
warningTexts,
|
|
513
|
+
secondsLeft
|
|
514
|
+
)}</span>
|
|
515
|
+
<a
|
|
516
|
+
href="https://help.archive.org/help/borrowing-from-the-lending-library"
|
|
517
|
+
target="_blank"
|
|
518
|
+
title="Get more info on borrowing from The Lending Library"
|
|
519
|
+
data-event-click-tracking="BookReader|BrowsableMoreInfo"
|
|
520
|
+
style="display:inline-flex;flex-shrink:0;line-height:0;"
|
|
407
521
|
>
|
|
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
|
-
),
|
|
522
|
+
${infoIcon}
|
|
523
|
+
</a>
|
|
524
|
+
</span>`,
|
|
435
525
|
});
|
|
436
526
|
|
|
437
|
-
const customModalContent = html
|
|
527
|
+
const customModalContent = html`
|
|
438
528
|
<div
|
|
439
|
-
id="
|
|
440
|
-
style="display:flex;justify-content:center;
|
|
529
|
+
id="book-action-bar-custom-buttons"
|
|
530
|
+
style="display:flex;flex-direction:column;justify-content:center;align-items:center;gap:8px;margin-top:10px;"
|
|
441
531
|
>
|
|
442
532
|
<button
|
|
443
|
-
disabled
|
|
444
533
|
style="${modalButtonStyle.iaButton} ${modalButtonStyle.renew}"
|
|
534
|
+
@click=${() => this.dismissWarningModal()}
|
|
445
535
|
>
|
|
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'}
|
|
536
|
+
Okay
|
|
467
537
|
</button>
|
|
468
538
|
</div> `;
|
|
469
539
|
|
|
540
|
+
this.modal.setAttribute('aria-live', 'assertive');
|
|
470
541
|
await this.modal?.showModal({ config, customModalContent });
|
|
471
542
|
}
|
|
472
543
|
|
|
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 });
|
|
544
|
+
/** Acknowledges the warning modal — closes it without renewing the loan */
|
|
545
|
+
dismissWarningModal() {
|
|
546
|
+
this.modal?.closeModal();
|
|
547
|
+
this.warningModalOpen = false;
|
|
548
|
+
this.warningModalDismissed = true;
|
|
510
549
|
}
|
|
511
550
|
|
|
512
551
|
/**
|
|
513
552
|
* Execute when loan is expired
|
|
514
553
|
*/
|
|
515
554
|
async browseHasExpired() {
|
|
516
|
-
log('
|
|
555
|
+
log('[IABookActions] browseHasExpired');
|
|
517
556
|
window?.IALendingIntervals?.clearAll();
|
|
518
557
|
|
|
519
558
|
const currStatus = {
|
|
@@ -532,31 +571,53 @@ export default class IABookActions extends LitElement {
|
|
|
532
571
|
this.loanRenewResult.texts =
|
|
533
572
|
'This book has been returned due to inactivity.';
|
|
534
573
|
|
|
535
|
-
|
|
574
|
+
this.modal?.closeModal();
|
|
536
575
|
|
|
537
576
|
this.sentryCaptureMsg(sentryLogs.browseHasExpired);
|
|
538
577
|
}
|
|
539
578
|
|
|
579
|
+
/**
|
|
580
|
+
* Show modal when the book can no longer be automatically renewed
|
|
581
|
+
* because another patron has checked it out.
|
|
582
|
+
*/
|
|
583
|
+
async showLoanUnavailableModal(errorMsg) {
|
|
584
|
+
const config = new ModalConfig({
|
|
585
|
+
headline: '',
|
|
586
|
+
showCloseButton: false,
|
|
587
|
+
closeOnBackdropClick: false,
|
|
588
|
+
headerColor: '#d9534f',
|
|
589
|
+
message:
|
|
590
|
+
errorMsg ||
|
|
591
|
+
'Due to inactivity, this book was returned, and someone else has now borrowed it. Please try again later.',
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const customModalContent = html`<br />
|
|
595
|
+
<div style="text-align: center">
|
|
596
|
+
<button
|
|
597
|
+
style="${modalButtonStyle.iaButton} ${modalButtonStyle.return}"
|
|
598
|
+
@click=${() => URLHelper.goToUrl(this.returnUrl, true)}
|
|
599
|
+
>
|
|
600
|
+
Okay
|
|
601
|
+
</button>
|
|
602
|
+
</div>`;
|
|
603
|
+
|
|
604
|
+
await this.modal?.showModal({ config, customModalContent });
|
|
605
|
+
}
|
|
606
|
+
|
|
540
607
|
async startBrowseTimer() {
|
|
541
608
|
window?.IALendingIntervals?.clearBrowseExpireTimeout();
|
|
542
609
|
|
|
543
|
-
const {
|
|
544
|
-
|
|
610
|
+
const {
|
|
611
|
+
browsingExpired,
|
|
612
|
+
user_has_browsed,
|
|
613
|
+
secondsLeftOnLoan,
|
|
614
|
+
} = this.lendingStatus;
|
|
545
615
|
|
|
546
616
|
if (!user_has_browsed || browsingExpired) {
|
|
547
|
-
log('startBrowseTimer --- !user_has_browsed || browsingExpired', {
|
|
548
|
-
user_has_browsed,
|
|
549
|
-
browsingExpired,
|
|
550
|
-
secondsLeftOnLoan,
|
|
551
|
-
});
|
|
552
617
|
return;
|
|
553
618
|
}
|
|
554
619
|
|
|
555
620
|
window.IALendingIntervals.browseExpireTimeout = setTimeout(() => {
|
|
556
|
-
log(
|
|
557
|
-
'startBrowseTimer > browseExpireTimeout --- will expire loan',
|
|
558
|
-
secondsLeftOnLoan
|
|
559
|
-
);
|
|
560
621
|
this.browseHasExpired();
|
|
561
622
|
}, secondsLeftOnLoan * 1000);
|
|
562
623
|
}
|
|
@@ -602,7 +663,6 @@ export default class IABookActions extends LitElement {
|
|
|
602
663
|
?disabled=${this.disableActionGroup}
|
|
603
664
|
?autoRenew=${this.loanRenewResult.renewNow}
|
|
604
665
|
?autoReturn=${this.lendingStatus.browsingExpired}
|
|
605
|
-
?returnNow=${this.returnNow}
|
|
606
666
|
@loanAutoRenewed=${this.handleLoanAutoRenewed}
|
|
607
667
|
@lendingActionError=${this.handleLendingActionError}
|
|
608
668
|
@toggleActionGroup=${this.handleToggleActionGroup}
|
|
@@ -628,6 +688,7 @@ export default class IABookActions extends LitElement {
|
|
|
628
688
|
const activeLoan = detail?.data?.loan;
|
|
629
689
|
const errorMessage = `Whoops, seems we hit a hiccup with renewing this book. Please refresh & retry. --- (Debug: ${detail?.data?.error})`;
|
|
630
690
|
if (!activeLoan) {
|
|
691
|
+
this.recoveringFromLoanExpiry = false;
|
|
631
692
|
this.showErrorModal(errorMessage, 'handleLoanAutoRenewed');
|
|
632
693
|
return;
|
|
633
694
|
}
|
|
@@ -637,15 +698,36 @@ export default class IABookActions extends LitElement {
|
|
|
637
698
|
const loanTime = await this.localCache.get(`${this.identifier}-loanTime`);
|
|
638
699
|
|
|
639
700
|
// number of seconds left in current loan
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
701
|
+
const rawSecondsLeft = Math.round((loanTime - new Date()) / 1000);
|
|
702
|
+
// Guard against a stale/missing loanTime read producing NaN, which
|
|
703
|
+
// would get the loan stuck "active" with a countdown that never moves.
|
|
704
|
+
const secondsLeft =
|
|
705
|
+
Number.isFinite(rawSecondsLeft) && rawSecondsLeft > 0
|
|
706
|
+
? rawSecondsLeft
|
|
707
|
+
: this.loanRenewTimeConfig.loanTotalTime;
|
|
708
|
+
log('[IABookActions] handleLoanAutoRenewed', {
|
|
709
|
+
secondsLeft,
|
|
710
|
+
rawSecondsLeft,
|
|
644
711
|
ajaxResponse: detail?.data,
|
|
645
|
-
loanRenewResult: this.loanRenewResult,
|
|
646
|
-
secondsLeftOnLoan: secondsLeft,
|
|
647
712
|
});
|
|
648
713
|
|
|
714
|
+
if (this.recoveringFromLoanExpiry) {
|
|
715
|
+
// The loan genuinely lapsed, and renew_loan has now CONFIRMED it's
|
|
716
|
+
// renewed server-side — only now is it safe to let BookReader
|
|
717
|
+
// re-initialize (create_token was minting its access cookie off
|
|
718
|
+
// the loan's expiry; doing this before confirmation risked reading
|
|
719
|
+
// the still-expired record). loanRenewInProgress already kept the
|
|
720
|
+
// token poller from restarting early, so it'll pick up fresh here
|
|
721
|
+
// once lendingStatus below re-triggers setupLendingToolbarActions().
|
|
722
|
+
// recoveringFromLoanExpiry itself is consumed/reset there, not
|
|
723
|
+
// here — this ONLY applies to an actual recovery, not a routine
|
|
724
|
+
// renewal (re-running br.init() on every routine top-up would be
|
|
725
|
+
// needlessly disruptive). create_token's own retry logic (see
|
|
726
|
+
// LoanTokenPoller) handles the datanode write-propagation race
|
|
727
|
+
// for both recovery and routine renewals.
|
|
728
|
+
this.postInitComplete = false;
|
|
729
|
+
}
|
|
730
|
+
|
|
649
731
|
const currStatus = {
|
|
650
732
|
...this.lendingStatus,
|
|
651
733
|
user_has_browsed: true,
|
|
@@ -659,7 +741,11 @@ export default class IABookActions extends LitElement {
|
|
|
659
741
|
this.modal.removeAttribute('id');
|
|
660
742
|
this.modal.customModalContent = nothing;
|
|
661
743
|
this.sentryCaptureMsg(sentryLogs.bookHasRenewed);
|
|
744
|
+
this.warningModalDismissed = false;
|
|
662
745
|
}
|
|
746
|
+
|
|
747
|
+
this.warningModalOpen = false;
|
|
748
|
+
this.loanRenewInProgress = false;
|
|
663
749
|
}
|
|
664
750
|
|
|
665
751
|
/**
|
|
@@ -695,29 +781,20 @@ export default class IABookActions extends LitElement {
|
|
|
695
781
|
// re-sync timer if gone off because of background window
|
|
696
782
|
// side effect: updates this.lendingStatus & kicks off lifecycle,
|
|
697
783
|
// if really updated, escape from here
|
|
698
|
-
const resyncd =
|
|
784
|
+
const resyncd = this.reSyncTimerIfGoneOff(secondsLeft);
|
|
699
785
|
|
|
700
786
|
if (resyncd.hasSynced) {
|
|
701
787
|
secondsLeft = resyncd.whatShouldLeft;
|
|
702
|
-
log('
|
|
788
|
+
log('[IABookActions] timer: timer re-synced', {
|
|
703
789
|
secondsLeft,
|
|
704
790
|
});
|
|
705
791
|
}
|
|
706
792
|
|
|
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
|
-
);
|
|
793
|
+
log('[IABookActions] timer', {
|
|
794
|
+
secondsLeft,
|
|
795
|
+
loanRenewAtLast: this.loanRenewTimeConfig.loanRenewAtLast,
|
|
796
|
+
pageChangedInLast: this.loanRenewTimeConfig.pageChangedInLast,
|
|
797
|
+
});
|
|
721
798
|
|
|
722
799
|
/**
|
|
723
800
|
* execute from last 10th minutes to 0th minute
|
|
@@ -742,7 +819,7 @@ export default class IABookActions extends LitElement {
|
|
|
742
819
|
* @param {number} timerSecondsLeft - actual seconds left get from setInterval
|
|
743
820
|
* @returns {Object} { hasSynced: [boolean], whatShouldLeft: [number] }
|
|
744
821
|
*/
|
|
745
|
-
|
|
822
|
+
reSyncTimerIfGoneOff(timerSecondsLeft) {
|
|
746
823
|
const currentTime = new Date();
|
|
747
824
|
|
|
748
825
|
// current time - loan time
|
|
@@ -752,27 +829,11 @@ export default class IABookActions extends LitElement {
|
|
|
752
829
|
const secondsShouldLeft =
|
|
753
830
|
this.lendingStatus.secondsLeftOnLoan - diffInSeconds;
|
|
754
831
|
|
|
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
832
|
// convert in minutes
|
|
765
833
|
const whatIsleft = Math.round(timerSecondsLeft);
|
|
766
834
|
const whatShouldLeft = Math.round(secondsShouldLeft);
|
|
767
835
|
const timerElSeconds = this.timerCountdownEl.secondsLeftOnLoan || 0;
|
|
768
836
|
|
|
769
|
-
log(`reSyncTimerIfGoneOff?`, {
|
|
770
|
-
whatIsleft,
|
|
771
|
-
whatShouldLeft,
|
|
772
|
-
timerElSeconds,
|
|
773
|
-
timeLeftInMin: Math.ceil(timerSecondsLeft / 60),
|
|
774
|
-
});
|
|
775
|
-
|
|
776
837
|
if (timerElSeconds !== whatShouldLeft || whatIsleft !== whatShouldLeft) {
|
|
777
838
|
// set lending status with new time to update decrementor
|
|
778
839
|
const currStatus = {
|
|
@@ -783,7 +844,9 @@ export default class IABookActions extends LitElement {
|
|
|
783
844
|
}
|
|
784
845
|
|
|
785
846
|
if (whatIsleft !== whatShouldLeft) {
|
|
786
|
-
log(
|
|
847
|
+
log(
|
|
848
|
+
`[IABookActions] reSyncTimerIfGoneOff ${whatIsleft} - ${whatShouldLeft}: re-syncing timer`
|
|
849
|
+
);
|
|
787
850
|
return { hasSynced: true, whatShouldLeft };
|
|
788
851
|
}
|
|
789
852
|
|
|
@@ -799,10 +862,6 @@ export default class IABookActions extends LitElement {
|
|
|
799
862
|
* @memberof IABookActions
|
|
800
863
|
*/
|
|
801
864
|
async loanRenewAttempt(secondsLeft) {
|
|
802
|
-
log('loanRenewAttempt ---', {
|
|
803
|
-
secondsLeft,
|
|
804
|
-
loanRenewResult: this.loanRenewResult,
|
|
805
|
-
});
|
|
806
865
|
let loanSecondsLeft = secondsLeft;
|
|
807
866
|
/**
|
|
808
867
|
* auto-renew is not possible in last seconds (let say 50 second) because,
|
|
@@ -812,7 +871,7 @@ export default class IABookActions extends LitElement {
|
|
|
812
871
|
* so if seconds left is < 50, just expire the loan
|
|
813
872
|
*/
|
|
814
873
|
if (loanSecondsLeft < 50) {
|
|
815
|
-
log('loanRenewAttempt
|
|
874
|
+
log('[IABookActions] loanRenewAttempt: < 50s left, expiring loan');
|
|
816
875
|
await this.browseHasExpired();
|
|
817
876
|
return;
|
|
818
877
|
}
|
|
@@ -820,7 +879,9 @@ export default class IABookActions extends LitElement {
|
|
|
820
879
|
await this.autoLoanRenewChecker(false);
|
|
821
880
|
|
|
822
881
|
// show warning modal with remaining time to auto returned it.
|
|
823
|
-
|
|
882
|
+
// once the patron has dismissed it, don't re-show it on every subsequent
|
|
883
|
+
// tick — only an actual renewal (see handleLoanAutoRenewed) re-arms it.
|
|
884
|
+
if (this.loanRenewResult.renewNow === false && !this.warningModalDismissed) {
|
|
824
885
|
/**
|
|
825
886
|
* so compensate for the 50 second buffer to handle above race conditions
|
|
826
887
|
* let's reduce 1 min from warning texts and early return the book when 1 min left.
|
|
@@ -888,42 +949,71 @@ export default class IABookActions extends LitElement {
|
|
|
888
949
|
handleLendingActionError(event) {
|
|
889
950
|
this.disableActionGroup = false;
|
|
890
951
|
|
|
891
|
-
// clear all intervals for lending system when error occured
|
|
892
|
-
window?.IALendingIntervals?.clearAll();
|
|
893
|
-
|
|
894
952
|
const action = event?.detail?.action;
|
|
895
953
|
const errorMsg = event?.detail?.data?.error;
|
|
896
954
|
|
|
897
|
-
// template not show create_token errors
|
|
898
|
-
if (errorMsg && action !== 'create_token')
|
|
899
|
-
this.showErrorModal(errorMsg, action);
|
|
900
|
-
|
|
901
|
-
// if error related to loan token
|
|
902
|
-
// - set user_has_browsed to `false`
|
|
903
955
|
if (action === 'create_token') {
|
|
904
|
-
|
|
956
|
+
// A create_token hiccup only affects BookReader's page-image access
|
|
957
|
+
// token — it says nothing about how much time is left on the loan
|
|
958
|
+
// itself, so it must not stop the reading countdown. Only clear the
|
|
959
|
+
// token poller so it can be retried.
|
|
960
|
+
window?.IALendingIntervals?.clearTokenPoller();
|
|
961
|
+
|
|
962
|
+
// The loan itself is fine — just this token refresh failed — but
|
|
963
|
+
// the action bar must reflect that it isn't safely browsable right
|
|
964
|
+
// now rather than silently doing nothing.
|
|
965
|
+
this.lendingStatus = {
|
|
905
966
|
...this.lendingStatus,
|
|
906
967
|
user_has_browsed: false,
|
|
907
968
|
available_to_browse: true,
|
|
908
969
|
};
|
|
909
|
-
this.lendingStatus = currStatus;
|
|
910
|
-
}
|
|
911
970
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
971
|
+
// By the time this handler runs, LoanTokenPoller has already
|
|
972
|
+
// exhausted its retries for the transient datanode-propagation
|
|
973
|
+
// race (or the error wasn't that in the first place) — this is a
|
|
974
|
+
// genuine failure, so let the patron know instead of doing this
|
|
975
|
+
// silently. showErrorModal has dedicated create_token messaging
|
|
976
|
+
// (refresh button + support email).
|
|
977
|
+
if (errorMsg) this.showErrorModal(errorMsg, action);
|
|
978
|
+
} else if (action === 'renew_loan') {
|
|
979
|
+
window?.IALendingIntervals?.clearAll();
|
|
980
|
+
this.loanRenewInProgress = false;
|
|
981
|
+
this.recoveringFromLoanExpiry = false;
|
|
982
|
+
|
|
983
|
+
// The loan was NOT actually renewed — reflect that immediately
|
|
984
|
+
// (show Borrow, and clear the stale timer value) instead of leaving
|
|
985
|
+
// the action bar showing "Return now" with a frozen countdown from
|
|
986
|
+
// before the failed attempt, which falsely implies the book is
|
|
987
|
+
// still actively being read.
|
|
988
|
+
this.lendingStatus = {
|
|
989
|
+
...this.lendingStatus,
|
|
990
|
+
user_has_browsed: false,
|
|
991
|
+
available_to_browse: true,
|
|
992
|
+
secondsLeftOnLoan: 0,
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
// Show the real error and refresh the page on dismissal (the
|
|
996
|
+
// modal's Okay button) so the client picks up whatever the server
|
|
997
|
+
// now authoritatively considers true — matching on "not available"
|
|
998
|
+
// to guess the reason was fragile since the server can return any
|
|
999
|
+
// message (e.g. a lending limit).
|
|
1000
|
+
this.showLoanUnavailableModal(errorMsg);
|
|
1001
|
+
} else {
|
|
1002
|
+
// Every other action failure genuinely affects loan state.
|
|
1003
|
+
window?.IALendingIntervals?.clearAll();
|
|
1004
|
+
|
|
1005
|
+
if (!errorMsg) return;
|
|
1006
|
+
|
|
1007
|
+
this.showErrorModal(errorMsg, action);
|
|
1008
|
+
|
|
1009
|
+
// update action bar state if book is not available to browse or borrow.
|
|
1010
|
+
if (errorMsg.match(/not available to borrow/gm)) {
|
|
1011
|
+
if (action === 'browse_book') {
|
|
1012
|
+
this.lendingStatus = { ...this.lendingStatus, available_to_browse: false };
|
|
1013
|
+
} else if (action === 'borrow_book') {
|
|
1014
|
+
this.lendingStatus = { ...this.lendingStatus, available_to_borrow: false };
|
|
1015
|
+
}
|
|
925
1016
|
}
|
|
926
|
-
this.lendingStatus = currStatus;
|
|
927
1017
|
}
|
|
928
1018
|
}
|
|
929
1019
|
|