@internetarchive/ia-book-actions 0.0.29-alpha20 → 0.0.29-alpha21

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
@@ -127,9 +127,9 @@
127
127
  iaBookActions.returnUrl = 'https://openlibrary.org';
128
128
  iaBookActions.localCache = localCache;
129
129
  iaBookActions.loanRenewConfig = {
130
- totalTime: 40, // 60 second = 1 min
131
- autoCheckAt: 35, // totalTime - 10th minute
132
- pageChangedInLast: 5, // within last 15 second/minute
130
+ totalTime: 20, // 60 second = 1 min
131
+ autoCheckAt: 15, // totalTime - 10th minute
132
+ pageChangedInLast: 15, // within last 15 second/minute
133
133
  };
134
134
 
135
135
  let lendingStatus1 = iaBookActions.lendingStatus;
@@ -226,8 +226,11 @@
226
226
  lendingStatus1.user_has_browsed = false;
227
227
  }
228
228
 
229
- let lending = {...defaultLendingStatus, ...lendingStatus1};
230
- iaBookActions.lendingStatus = lending;
229
+ // wait for a while
230
+ setTimeout(() => {
231
+ let lending = {...defaultLendingStatus, ...lendingStatus1};
232
+ iaBookActions.lendingStatus = lending;
233
+ }, 10);
231
234
  });
232
235
  });
233
236
 
@@ -281,7 +284,7 @@
281
284
  // dispatch event to say book page has flipped
