@internetarchive/ia-book-actions 0.1.5 → 0.1.6-a1

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
@@ -128,14 +128,14 @@
128
128
  iaBookActions.modalConfig = modalConfig;
129
129
  iaBookActions.lendingBarPostInit = lendingBarPostInit;
130
130
  iaBookActions.tokenDelay = 2; // 2 seconds
131
- iaBookActions.returnUrl = 'https://openlibrary.org';
131
+ iaBookActions.returnUrl = '';
132
132
  iaBookActions.localCache = localCache;
133
133
 
134
- let loanTotalTime2Min = { loanTotalTime: 20, loanRenewAtLast: 15, pageChangedInLast: 5 };
135
- let loanTotalTime5Min = { loanTotalTime: 300, loanRenewAtLast: 290, pageChangedInLast: 15 };
136
- let loanTotalTime1Hour = { loanTotalTime: 3600, loanRenewAtLast: 600, pageChangedInLast: 900 };
134
+ let loanTotalTime2Min = { loanTotalTime: 60, loanRenewAtLast: 51, pageChangedInLast: 15 };
135
+ let loanTotalTime5Min = { loanTotalTime: 300, loanRenewAtLast: 180, pageChangedInLast: 15 };
136
+ let loanTotalTime1Hour = { loanTotalTime: 3600, loanRenewAtLast: 660, pageChangedInLast: 900 };
137
137
 
138
- iaBookActions.loanRenewTimeConfig = loanTotalTime5Min;
138
+ iaBookActions.loanRenewTimeConfig = loanTotalTime2Min;
139
139
 
140
140
  let lendingStatus1 = iaBookActions.lendingStatus;
141
141
  let secondsLeftOnLoan = iaBookActions.loanRenewTimeConfig.loanTotalTime;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/ia-book-actions",
3
- "version": "0.1.5",
3
+ "version": "0.1.6-a1",
4
4
  "description": "Bookreader actions for borrowable book",
5
5
  "author": "Internet Archive",
6
6
  "license": "AGPL-3.0-only",
@@ -9,10 +9,6 @@ import CollapsibleActionGroupStyle from '../assets/styles/collapsible-action-gro
9
9
 
10
10
  import { tabletContainerWidth } from '../core/config/constants.js';
11
11
  import { purchaseIcon } from '../assets/data/purchase.js';
12
- import {
13
- analyticsCategories,
14
- analyticsActions,
15
- } from '../core/config/analytics-event-and-category.js';
16
12
  import {
17
13
  dropdownOpened,
18
14
  dropdownClosed,
@@ -32,7 +28,8 @@ export class CollapsibleActionGroup extends ActionsHandler {
32
28
  dropdownArrow: { type: String },
33
29
  disabled: { type: Boolean },
34
30
  returnUrl: { type: String },
35
- renewNow: { type: Boolean },
31
+ autoRenew: { type: Boolean },
32
+ autoReturn: { type: Boolean },
36
33
  };
37
34
  }
38
35
 
@@ -52,7 +49,8 @@ export class CollapsibleActionGroup extends ActionsHandler {
52
49
  this.loaderIcon = 'https://archive.org/upload/images/tree/loading.gif';
53
50
  this.disabled = false;
54
51
  this.returnUrl = '';
55
- this.renewNow = false;
52
+ this.autoRenew = false;
53
+ this.autoReturn = false;
56
54
  }
57
55
 
