@internetarchive/ia-book-actions 0.1.11-b6 → 0.1.11-b60

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/demo/index.html CHANGED
@@ -131,11 +131,11 @@
131
131
  iaBookActions.returnUrl = '';
132
132
  iaBookActions.localCache = localCache;
133
133
 
134
- let loanTotalTime2Min = { loanTotalTime: 65, loanRenewAtLast: 60, pageChangedInLast: 15 };
134
+ let loanTotalTime2Min = { loanTotalTime: 65, loanRenewAtLast: 60, pageChangedInLast: 30 };
135
135
  let loanTotalTime5Min = { loanTotalTime: 300, loanRenewAtLast: 180, pageChangedInLast: 15 };
136
136
  let loanTotalTime1Hour = { loanTotalTime: 3600, loanRenewAtLast: 660, pageChangedInLast: 900 };
137
137
 
138
- iaBookActions.loanRenewTimeConfig = loanTotalTime2Min;
138
+ iaBookActions.loanRenewTimeConfig = loanTotalTime5Min;
139
139
 
140
140
  let lendingStatus1 = iaBookActions.lendingStatus;
141
141
  let secondsLeftOnLoan = iaBookActions.loanRenewTimeConfig.loanTotalTime;
@@ -242,6 +242,31 @@
242
242
  });
243
243
  });
244
244
 
245
+ document.querySelector('#show_warning_modal').addEventListener('click', async () => {
246
+ if (!iaBookActions.lendingStatus.user_has_browsed) {
247
+ const newLendingStatus = { ...iaBookActions.lendingStatus, user_has_browsed: true };
248
+ iaBookActions.lendingStatus = newLendingStatus;
249
+ await iaBookActions.updateComplete;
250
+ }
251
+
252
+ iaBookActions.showWarningModal();
253
+
254
+ if (iaBookActions.lendingStatus.user_has_browsed) {
255
+ iaBookActions.showWarningModal();
256
+ }
257
+ });
258
+
259
+ document.querySelector('#show_expired_modal').addEventListener('click', async () => {
260
+ if (!iaBookActions.lendingStatus.user_has_browsed) {
261
+ const newLendingStatus = { ...iaBookActions.lendingStatus, user_has_browsed: true };
262
+ iaBookActions.lendingStatus = newLendingStatus;
263
+ await iaBookActions.updateComplete;
264
+ }
265
+ await new Promise(resolve => setTimeout(resolve, 5000));
266
+
267
+ iaBookActions.browseHasExpired();
268
+ });
269
+
245
270
  document.querySelectorAll('.availableToBorrow input[type=radio]').forEach((item) => {
246
271
  item.addEventListener('click', (e) => {
247
272
  if (e.target.value === 'available_to_borrow') {
@@ -298,6 +323,12 @@
298
323
  })
299
324
  );
300
325
  })
326
+
327
+ // listen for IABookReader:BrowsingHasExpired
328
+ window.addEventListener('IABookReader:BrowsingHasExpired', () => {
329
+ console.log('IABookReader:BrowsingHasExpired EVENT FIRED');
330
+ alert('IABookReader:BrowsingHasExpired EVENT FIRED');
331
+ });
301
332
  </script>
302
333
 
303
334
  <div>
@@ -334,6 +365,13 @@
334
365
  <label><input type="radio" name="browse" value="available_to_browse" />available_to_browse</label>
335
366
  <label><input type="radio" name="browse" value="user_has_browsed" />user_has_browsed</label>
336
367
  <label><input type="radio" name="browse" value="browsingExpired" />browsingExpired</label>
368
+ <div>
369
+ <button id="show_warning_modal">When Browsing (`user_has_browsed`), show Warning Modal</button>
370
+ </div>
371
+ <div>
372
+ <button id="show_expired_modal">While Browsing (`user_has_browsed`), Expire book & show its modal</button>
373
+ how_expired_modal
374
+ </div>
337
375
  </td>