282
285
  document.querySelector('ia-book-actions').dispatchEvent(
283
286
  new CustomEvent('BookReader:pageChanged', {
284
- detail: { isPageChanged: true },
287
+ detail: { hasPageChanged: true },
285
288
  bubbles: true,
286
289
  composed: true,
287
290
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/ia-book-actions",
3
- "version": "0.0.29-alpha20",
3
+ "version": "0.0.29-alpha21",
4
4
  "description": "Bookreader actions for borrowable book",
5
5
  "author": "Internet Archive",
6
6
  "license": "AGPL-3.0-only",
@@ -22,8 +22,8 @@
22
22
  "@internetarchive/local-cache": "^0.2.0",
23
23
  "@internetarchive/modal-manager": "^0.2.1",
24
24
  "@internetarchive/shared-resize-observer": "^0.2.0",
25
- "@internetarchive/toast-manager": "0.0.1-alpha5",
26
- "lit": "^2.2.1"
25
+ "@internetarchive/toast-manager": "0.0.1-alpha8",
26
+ "lit": "^2.5.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@open-wc/eslint-config": "^4.2.0",
@@ -12,6 +12,7 @@ export default css`
12
12
  }
13
13
  .actiongroup {
14
14
  display: flex;
15
+ margin-right: 10px;
15
16
  }
16
17
  .action-buttons {
17
18
  display: inline-flex;
@@ -33,12 +34,12 @@ export default css`
33
34
  }
34
35
  .primary {
35
36
  background-color: ${whiteColor};
37
+ margin-right: 4px;
36
38
  }
37
39
  .primary,
38
40
  .secondary {
39
41
  position: relative;
40
42
  border-radius: 5px;
41
- margin: 0 3px;
42
43
  }
43
44
  .primary .initial {
44
45
  border-radius: 4px 0 0 4px;
@@ -43,9 +43,6 @@ export class InfoIcon extends LitElement {
43
43
  vertical-align: middle;
44
44
  --iconFillColor: white;
45
45
  }
46
- .more-info-icon {
47
- margin-left: 10px;
48
- }
49
46
  .more-info-icon img {
50
47
  width: 24px;
51
48
  height: 24px;
@@ -26,7 +26,7 @@ export class TextGroup extends LitElement {
26
26
  display: inline-block;
27
27
  }
28
28
  .variable-texts {
29
- margin-left: 10px;
29
+ margin-right: 10px;
30
30
  vertical-align: middle;
31
31
  font-size: 1.7rem;
32
32
  }
@@ -32,12 +32,15 @@ export default class TimerCountdown extends LitElement {
32
32
  this.timerInterval = setInterval(() => {
33
33
  this.time -= 1;
34
34
 
35
- // execute 50th minute check
36
- if (Math.round(this.time) === this.autoCheckAt) {
35
+ // execute from last 10th minute to 0th minute
36
+ // - 10th - to check if user has viewed
37
+ // - till 0th - to show warning msg with remaining time to auto returned
38
+ if (Math.round(this.time) <= this.autoCheckAt) {
37
39
  this.dispatchEvent(
38
40
  new CustomEvent('IABookActions:loanRenew', {
39
41
  detail: {
40
- isPageChanged: false,
42
+ hasPageChanged: false,
43
+ timeLeft: Math.round(this.time),
41
44
  },
42
45
  bubbles: true,
43
46
  composed: true,
@@ -45,20 +48,30 @@ export default class TimerCountdown extends LitElement {
45
48
  );
46
49
  }
47
50
 
48
- if (this.time === 0) clearInterval(this.timerInterval);
51
+ // clear interval if timer is < this.autoCheckAt
52
+ if (this.time < this.autoCheckAt) {
53
+ clearInterval(this.timerInterval);
54
+ }
49
55
  }, 1000);
50
56
  }
51
57
 
52
58
  render() {
53
- return html`<div id="countdown">
54
- <div id="countdown-number">${Math.round(this.time)}</div>
55
- <svg>
56
- <circle r="18" cx="20" cy="20"></circle>
59
+ return html`
60
+ <svg
61
+ width="30"
62
+ height="30"
63
+ viewBox="0 0 100 100"
64
+ xmlns="http://www.w3.org/2000/svg"
65
+ >
66
+ <circle class="circle" cx="50" cy="50" r="50" />
57
67
  </svg>
58
- </div>`;
68
+ `;
59
69
  }
60
70
 
61
71
  static get styles() {
72
+ const white = css`var(--white, #fff)`;
73
+ const secondsLeft = css`var(--secondsLeft, 60s)`;
74
+
62
75
  return css`
63
76
  :host {
64
77
  right: 5px;
@@ -66,46 +79,34 @@ export default class TimerCountdown extends LitElement {
66
79
  position: absolute;
67
80
  }
68
81
 
69
- #countdown-number {
70
- color: white;
71
- display: inline-block;
72
- line-height: 40px;
73
- margin-right: 12px;
82
+ @keyframes circletimer {
83
+ 0% {
84
+ stroke-dashoffset: 320;
85
+ stroke-dasharray: 320;
86
+ }
87
+ 100% {
88
+ stroke-dashoffset: 0;
89
+ stroke-dasharray: 320;
90
+ }
74
91
  }
75
92
 
76
93
  svg {
94
+ background-color: transparent;
77
95
  position: absolute;
78
- top: 0;
96
+ top: 50%;
79
97
  right: 0;
80
- width: 40px;
81
- height: 40px;
82
- transform: rotateY(-180deg) rotateZ(-90deg);
83
- }
84
-
85
- svg circle {
86
- stroke-dasharray: 113px;
87
- stroke-dashoffset: 0px;
88
- stroke-linecap: round;
89
- stroke-width: 3px;
90
- stroke: white;
91
- fill: none;
92
- animation: countdown 60s linear infinite forwards;
93
- }
94
-
95
- @keyframes countdown {
96
- from {
97
- stroke-dashoffset: 0px;
98
- }
99
- to {
100
- stroke-dashoffset: 113px;
101
- }
98
+ border-radius: 50px;
99
+ border: 2px solid ${white};
100
+ transform: translate(-50%, -50%) rotateZ(-90deg);
102
101
  }
103
102
 
104
- .hidden {
105
- display: none;
106
- }
107
- .visible {
108
- display: inline-block;
103
+ svg .circle {
104
+ stroke: ${white};
105
+ stroke-width: 100px;
106
+ fill: transparent;
107
+ stroke-dashoffset: 320;
108
+ stroke-dasharray: 0;
109
+ animation: ${secondsLeft} circletimer linear;
109
110
  }
110
111
  `;
111
112
  }
@@ -119,12 +119,19 @@ export default class ActionsHandler extends LitElement {
119
119
 
120
120
  handleLoanRenew() {
121
121
  const action = 'renew_loan';
122
- this.setBrowseTimeSession();
123
122
 
124
123
  ActionsHandlerService({
125
124
  action,
126
125
  identifier: this.identifier,
127
- success: () => {},
126
+ success: data => {
127
+ // success analytics by isa?
128
+ this.setBrowseTimeSession();
129
+ this.dispatchEvent(
130
+ new CustomEvent('loanAutoRenewed', {
131
+ detail: { action, data },
132
+ })
133
+ );
134
+ },
128
135
  error: data => {
129
136
  this.dispatchActionError(action, data);
130
137
  },
@@ -286,13 +293,17 @@ export default class ActionsHandler extends LitElement {
286
293
  */
287
294
  async setBrowseTimeSession() {
288
295
  try {
296
+ const { totalTime } = this.loanRenewConfig;
297
+ const expireDate = new Date(new Date().getTime() + totalTime * 1000);
298
+
299
+ // set a value
289
300
  await this.localCache.set({
290
301
  key: `${this.identifier}-loanTime`,
291
- value: new Date(), // current time
292
- ttl: Number(this.loanRenewConfig.totalTime),
302
+ value: expireDate,
303
+ ttl: Number(totalTime),
293
304
  });
294
305
 
295
- // delete flip time
306
+ // delete pageChangedTime when book is auto renew at nth minute
296
307
  await this.localCache.delete(`${this.identifier}-pageChangedTime`);
297
308
  } catch (error) {
298
309
  console.log(error);
@@ -19,7 +19,7 @@ import ActionsConfig from './actions-config.js';
19
19
  *
20
20
  * Response of this class contain an object as follows:-
21
21
  * {
22
- * primaryTitle: 'Renewable every hour, pending availability.',
22
+ * primaryTitle: '',
23
23
  * primaryActions: [this.actionsConfig.returnBookConfig()],
24
24
  * primaryColor: 'primary',
25
25
  * secondaryActions: [this.actionsConfig.purchaseConfig()],
@@ -40,7 +40,7 @@ import ActionsConfig from './actions-config.js';
40
40
  * @enum {string}
41
41
  */
42
42
  export const bookTitles = {
43
- available_1hr: 'Renewable every hour, pending availability.',
43
+ available_1hr: '',
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: 'Your loan has expired.',
52
+ session_expired: '',
53
53
  unavailable: 'This book is not available at this time.',
54
54
  };
55
55
 
@@ -98,7 +98,7 @@ export class GetLendingActions {
98
98
  let isBrowsing =
99
99
  lendingStatus.user_has_browsed && !lendingStatus.browsingExpired;
100
100
  if (isBrowsing) {
101
- primaryTitleText = this.getBrowseCountdownTitle();
101
+ primaryTitleText = '';
102
102
  } else {
103
103
  primaryTitleText = `Your loan of this book has ${lendingStatus.daysLeftOnLoan} days left.`;
104
104
  }
@@ -251,13 +251,6 @@ export class GetLendingActions {
251
251
  borrow1HrAction() {
252
252
  const lendingStatus = this.lendingStatus || {};
253
253
 
254
- const possibleTitles = {
255
- one_hour: 'Renewable every hour, pending availability.',
256
- session_tryagain:
257
- 'Another patron is using this book. Please check back later.',
258
- session_expired: 'Your loan has expired.',
259
- };
260
-
261
254
  // assess borrowable state
262
255
  const browsingHasExpired =
263
256
  !lendingStatus.available_to_browse && lendingStatus.browsingExpired;
@@ -364,6 +357,8 @@ export class GetLendingActions {
364
357
  }
365
358
 
366
359
  /**
360
+ * @deprecated
361
+ *
367
362
  * Builds the countdown toolbar title.
368
363
  *
369
364
  * @return string innerHTML for the dialogOpts title attribute
@@ -0,0 +1,159 @@
1
+ import { nothing } from 'lit';
2
+ import { ToastConfig } from '@internetarchive/toast-manager';
3
+
4
+ /**
5
+ * This class is used to determine if use is eligible for auto renew loan.
6
+ */
7
+ export class LoanRenewHelper {
8
+ constructor(hasPageChanged, identifier, localCache, loanRenewConfig) {
9
+ this.hasPageChanged = hasPageChanged;
10
+ this.identifier = identifier;
11
+ this.localCache = localCache;
12
+ this.loanRenewConfig = loanRenewConfig;
13
+
14
+ // messages for auto return machenism
15
+ this.loanRenewMessage = 'This book has been renewed for 1 hour.';
16
+ this.loanReturnWarning =
17
+ 'This book will be automatically returned in #time minutes unless you turn a page.';
18
+
19
+ // private props
20
+ this.result = {
21
+ texts: null,
22
+ renewNow: false,
23
+ timeLeft: 0,
24
+ };
25
+ }
26
+
27
+ handleLoanRenew() {
28
+ try {
29
+ if (this.hasPageChanged) {
30
+ // when user click/flip on book page
31
+ this.showToastMessage();
32
+ return this.pageChanged(); // user clicked on page
33
+ }
34
+ return this.autoChecker(); // auto checker at 50th minute
35
+ } catch (error) {
36
+ console.log(error);
37
+ }
38
+
39
+ return nothing;
40
+ }
41
+
42
+ /**
43
+ * Trigger this function when user has browsed a book and change the post
44
+ * - every time user change the page, we set current time in indexedDB
45
+ * - also check if need to auto renew current loan
46
+ *
47
+ * @returns {this.result}
48
+ */
49
+ async pageChanged() {
50
+ const {
51
+ totalTime, // 60
52
+ autoCheckAt, // 50
53
+ } = this.loanRenewConfig;
54
+ const currentTime = new Date();
55
+ const loanTime = await this.localCache.get(`${this.identifier}-loanTime`);
56
+
57
+ const lastTimeFrame = this.changeTime(
58
+ loanTime,
59
+ totalTime - autoCheckAt,
60
+ 'add'
61
+ );
62
+
63
+ // if user viewed new page in last 10 minutes, renew immediately
64
+ if (currentTime >= lastTimeFrame) {
65
+ this.result.texts = this.loanRenewMessage;
66
+ this.result.renewNow = true;
67
+ }
68
+
69
+ this.setPageChangedTime();
70
+ return this.result;
71
+ }
72
+
73
+ /**
74
+ * Trigger this function when countdown hits last 10 minutes
75
+ * - if user is open new page, just renew the loan at last 10th minute
76
+ *
77
+ * @returns {this.result}
78
+ */
79
+ async autoChecker() {
80
+ const currentTime = new Date();
81
+ const { pageChangedInLast } = this.loanRenewConfig;
82
+ const lastPageFlipTime = await this.localCache.get(
83
+ `${this.identifier}-pageChangedTime`
84
+ );
85
+
86
+ // last 15 min if used flipped a page.
87
+ const lastFlipTimeFrame = this.changeTime(
88
+ currentTime,
89
+ pageChangedInLast,
90
+ 'sub'
91
+ ); // 15 seconds
92
+
93
+ if (
94
+ lastPageFlipTime === undefined ||
95
+ lastPageFlipTime <= lastFlipTimeFrame
96
+ ) {
97
+ this.result.texts = this.loanReturnWarning;
98
+ this.result.renewNow = false; // not viewed
99
+ this.showToastMessage();
100
+ } else if (lastPageFlipTime >= lastFlipTimeFrame) {
101
+ this.result.texts = '';
102
+ this.result.renewNow = true; // viewed in last time frame
103
+ }
104
+
105
+ return this.result;
106
+ }
107
+
108
+ /**
109
+ * set current time in indexedDB when user viewed a new page
110
+ */
111
+ async setPageChangedTime() {
112
+ await this.localCache.set({
113
+ key: `${this.identifier}-pageChangedTime`,
114
+ value: new Date(), // current time
115
+ ttl: Number(this.loanRenewConfig.totalTime),
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Show toast messages on some specific loan renew features. e.g.
121
+ * - show success msg when book is auto renewed
122
+ * - show success msg when book is auto returned
123
+ * - show warninig msg when book is about to auto returned
124
+ */
125
+ async showToastMessage() {
126
+ const iaBookActions = document.querySelector('ia-book-actions').shadowRoot;
127
+ let toastTemplate = iaBookActions.querySelector('toast-template');
128
+ if (!toastTemplate) {
129
+ toastTemplate = document.createElement('toast-template');
130
+ }
131
+ await iaBookActions.appendChild(toastTemplate);
132
+
133
+ const config = new ToastConfig();
134
+ config.texts = this.result.texts?.replace(/#time/, this.result.timeLeft);
135
+ config.dismisOnClick = true;
136
+ toastTemplate.showToast({
137
+ config,
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Helper function to get time difference
143
+ *
144
+ * @param {Date} date
145
+ * @param {Number} minutes
146
+ * @param {String} op
147
+ *
148
+ * @returns {Object} Date
149
+ */
150
+ changeTime(date, minutes, op) {
151
+ if (date === undefined) return nothing;
152
+
153
+ if (op === 'sub') {
154
+ return new Date(date.getTime() - minutes * 1000);
155
+ }
156
+
157
+ return new Date(date.getTime() + minutes * 1000);
158
+ }
159
+ }
@@ -3,8 +3,6 @@ import { html, css, LitElement, nothing } from 'lit';
3
3
 
4
4
  import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
5
5
  import { ModalConfig } from '@internetarchive/modal-manager';
6
- import '@internetarchive/toast-manager';
7
- import { ToastConfig } from '@internetarchive/toast-manager';
8
6
  import { LocalCache } from '@internetarchive/local-cache';
9
7
 
10
8
  import './components/collapsible-action-group.js';
@@ -13,10 +11,10 @@ import './components/text-group.js';
13
11
  import './components/info-icon.js';
14
12
  import './components/timer-countdown.js';
15
13
 
16
- import { mobileContainerWidth } from './core/config/constants.js';
17
14
  import { GetLendingActions } from './core/services/get-lending-actions.js';
15
+ import { mobileContainerWidth } from './core/config/constants.js';
18
16
  import { LoanTokenPoller } from './core/services/loan-token-poller.js';
19
- import { OneHourLoanRenew } from './core/services/one-hour-loan-renew.js';
17
+ import { LoanRenewHelper } from './core/services/loan-renew-helper.js';
20
18
 
21
19
  export const events = {
22
20
  browseExpired: 'IABookReader:BrowsingHasExpired',
@@ -62,17 +60,6 @@ export default class IABookActions extends LitElement {
62
60
  this.disableActionGroup = false;
63
61
  this.modal = undefined;
64
62
  this.tokenDelay = 120000; // 2 minutes
65
- this.localCache = new LocalCache({
66
- namespace: 'loanRenew',
67
- });
68
-
69
- // loan auto renew
70
- this.isPageChanged = false;
71
- this.loanRenewConfig = {};
72
- this.loanRenewResult = {
73
- texts: null,
74
- renewNow: false,
75
- };
76
63
 
77
64
  // private props
78
65
  this.primaryActions = [];
@@ -82,19 +69,39 @@ export default class IABookActions extends LitElement {
82
69
  this.lendingOptions = {};
83
70
  this.borrowType = ''; // (browsed|borrowed)
84
71
  this.consecutiveLoanCounts = 1; // consecutive loan count
72
+
73
+ // loan auto renew
85
74
  this.browserTimer = undefined;
75
+ this.loanRenewConfig = {};
86
76
 
87
- this._shadowRoot = this.attachShadow({ mode: "open" });
88
- this.bindLoanRenewEvents();
77
+ /**
78
+ * contains loan auto-renew response
79
+ * - texts:- texts messages need to show in toast message
80
+ * - renewNow:- key to determine if need to renew now
81
+ */
82
+ this.loanRenewResult = {
83
+ texts: null,
84
+ renewNow: false,
85
+ };
86
+
87
+ this._shadowRoot = this.attachShadow({ mode: 'open' });
89
88
  }
90
89
 
91
90
  disconnectedCallback() {
92
- this.tokenPoller?.disconnectedInterval();
91
+ this.tokenPoller.disconnectedInterval();
93
92
  window?.Sentry?.captureMessage('disconnectedCallback');
94
93
  this.disconnectResizeObserver();
95
94
  }
96
95
 
97
96
  firstUpdated() {
97
+ // bind auto loan renew events
98
+ this.bindLoanRenewEvents();
99
+
100
+ // localCache used for auto-loan-renew
101
+ this.localCache = new LocalCache({
102
+ namespace: 'loanRenew',
103
+ });
104
+
98
105
  if (!this.sharedObserver) {
99
106
  this.sharedObserver = new SharedResizeObserver();
100
107
  this.setupResizeObserver();
@@ -113,112 +120,6 @@ export default class IABookActions extends LitElement {
113
120
  }
114
121
  }
115
122
 
116
- bindLoanRenewEvents() {
117
- // dispatched this event from bookreader page changed
118
- window.addEventListener('BookReader:pageChanged', () => {
119
- // early return if book is not browsed (1 hour borrow)
120
- if (this.borrowType === 'browsed') {
121
- this.isPageChanged = true;
122
- this.executeLoanRenew();
123
- }
124
-
125
- return nothing;
126
- });
127
-
128
- // dispatched this event from timer-countdown
129
- this.addEventListener('IABookActions:loanRenew', () => {
130
- this.isPageChanged = false;
131
- this.executeLoanRenew();
132
- });
133
- }
134
-
135
- async executeLoanRenew() {
136
- this.loanRenewResult = await new OneHourLoanRenew(
137
- this.isPageChanged,
138
- this.identifier,
139
- this.localCache,
140
- this.loanRenewConfig
141
- );
142
-
143
- console.log(this.loanRenewResult);
144
-
145
- this.showToastMessage();
146
-
147
- if (this.loanRenewResult.renewNow) {
148
- this.tokenPoller.disconnectedInterval();
149
- await this.browseHasRenew();
150
- }
151
-
152
- return nothing;
153
- }
154
-
155
- async showToastMessage() {
156
- let toastTemplate = this.shadowRoot.querySelector('toast-template');
157
- if (!toastTemplate) {
158
- toastTemplate = document.createElement('toast-template');
159
- }
160
-
161
- await this.shadowRoot.appendChild(toastTemplate)
162
- // await document.body.appendChild(toastTemplate);
163
-
164
- const config = new ToastConfig();
165
- config.texts = this.loanRenewResult?.texts;
166
- config.dismisOnClick = true;
167
-
168
- toastTemplate.showToast({
169
- config
170
- });
171
- }
172
-
173
- async browseHasExpired() {
174
- const currStatus = { ...this.lendingStatus, browsingExpired: true };
175
- this.lendingStatus = currStatus;
176
-
177
- // remove respected key:value
178
- await this.localCache.delete(`${this.identifier}-loanTime`);
179
- await this.localCache.delete(`${this.identifier}-pageChangedTime`);
180
-
181
- // show message after browsed book is expired.
182
- this.loanRenewResult.renewNow = false;
183
- this.loanRenewResult.texts =
184
- 'This book has been auto-returned due to inactivity.';
185
-
186
- this.showToastMessage();
187
- }
188
-
189
- async browseHasRenew() {
190
- const loanTime = await this.localCache.get(`${this.identifier}-loanTime`);
191
- const secondsLeft = (loanTime - new Date()) / 1000; // different in seconds
192
-
193
- const currStatus = {
194
- ...this.lendingStatus,
195
- user_has_browsed: true,
196
- browsingExpired: false,
197
- secondsLeftOnLoan: secondsLeft,
198
- };
199
- this.lendingStatus = currStatus;
200
-
201
- this.startBrowseTimer();
202
- }
203
-
204
- startBrowseTimer() {
205
- clearTimeout(this.browserTimer);
206
-
207
- const {
208
- browsingExpired,
209
- user_has_browsed,
210
- secondsLeftOnLoan,
211
- } = this.lendingStatus;
212
- if (!user_has_browsed || browsingExpired) {
213
- return;
214
- }
215
-
216
- const timeLeft = secondsLeftOnLoan * 1000;
217
- this.browserTimer = setTimeout(() => {
218
- this.browseHasExpired();
219
- }, timeLeft);
220
- }
221
-
222
123
  /** SharedObserver resize handler */
223
124
  handleResize(entry) {
224
125
  // if you are observing multiple targets,
@@ -277,7 +178,7 @@ export default class IABookActions extends LitElement {
277
178
  'browsingExpired' in this.lendingStatus &&
278
179
  this.lendingStatus?.browsingExpired;
279
180
  if (hasExpired) {
280
- if (!this.tokenPoller.disconnectedInterval) {
181
+ if (!this.tokenPoller) {
281
182
  window?.Sentry?.captureMessage(
282
183
  'setupLendingToolbarActions hasExpired - no tokenPoller'
283
184
  );
@@ -306,18 +207,140 @@ export default class IABookActions extends LitElement {
306
207
  return;
307
208
  }
308
209
 
309
- // if (book is about to renew)
310
- // - start token to poller around this.tokenDelay time until renew is completed
311
- // else
312
- // - start token poller immediately
210
+ /**
211
+ * tokenPoller determines if user has loan token for this book
212
+ * - if book is going to renew, need to wait until renew is completed
213
+ * - otherwise execute immediately
214
+ */
313
215
  setTimeout(
314
216
  () => {
315
217
  this.startLoanTokenPoller();
218
+
219
+ if (!this.loanRenewResult.renewNow) {
220
+ this.resetTimerCountState();
221
+ }
316
222
  },
317
223
  this.loanRenewResult.renewNow ? this.tokenDelay : 100
318
224
  );
319
225
  }
320
226
 
227
+ /**
228
+ * Bind 1 hour loan auto renew,
229
+ * There are two events we want to use,
230
+ * 1. BookReader:pageChanged - dispatched from bookreader side
231
+ * 2. IABookActions:loanRenew - dispatched from timer-countdown component
232
+ */
233
+ bindLoanRenewEvents() {
234
+ // dispatched this event from bookreader page changed
235
+ window.addEventListener('BookReader:pageChanged', () => {
236
+ // early return if book is not browsed (1 hour borrow)
237
+ if (this.borrowType === 'browsed') {
238
+ this.autoLoanRenewChecker(true);
239
+ }
240
+
241
+ return nothing;
242
+ });
243
+
244
+ /**
245
+ * this event dispatched from timer-countdown component for:
246
+ * 1. user turned book page after showing auto-returned warning message
247
+ * 2. show warning message having remaining time
248
+ */
249
+ this.addEventListener('IABookActions:loanRenew', async event => {
250
+ const eventDetails = event.detail;
251
+
252
+ await this.autoLoanRenewChecker(false);
253
+
254
+ // show warning message with remaining time to auto returned it.
255
+ if (this.loanRenewHelper.result.renewNow === false) {
256
+ this.loanRenewHelper.result.timeLeft = eventDetails.timeLeft;
257
+ this.loanRenewHelper.showToastMessage();
258
+ }
259
+ });
260
+ }
261
+
262
+ async autoLoanRenewChecker(hasPageChanged = false) {
263
+ this.loanRenewHelper = new LoanRenewHelper(
264
+ hasPageChanged,
265
+ this.identifier,
266
+ this.localCache,
267
+ this.loanRenewConfig
268
+ );
269
+
270
+ await this.loanRenewHelper.handleLoanRenew();
271
+ this.loanRenewResult = this.loanRenewHelper.result;
272
+ }
273
+
274
+ async browseHasRenew() {
275
+ const loanTime = await this.localCache.get(`${this.identifier}-loanTime`);
276
+ const secondsLeft = (loanTime - new Date()) / 1000; // different in seconds
277
+
278
+ const currStatus = {
279
+ ...this.lendingStatus,
280
+ user_has_browsed: true,
281
+ browsingExpired: false,
282
+ secondsLeftOnLoan: secondsLeft,
283
+ };
284
+ this.lendingStatus = currStatus;
285
+
286
+ this.startBrowseTimer();
287
+ }
288
+
289
+ startBrowseTimer() {
290
+ clearTimeout(this.browserTimer);
291
+
292
+ const {
293
+ browsingExpired,
294
+ user_has_browsed,
295
+ secondsLeftOnLoan,
296
+ } = this.lendingStatus;
297
+ if (!user_has_browsed || browsingExpired) {
298
+ return;
299
+ }
300
+
301
+ const timeLeft = secondsLeftOnLoan * 1000;
302
+ this.browserTimer = setTimeout(() => {
303
+ this.browseHasExpired();
304
+ }, timeLeft);
305
+ }
306
+
307
+ /**
308
+ * Execute when loan is expired
309
+ */
310
+ async browseHasExpired() {
311
+ const currStatus = { ...this.lendingStatus, browsingExpired: true };
312
+ this.lendingStatus = currStatus;
313
+
314
+ // remove respected key:value for loan-renew
315
+ await this.localCache.delete(`${this.identifier}-loanTime`);
316
+ await this.localCache.delete(`${this.identifier}-pageChangedTime`);
317
+
318
+ // show message after browsed book is expired.
319
+ this.loanRenewResult.renewNow = false;
320
+ this.loanRenewResult.texts =
321
+ 'This book has been automatically returned due to inactivity.';
322
+
323
+ this.loanRenewHelper.showToastMessage();
324
+ }
325
+
326
+ /**
327
+ * Reset timer animation state after book renewed
328
+ */
329
+ async resetTimerCountState() {
330
+ const timerCountdown = this.shadowRoot.querySelector('timer-countdown');
331
+ const animationCircle = timerCountdown.shadowRoot.querySelector('.circle');
332
+
333
+ timerCountdown.style?.setProperty(
334
+ '--secondsLeft',
335
+ `${Number(this.lendingStatus.secondsLeftOnLoan)}s`
336
+ );
337
+
338
+ animationCircle.style.animationName = 'none';
339
+ setTimeout(() => {
340
+ animationCircle.style.animationName = 'circletimer';
341
+ }, 100);
342
+ }
343
+
321
344
  render() {
322
345
  if (this.barType === 'title') {
323
346
  return html`<section class="lending-wrapper">
@@ -340,7 +363,7 @@ export default class IABookActions extends LitElement {
340
363
  get timerCountdownTemplate() {
341
364
  return this.borrowType === 'browsed'
342
365
  ? html`<timer-countdown
343
- time=${Number(this.lendingStatus.secondsLeftOnLoan)}
366
+ .time=${Number(this.lendingStatus.secondsLeftOnLoan)}
344
367
  .autoCheckAt=${this.loanRenewConfig.autoCheckAt}
345
368
  ></timer-countdown>`
346
369
  : nothing;
@@ -362,6 +385,7 @@ export default class IABookActions extends LitElement {
362
385
  ?hasAdminAccess=${this.hasAdminAccess}
363
386
  ?disabled=${this.disableActionGroup}
364
387
  ?renewNow=${this.loanRenewResult.renewNow}
388
+ @loanAutoRenewed=${this.handleLoanAutoRenewed}
365
389
  @lendingActionError=${this.handleLendingActionError}
366
390
  @toggleActionGroup=${this.handleToggleActionGroup}
367
391
  >
@@ -371,6 +395,21 @@ export default class IABookActions extends LitElement {
371
395
  `;
372
396
  }
373
397
 
398
+ /**
399
+ * Execute after auto loan renewed is completed
400
+ * - show success message
401
+ * - then change the remaining time
402
+ * - reset timer state
403
+ */
404
+ async handleLoanAutoRenewed() {
405
+ if (this.loanRenewResult.renewNow) {
406
+ this.tokenPoller.disconnectedInterval();
407
+ await this.loanRenewHelper.showToastMessage();
408
+ await this.browseHasRenew();
409
+ await this.resetTimerCountState();
410
+ }
411
+ }
412
+
374
413
  /**
375
414
  * enable access of borrowed/browsed books
376
415
  */
@@ -509,11 +548,13 @@ export default class IABookActions extends LitElement {
509
548
  }
510
549
 
511
550
  get textGroupTemplate() {
512
- return html`<text-group
513
- textClass=${this.textClass}
514
- .texts=${this.primaryTitle}
515
- >
516
- </text-group>`;
551
+ return this.primaryTitle
552
+ ? html`<text-group
553
+ textClass=${this.textClass}
554
+ .texts=${this.primaryTitle}
555
+ >
556
+ </text-group>`
557
+ : nothing;
517
558
  }
518
559
 
519
560
  get hasAdminAccess() {
@@ -182,9 +182,7 @@ describe('Borrow status actions', () => {
182
182
  },
183
183
  ];
184
184
 
185
- expect(el.primaryTitle).to.equal(
186
- 'Renewable every hour, pending availability.'
187
- );
185
+ expect(el.primaryTitle).to.equal('');
188
186
  expect(el.primaryActions.length).to.equal(2);
189
187
  expect(el.primaryActions[0].text).to.equal('Log In and Borrow');
190
188
  expect(el.primaryActions[0].text).to.equal(expectedPrimaryActions[0].text);
@@ -253,7 +251,6 @@ describe('Browsing expired status', () => {
253
251
  await aTimeout(1500); // wait for 1.5 second
254
252
  await el.updateComplete;
255
253
 
256
- expect(el.primaryTitle).contains('Your loan has expired.');
257
254
  expect(el.primaryActions[0].text).to.equal('Borrow again');
258
255
  });
259
256
 
@@ -289,7 +286,6 @@ describe('Browsing expired status', () => {
289
286
  await aTimeout(1500); // wait for 1.5 sec
290
287
 
291
288
  expect(eventReceived).to.equal(true);
292
- expect(el.primaryTitle).contains('Your loan has expired.');
293
289
  expect(el.primaryActions[0].text).to.equal('Borrow again');
294
290
  expect(el.tokenPoller.loanTokenInterval).to.equal(undefined);
295
291
  });
@@ -1,148 +0,0 @@
1
- import { nothing } from 'lit';
2
-
3
- /**
4
- * This class is used to determine if use is eligible for auto renew loan.
5
- *
6
- */
7
- export class OneHourLoanRenew {
8
- constructor(isPageChanged, identifier, localCache, loanRenewConfig) {
9
- this.isPageChanged = isPageChanged;
10
- this.identifier = identifier;
11
- this.localCache = localCache;
12
- this.loanRenewConfig = loanRenewConfig;
13
-
14
- // messages for auto return machenism
15
- this.loanRenewMessage = 'This book has been renewed for another hour.';
16
- this.loanReturnMessage =
17
- 'This book has been auto-returned due to inactivity.';
18
- this.loanReturnWarning =
19
- 'This book will be auto-returned in 10 minutes unless you turn a page.';
20
-
21
- // private props
22
- this.loanRenewResult = {
23
- texts: null,
24
- renewNow: false,
25
- };
26
-
27
- return this.handleLoanRenew();
28
- }
29
-
30
- handleLoanRenew() {
31
- try {
32
- // when user click/flip on book page
33
- if (this.isPageChanged) {
34
- console.log('page changed!!');
35
- return this.pageChanged(); // user clicked on page
36
- }
37
-
38
- return this.autoChecker(); // auto checker at 50th minute
39
- } catch (error) {
40
- console.log(error);
41
- }
42
-
43
- return nothing;
44
- }
45
-
46
- async pageChanged() {
47
- const {
48
- totalTime, // 60
49
- autoCheckAt, // 50
50
- } = this.loanRenewConfig;
51
- const currentTime = new Date();
52
- const loanTime = await this.localCache.get(`${this.identifier}-loanTime`);
53
-
54
- const lastTimeFrame = this.changeTime(
55
- loanTime,
56
- totalTime - autoCheckAt,
57
- 'add'
58
- );
59
-
60
- // if user viewed new page in last 10 minutes, renew immediately
61
- if (currentTime >= lastTimeFrame) {
62
- console.log('Yes, borrow it again now!');
63
- this.loanRenewResult.texts = this.loanRenewMessage;
64
- this.loanRenewResult.renewNow = true;
65
-
66
- this.setLoanRenewedTime();
67
- }
68
-
69
- this.setPageChangedTime();
70
- return this.loanRenewResult;
71
- }
72
-
73
- async autoChecker() {
74
- const currentTime = new Date();
75
- const { pageChangedInLast } = this.loanRenewConfig;
76
- const lastPageFlipTime = await this.localCache.get(
77
- `${this.identifier}-pageChangedTime`
78
- );
79
-
80
- // last 15 min if used flipped a page.
81
- const lastFlipTimeFrame = this.changeTime(
82
- currentTime,
83
- pageChangedInLast,
84
- 'sub'
85
- ); // 15 seconds
86
-
87
- if (
88
- lastPageFlipTime === undefined ||
89
- lastPageFlipTime <= lastFlipTimeFrame
90
- ) {
91
- // not viewed
92
- console.log("Not viewed, don't borrow it!");
93
- this.loanRenewResult.texts = this.loanReturnWarning;
94
- this.loanRenewResult.renewNow = false;
95
- } else if (lastPageFlipTime >= lastFlipTimeFrame) {
96
- // viewed in last time frame
97
- console.log('Yes viewed, borrow it again!');
98
- this.loanRenewResult.texts = this.loanRenewMessage;
99
- this.loanRenewResult.renewNow = true;
100
-
101
- this.setLoanRenewedTime();
102
- }
103
-
104
- return this.loanRenewResult;
105
- }
106
-
107
- // save page changed time in indexedDB
108
- async setPageChangedTime() {
109
- await this.localCache.set({
110
- key: `${this.identifier}-pageChangedTime`,
111
- value: new Date(), // current time
112
- ttl: Number(this.loanRenewConfig.totalTime),
113
- });
114
- }
115
-
116
- async setLoanRenewedTime() {
117
- try {
118
- // last 15 min if used flipped a page.
119
- const expireDate = this.changeTime(
120
- new Date(),
121
- this.loanRenewConfig.totalTime,
122
- 'add'
123
- ); // 15 seconds
124
-
125
- // set a value
126
- await this.localCache.set({
127
- key: `${this.identifier}-loanTime`,
128
- value: expireDate, // expireDate time
129
- ttl: Number(this.loanRenewConfig.totalTime),
130
- });
131
-
132
- // delete pageChangedTime when book is auto renew at nth minute
133
- await this.localCache.delete(`${this.identifier}-pageChangedTime`);
134
- } catch (error) {
135
- console.log(error);
136
- }
137
- }
138
-
139
- changeTime(date, minutes, op) {
140
- if (date === undefined) return nothing;
141
-
142
- if (op === 'sub') {
143
- return new Date(date.getTime() - minutes * 1000);
144
- }
145
-
146
- return new Date(date.getTime() + minutes * 1000);
147
- }
148
- }