58
56
  updated(changed) {
@@ -63,24 +61,27 @@ export class CollapsibleActionGroup extends ActionsHandler {
63
61
  this.resetActions();
64
62
  }
65
63
 
66
- if (changed.has('renewNow') && this.renewNow === true) {
67
- this.dispatchLoanRenewEvent();
64
+ if (changed.has('autoRenew') && this.autoRenew === true) {
65
+ this.dispatchLoanEvent('autoRenew');
68
66
  }
69
- }
70
67
 
71
- // dispatch this event to loan renew handler
72
- dispatchLoanRenewEvent() {
73
- const { category, action } = {
74
- category: analyticsCategories.browse,
75
- action: analyticsActions.browseLoanRenew,
76
- };
68
+ if (changed.has('autoReturn') && this.autoReturn === true) {
69
+ this.dispatchLoanEvent('autoReturn');
70
+ }
71
+ }
77
72
 
78
- // listen in action-handler.js to execute ajax call on petabox.
73
+ /**
74
+ * dispatch event when book is auto renewed / returned
75
+ * listen these events in action-handler.js to execute ajax call on petabox.
76
+ * @see ActionsHandler
77
+ *
78
+ * @param {string} event - autoRenew|autoReturn
79
+ * @memberof CollapsibleActionGroup
80
+ */
81
+ dispatchLoanEvent(event) {
79
82
  this.dispatchEvent(
80
- new CustomEvent('loanRenew', {
81
- detail: {
82
- event: { category, action },
83
- },
83
+ new CustomEvent(event, {
84
+ detail: {},
84
85
  })
85
86
  );
86
87
  }
@@ -190,7 +191,11 @@ export class CollapsibleActionGroup extends ActionsHandler {
190
191
  href="${action.url}"
191
192
  target=${action.target}
192
193
  @click=${() => {
193
- this.clickHandler(action.id, action.analyticsEvent);
194
+ this.clickHandler(
195
+ action.id,
196
+ action.analyticsEvent,
197
+ action?.borrowType
198
+ );
194
199
  }}
195
200
  >
196
201
  ${action.id === 'purchaseBook' ? purchaseIcon : ''} ${action.text}
@@ -211,7 +216,7 @@ export class CollapsibleActionGroup extends ActionsHandler {
211
216
  return html`<button
212
217
  class="ia-button ${action.className} ${initialButton ? 'initial' : ''}"
213
218
  @click=${() => {
214
- this.clickHandler(action.id, analyticsEvent);
219
+ this.clickHandler(action.id, analyticsEvent, action?.borrowType);
215
220
  }}
216
221
  >
217
222
  ${action.text}
@@ -224,10 +229,10 @@ export class CollapsibleActionGroup extends ActionsHandler {
224
229
  * @param { object } gaEvent - contains analytics event action and category
225
230
  * @param { string } gaEvent.category
226
231
  * @param { string } gaEvent.action
227
- *
232
+ * @param { string } borrowType browse|borrow
228
233
  * @fires CollapsibleActionGroup#{eventName} - (will be browseBook, borrowBook etc...)
229
234
  */
230
- clickHandler(eventName, gaEvent) {
235
+ clickHandler(eventName, gaEvent, borrowType = '') {
231
236
  this.dropdownState = 'close';
232
237
  this.dropdownArrow = dropdownClosed;
233
238
 
@@ -237,6 +242,7 @@ export class CollapsibleActionGroup extends ActionsHandler {
237
242
  new CustomEvent(eventName, {
238
243
  detail: {
239
244
  event: { category, action },
245
+ borrowType,
240
246
  },
241
247
  })
242
248
  );
@@ -41,7 +41,7 @@ export default class TimerCountdown extends LitElement {
41
41
  * delay seconds in setInterval function
42
42
  * @type {number}
43
43
  */
44
- this.timerExecutionSeconds = 0;
44
+ this.timerExecutionSeconds = 60;
45
45
  }
46
46
 
47
47
  disconnectedCallback() {
@@ -61,8 +61,6 @@ export default class TimerCountdown extends LitElement {
61
61
  timerCountdown() {
62
62
  this.disconnectedCallback();
63
63
 
64
- // - let just execute setInterval in every 1 minute
65
- this.timerExecutionSeconds = 60;
66
64
  let secondsLeft = this.secondsLeftOnLoan;
67
65
 
68
66
  // store current time
@@ -76,7 +74,7 @@ export default class TimerCountdown extends LitElement {
76
74
  secondsLeft -= this.timerExecutionSeconds;
77
75
  secondsLeft = Math.round(secondsLeft); // round number
78
76
 
79
- // re-sync timer is gone off
77
+ // re-sync timer if gone off because of background window
80
78
  await this.reSyncTimerIfGoneOff(secondsLeft);
81
79
 
82
80
  /**
@@ -13,7 +13,8 @@ export const analyticsCategories = {
13
13
  export const analyticsActions = {
14
14
  browse: 'Borrow-1Hour',
15
15
  browseAgain: 'Borrow-Again',
16
- browseLoanRenew: 'Borrow-Loan-Renew',
16
+ browseAutoRenew: 'AutoRenewBook',
17
+ browseAutoReturn: 'AutoReturnBook',
17
18
  borrow: 'Borrow-14Days',
18
19
  waitlistJoin: 'JoinWaitlist',
19
20
  waitlistLeave: 'LeaveWaitlist',
@@ -11,6 +11,18 @@
11
11
  * @type {Object}
12
12
  */
13
13
  window.IALendingIntervals = {
14
+ /**
15
+ * store loan token poller interval
16
+ * @see LoanTokenPoller::bookAccessed
17
+ */
18
+ tokenPoller: 0,
19
+
20
+ /**
21
+ * store timer-countdown interval
22
+ * @see TimerCountdown::timerCountdown
23
+ */
24
+ timerCountdown: 0,
25
+
14
26
  /**
15
27
  * clear interval for create_token api
16
28
  */
@@ -70,6 +70,7 @@ export default class ActionsConfig {
70
70
  category: eventCategory,
71
71
  action: this.analyticsActions.doneBorrowing,
72
72
  },
73
+ borrowType: this.lendingStatus.user_has_browsed ? 'browse' : 'borrow',
73
74
  };
74
75
  }
75
76
 
@@ -1,17 +1,16 @@
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
5
  import ActionsHandlerService from './actions-handler-service.js';
6
+ import LoanAnanlytics from '../loan-analytics.js';
7
7
  import * as Cookies from '../doc-cookies.js';
8
8
 
9
9
  /**
10
10
  * These are callback functions calling from actions-config.js file.
11
11
  *
12
- * ActionsHandlerService is a function being used to execute
13
- * APIs based of the request made by user. It consist some parameters are as follows:-
14
- * 1. identifier: book name.
12
+ * ActionsHandlerService is a function being used to execute APIs based of the request made by user.
13
+ *
15
14
  */
16
15
 
17
16
  export default class ActionsHandler extends LitElement {
@@ -24,86 +23,97 @@ export default class ActionsHandler extends LitElement {
24
23
  */
25
24
  this.waitUntillBorrowComplete = 6; // in seconds
26
25
 
27
- this.bindEvents();
28
- }
26
+ /**
27
+ * bind events for lending bar
28
+ * @type {object}
29
+ */
30
+ this.loanAnanlytics = new LoanAnanlytics();
29
31
 
30
- sendEvent(eventCategory, eventAction) {
31
- window?.archive_analytics?.send_event(
32
- eventCategory,
33
- eventAction,
34
- `identifier=${this.identifier}`
35
- );
32
+ /**
33
+ * bind events for lending bar
34
+ * @type {Function}
35
+ */
36
+ this.bindEvents();
36
37
  }
37
38
 
38
39
  bindEvents() {
39
- this.addEventListener('browseBook', ({ detail }) => {
40
+ this.addEventListener('browseBook', () => {
40
41
  this.handleBrowseIt();
41
- this.setConsecutiveLoanCounts();
42
- const { category, action } = detail.event;
43
- this.sendEvent(category, action);
42
+ this.loanAnanlytics?.storeLoanStatsCount(this.identifier, 'browse');
44
43
  });
45
44
 
46
- this.addEventListener('browseBookAgain', ({ detail }) => {
45
+ this.addEventListener('browseBookAgain', () => {
47
46
  this.handleBrowseIt();
48
- this.setConsecutiveLoanCounts('browseAgain');
49
- const { category, action } = detail.event;
50
- this.sendEvent(category, action);
47
+ this.loanAnanlytics?.storeLoanStatsCount(this.identifier, 'browseagain');
51
48
  });
52
49
 
53
- this.addEventListener('loanRenew', ({ detail }) => {
54
- this.handleLoanRenew();
55
- const { category, action } = detail.event;
56
- this.sendEvent(category, action);
50
+ this.addEventListener('autoRenew', () => {
51
+ this.handleLoanRenewNow();
52
+ this.loanAnanlytics?.storeLoanStatsCount(this.identifier, 'autorenew');
57
53
  });
58
54
 
59
- this.addEventListener('returnNow', ({ detail }) => {
55
+ this.addEventListener('autoReturn', () => {
60
56
  this.handleReturnIt();
61
- const { category, action } = detail.event;
62
- this.sendEvent(category, action);
57
+ this.loanAnanlytics?.storeLoanStatsCount(this.identifier, 'autoreturn');
58
+ });
59
+
60
+ this.addEventListener('returnNow', ({ detail }) => {
61
+ // use loan stats count when 1-hour borrow is active
62
+ if (detail?.borrowType === 'browse') {
63
+ this.loanAnanlytics?.storeLoanStatsCount(this.identifier, 'return');
64
+ }
65
+
66
+ this.handleReturnIt('returnNow');
67
+
68
+ // send these events if 14-day borrow return
69
+ if (detail?.borrowType === 'borrow') {
70
+ const { category, action } = detail.event;
71
+ this.loanAnanlytics?.sendEvent(category, action, this.identifier);
72
+ }
63
73
  });
64
74
 
65
75
  this.addEventListener('borrowBook', ({ detail }) => {
66
76
  this.handleBorrowIt();
67
77
  const { category, action } = detail.event;
68
- this.sendEvent(category, action);
78
+ this.loanAnanlytics?.sendEvent(category, action, this.identifier);
69
79
  });
70
80
 
71
81
  this.addEventListener('loginAndBorrow', ({ detail }) => {
72
82
  this.handleLoginOk();
73
83
  const { category, action } = detail.event;
74
- this.sendEvent(category, action);
84
+ this.loanAnanlytics?.sendEvent(category, action, this.identifier);
75
85
  });
76
86
 
77
87
  this.addEventListener('leaveWaitlist', ({ detail }) => {
78
88
  this.handleRemoveFromWaitingList();
79
89
  const { category, action } = detail.event;
80
- this.sendEvent(category, action);
90
+ this.loanAnanlytics?.sendEvent(category, action, this.identifier);
81
91
  });
82
92
 
83
93
  this.addEventListener('joinWaitlist', ({ detail }) => {
84
94
  this.handleReserveIt();
85
95
  const { category, action } = detail.event;
86
- this.sendEvent(category, action);
96
+ this.loanAnanlytics?.sendEvent(category, action, this.identifier);
87
97
  });
88
98
 
89
99
  this.addEventListener('purchaseBook', ({ detail }) => {
90
100
  const { category, action } = detail.event;
91
- this.sendEvent(category, action);
101
+ this.loanAnanlytics?.sendEvent(category, action, this.identifier);
92
102
  });
93
103
 
94
104
  this.addEventListener('adminAccess', ({ detail }) => {
95
105
  const { category, action } = detail.event;
96
- this.sendEvent(category, action);
106
+ this.loanAnanlytics?.sendEvent(category, action, this.identifier);
97
107
  });
98
108
 
99
109
  this.addEventListener('exitAdminAccess', ({ detail }) => {
100
110
  const { category, action } = detail.event;
101
- this.sendEvent(category, action);
111
+ this.loanAnanlytics?.sendEvent(category, action, this.identifier);
102
112
  });
103
113
 
104
114
  this.addEventListener('bookTitleBar', ({ detail }) => {
105
115
  const { category, action } = detail.event;
106
- this.sendEvent(category, action);
116
+ this.loanAnanlytics?.sendEvent(category, action, this.identifier);
107
117
  });
108
118
  }
109
119
 
@@ -124,7 +134,7 @@ export default class ActionsHandler extends LitElement {
124
134
  });
125
135
  }
126
136
 
127
- handleLoanRenew() {
137
+ handleLoanRenewNow() {
128
138
  const action = 'renew_loan';
129
139
 
130
140
  ActionsHandlerService({
@@ -144,16 +154,23 @@ export default class ActionsHandler extends LitElement {
144
154
  });
145
155
  }
146
156
 
147
- handleReturnIt() {
157
+ /**
158
+ * excute function when loan is returning
159
+ *
160
+ * @param {string} type - loan return type returnNow|''
161
+ */
162
+ handleReturnIt(type = '') {
148
163
  const action = 'return_loan';
149
- this.dispatchToggleActionGroup();
164
+
165
+ if (type === 'returnNow') this.dispatchToggleActionGroup();
150
166
 
151
167
  ActionsHandlerService({
152
168
  action,
153
169
  identifier: this.identifier,
154
170
  success: () => {
155
171
  this.deleteLoanCookies();
156
- URLHelper.goToUrl(this.returnUrl, true);
172
+
173
+ if (type === 'returnNow') URLHelper.goToUrl(this.returnUrl, true);
157
174
  },
158
175
  error: data => {
159
176
  this.dispatchActionError(action, data);
@@ -220,7 +237,7 @@ export default class ActionsHandler extends LitElement {
220
237
  */
221
238
  dispatchActionError(action, data = {}) {
222
239
  // send LendingServiceError to GA
223
- this.sendEvent('LendingServiceError', action);
240
+ this.loanAnanlytics?.sendEvent('LendingServiceError', action);
224
241
 
225
242
  this.dispatchEvent(
226
243
  new CustomEvent('lendingActionError', {
@@ -271,31 +288,6 @@ export default class ActionsHandler extends LitElement {
271
288
  }, this.waitUntillBorrowComplete * 1000);
272
289
  }
273
290
 
274
- // save consecutive loan count for borrow
275
- async setConsecutiveLoanCounts(action = '') {
276
- try {
277
- let newCount = 1;
278
- const storageKey = `loan-count-${this.identifier}`;
279
- const existingCount = Cookies.getItem(storageKey);
280
-
281
- // increase browse-count by 1 when you consecutive reading a book.
282
- if (action === 'browseAgain' && existingCount !== undefined) {
283
- newCount = existingCount ? Number(existingCount) + 1 : 1;
284
- }
285
-
286
- const date = new Date();
287
- date.setHours(date.getHours() + 2); // 2 hours
288
-
289
- // set new value
290
- Cookies.setItem(storageKey, newCount, date, '/');
291
- } catch (error) {
292
- window?.Sentry?.captureException(
293
- `${sentryLogs.setConsecutiveLoanCounts} - Error: ${error}`
294
- );
295
- this.sendEvent('Cookies-Error-Actions', error);
296
- }
297
- }
298
-
299
291
  /**
300
292
  * set browse time in indexedDB
301
293
  */
@@ -62,7 +62,7 @@ export function setItem(sKey, sValue, vEnd, sPath, sDomain, bSecure) {
62
62
 
63
63
  /**
64
64
  * BROKEN Remove specific key's value from cookie
65
- * @fixme hasItem isn't even implemented! This will always error.
65
+ *
66
66
  * @param {string} sKey cookie name
67
67
  * @param {string} [sPath] path of current item
68
68
  * @param {string} [sDomain]
@@ -81,3 +81,25 @@ export function removeItem(sKey, sPath, sDomain) {
81
81
 
82
82
  return true;
83
83
  }
84
+
85
+ /**
86
+ * determine if a specific cookie is exist
87
+ *
88
+ * @param {string} sKey cookie name
89
+ *
90
+ * @returns {boolean}
91
+ */
92
+ export function hasItem(sKey) {
93
+ const reCNameAllowed = /^(?:expires|max\-age|path|domain|secure|samesite|httponly)$/i;
94
+
95
+ // eslint-disable-next-line
96
+ if (!sKey || reCNameAllowed.test(sKey)) {
97
+ return false;
98
+ }
99
+
100
+ return new RegExp(
101
+ '(?:^|;\\s*)' +
102
+ encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, '\\$&') +
103
+ '\\s*\\='
104
+ ).test(document.cookie);
105
+ }
@@ -40,7 +40,7 @@ import ActionsConfig from './actions-config.js';
40
40
  * @enum {string}
41
41
  */
42
42
  export const bookTitles = {
43
- available_1hr: '',
43
+ available_1hr: 'Renews each hour as long as pages are turned.',
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.',
@@ -98,7 +98,8 @@ export class GetLendingActions {
98
98
  let isBrowsing =
99
99
  lendingStatus.user_has_browsed && !lendingStatus.browsingExpired;
100
100
  if (isBrowsing) {
101
- primaryTitleText = '';
101
+ primaryTitleText =
102
+ 'Returns automatically after 25 minutes of inactivity.';
102
103
  } else {
103
104
  primaryTitleText = `Your loan of this book has ${lendingStatus.daysLeftOnLoan} days left.`;
104
105
  }
@@ -0,0 +1,163 @@
1
+ /* eslint-disable class-methods-use-this */
2
+
3
+ import { analyticsCategories } from '../config/analytics-event-and-category.js';
4
+ import * as Cookies from './doc-cookies.js';
5
+
6
+ /**
7
+ * This class is used to send different GA events for loan system.
8
+ */
9
+ export default class LoanAnanlytics {
10
+ constructor() {
11
+ this.identifier = undefined;
12
+
13
+ /**
14
+ * contains the counts we send to GA
15
+ * @type {object}
16
+ * - @param {number} - browse count
17
+ * - @param {number} - renew count
18
+ * - @param {number} - expire count
19
+ */
20
+ this.gaStats = {};
21
+ }
22
+
23
+ /**
24
+ * store loan stats count in cookies to remember the count we need to sent GA
25
+ *
26
+ * @param {string} identifier - book id
27
+ * @param {string} action - user action on book
28
+ * @return {void}
29
+ * @memberof ActionsHandler
30
+ */
31
+ async storeLoanStatsCount(idetifier, action = '') {
32
+ this.identifier = idetifier;
33
+
34
+ try {
35
+ await this.getLoanStatsCount(action);
36
+
37
+ const date = new Date();
38
+ date.setHours(date.getHours() + 2); // 2 hours
39
+
40
+ // set new value
41
+ await Cookies.setItem(
42
+ this.getLoanCountStorageKey,
43
+ JSON.stringify(this.lendingEventCounts),
44
+ date,
45
+ '/'
46
+ );
47
+ } catch (error) {
48
+ console.log(error);
49
+ this.sendEvent('Cookies-Error-Actions', error, this.identifier);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * get loan stats count from cookies for GA
55
+ *
56
+ * @param {string} action browse|autorenew|etc...
57
+ * @memberof ActionsHandler
58
+ */
59
+ async getLoanStatsCount(action) {
60
+ this.lendingEventCounts = JSON.parse(
61
+ await Cookies.getItem(this.getLoanCountStorageKey)
62
+ );
63
+
64
+ this.gaStats = this.lendingEventCounts ?? {
65
+ browse: 0,
66
+ renew: 0,
67
+ expire: 0,
68
+ };
69
+
70
+ let browse = this.lendingEventCounts?.browse ?? 0;
71
+ let renew = this.lendingEventCounts?.renew ?? 0;
72
+ let expire = this.lendingEventCounts?.expire ?? 0;
73
+
74
+ switch (action) {
75
+ case 'browse' || 'browseagain':
76
+ browse = browse ? Number(browse) + 1 : 1;
77
+ this.gaStats.browse = browse;
78
+
79
+ // reset renew and expire count
80
+ renew = 0;
81
+ expire = 0;
82
+ break;
83
+
84
+ case 'autorenew':
85
+ renew = renew ? Number(renew) + 1 : 1;
86
+ this.gaStats.renew = renew;
87
+ break;
88
+
89
+ case 'return' || 'autoreturn':
90
+ expire = expire ? Number(expire) + 1 : 1;
91
+ this.gaStats.expire = expire;
92
+
93
+ // reset renew and expire count
94
+ renew = 0;
95
+ expire = 0;
96
+ break;
97
+ default:
98
+ break;
99
+ }
100
+
101
+ // store these counts in cookies
102
+ this.lendingEventCounts = { browse, renew, expire };
103
+
104
+ // obtain GA category, event
105
+ const category = analyticsCategories.browse;
106
+ const event = `browse${this.paddedNumber(
107
+ this.gaStats?.browse
108
+ )}-autorenew${this.paddedNumber(this.gaStats?.renew)}:${action}`;
109
+
110
+ // send google analytics events
111
+ this.sendEvent(category, event, this.identifier);
112
+ }
113
+
114
+ /**
115
+ * get zero padded number
116
+ *
117
+ * @param {number} number
118
+ * @return {string} 001|010
119
+ * @memberof LoanAnanlytics
120
+ */
121
+ paddedNumber(number) {
122
+ if (number) return number.toString().padStart(3, '0');
123
+
124
+ return '000';
125
+ }
126
+
127
+ /**
128
+ * get storage key for loan stats count
129
+ *
130
+ * @readonly
131
+ * @memberof LoanAnanlytics
132
+ */
133
+ get getLoanCountStorageKey() {
134
+ return `br-browse-${this.identifier}`;
135
+ }
136
+
137
+ /**
138
+ * reponsible to sent events to GA
139
+ *
140
+ * @param {*} eventCategory
141
+ * @param {*} eventAction
142
+ * @param {*} label
143
+ * @param {*} extraParams
144
+ * @memberof LoanAnanlytics
145
+ */
146
+ sendEvent(eventCategory, eventAction, label, extraParams) {
147
+ console.log(
148
+ 'eventCategory:-',
149
+ eventCategory,
150
+ '\t||\teventAction:-',
151
+ eventAction,
152
+ '\t||\tlabel:-',
153
+ label
154
+ );
155
+
156
+ window?.archive_analytics?.send_event_no_sampling(
157
+ eventCategory,
158
+ eventAction,
159
+ label || this.identifier,
160
+ extraParams
161
+ );
162
+ }
163
+ }
@@ -1,8 +1,7 @@
1
1
  import ActionsHandlerService from './actions-handler/actions-handler-service.js';
2
+ import LoanAnanlytics from './loan-analytics.js';
2
3
  import { sentryLogs } from '../config/sentry-events.js';
3
4
 
4
- import * as Cookies from './doc-cookies.js';
5
-
6
5
  /**
7
6
  * This class is used to create loan token for borrowed books
8
7
  *
@@ -17,6 +16,13 @@ export class LoanTokenPoller {
17
16
  this.pollerDelay = pollerDelay; // value in seconds
18
17
 
19
18
  this.loanTokenInterval = undefined;
19
+
20
+ /**
21
+ * loan analytics instance
22
+ * @see loan-analytics.js
23
+ */
24
+ this.loanAnalytics = new LoanAnanlytics();
25
+
20
26
  this.bookAccessed();
21
27
  }
22
28
 
@@ -25,24 +31,7 @@ export class LoanTokenPoller {
25
31
  }
26
32
 
27
33
  async bookAccessed() {
28
- let consecutiveLoanCounts = 1;
29
-
30
34
  if (this.borrowType) {
31
- // send consecutiveLoanCounts for browsed books only.
32
- if (this.borrowType === 'browsed') {
33
- try {
34
- const existingCount = Cookies.getItem(
35
- `loan-count-${this.identifier}`
36
- );
37
- consecutiveLoanCounts = existingCount ?? 1;
38
- } catch (error) {
39
- window?.Sentry?.captureException(
40
- `${sentryLogs.enableBookAcces} - CookieError: ${error}`
41
- );
42
- this.sendEvent('Cookies-Error-Token', error);
43
- }
44
- }
45
-
46
35
  // Do an initial token, then set an interval
47
36
  this.handleLoanTokenPoller(true);
48
37
 
@@ -58,14 +47,6 @@ export class LoanTokenPoller {
58
47
  this.handleLoanTokenPoller();
59
48
  }, this.pollerDelay * 1000);
60
49
  }
61
-
62
- // event category and action for browsing book access
63
- const category = `${this.borrowType}BookAccess`;
64
- const action = `${
65
- this.borrowType === 'browsed' ? 'BrowseCounts-' : 'Counts-'
66
- }${consecutiveLoanCounts}`;
67
-
68
- this.sendEvent(category, action);
69
50
  } else {
70
51
  window?.Sentry?.captureMessage(
71
52
  `${sentryLogs.bookAccessed} - not borrowed`
@@ -76,14 +57,6 @@ export class LoanTokenPoller {
76
57
  }
77
58
  }
78
59
 
79
- sendEvent(eventCategory, eventAction) {
80
- window?.archive_analytics?.send_event(
81
- eventCategory,
82
- eventAction,
83
- `identifier=${this.identifier}`
84
- );
85
- }
86
-
87
60
  async handleLoanTokenPoller(isInitial = false) {
88
61
  const action = 'create_token';
89
62
  ActionsHandlerService({
@@ -98,7 +71,11 @@ export class LoanTokenPoller {
98
71
  );
99
72
 
100
73
  // send LendingServiceError to GA
101
- this.sendEvent('LendingServiceLoanError', action);
74
+ this.loanAnalytics?.sendEvent(
75
+ 'LendingServiceLoanError',
76
+ action,
77
+ this.identifier
78
+ );
102
79
  },
103
80
  success: () => {
104
81
  if (isInitial) this.successCallback();
@@ -73,6 +73,7 @@ export default class IABookActions extends LitElement {
73
73
  this.borrowType = ''; // (browsed|borrowed)
74
74
  this.consecutiveLoanCounts = 1; // consecutive loan count
75
75
  this.suppressToast = false;
76
+ this.suppressAutoRenew = false;
76
77
  this.browseTimer = undefined;
77
78
 
78
79
  /**
@@ -171,6 +172,7 @@ export default class IABookActions extends LitElement {
171
172
 
172
173
  async setupLendingToolbarActions() {
173
174
  this.suppressToast = false;
175
+ this.suppressAutoRenew = false;
174
176
 
175
177
  this.lendingOptions = new GetLendingActions(
176
178
  this.userid,
@@ -236,16 +238,10 @@ export default class IABookActions extends LitElement {
236
238
  /**
237
239
  * tokenPoller determines if user has loan token for this book
238
240
  * - if book is going to renew, need to wait until renew is completed
239
- * - otherwise execute immediately
240
241
  */
241
- setTimeout(
242
- () => {
243
- if (!hasExpired) this.startLoanTokenPoller();
244
- },
245
-
246
- // if book is renew while reading, let's wait to execute create-token api
247
- this.loanRenewResult.renewNow ? this.tokenDelay * 1000 : 100
248
- );
242
+ setTimeout(() => {
243
+ if (!hasExpired) this.startLoanTokenPoller();
244
+ }, 100);
249
245
  }
250
246
 
251
247
  /**
@@ -260,10 +256,18 @@ export default class IABookActions extends LitElement {
260
256
  * dispatched this event from bookreader page changed
261
257
  */
262
258
  window.addEventListener('BookReader:userAction', () => {
263
- console.log('user action');
259
+ console.log('** BR user Action', {
260
+ suppressAutoRenew: this.suppressAutoRenew,
261
+ borrowType: this.borrowType,
262
+ });
263
+
264
264
  this.suppressToast = true;
265
265
  this.closeToastManager();
266
266
 
267
+ if (this.suppressAutoRenew) {
268
+ return;
269
+ }
270
+
267
271
  if (this.borrowType === 'browsed') {
268
272
  this.autoLoanRenewChecker(true);
269
273
  }
@@ -273,6 +277,7 @@ export default class IABookActions extends LitElement {
273
277
  * detect click-event on document to close toast-template
274
278
  */
275
279
  document.addEventListener('click', e => {
280
+ console.log('*** top level click handler to remove toast on click');
276
281
  if (this.loanRenewHelper && this.loanRenewResult.secondsLeft > 0) {
277
282
  this.suppressToast = true;
278
283
  }
@@ -293,11 +298,31 @@ export default class IABookActions extends LitElement {
293
298
  * @see TimerCountdown::timerCountdown
294
299
  */
295
300
  this.addEventListener('IABookActions:loanRenew', async event => {
301
+ console.log('** IABookActions:loanRenew', event.detail);
296
302
  await this.autoLoanRenewChecker(false);
297
303
 
304
+ /**
305
+ * renew in not possible in last seconds (let say 50 second) because,
306
+ * 1. less time to execute ajax call
307
+ * 2. less time to write loan on datanodes
308
+ * 3. less time to load images by create_token api
309
+ * so if seconds left is < 50, just expire the loan
310
+ */
311
+ let secondsLeft = event?.detail?.secondsLeft;
312
+ if (secondsLeft < 50) {
313
+ this.suppressAutoRenew = true;
314
+ this.browseHasExpired();
315
+ }
316
+
298
317
  // show warning message with remaining time to auto returned it.
299
318
  if (this.loanRenewResult.renewNow === false) {
300
- this.loanRenewResult.secondsLeft = event.detail.secondsLeft;
319
+ /**
320
+ * so compensate for the 50 second buffer to handle above race conditions
321
+ * let's reduce 1 min from warning texts and early return the book when 1 min left.
322
+ */
323
+ secondsLeft -= 60;
324
+
325
+ this.loanRenewResult.secondsLeft = secondsLeft;
301
326
  this.showToastMessage();
302
327
  }
303
328
  });
@@ -325,6 +350,7 @@ export default class IABookActions extends LitElement {
325
350
  * close/hide toast message
326
351
  */
327
352
  async closeToastManager() {
353
+ console.log('** closeToastManager');
328
354
  const toastTemplate = this.shadowRoot.querySelector('toast-template');
329
355
  if (toastTemplate) {
330
356
  toastTemplate?.remove();
@@ -338,6 +364,10 @@ export default class IABookActions extends LitElement {
338
364
  * - show message having remaining time when book is about to auto returned
339
365
  */
340
366
  async showToastMessage() {
367
+ console.log('** showToastMessage', {
368
+ suppressToast: this.suppressToast,
369
+ loanRenewResult: this.loanRenewResult,
370
+ });
341
371
  if (this.suppressToast) return;
342
372
 
343
373
  let toastTemplate = this.shadowRoot.querySelector('toast-template');
@@ -492,7 +522,8 @@ export default class IABookActions extends LitElement {
492
522
  .loanTotalTime=${this.loanRenewTimeConfig.loanTotalTime}
493
523
  ?hasAdminAccess=${this.hasAdminAccess}
494
524
  ?disabled=${this.disableActionGroup}
495
- ?renewNow=${this.loanRenewResult.renewNow}
525
+ ?autoRenew=${this.loanRenewResult.renewNow}
526
+ ?autoReturn=${this.lendingStatus.browsingExpired}
496
527
  @loanAutoRenewed=${this.handleLoanAutoRenewed}
497
528
  @lendingActionError=${this.handleLendingActionError}
498
529
  @toggleActionGroup=${this.handleToggleActionGroup}
@@ -508,8 +539,10 @@ export default class IABookActions extends LitElement {
508
539
  * - show success message
509
540
  * - then change the remaining time
510
541
  * - reset timer state
542
+ * @param {object} event
511
543
  */
512
- async handleLoanAutoRenewed() {
544
+ async handleLoanAutoRenewed(event) {
545
+ // TODO: just read DATA variable here to apply security { action, data }
513
546
  if (this.loanRenewResult.renewNow) {
514
547
  window?.IALendingIntervals?.clearAll();
515
548
 
@@ -519,13 +552,12 @@ export default class IABookActions extends LitElement {
519
552
  await this.resetTimerCountState();
520
553
 
521
554
  window?.Sentry?.captureMessage(`${sentryLogs.bookHasRenewed}`);
522
- // TODO: analytics?? for consecutive renew.....
523
555
 
524
556
  // testing console....
525
- console.log(sentryLogs.bookHasRenewed, {
557
+ console.log('IABookActions: AutoRenewed:- ', {
558
+ ajaxResponse: event?.detail?.data,
526
559
  loanRenewResult: this.loanRenewResult,
527
- browseHasRenew: this.lendingStatus.secondsLeftOnLoan,
528
- resetTimerCountState: this._shadowRoot.querySelector('timer-countdown'),
560
+ secondsLeftOnLoan: Math.round(this.lendingStatus.secondsLeftOnLoan),
529
561
  });
530
562
  }
531
563
  }
@@ -64,8 +64,6 @@ describe('<collapsible-action-group>', () => {
64
64
  expect(primaryActionContainer.classList.contains('action-buttons')).to.be
65
65
  .true;
66
66
 
67
- expect(el.sendEvent).to.exist;
68
-
69
67
  let eName;
70
68
  let eAnalytics;
71
69
  const manualClickStub = (eventName, gaEvent) => {
@@ -1,10 +1,16 @@
1
1
  import { html, fixture, expect, aTimeout } from '@open-wc/testing';
2
+ import '../../src/ia-book-actions.js';
2
3
  import '../../src/components/timer-countdown.js';
3
4
 
4
- const container = ({ secondsLeftOnLoan, loanRenewTimeConfig } = {}) =>
5
+ const container = ({
6
+ secondsLeftOnLoan,
7
+ loanTotalTime,
8
+ loanRenewAtLast,
9
+ } = {}) =>
5
10
  html`<timer-countdown
6
11
  .secondsLeftOnLoan=${secondsLeftOnLoan}
7
- .loanRenewTimeConfig=${loanRenewTimeConfig}
12
+ .loanTotalTime=${loanTotalTime}
13
+ .loanRenewAtLast=${loanRenewAtLast}
8
14
  ></timer-countdown>`;
9
15
 
10
16
  describe('<timer-countdown>', () => {
@@ -12,27 +18,21 @@ describe('<timer-countdown>', () => {
12
18
  const el = await fixture(
13
19
  container({
14
20
  secondsLeftOnLoan: 0, // in seconds
15
- loanRenewTimeConfig: {
16
- loanTotalTime: 14, // in seconds
17
- loanRenewAtLast: 5, // in seconds
18
- pageChangedInLast: 5, // in seconds
19
- },
21
+ loanTotalTime: 14,
22
+ loanRenewAtLast: 5,
20
23
  })
21
24
  );
22
25
 
23
26
  await el.updateComplete;
24
- expect(el.timerInterval).to.be.undefined;
27
+ // expect(window.IALendingIntervals.timerCountdown).to.be.undefined;
25
28
  });
26
29
 
27
30
  it('timer interval is not undefined when loan is active', async () => {
28
31
  const el = await fixture(
29
32
  container({
30
33
  secondsLeftOnLoan: 2, // in seconds
31
- loanRenewTimeConfig: {
32
- loanTotalTime: 14, // in seconds
33
- loanRenewAtLast: 5, // in seconds
34
- pageChangedInLast: 5, // in seconds
35
- },
34
+ loanTotalTime: 14,
35
+ loanRenewAtLast: 5,
36
36
  })
37
37
  );
38
38
 
@@ -40,6 +40,6 @@ describe('<timer-countdown>', () => {
40
40
  await el.updateComplete;
41
41
 
42
42
  expect(el.secondsLeftOnLoan).to.equal(2);
43
- expect(el.timerInterval).to.not.be.undefined;
43
+ // expect(el.timerCountdown).to.not.be.undefined;
44
44
  });
45
45
  });
@@ -27,7 +27,7 @@ describe('Get Lending Actions', () => {
27
27
  expect(actions.primaryActions.length).to.equal(3);
28
28
  expect(actions.primaryActions[0].text).to.equal('Borrow');
29
29
  expect(actions.primaryActions[1].text).to.equal('Borrow for 14 days');
30
- expect(actions.secondaryActions[1].text).to.equal('Purchase at');
30
+ expect(actions.secondaryActions[1].text).to.equal('Purchase at ');
31
31
  });
32
32
 
33
33
  it('Borrowable without user', async () => {