338
376
  <td class="availableToBorrow">
339
377
  <label><input type="radio" name="borrow" value="available_to_borrow" />available_to_borrow</label>
@@ -356,5 +394,6 @@
356
394
  </table>
357
395
  </fieldset>
358
396
  </div>
397
+ <modal-manager></modal-manager>
359
398
  </body>
360
399
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/ia-book-actions",
3
- "version": "0.1.11-b6",
3
+ "version": "0.1.11-b60",
4
4
  "description": "Bookreader actions for borrowable book",
5
5
  "author": "Internet Archive",
6
6
  "license": "AGPL-3.0-only",
@@ -18,7 +18,6 @@
18
18
  "test:watch": "web-test-runner --watch --playwright --browsers chromium firefox webkit"
19
19
  },
20
20
  "dependencies": {
21
- "@internetarchive/ia-activity-indicator": "^0.0.4",
22
21
  "@internetarchive/icon-info": "^1.2.6",
23
22
  "@internetarchive/local-cache": "^0.2.1",
24
23
  "@internetarchive/modal-manager": "^0.2.12",
@@ -63,15 +63,16 @@ export class CollapsibleActionGroup extends ActionsHandler {
63
63
  this.resetActions();
64
64
  }
65
65
 
66
- if (changed.has('autoRenew') && this.autoRenew === true) {
66
+ if (changed.has('autoRenew') && this.autoRenew) {
67
67
  this.dispatchLoanEvent('autoRenew');
68
68
  }
69
-
70
- if (changed.has('autoReturn') && this.autoReturn === true) {
69
+
70
+ const requestingAutoreturn = changed.has('autoReturn') && this.autoReturn;
71
+ if (requestingAutoreturn) {
71
72
  this.dispatchLoanEvent('autoReturn');
72
73
  }
73
74
 
74
- if (changed.has('returnNow') && this.returnNow === true) {
75
+ if (changed.has('returnNow') && this.returnNow && !requestingAutoreturn) {
75
76
  this.dispatchLoanEvent('returnNow', { borrowType: 'browse' });
76
77
  }
77
78
  }
@@ -3,32 +3,43 @@ import { html, css, LitElement } from 'lit';
3
3
  export default class TimerCountdown extends LitElement {
4
4
  static get properties() {
5
5
  return {
6
- secondsLeftOnLoan: { type: Number }, // in seconds
6
+ secondsLeftOnLoan: { type: Number },
7
+ displayTime: { type: Boolean },
7
8
  };
8
9
  }
9
10
 
10
11
  constructor() {
11
12
  super();
12
13
 
13
- /**
14
- * seconds left in current loan
15
- * @type {number}
16
- */
17
14
  this.secondsLeftOnLoan = 0;
15
+ this.displayTime = false;
16
+ }
17
+
18
+ get minutesLeftOnLoan() {
19
+ let timeLeft = Math.round(this.secondsLeftOnLoan);
20
+
21
+ // convert time from second to minute
22
+ timeLeft = Math.ceil(timeLeft / 60);
23
+
24
+ if (timeLeft < 10) {
25
+ timeLeft = `0:0${timeLeft}`;
26
+ } else if (timeLeft === 60) {
27
+ timeLeft = `1:00`;
28
+ } else {
29
+ timeLeft = `0:${timeLeft}`;
30
+ }
31
+ return timeLeft;
18
32
  }
19
33
 
20
34
  /**
21
35
  * get remaining time with timeunit
22
36
  *
23
37
  * @memberof TimerCountdown
24
- * @return string
38
+ * @return string - seconds left
25
39
  */
26
40
  get remainingTime() {
27
41
  const unitOfTime = 'minute';
28
- let timeLeft = Math.round(this.secondsLeftOnLoan);
29
-
30
- // convert time from second to minute
31
- timeLeft = Math.ceil(timeLeft / 60);
42
+ const timeLeft = this.minutesLeftOnLoan;
32
43
 
33
44
  return timeLeft !== 1
34
45
  ? `${timeLeft} ${unitOfTime}s`
@@ -36,58 +47,28 @@ export default class TimerCountdown extends LitElement {
36
47
  }
37
48
 
38
49
  render() {
50
+ const viewClass = this.displayTime ? 'view' : 'hide';
39
51
  return html`
40
- <svg
41
- width="14"
42
- height="14"
43
- viewBox="0 0 100 100"
44
- xmlns="http://www.w3.org/2000/svg"
52
+ <button
53
+ id="timer-counter"
54
+ class=${viewClass}
55
+ @click=${() => {
56
+ this.displayTime = !this.displayTime;
57
+ }}
58
+ role="timer"
45
59
  >
46
- <circle class="circle" cx="50" cy="50" r="50" />
47
- </svg>
48
- <span class="sr-only">${this.remainingTime} left</span>
60
+ <span>${this.minutesLeftOnLoan}</span>
61
+ <span class="sr-only">${this.remainingTime} left</span>
62
+ </button>
49
63
  `;
50
64
  }
51
65
 
52
66
  static get styles() {
53
- const white = css`var(--white, #fff)`;
54
- const timerSecondsLeft = css`var(--secondsLeftOnLoan, 6000s)`; //
55
- const timerStrokeLeft = css`var(--strokeLeftOnLoan, 315)`;
56
-
57
67
  return css`
58
68
  :host {
59
69
  right: 0;
60
70
  margin-right: 10px;
61
71
  position: absolute;
62
- height: 20px;
63
- width: 20px;
64
- }
65
-
66
- @keyframes circletimer {
67
- 0% {
68
- stroke-dashoffset: ${timerStrokeLeft};
69
- stroke-dasharray: 315;
70
- }
71
- 100% {
72
- stroke-dashoffset: 0;
73
- stroke-dasharray: 315;
74
- }
75
- }
76
-
77
- svg {
78
- background-color: ${white};
79
- border-radius: 50px;
80
- border: 2px solid ${white};
81
- transform: rotateZ(-90deg);
82
- }
83
-
84
- svg .circle {
85
- stroke: #000;
86
- stroke-width: 100px;
87
- fill: ${white};
88
- stroke-dashoffset: 315;
89
- stroke-dasharray: 0;
90
- animation: ${timerSecondsLeft} circletimer linear;
91
72
  }
92
73
 
93
74
  .sr-only {
@@ -100,6 +81,18 @@ export default class TimerCountdown extends LitElement {
100
81
  border: none;
101
82
  overflow: hidden;
102
83
  }
84
+
85
+ button#timer-counter {
86
+ cursor: pointer;
87
+ }
88
+
89
+ .hide {
90
+ opacity: 0;
91
+ }
92
+
93
+ .show {
94
+ opacity: 1;
95
+ }
103
96
  `;
104
97
  }
105
98
  }
@@ -6,4 +6,4 @@ export const mobileContainerWidth = 700;
6
6
  /**
7
7
  * shift the actions buttons in dropdown
8
8
  */
9
- export const tabletContainerWidth = 895;
9
+ export const tabletContainerWidth = 800;
@@ -1,3 +1,4 @@
1
+ /* global: window */
1
2
  /**
2
3
  * Collection of methods for managing intervals for lending system
3
4
  *
@@ -18,16 +19,19 @@ window.IALendingIntervals = {
18
19
  tokenPoller: 0,
19
20
 
20
21
  /**
21
- * store timer-countdown interval
22
- * @see TimerCountdown::timerCountdown
22
+ * renewal check interval
23
23
  */
24
24
  timerCountdown: 0,
25
25
 
26
+ /** expiration timer */
27
+ browseExpireTimeout: 0,
28
+
26
29
  /**
27
30
  * clear interval for create_token api
28
31
  */
29
32
  clearTokenPoller: () => {
30
- window.clearInterval(window?.IALendingIntervals?.tokenPoller);
33
+ window.clearInterval(window.IALendingIntervals.tokenPoller);
34
+ window.IALendingIntervals.tokenPoller = 0;
31
35
  },
32
36
 
33
37
  /**
@@ -35,13 +39,23 @@ window.IALendingIntervals = {
35
39
  */
36
40
  clearTimerCountdown: () => {
37
41
  window.clearInterval(window.IALendingIntervals.timerCountdown);
42
+ window.IALendingIntervals.timerCountdown = 0;
43
+ },
44
+
45
+ /**
46
+ * clear interval for graphic timer for one-hour loan renew
47
+ */
48
+ clearBrowseExpireTimeout: () => {
49
+ window.clearTimeout(window.IALendingIntervals.browseExpireTimeout);
50
+ window.IALendingIntervals.browseExpireTimeout = 0;
38
51
  },
39
52
 
40
53
  /**
41
54
  * clear all intervals being used acros lending system
42
55
  */
43
56
  clearAll: () => {
44
- window.clearInterval(window?.IALendingIntervals?.tokenPoller);
45
- window.clearInterval(window?.IALendingIntervals?.timerCountdown);
57
+ window?.IALendingIntervals?.clearTokenPoller();
58
+ window?.IALendingIntervals?.clearTimerCountdown();
59
+ window?.IALendingIntervals?.clearBrowseExpireTimeout();
46
60
  },
47
61
  };
@@ -21,7 +21,13 @@ export default async function ActionsHandlerService(options) {
21
21
  const tokenError = 'loan token not found. please try again later.';
22
22
  const borrowError =
23
23
  'This book is not available to borrow at this time. Please try again later.';
24
- const erroneousActions = ['browse_book', 'borrow_book', 'create_token'];
24
+ const erroneousActions = [
25
+ 'browse_book',
26
+ 'borrow_book',
27
+ 'create_token',
28
+ 'renew_loan',
29
+ 'return_loan',
30
+ ];
25
31
  const shouldReturnError =
26
32
  location?.href?.indexOf('?error=true') !== -1 &&
27
33
  location?.hostname !== 'archive.org';
@@ -36,7 +42,7 @@ export default async function ActionsHandlerService(options) {
36
42
  method: 'POST',
37
43
  body: formData,
38
44
  })
39
- .then(response => {
45
+ .then(async response => {
40
46
  // intentional error on localhost
41
47
  if (shouldReturnError && erroneousActions.includes(option?.action)) {
42
48
  return {
@@ -47,7 +53,12 @@ export default async function ActionsHandlerService(options) {
47
53
 
48
54
  // return success response for /demo/ server...
49
55
  if (baseHost == '/demo/1' || baseHost == '/demo/') {
50
- if (option?.action == 'renew_loan') {
56
+ if (
57
+ option?.action == 'renew_loan' ||
58
+ option?.action == 'return_loan'
59
+ ) {
60
+ // wait a few seconds so that the user can see the loading state
61
+ await new Promise(resolve => setTimeout(resolve, 5000));
51
62
  return {
52
63
  success: true,
53
64
  loan: { renewal: true },
@@ -2,10 +2,15 @@ import { LitElement } from 'lit';
2
2
 
3
3
  import { URLHelper } from '../../config/url-helper.js';
4
4
  import { sentryLogs } from '../../config/sentry-events.js';
5
+ import log from '../log.js';
5
6
 
6
7
  import ActionsHandlerService from './actions-handler-service.js';
7
8
  import LoanAnanlytics from '../loan-analytics.js';
8
9
  import * as Cookies from '../doc-cookies.js';
10
+ import {
11
+ analyticsCategories,
12
+ analyticsActions,
13
+ } from '../../config/analytics-event-and-category.js';
9
14
 
10
15
  /**
11
16
  * These are callback functions calling from actions-config.js file.
@@ -38,24 +43,33 @@ export default class ActionsHandler extends LitElement {
38
43
  }
39
44
 
40
45
  bindEvents() {
41
- this.addEventListener('browseBook', () => {
46
+ this.addEventListener('browseBook', async () => {
42
47
  this.handleBrowseIt();
43
- this.loanAnanlytics?.storeLoanStatsCount(this.identifier, 'browse');
48
+ await this.loanAnanlytics?.storeLoanStatsCount(this.identifier, 'browse');
44
49
  });
45
50
 
46
- this.addEventListener('browseBookAgain', () => {
51
+ this.addEventListener('browseBookAgain', async () => {
47
52
  this.handleBrowseIt();
48
- this.loanAnanlytics?.storeLoanStatsCount(this.identifier, 'browseagain');
53
+ await this.loanAnanlytics?.storeLoanStatsCount(
54
+ this.identifier,
55
+ 'browseagain'
56
+ );
49
57
  });
50
58
 
51
- this.addEventListener('autoRenew', () => {
59
+ this.addEventListener('autoRenew', async () => {
52
60
  this.handleLoanRenewNow();
53
- this.loanAnanlytics?.storeLoanStatsCount(this.identifier, 'autorenew');
61
+ await this.loanAnanlytics?.storeLoanStatsCount(
62
+ this.identifier,
63
+ 'autorenew'
64
+ );
54
65
  });
55
66
 
56
- this.addEventListener('autoReturn', () => {
67
+ this.addEventListener('autoReturn', async () => {
57
68
  this.handleReturnIt();
58
- this.loanAnanlytics?.storeLoanStatsCount(this.identifier, 'autoreturn');
69
+ await this.loanAnanlytics?.storeLoanStatsCount(
70
+ this.identifier,
71
+ 'autoreturn'
72
+ );
59
73
  });
60
74
 
61
75
  this.addEventListener('returnNow', ({ detail }) => {
@@ -142,19 +156,37 @@ export default class ActionsHandler extends LitElement {
142
156
  action,
143
157
  identifier: this.identifier,
144
158
  success: data => {
145
- console.log("RENEW_LOAN --- ", data, action, data.loan, this.identifier);
146
- if (data?.success === true && data?.loan?.renewal === true) {
159
+ log('RENEW_LOAN --- ', data, action, data.loan, this.identifier);
160
+ const activeLoan = data.loan ? data.loan : undefined;
161
+ const isRenewal = activeLoan.renewal;
162
+
163
+ if (activeLoan && isRenewal) {
164
+ // when loan is renewed, let's reset timer & let everyone know.
147
165
  this.setBrowseTimeSession();
148
- this.dispatchEvent(
149
- new CustomEvent('loanAutoRenewed', {
150
- detail: { action, data },
151
- })
152
- );
153
166
  } else {
167
+ log('RENEW_LOAN ERROR --- ', {
168
+ action,
169
+ isRenewal,
170
+ activeLoan,
171
+ data,
172
+ id: this.identifier,
173
+ });
154
174
  window?.Sentry?.captureMessage(
155
175
  `${sentryLogs.bookRenewFailed} - Error: ${JSON.stringify(data)}`
156
176
  );
177
+ this.dispatchActionError(action, {
178
+ data,
179
+ error: true,
180
+ message: 'Loan renewal failed: no loan active.',
181
+ });
157
182
  }
183
+
184
+ // dispatch outcome of loan renewal
185
+ this.dispatchEvent(
186
+ new CustomEvent('loanAutoRenewed', {
187
+ detail: { action, data: { ...data, loan: activeLoan } },
188
+ })
189
+ );
158
190
  },
159
191
  error: data => {
160
192
  this.dispatchActionError(action, data);
@@ -316,7 +348,7 @@ export default class ActionsHandler extends LitElement {
316
348
  // delete pageChangedTime when book is auto renew at nth minute
317
349
  await this.localCache.delete(`${this.identifier}-pageChangedTime`);
318
350
  } catch (error) {
319
- console.log(error);
351
+ log(error);
320
352
  }
321
353
  }
322
354
 
@@ -40,7 +40,7 @@ import ActionsConfig from './actions-config.js';
40
40
  * @enum {string}
41
41
  */
42
42
  export const bookTitles = {
43
- available_1hr: 'Renews automatically as long as pages are turned.',
43
+ available_1hr: 'Renews automatically with continued use.',
44
44
  available_14d: 'This book can be borrowed for 14 days.',
45
45
  available_pd: 'Book available to patrons with print disabilities.',
46
46
  available_waitlist: 'A waitlist is available.',
@@ -49,7 +49,7 @@ export const bookTitles = {
49
49
  being_borrowed: 'Another patron is using this book. Please check back later.',
50
50
  eligible_pd: 'You are eligible for print-disabled access.',
51
51
  on_waitlist: 'You are on the waitlist for this book.',
52
- session_expired: 'Renews automatically as long as pages are turned.',
52
+ session_expired: 'Renews automatically with continued use.',
53
53
  unavailable: 'This book is not available at this time.',
54
54
  };
55
55
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { analyticsCategories } from '../config/analytics-event-and-category.js';
4
4
  import * as Cookies from './doc-cookies.js';
5
+ import log from './log.js';
5
6
 
6
7
  /**
7
8
  * This class is used to send different GA events for loan system.
@@ -45,7 +46,7 @@ export default class LoanAnanlytics {
45
46
  '/'
46
47
  );
47
48
  } catch (error) {
48
- console.log(error);
49
+ log(error);
49
50
  this.sendEvent('Cookies-Error-Actions', error, this.identifier);
50
51
  }
51
52
  }
@@ -144,7 +145,7 @@ export default class LoanAnanlytics {
144
145
  * @memberof LoanAnanlytics
145
146
  */
146
147
  sendEvent(eventCategory, eventAction, label, extraParams) {
147
- // console.log(
148
+ // log(
148
149
  // 'eventCategory:-',
149
150
  // eventCategory,
150
151
  // '\t||\teventAction:-',
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable class-methods-use-this */
2
2
 
3
3
  import { nothing } from 'lit';
4
+ import log from './log.js';
4
5
 
5
6
  /**
6
7
  * This class is used to determine if use is eligible for auto renew loan.
@@ -32,7 +33,7 @@ export class LoanRenewHelper {
32
33
  }
33
34
  return this.autoChecker(); // auto checker at last 10th minute
34
35
  } catch (error) {
35
- console.log(error);
36
+ log(error);
36
37
  }
37
38
 
38
39
  return nothing;
@@ -1,3 +1,4 @@
1
+ /* global: window */
1
2
  import ActionsHandlerService from './actions-handler/actions-handler-service.js';
2
3
  import LoanAnanlytics from './loan-analytics.js';
3
4
  import { sentryLogs } from '../config/sentry-events.js';
@@ -0,0 +1,16 @@
1
+ /* eslint-disable no-restricted-globals */
2
+ /* eslint-disable semi */
3
+ /**
4
+ * log() utility -- production ignores 'log()' JS calls; dev invokes 'log()'
5
+ */
6
+ const log =
7
+ location.hostname === 'localhost' ||
8
+ location.host.match(/^(www|cat)-[a-z0-9]+\.archive\.org$/) ||
9
+ location.host.match(/\.code\.archive\.org$/) ||
10
+ location.host.match(/\.dev\.archive\.org$/) ||
11
+ location.host.match(/^ia-petabox-/)
12
+ ? // eslint-disable-next-line no-console
13
+ console.log.bind(console) // convenient, no? Stateless function
14
+ : () => {};
15
+
16
+ export { log as default };