@internetarchive/ia-book-actions 0.0.29-alpha9 → 0.1.0

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.
@@ -1,20 +1,47 @@
1
1
  import { html, css, LitElement } from 'lit';
2
+ import { sentryLogs } from '../core/config/sentry-events.js';
2
3
 
3
4
  export default class TimerCountdown extends LitElement {
4
5
  static get properties() {
5
6
  return {
6
- time: { type: Number },
7
- autoCheckAt: { type: Number },
8
- resetTimerCountdown: { type: Boolean },
7
+ secondsLeftOnLoan: { type: Number }, // in seconds
8
+ loanTotalTime: { type: Number }, // in seconds
9
+ loanRenewAtLast: { type: Number }, // in seconds
9
10
  };
10
11
  }
11
12
 
12
13
  constructor() {
13
14
  super();
14
- this.time = 0;
15
- this.autoCheckAt = 0;
15
+
16
+ /**
17
+ * seconds left in current loan
18
+ * @type {number}
19
+ */
20
+ this.secondsLeftOnLoan = 0;
21
+
22
+ /**
23
+ * total seconds a loan does have
24
+ * @type {number}
25
+ */
26
+ this.loanTotalTime = 0;
27
+
28
+ /**
29
+ * at the remaining time, we attempt to renew current loan
30
+ * @type {number}
31
+ */
32
+ this.loanRenewAtLast = 0;
33
+
34
+ /**
35
+ * setInterval executed interval
36
+ * @type {number}
37
+ */
16
38
  this.timerInterval = undefined;
17
- this.resetTimerCountdown = false;
39
+
40
+ /**
41
+ * delay seconds in setInterval function
42
+ * @type {number}
43
+ */
44
+ this.timerExecutionSeconds = 0;
18
45
  }
19
46
 
20
47
  disconnectedCallback() {
@@ -22,23 +49,32 @@ export default class TimerCountdown extends LitElement {
22
49
  }
23
50
 
24
51
  updated(changed) {
25
- if (changed.has('time') && this.time > 0) {
52
+ if (changed.has('secondsLeftOnLoan') && this.secondsLeftOnLoan > 0) {
26
53
  clearInterval(this.timerInterval);
27
54
  this.timerCountdown();
28
55
  }
29
56
  }
30
57
 
31
58
  timerCountdown() {
59
+ // - let just execute setInterval in every 1 minute
60
+ this.timerExecutionSeconds = 60;
61
+
32
62
  this.timerInterval = setInterval(() => {
33
- this.time -= 1;
63
+ this.secondsLeftOnLoan -= this.timerExecutionSeconds;
64
+ const secondsLeft = Math.round(this.secondsLeftOnLoan);
34
65
 
35
- // execute 50th minute check
36
- if (Math.round(this.time) === this.autoCheckAt) {
66
+ /**
67
+ * execute from last 10th minute to 0th minute
68
+ * - 10th - to check if user has viewed
69
+ * - till 0th - to show warning msg with remaining time to auto expired
70
+ * @see IABookActions::bindLoanRenewEvents
71
+ */
72
+ if (secondsLeft <= this.loanRenewAtLast) {
37
73
  this.dispatchEvent(
38
- new CustomEvent('autoRenewAttempt', {
74
+ new CustomEvent('IABookActions:loanRenew', {
39
75
  detail: {
40
- remainingT: this.time,
41
- bookPageChange: false,
76
+ hasPageChanged: false,
77
+ secondsLeft,
42
78
  },
43
79
  bubbles: true,
44
80
  composed: true,
@@ -46,65 +82,94 @@ export default class TimerCountdown extends LitElement {
46
82
  );
47
83
  }
48
84
 
49
- if (this.time === 0) clearInterval(this.timerInterval);
50
- }, 1000);
85
+ // clear interval
86
+ if (secondsLeft <= 60) {
87
+ clearInterval(this.timerInterval);
88
+ window?.Sentry?.captureMessage(sentryLogs.clearOneHourTimer);
89
+ }
90
+ }, this.timerExecutionSeconds * 1000);
91
+ }
92
+
93
+ /**
94
+ * get remaining time with timeunit
95
+ *
96
+ * @memberof TimerCountdown
97
+ * @return string
98
+ */
99
+ get remainingTime() {
100
+ const unitOfTime = 'minute';
101
+ let timeLeft = Math.round(this.secondsLeftOnLoan);
102
+
103
+ // convert time from second to minute
104
+ timeLeft = Math.ceil(timeLeft / 60);
105
+
106
+ return timeLeft !== 1
107
+ ? `${timeLeft} ${unitOfTime}s`
108
+ : `${timeLeft} ${unitOfTime}`;
51
109
  }
52
110
 
53
111
  render() {
54
- return html`<div id="countdown">
55
- <div id="countdown-number">${Math.round(this.time)}</div>
56
- <svg>
57
- <circle r="18" cx="20" cy="20"></circle>
112
+ return html`
113
+ <svg
114
+ width="16"
115
+ height="16"
116
+ viewBox="0 0 100 100"
117
+ xmlns="http://www.w3.org/2000/svg"
118
+ >
119
+ <circle class="circle" cx="50" cy="50" r="50" />
58
120
  </svg>
59
- </div>`;
121
+ <span class="sr-only">${this.remainingTime}</span>
122
+ <span>${this.remainingTime}</span>
123
+ `;
60
124
  }
61
125
 
62
126
  static get styles() {
127
+ const white = css`var(--white, #fff)`;
128
+ const timerSecondsLeft = css`var(--secondsLeftOnLoan, 6000s)`; //
129
+ const timerStrokeLeft = css`var(--strokeLeftOnLoan, 315)`;
130
+
63
131
  return css`
64
132
  :host {
65
133
  right: 5px;
66
- font-size: 14px;
67
134
  position: absolute;
68
135
  }
69
- #countdown-number {
70
- color: white;
71
- display: inline-block;
72
- line-height: 40px;
73
- margin-right: 12px;
136
+
137
+ @keyframes circletimer {
138
+ 0% {
139
+ stroke-dashoffset: ${timerStrokeLeft};
140
+ stroke-dasharray: 315;
141
+ }
142
+ 100% {
143
+ stroke-dashoffset: 0;
144
+ stroke-dasharray: 315;
145
+ }
74
146
  }
75
147
 
76
148
  svg {
77
- position: absolute;
78
- top: 0;
79
- right: 0;
80
- width: 40px;
81
- height: 40px;
82
- transform: rotateY(-180deg) rotateZ(-90deg);
149
+ background-color: ${white};
150
+ border-radius: 50px;
151
+ border: 2px solid ${white};
152
+ transform: rotateZ(-90deg);
83
153
  }
84
154
 
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;
155
+ svg .circle {
156
+ stroke: #000;
157
+ stroke-width: 100px;
158
+ fill: ${white};
159
+ stroke-dashoffset: 315;
160
+ stroke-dasharray: 0;
161
+ animation: ${timerSecondsLeft} circletimer linear;
93
162
  }
94
163
 
95
- @keyframes countdown {
96
- from {
97
- stroke-dashoffset: 0px;
98
- }
99
- to {
100
- stroke-dashoffset: 113px;
101
- }
102
- }
103
- .hidden {
104
- display: none;
105
- }
106
- .visible {
107
- display: inline-block;
164
+ .sr-only {
165
+ position: absolute;
166
+ left: -9999px;
167
+ width: 1px;
168
+ height: 1px;
169
+ margin: 0;
170
+ padding: 0;
171
+ border: none;
172
+ overflow: hidden;
108
173
  }
109
174
  `;
110
175
  }
@@ -13,6 +13,7 @@ export const analyticsCategories = {
13
13
  export const analyticsActions = {
14
14
  browse: 'Borrow-1Hour',
15
15
  browseAgain: 'Borrow-Again',
16
+ browseLoanRenew: 'Borrow-Loan-Renew',
16
17
  borrow: 'Borrow-14Days',
17
18
  waitlistJoin: 'JoinWaitlist',
18
19
  waitlistLeave: 'LeaveWaitlist',
@@ -0,0 +1,20 @@
1
+ /**
2
+ * sentry logs/messages we send for different actions....
3
+ */
4
+ export const sentryLogs = {
5
+ disconnectedCallback: 'IABookActions:disconnectedCallback',
6
+ bookHasRenewed:
7
+ 'IABookActions:handleLoanAutoRenewed - book has renewed for next one hour',
8
+ browseHasExpired:
9
+ 'IABookActions:browseHasExpired - one-hour loan has been expired',
10
+ bookWasExpired:
11
+ 'IABookActions:setupLendingToolbarActions - book was expired at intial, no tokenPoller',
12
+ clearTokenPoller:
13
+ 'IABookActions:startLoanTokenPoller - clearing token poller interval',
14
+ clearOneHourTimer:
15
+ 'IABookActions:timerCountdown - one-hour timer interval cleared',
16
+ bookAccessed: 'IABookActions:bookAccessed',
17
+ handleLoanTokenPoller: 'IABookActions:handleLoanTokenPoller',
18
+ setConsecutiveLoanCounts: 'IABookActions:setConsecutiveLoanCounts',
19
+ actionsHandlerService: 'IABookActions:actionsHandlerService',
20
+ };
@@ -36,7 +36,7 @@ export default class ActionsConfig {
36
36
  firstBrowseConfig() {
37
37
  return {
38
38
  id: 'browseBook',
39
- text: 'Borrow for 1 hour',
39
+ text: 'Borrow',
40
40
  className: 'primary',
41
41
  analyticsEvent: {
42
42
  category: this.analyticsCategories.preview,
@@ -48,7 +48,7 @@ export default class ActionsConfig {
48
48
  browseAgainConfig() {
49
49
  return {
50
50
  id: 'browseBookAgain',
51
- text: 'Borrow again',
51
+ text: 'Borrow',
52
52
  className: 'primary',
53
53
  analyticsEvent: {
54
54
  category: this.analyticsCategories.browse,
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable */
2
+ import { sentryLogs } from '../../config/sentry-events.js';
2
3
 
3
4
  /**
4
5
  * Helper to call loan service
@@ -64,7 +65,9 @@ export default async function ActionsHandlerService(options) {
64
65
  option?.error(data);
65
66
  }
66
67
  });
67
- } catch (err) {
68
- window?.Sentry?.captureException(err);
68
+ } catch (error) {
69
+ window?.Sentry?.captureException(
70
+ `${sentryLogs.actionsHandlerService} - Error: ${error}`
71
+ );
69
72
  }
70
73
  }
@@ -1,6 +1,8 @@
1
1
  import { LitElement } from 'lit';
2
2
 
3
3
  import { URLHelper } from '../../config/url-helper.js';
4
+ import { sentryLogs } from '../../config/sentry-events.js';
5
+
4
6
  import ActionsHandlerService from './actions-handler-service.js';
5
7
  import * as Cookies from '../doc-cookies.js';
6
8
 
@@ -15,10 +17,13 @@ import * as Cookies from '../doc-cookies.js';
15
17
  export default class ActionsHandler extends LitElement {
16
18
  constructor() {
17
19
  super();
18
- this.identifier = '';
19
- this.ajaxTimeout = 6000;
20
- this.returnUrl = '';
21
- this.autoRenewConfig = {};
20
+
21
+ /**
22
+ * wait untill borrow is complete, then refresh the page
23
+ * @type {number}
24
+ */
25
+ this.waitUntillBorrowComplete = 6; // in seconds
26
+
22
27
  this.bindEvents();
23
28
  }
24
29
 
@@ -30,7 +35,7 @@ export default class ActionsHandler extends LitElement {
30
35
  );
31
36
  }
32
37
 
33
- async bindEvents() {
38
+ bindEvents() {
34
39
  this.addEventListener('browseBook', ({ detail }) => {
35
40
  this.handleBrowseIt();
36
41
  this.setConsecutiveLoanCounts();
@@ -45,9 +50,8 @@ export default class ActionsHandler extends LitElement {
45
50
  this.sendEvent(category, action);
46
51
  });
47
52
 
48
- this.addEventListener('autoBrowseBook', ({ detail }) => {
49
- console.log('auto renew handlerExecuted!!');
50
- this.handleAutoBrowseIt();
53
+ this.addEventListener('loanRenew', ({ detail }) => {
54
+ this.handleLoanRenew();
51
55
  const { category, action } = detail.event;
52
56
  this.sendEvent(category, action);
53
57
  });
@@ -106,12 +110,12 @@ export default class ActionsHandler extends LitElement {
106
110
  handleBrowseIt() {
107
111
  const action = 'browse_book';
108
112
  this.dispatchToggleActionGroup();
109
- this.setBrowseTimeSession();
110
113
 
111
114
  ActionsHandlerService({
112
115
  action,
113
116
  identifier: this.identifier,
114
117
  success: () => {
118
+ this.setBrowseTimeSession();
115
119
  this.handleReadItNow();
116
120
  },
117
121
  error: data => {
@@ -120,15 +124,19 @@ export default class ActionsHandler extends LitElement {
120
124
  });
121
125
  }
122
126
 
123
- handleAutoBrowseIt() {
127
+ handleLoanRenew() {
124
128
  const action = 'renew_loan';
125
- this.setBrowseTimeSession();
126
129
 
127
130
  ActionsHandlerService({
128
131
  action,
129
132
  identifier: this.identifier,
130
- success: () => {
131
- // console.log('success!');
133
+ success: data => {
134
+ this.setBrowseTimeSession();
135
+ this.dispatchEvent(
136
+ new CustomEvent('loanAutoRenewed', {
137
+ detail: { action, data },
138
+ })
139
+ );
132
140
  },
133
141
  error: data => {
134
142
  this.dispatchActionError(action, data);
@@ -260,7 +268,7 @@ export default class ActionsHandler extends LitElement {
260
268
  // redirection on details page after 5 seconds because borrowing book takes time to create item creation.
261
269
  setTimeout(() => {
262
270
  URLHelper.goToUrl(redirectTo, true);
263
- }, this.ajaxTimeout);
271
+ }, this.waitUntillBorrowComplete * 1000);
264
272
  }
265
273
 
266
274
  // save consecutive loan count for borrow
@@ -281,21 +289,31 @@ export default class ActionsHandler extends LitElement {
281
289
  // set new value
282
290
  Cookies.setItem(storageKey, newCount, date, '/');
283
291
  } catch (error) {
284
- window?.Sentry?.captureException(error);
292
+ window?.Sentry?.captureException(
293
+ `${sentryLogs.setConsecutiveLoanCounts} - Error: ${error}`
294
+ );
285
295
  this.sendEvent('Cookies-Error-Actions', error);
286
296
  }
287
297
  }
288
298
 
299
+ /**
300
+ * set browse time in indexedDB
301
+ */
289
302
  async setBrowseTimeSession() {
290
303
  try {
304
+ const expireDate = new Date(
305
+ new Date().getTime() + this.loanTotalTime * 1000
306
+ );
307
+
291
308
  // set a value
292
309
  await this.localCache.set({
293
310
  key: `${this.identifier}-loanTime`,
294
- value: new Date(), // current time
295
- ttl: Number(this.autoRenewConfig.totalTime),
311
+ value: expireDate,
312
+ ttl: Number(this.loanTotalTime),
296
313
  });
297
314
 
298
- await this.localCache.delete(`${this.identifier}-pageFlipTime`);
315
+ // delete pageChangedTime when book is auto renew at nth minute
316
+ await this.localCache.delete(`${this.identifier}-pageChangedTime`);
299
317
  } catch (error) {
300
318
  console.log(error);
301
319
  }
@@ -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,161 @@
1
+ /* eslint-disable class-methods-use-this */
2
+
3
+ import { nothing } from 'lit';
4
+
5
+ /**
6
+ * This class is used to determine if use is eligible for auto renew loan.
7
+ */
8
+ export class LoanRenewHelper {
9
+ constructor(hasPageChanged, identifier, localCache, loanRenewTimeConfig) {
10
+ this.hasPageChanged = hasPageChanged;
11
+ this.identifier = identifier;
12
+ this.localCache = localCache;
13
+ this.loanRenewTimeConfig = loanRenewTimeConfig;
14
+
15
+ // messages for auto return machenism
16
+ this.loanRenewMessage = 'This book has been renewed for 1 hour.';
17
+ this.loanReturnWarning =
18
+ 'This book will be automatically returned in #time #unitsOfTime unless you turn a page.';
19
+
20
+ // private props
21
+ this.result = {
22
+ texts: null,
23
+ renewNow: false,
24
+ };
25
+ }
26
+
27
+ handleLoanRenew() {
28
+ try {
29
+ if (this.hasPageChanged) {
30
+ return this.pageChanged(); // user clicked on page
31
+ }
32
+ return this.autoChecker(); // auto checker at last 10th minute
33
+ } catch (error) {
34
+ console.log(error);
35
+ }
36
+
37
+ return nothing;
38
+ }
39
+
40
+ /**
41
+ * Trigger this function when user has browsed a book and change the post
42
+ * - every time user change the page, we set current time in indexedDB
43
+ * - also check if need to auto renew current loan
44
+ *
45
+ * @returns {this.result}
46
+ */
47
+ async pageChanged() {
48
+ const {
49
+ loanTotalTime, // 60
50
+ loanRenewAtLast, // 50
51
+ } = this.loanRenewTimeConfig;
52
+ const currentTime = new Date();
53
+ const loanTime = await this.localCache.get(`${this.identifier}-loanTime`);
54
+
55
+ const lastTimeFrame = this.changeTime(
56
+ loanTime,
57
+ loanTotalTime - loanRenewAtLast,
58
+ 'add'
59
+ );
60
+
61
+ // if user viewed new page in last 10 minutes, renew immediately
62
+ if (lastTimeFrame !== null && currentTime >= lastTimeFrame) {
63
+ this.result.texts = this.loanRenewMessage;
64
+ this.result.renewNow = true;
65
+ }
66
+
67
+ this.setPageChangedTime();
68
+ return this.result;
69
+ }
70
+
71
+ /**
72
+ * Trigger this function when countdown hits at last 10th minute
73
+ * - if user is active, just renew the loan at last 10th minute
74
+ *
75
+ * @returns {this.result}
76
+ */
77
+ async autoChecker() {
78
+ const { pageChangedInLast } = this.loanRenewTimeConfig;
79
+ const pageChangedTime = await this.localCache.get(
80
+ `${this.identifier}-pageChangedTime`
81
+ );
82
+
83
+ // in last 15 min if user make any activity.
84
+ const pageChangeTimeFrame = this.changeTime(
85
+ new Date(),
86
+ pageChangedInLast,
87
+ 'sub'
88
+ ); // 15 seconds
89
+
90
+ if (
91
+ pageChangedTime === undefined ||
92
+ pageChangedTime <= pageChangeTimeFrame
93
+ ) {
94
+ this.result.texts = this.loanReturnWarning;
95
+ this.result.renewNow = false; // not viewed
96
+ } else if (pageChangedTime >= pageChangeTimeFrame) {
97
+ this.result.texts = '';
98
+ this.result.renewNow = true; // viewed in last time frame
99
+ }
100
+
101
+ return this.result;
102
+ }
103
+
104
+ /**
105
+ * set current time in indexedDB when user viewed a new page
106
+ */
107
+ async setPageChangedTime() {
108
+ await this.localCache.set({
109
+ key: `${this.identifier}-pageChangedTime`,
110
+ value: new Date(), // current time
111
+ ttl: Number(this.loanRenewTimeConfig.loanTotalTime),
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Texts we want to show in toast template including remaining time
117
+ * - eg. 1 minute, 2 minutes
118
+ *
119
+ * @param {String} texts
120
+ * @param {Number} secondsLeft
121
+ *
122
+ * @returns {String} // texts will be appear in toast template
123
+ */
124
+ getMessageTexts(texts, secondsLeft) {
125
+ const unitOfTime = 'minute';
126
+
127
+ let toastTexts = texts;
128
+ let timeLeft = secondsLeft;
129
+
130
+ // convert time from second to minute
131
+ timeLeft = Math.ceil(timeLeft / 60);
132
+
133
+ // replace #time variable with remaining time
134
+ toastTexts = toastTexts?.replace(/#time/, timeLeft);
135
+
136
+ // replace #unitsOfTime variable with minute/minutes
137
+ return toastTexts?.replace(
138
+ /#unitsOfTime/,
139
+ timeLeft !== 1 ? `${unitOfTime}s` : unitOfTime
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Helper function to get time difference
145
+ *
146
+ * @param {Date} date
147
+ * @param {Number} seconds
148
+ * @param {String} op - like sub, add, mul, sub
149
+ *
150
+ * @returns {Object} Date
151
+ */
152
+ changeTime(date, seconds, op) {
153
+ if (date === undefined) return null;
154
+
155
+ if (op === 'sub') {
156
+ return new Date(date.getTime() - seconds * 1000);
157
+ }
158
+
159
+ return new Date(date.getTime() + seconds * 1000);
160
+ }
161
+ }