@internetarchive/ia-book-actions 0.0.7-2 → 0.0.7-5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/ia-book-actions",
3
- "version": "0.0.7-02",
3
+ "version": "0.0.7-5",
4
4
  "description": "Bookreader actions for borrowable book",
5
5
  "author": "Internet Archive",
6
6
  "license": "AGPL-3.0-only",
@@ -22,6 +22,7 @@
22
22
  "@internetarchive/icon-bookmark": "^1.1.3",
23
23
  "@internetarchive/icon-close": "^1.1.3",
24
24
  "@internetarchive/icon-info": "^1.2.6",
25
+ "@internetarchive/local-cache": "^0.1.1",
25
26
  "@internetarchive/modal-manager": "^0.1.0",
26
27
  "@internetarchive/shared-resize-observer": "^0.0.1",
27
28
  "lit-element": "^2.4.0",
@@ -63,25 +63,38 @@ export class CollapsibleActionGroup extends ActionsHandler {
63
63
  }
64
64
  }
65
65
 
66
- /* emit custom event to fetch loan token */
67
- emitEnableBookAccess() {
66
+ /**
67
+ * Dispatches event when patron is borrowing book.
68
+ * Notes borrow type, and consecutive borrow counts
69
+ *
70
+ * @fires CollapsibleActionGroup#enableBookAccess
71
+ */
72
+ async emitEnableBookAccess() {
68
73
  // send consecutiveLoanCounts for browsed books only.
69
- if (this.borrowType === 'browsing') {
70
- this.consecutiveLoanCounts =
71
- localStorage.getItem('consecutive-loan-count') ?? 1;
74
+ if (this.borrowType === 'browsed') {
75
+ try {
76
+ const existingCount = await this.localCache.get(
77
+ 'consecutive-loan-count'
78
+ );
79
+ if (existingCount !== undefined) {
80
+ this.consecutiveLoanCounts = existingCount.value ?? 1;
81
+ }
82
+ } catch (error) {
83
+ console.log(error);
84
+ }
72
85
  }
73
86
 
74
87
  // event category and action for browsing book access
75
88
  const eventCategory = `${this.borrowType}BookAccess`;
76
89
  const eventAction = `${
77
- this.borrowType === 'browsing' ? 'BrowseCounts-' : 'Counts-'
90
+ this.borrowType === 'browsed' ? 'BrowseCounts-' : 'Counts-'
78
91
  }${this.consecutiveLoanCounts}`;
79
92
 
80
93
  this.dispatchEvent(
81
94
  new CustomEvent('enableBookAccess', {
82
95
  detail: {
83
96
  event: {
84
- category: eventCategory, // brosingBookAccess | borrowingBookAccess
97
+ category: eventCategory, // browsedBookAccess | borrowedBookAccess
85
98
  action: eventAction, // (BrowseCounts-N|Counts-N)
86
99
  },
87
100
  borrowType: this.borrowType,
@@ -222,11 +235,13 @@ export class CollapsibleActionGroup extends ActionsHandler {
222
235
  }
223
236
 
224
237
  /**
225
- * Click handler to emit custom event on action click
226
- * @param { string } eventName
227
- * @param { object } gaEvent
238
+ * Dispatches click events when patron clicks on action buttons
239
+ * @param { string } eventName - actions like 'browseBook', 'borrowBook' etc...
240
+ * @param { object } gaEvent - contains analytics event action and category
228
241
  * @param { string } gaEvent.category
229
242
  * @param { string } gaEvent.action
243
+ *
244
+ * @fires CollapsibleActionGroup#{eventName} - (will be browseBook, borrowBook etc...)
230
245
  */
231
246
  clickHandler(eventName, gaEvent) {
232
247
  this.dropdownState = 'close';
@@ -16,6 +16,11 @@ export default function ActionsHandlerService(options) {
16
16
  let baseHost = '/services/loans/loan';
17
17
  const location = window?.location;
18
18
 
19
+ // return intentional error on dev environment
20
+ const shouldReturnError =
21
+ location?.href?.indexOf('?error=true') !== -1 &&
22
+ location?.hostname !== 'archive.org';
23
+
19
24
  if (location?.pathname === '/demo/') baseHost = `/demo/`;
20
25
 
21
26
  let formData = new FormData();
@@ -27,12 +32,11 @@ export default function ActionsHandlerService(options) {
27
32
  body: formData,
28
33
  })
29
34
  .then(response => {
30
- let shouldReturnError = location?.href?.indexOf('?error=true') !== -1;
31
35
  const tokenError = 'loan token not found. please try again later.';
32
36
  const borrowError =
33
37
  'This book is not available to borrow at this time. Please try again later.';
34
38
 
35
- // return error reponse if query param has ?error=true param...
39
+ // return error reponse when not production and has ?error=true param...
36
40
  const erroneousActions = ['browse_book', 'borrow_book', 'create_token'];
37
41
  if (shouldReturnError && erroneousActions.includes(option?.action)) {
38
42
  return {
@@ -1,4 +1,6 @@
1
1
  import { LitElement } from 'lit-element';
2
+ import { LocalCache } from '@internetarchive/local-cache';
3
+
2
4
  import { URLHelper } from '../../config/url-helper.js';
3
5
  import ActionsHandlerService from './actions-handler-service.js';
4
6
 
@@ -14,10 +16,11 @@ export default class ActionsHandler extends LitElement {
14
16
  constructor(identifier) {
15
17
  super();
16
18
  this.identifier = identifier;
17
- this.ajaxTimeout = 6000;
19
+ this.ajaxTimeout = 7000;
18
20
  this.loanTokenPollingDelay =
19
21
  window.location.pathname === '/demo/' ? 2000 : 120000; // 120000 ms = 2 min
20
22
  this.loanTokenInterval = undefined;
23
+ this.localCache = new LocalCache();
21
24
  this.bindEvents();
22
25
  }
23
26
 
@@ -119,106 +122,121 @@ export default class ActionsHandler extends LitElement {
119
122
  }
120
123
 
121
124
  handleBrowseIt() {
122
- const context = 'browse_book';
125
+ const action = 'browse_book';
123
126
  this.dispatchToggleActionGroup();
124
127
 
125
128
  ActionsHandlerService({
126
- action: context,
129
+ action,
127
130
  identifier: this.identifier,
128
131
  success: () => {
129
132
  this.handleReadItNow();
130
133
  },
131
134
  error: data => {
132
- this.dispatchActionError(context, data);
135
+ this.dispatchActionError(action, data);
133
136
  },
134
137
  });
135
138
  }
136
139
 
137
140
  handleReturnIt() {
138
- const context = 'return_loan';
141
+ const action = 'return_loan';
139
142
  this.dispatchToggleActionGroup();
140
143
 
141
144
  ActionsHandlerService({
142
- action: context,
145
+ action,
143
146
  identifier: this.identifier,
144
147
  success: () => {
145
148
  this.deleteLoanCookies();
146
149
  URLHelper.goToUrl(`/details/${this.identifier}`, true);
147
150
  },
148
151
  error: data => {
149
- this.dispatchActionError(context, data);
152
+ this.dispatchActionError(action, data);
150
153
  },
151
154
  });
152
155
  }
153
156
 
154
157
  handleBorrowIt() {
155
- const context = 'borrow_book';
158
+ const action = 'borrow_book';
156
159
  this.dispatchToggleActionGroup();
157
160
 
158
161
  ActionsHandlerService({
159
- action: context,
162
+ action,
160
163
  identifier: this.identifier,
161
164
  success: () => {
162
165
  this.handleReadItNow();
163
166
  },
164
167
  error: data => {
165
- this.dispatchActionError(context, data);
168
+ this.dispatchActionError(action, data);
166
169
  },
167
170
  });
168
171
  }
169
172
 
170
173
  handleReserveIt() {
171
- const context = 'join_waitlist';
174
+ const action = 'join_waitlist';
172
175
  this.dispatchToggleActionGroup();
173
176
 
174
177
  ActionsHandlerService({
175
- action: context,
178
+ action,
176
179
  identifier: this.identifier,
177
180
  success: () => {
178
181
  URLHelper.goToUrl(URLHelper.getRedirectUrl(), true);
179
182
  },
180
183
  error: data => {
181
- this.dispatchActionError(context, data);
184
+ this.dispatchActionError(action, data);
182
185
  },
183
186
  });
184
187
  }
185
188
 
186
189
  handleRemoveFromWaitingList() {
187
- const context = 'leave_waitlist';
190
+ const action = 'leave_waitlist';
188
191
  this.dispatchToggleActionGroup();
189
192
 
190
193
  ActionsHandlerService({
191
- action: context,
194
+ action,
192
195
  identifier: this.identifier,
193
196
  success: () => {
194
197
  URLHelper.goToUrl(URLHelper.getRedirectUrl(), true);
195
198
  },
196
199
  error: data => {
197
- this.dispatchActionError(context, data);
200
+ this.dispatchActionError(action, data);
198
201
  },
199
202
  });
200
203
  }
201
204
 
202
205
  handleLoanTokenPoller() {
203
- const context = 'create_token';
206
+ const action = 'create_token';
204
207
  ActionsHandlerService({
205
208
  identifier: this.identifier,
206
- action: context,
209
+ action,
207
210
  error: data => {
208
- this.dispatchActionError(context, data);
211
+ this.dispatchActionError(action, data);
209
212
  clearInterval(this.loanTokenInterval); // stop token fetch api
210
213
  },
211
214
  });
212
215
  }
213
216
 
214
- dispatchActionError(context, data = {}) {
217
+ /**
218
+ * Dispatches event when an error occured on action
219
+ * Notes:- toggle <collapsible-action-group> visibility (enable/disable).
220
+ *
221
+ * @param { String } action - name of action like browse_book, borrow_book
222
+ * @param { Object } data - erroneous response from api call
223
+ *
224
+ * @fires ActionsHandler#lendingActionError
225
+ */
226
+ dispatchActionError(action, data = {}) {
215
227
  this.dispatchEvent(
216
228
  new CustomEvent('lendingActionError', {
217
- detail: { context, data },
229
+ detail: { action, data },
218
230
  })
219
231
  );
220
232
  }
221
233
 
234
+ /**
235
+ * Dispatches event when patron is clicked on action buttons.
236
+ * Notes:- toggle <collapsible-action-group> disable/enable.
237
+ *
238
+ * @fires ActionsHandler#toggleActionGroup
239
+ */
222
240
  dispatchToggleActionGroup() {
223
241
  this.dispatchEvent(new CustomEvent('toggleActionGroup'));
224
242
  }
@@ -256,19 +274,25 @@ export default class ActionsHandler extends LitElement {
256
274
  }
257
275
 
258
276
  // save consecutive loan count for borrow
259
- setConsecutiveLoanCounts(action = '') {
260
- const { localStorage } = window;
261
- if (localStorage) {
277
+ async setConsecutiveLoanCounts(action = '') {
278
+ try {
262
279
  let newCount = 1;
263
280
  const storageKey = `consecutive-loan-count`;
264
- const existingCount = Number(localStorage.getItem(storageKey));
281
+ const existingCount = await this.localCache.get(storageKey);
265
282
 
266
- // want to increase browse-count by 1 if,
267
- // you consecutive reading a book.
268
- if (action === 'browseAgain') {
283
+ // increase browse-count by 1 when you consecutive reading a book.
284
+ if (action === 'browseAgain' && existingCount !== undefined) {
269
285
  newCount = existingCount ? existingCount + 1 : 1;
270
286
  }
271
- localStorage.setItem(storageKey, newCount);
287
+
288
+ // set new value
289
+ await this.localCache.set({
290
+ key: storageKey,
291
+ value: newCount,
292
+ ttl: 7200, // 2 hours
293
+ });
294
+ } catch (error) {
295
+ console.log(error);
272
296
  }
273
297
  }
274
298
 
@@ -96,7 +96,7 @@ export default class GetLendingActions {
96
96
  this.actionsConfig.adminAccessConfig(),
97
97
  this.actionsConfig.purchaseConfig(),
98
98
  ],
99
- borrowType: isBrowsing ? 'browsing' : 'borrowing',
99
+ borrowType: isBrowsing ? 'browsed' : 'borrowed',
100
100
  };
101
101
  }
102
102
 
@@ -24,6 +24,7 @@ export default class IABookActions extends LitElement {
24
24
  barType: { type: String },
25
25
  sharedObserver: { attribute: false },
26
26
  disableActionGroup: { type: Boolean },
27
+ modal: { Object },
27
28
  };
28
29
  }
29
30
 
@@ -37,12 +38,15 @@ export default class IABookActions extends LitElement {
37
38
  this.bwbPurchaseUrl = '';
38
39
  this.barType = 'action'; // 'title'|'action'
39
40
  this.sharedObserver = undefined;
41
+ this.disableActionGroup = false;
42
+ this.modal = undefined;
43
+
44
+ // private props
40
45
  this.primaryActions = [];
41
46
  this.primaryTitle = '';
42
47
  this.primaryColor = 'primary';
43
48
  this.secondaryActions = [];
44
49
  this.lendingOptions = {};
45
- this.disableActionGroup = false;
46
50
  }
47
51
 
48
52
  disconnectedCallback() {
@@ -144,8 +148,8 @@ export default class IABookActions extends LitElement {
144
148
  });
145
149
 
146
150
  this.borrowType = actions.borrowType;
147
- if (this.borrowType === 'browsing') {
148
- // start timer for browsing.
151
+ if (this.borrowType === 'browsed') {
152
+ // start timer for browsed.
149
153
  // when browse is completed, we shows browse-again button
150
154
  this.startBrowseTimer();
151
155
  }
@@ -201,28 +205,27 @@ export default class IABookActions extends LitElement {
201
205
  *
202
206
  * @event IABookActions#lendingActionError
203
207
  * @param {Object} event - The employee who is responsible for the project
204
- * @param {string} event.detail.context - action when error occurred like 'browseBook', 'borrowBook'
205
- * @param {string} event.detail.data.error - error message
208
+ * @param {string} event.detail.action - action when error occurred like 'browseBook', 'borrowBook'
209
+ * @param {string} event.detail.data.error - error message
206
210
  */
207
211
  handleLendingActionError(event) {
208
- console.log(event?.detail?.data);
209
212
  // toggle activity loader
210
213
  this.handleToggleActionGroup();
211
214
 
212
- const context = event?.detail?.context;
215
+ const action = event?.detail?.action;
213
216
  const errorMsg = event?.detail?.data?.error;
214
217
 
215
- if (errorMsg) this.showErrorModal(errorMsg);
218
+ if (errorMsg) this.showErrorModal(errorMsg, action);
216
219
 
217
220
  // update action bar state if book is not available to browse or borrow.
218
221
  if (errorMsg && errorMsg.match(/not available to borrow/gm)) {
219
222
  let currStatus = this.lendingStatus;
220
- if (context === 'browse_book') {
223
+ if (action === 'browse_book') {
221
224
  currStatus = {
222
225
  ...this.lendingStatus,
223
226
  available_to_browse: false,
224
227
  };
225
- } else if (context === 'borrow_book') {
228
+ } else if (action === 'borrow_book') {
226
229
  currStatus = {
227
230
  ...this.lendingStatus,
228
231
  available_to_borrow: false,
@@ -233,22 +236,42 @@ export default class IABookActions extends LitElement {
233
236
  }
234
237
 
235
238
  /* show error message if something went wrong */
236
- async showErrorModal(errorMsg) {
239
+ async showErrorModal(errorMsg, action) {
237
240
  this.disableActionGroup = false;
238
241
 
239
- this.modal = document.querySelector('#action-bar-modal');
242
+ // check if this.modal passed as prop
240
243
  if (!this.modal) {
241
- this.modal = document.createElement('modal-manager');
242
- this.modal.id = 'action-bar-modal';
244
+ this.modal = document.querySelector('modal-manager');
245
+
246
+ // check the DOM if <modal-manager> already there
247
+ if (!this.modal) this.modal = document.createElement('modal-manager');
243
248
  }
249
+
250
+ this.modal.id = 'action-bar-modal';
244
251
  await document.body.appendChild(this.modal);
245
252
 
253
+ const modalConfig = new ModalConfig({
254
+ title: 'Lending error',
255
+ message: errorMsg,
256
+ headerColor: '#d9534f',
257
+ });
258
+
259
+ if (action === 'create_token') {
260
+ modalConfig.closeOnBackdropClick = false;
261
+ modalConfig.showCloseButton = false;
262
+ modalConfig.message = html` Uh oh, something went wrong trying to access
263
+ this book.<br />
264
+ Please <a href="${() => window.location.reload(true)}">refresh</a> to
265
+ try again or send us an email to
266
+ <a
267
+ href="mailto:info@archive.org?subject=Help: cannot access my borrowed book: ${this
268
+ .identifier}"
269
+ >info@archive.org</a
270
+ >`;
271
+ }
272
+
246
273
  this.modal.showModal({
247
- config: new ModalConfig({
248
- title: 'Lending error',
249
- message: errorMsg,
250
- headerColor: '#d9534f',
251
- }),
274
+ config: modalConfig,
252
275
  });
253
276
  }
254
277
 
@@ -1,6 +1,5 @@
1
1
  import { html, fixture, expect } from '@open-wc/testing';
2
2
  import '../../src/components/collapsible-action-group.js';
3
- import Sinon from 'sinon';
4
3
 
5
4
  const container = ({
6
5
  primaryActions = [
@@ -85,27 +84,3 @@ describe('<collapsible-action-group>', () => {
85
84
  expect(eAnalytics.action).to.equal('Browse');
86
85
  });
87
86
  });
88
-
89
- describe('fetch loan token and localStorage for browsed/borrowsed book', () => {
90
- afterEach(() => {
91
- const localStorageMock = {
92
- getItem: Sinon.spy(),
93
- setItem: Sinon.spy(),
94
- };
95
- Object.defineProperty(window, 'localStorage', {
96
- value: localStorageMock,
97
- });
98
- });
99
-
100
- it('test localStorage function when borrowType is browsed', async () => {
101
- await fixture(container({ borrowType: 'browsed' }));
102
- expect(localStorage.getItem).to.be.calledOnce;
103
- expect(localStorage.getItem).to.be.calledWith('consecutive-loan-count');
104
- });
105
-
106
- it('test localStorage function when borrowType is borrowed', async () => {
107
- await fixture(container({ borrowType: 'borrowed' }));
108
- expect(localStorage.getItem).to.not.be.calledOnce;
109
- expect(localStorage.getItem).to.not.be.calledWith('consecutive-loan-count');
110
- });
111
- });
@@ -122,7 +122,7 @@ describe('Borrow status actions', () => {
122
122
  el.addEventListener('lendingActionError', () => {
123
123
  el.handleLendingActionError({
124
124
  detail: {
125
- context: 'browse_book',
125
+ action: 'browse_book',
126
126
  data: { error: 'not available to borrow' },
127
127
  },
128
128
  });
@@ -151,7 +151,7 @@ describe('Borrow status actions', () => {
151
151
  el.addEventListener('lendingActionError', () => {
152
152
  el.handleLendingActionError({
153
153
  detail: {
154
- context: 'borrow_book',
154
+ action: 'borrow_book',
155
155
  data: { error: 'not available to borrow' },
156
156
  },
157
157
  });