@ordergroove/offers 2.40.2 → 2.40.4-alpha-PR-1091-3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ordergroove/offers",
3
- "version": "2.40.2",
3
+ "version": "2.40.4-alpha-PR-1091-3.0+b3ec3088",
4
4
  "description": "offer state component",
5
5
  "author": "Eugenio Lattanzio <eugenio63@gmail.com>",
6
6
  "homepage": "https://github.com/ordergroove/plush-toys#readme",
@@ -49,5 +49,5 @@
49
49
  "@ordergroove/offers-templates": "^0.9.6",
50
50
  "@types/lodash.memoize": "^4.1.9"
51
51
  },
52
- "gitHead": "d2b9d0b60d78396e064037cffebf1cb2a494cd5f"
52
+ "gitHead": "b3ec3088230992d2339dd18c604084b6f4cd2044"
53
53
  }
@@ -157,49 +157,7 @@ describe('redux actions', function () {
157
157
  });
158
158
 
159
159
  it('fetchOffer should return a function', () => {
160
- expect(fetchOffer()).toEqual(jasmine.any(Function));
161
- });
162
-
163
- it('should call api.fetchOffer with environment.apiUrl from state as first param ', async () => {
164
- const state = {
165
- merchantId: 'the merchantId',
166
- sessionId: 'the sessionId',
167
- environment: { apiUrl: 'the environment.apiUrl' }
168
- };
169
- const getState = jasmine.createSpy('getState').and.returnValue(state);
170
- const fetchOfferSpy = spyOn(api, 'fetchOffer').and.resolveTo({ hey: 'ho' });
171
- await fetchOffer('the product')(this.dispatch, getState);
172
- expect(fetchOfferSpy).toHaveBeenCalledWith(
173
- 'the environment.apiUrl',
174
- 'the merchantId',
175
- 'the sessionId',
176
- 'the product',
177
- 'pdp',
178
- state
179
- );
180
- });
181
-
182
- it('should dispatch the receiveOffer if api returns', async () => {
183
- const fetchOfferSpy = spyOn(api, 'fetchOffer').and.resolveTo({ hey: 'ho' });
184
-
185
- await fetchOffer('yum product')(this.dispatch, this.getState);
186
-
187
- expect(this.dispatch.calls.count()).toEqual(3);
188
- expect(this.dispatch.calls.argsFor(0)[0]).toEqual(requestOffer('yum product', 'pdp'));
189
- expect(this.dispatch.calls.argsFor(1)[0]).toEqual(receiveOffer({ hey: 'ho' }));
190
- expect(this.getState).toHaveBeenCalled();
191
- expect(fetchOfferSpy).toHaveBeenCalledWith('some-apiUrl', 'foo', 'bar', 'yum product', 'pdp', theState);
192
- });
193
-
194
- it('should dispatch fetchResponseError if api fails', async () => {
195
- const fetchOfferSpy = spyOn(api, 'fetchOffer').and.rejectWith(Error({ hey: 'ho' }));
196
- await fetchOffer('yum product')(this.dispatch, this.getState);
197
-
198
- expect(this.dispatch.calls.count()).toEqual(3);
199
- expect(this.dispatch.calls.argsFor(0)[0]).toEqual(requestOffer('yum product', 'pdp'));
200
- expect(this.dispatch.calls.argsFor(1)[0]).toEqual(fetchResponseError(Error({ hey: 'ho' })));
201
- expect(fetchOfferSpy).toHaveBeenCalledWith('some-apiUrl', 'foo', 'bar', 'yum product', 'pdp', theState);
202
- expect(this.getState).toHaveBeenCalled();
160
+ expect(fetchOffer()).toEqual(requestOffer());
203
161
  });
204
162
  });
205
163
 
@@ -60,6 +60,7 @@ describe('loadState', () => {
60
60
  });
61
61
 
62
62
  it('should load empty object if preview mode', () => {
63
+ window.og = window.og || {};
63
64
  const tmp = window.og.previewMode;
64
65
  window.og.previewMode = true;
65
66
  expect(loadState()).toEqual({});
@@ -68,10 +69,21 @@ describe('loadState', () => {
68
69
  });
69
70
 
70
71
  describe('saveState', () => {
72
+ let cookieSpy;
71
73
  let getItemSpy, setItemSpy;
72
74
  beforeEach(() => {
73
75
  getItemSpy = spyOn(Object.getPrototypeOf(localStorage), 'getItem');
74
76
  setItemSpy = spyOn(Object.getPrototypeOf(localStorage), 'setItem');
77
+ cookieSpy = '';
78
+ Object.defineProperty(document, 'cookie', {
79
+ get: function () {
80
+ return cookieSpy;
81
+ },
82
+ set: function (value) {
83
+ cookieSpy = value;
84
+ },
85
+ configurable: true // Ensure property can be redefined in future tests
86
+ });
75
87
  });
76
88
 
77
89
  it('should not save empty state', function () {
@@ -83,6 +95,7 @@ describe('saveState', () => {
83
95
  it('should not save if same state', function () {
84
96
  getItemSpy.and.returnValue(JSON.stringify({ sessionId: 'yum' }));
85
97
  saveState({ sessionId: 'yum' });
98
+ expect(cookieSpy).toEqual('og_session_id=yum; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Lax');
86
99
  expect(getItemSpy).toHaveBeenCalledWith(STORE_ROOT);
87
100
  expect(setItemSpy).not.toHaveBeenCalled();
88
101
  });
@@ -90,6 +103,7 @@ describe('saveState', () => {
90
103
  it('should save if different state', function () {
91
104
  const payload = { sessionId: 'yum' };
92
105
  saveState(payload);
106
+ expect(cookieSpy).toEqual('og_session_id=yum; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Lax');
93
107
  expect(getItemSpy).toHaveBeenCalledWith(STORE_ROOT);
94
108
  expect(setItemSpy).toHaveBeenCalledWith(STORE_ROOT, JSON.stringify({ sessionId: 'yum' }));
95
109
  });
@@ -0,0 +1,81 @@
1
+ import { offerRequestMiddleware } from '../offerRequest';
2
+
3
+ import { environment, merchantId, offer, sessionId } from '../reducer';
4
+ import { requestOffer } from '../actions';
5
+ import { createStore, combineReducers, applyMiddleware } from 'redux';
6
+ import { FETCH_RESPONSE_ERROR } from '../constants';
7
+
8
+ const mockOfferResponse = (productId, inStock = true, autoship = true, defaultFrequency) => {
9
+ return Promise.resolve({
10
+ json() {
11
+ return Promise.resolve({
12
+ in_stock: { [productId]: inStock },
13
+ eligibility_groups: { [productId]: ['subscription', 'upsell'] },
14
+ result: 'success',
15
+ autoship: { [productId]: autoship },
16
+ autoship_by_default: { [productId]: false },
17
+ modifiers: {},
18
+ module_view: { regular: '096135e6650111e9a444bc764e106cf4' },
19
+ incentives_display: {},
20
+ incentives: {
21
+ [productId]: { initial: [], ongoing: [] }
22
+ },
23
+ ...(defaultFrequency && {
24
+ default_frequencies: {
25
+ [productId]: defaultFrequency
26
+ }
27
+ })
28
+ });
29
+ }
30
+ });
31
+ };
32
+
33
+ describe('offerRequest', () => {
34
+ let fetch, store;
35
+ const lastError = (acc = null, cur) => (cur.type == FETCH_RESPONSE_ERROR ? cur : acc);
36
+ beforeEach(() => {
37
+ fetch = spyOn(window, 'fetch');
38
+
39
+ store = createStore(
40
+ combineReducers({ offer, environment, merchantId, sessionId, lastError }),
41
+ {
42
+ environment: { apiUrl: 'http://localhost' },
43
+ merchantId: 'abc-merchant',
44
+ sessionId: 'x.y.z',
45
+ lastError: null
46
+ },
47
+ applyMiddleware(offerRequestMiddleware)
48
+ );
49
+ });
50
+
51
+ it('should call fetch offer with environment apiUrl from state as first param', async () => {
52
+ fetch.and.returnValue(mockOfferResponse('123', true, true));
53
+ store.dispatch(requestOffer('123'));
54
+ expect(fetch).toHaveBeenCalledTimes(1);
55
+ expect(fetch).toHaveBeenCalledWith(
56
+ new URL(
57
+ 'http://localhost/offer/abc-merchant/pdp?session_id=x.y.z&page_type=1&module_view=%5B%22regular%22%5D&p=%5B%22123%22%5D'
58
+ )
59
+ );
60
+
61
+ await new Promise(r => setTimeout(r, 10));
62
+ expect(store.getState().offer.offerId).toEqual('096135e6650111e9a444bc764e106cf4');
63
+ });
64
+
65
+ it('should dispatch fetchResponseError if api fails', async () => {
66
+ fetch.and.rejectWith({
67
+ result: 'error'
68
+ });
69
+ store.dispatch(requestOffer('123'));
70
+ expect(fetch).toHaveBeenCalledTimes(1);
71
+ expect(fetch).toHaveBeenCalledWith(
72
+ new URL(
73
+ 'http://localhost/offer/abc-merchant/pdp?session_id=x.y.z&page_type=1&module_view=%5B%22regular%22%5D&p=%5B%22123%22%5D'
74
+ )
75
+ );
76
+
77
+ await new Promise(r => setTimeout(r, 10));
78
+
79
+ expect(store.getState().lastError).toEqual({ type: FETCH_RESPONSE_ERROR, payload: { result: 'error' } });
80
+ });
81
+ });
@@ -199,26 +199,7 @@ export const requestOffer = (product, module = constants.DEFAULT_OFFER_MODULE, o
199
199
  });
200
200
 
201
201
  export const fetchOffer = (product, module = constants.DEFAULT_OFFER_MODULE, offerElement) =>
202
- function fetchOfferThunk(dispatch, getState) {
203
- const state = getState();
204
- const {
205
- merchantId,
206
- sessionId,
207
- environment: { apiUrl }
208
- } = state;
209
- const requestAction = requestOffer(product, module, offerElement);
210
- dispatch(requestAction);
211
-
212
- const productId = safeProductId(product);
213
- if (!productId) return null;
214
- return api
215
- .fetchOffer(apiUrl, merchantId, sessionId, productId, module, state)
216
- .then(
217
- response => dispatch(receiveOffer(response, offerElement)),
218
- err => dispatch(fetchResponseError(err))
219
- )
220
- .finally(() => dispatch(fetchDone(requestAction)));
221
- };
202
+ requestOffer(product, module, offerElement);
222
203
 
223
204
  export const checkout = () => ({
224
205
  type: constants.CHECKOUT
@@ -1,4 +1,4 @@
1
- import { RECEIVE_MERCHANT_SETTINGS, SET_EXPERIMENT_VARIANT } from './constants';
1
+ import { RECEIVE_MERCHANT_SETTINGS, REQUEST_OFFER, SET_EXPERIMENT_VARIANT } from './constants';
2
2
  import murmur from 'murmurhash-js';
3
3
 
4
4
  /**
@@ -82,22 +82,33 @@ export function getAssignedExperimentVariant(experimentSettings, sessionId) {
82
82
  * @return {function} A function that takes the next middleware function and an action as arguments.
83
83
  */
84
84
  export function experimentsMiddleware(store) {
85
+ let variant, experimentSettings;
86
+
85
87
  return next => action => {
86
88
  const response = next(action);
87
- if (action.type !== RECEIVE_MERCHANT_SETTINGS) {
88
- return response;
89
- }
89
+ if (action.type === RECEIVE_MERCHANT_SETTINGS) {
90
+ experimentSettings = action.payload.experiments;
90
91
 
91
- const { experiments: experimentSettings, sessionId } = store.getState();
92
- const variant = getAssignedExperimentVariant(experimentSettings, sessionId);
92
+ const { sessionId } = store.getState();
93
93
 
94
- if (variant) {
95
- store.dispatch({
96
- type: SET_EXPERIMENT_VARIANT,
97
- payload: variant
98
- });
99
- }
94
+ variant = getAssignedExperimentVariant(experimentSettings, sessionId);
100
95
 
96
+ if (variant) {
97
+ store.dispatch({
98
+ type: SET_EXPERIMENT_VARIANT,
99
+ payload: variant
100
+ });
101
+ }
102
+ } else if (action.type === REQUEST_OFFER) {
103
+ if (variant) {
104
+ action.payload.searchParams = {
105
+ ...action.payload.searchParams,
106
+ variant: variant.public_id
107
+ };
108
+ }
109
+ }
101
110
  return response;
102
111
  };
103
112
  }
113
+
114
+ experimentsMiddleware.position = -1;
@@ -39,6 +39,13 @@ export const serializeState = state => {
39
39
 
40
40
  export const saveState = state => {
41
41
  if (isPreviewMode()) return;
42
+ if (state && state.sessionId) {
43
+ document.cookie =
44
+ 'og_session_id=' +
45
+ encodeURIComponent(state.sessionId) +
46
+ '; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Lax';
47
+ }
48
+
42
49
  const serializedState = serializeState(state);
43
50
  if (serializedState && localStorage.getItem(STORE_ROOT) !== serializedState) {
44
51
  localStorage.setItem(STORE_ROOT, serializedState);
@@ -0,0 +1,96 @@
1
+ import { toProductId } from './api';
2
+ import { DEFAULT_OFFER_MODULE, FETCH_RESPONSE_ERROR, RECEIVE_FETCH, RECEIVE_OFFER, REQUEST_OFFER } from './constants';
3
+ import { safeProductId } from './utils';
4
+
5
+ function mapStateToOfferURL(state, { module = DEFAULT_OFFER_MODULE, ...params } = {}) {
6
+ const {
7
+ merchantId,
8
+ sessionId,
9
+ environment: { apiUrl }
10
+ } = state;
11
+
12
+ const url = new URL(`/offer/${merchantId}/${module}`, apiUrl);
13
+
14
+ [
15
+ ['session_id', sessionId],
16
+ ['page_type', 1],
17
+ ['module_view', JSON.stringify(['regular'])],
18
+ ...Object.entries(params)
19
+ ].forEach(([k, v]) => url.searchParams.set(k, v));
20
+
21
+ return url;
22
+ }
23
+
24
+ /**
25
+ * Creates a function that processes a batch of actions by sending an offer request
26
+ * based on the product IDs extracted from the actions' payloads. The created function
27
+ * dispatches appropriate actions to the store depending on the success or failure of the offer request.
28
+ *
29
+ * @function makeDoOfferRequest
30
+ * @param {Object} store - The Redux store instance used to dispatch actions and retrieve the state.
31
+ * @returns {Function} A function that accepts a spread of actions and processes them
32
+ * by making an offer request and dispatching the appropriate responses.
33
+ *
34
+ * @example
35
+ * const doOfferRequest = makeDoOfferRequest(store);
36
+ *
37
+ * const actions = [
38
+ * { payload: { product: 'product1', offer: 'offer1' } },
39
+ * { payload: { product: 'product2', offer: 'offer2' } }
40
+ * ];
41
+ *
42
+ * doOfferRequest(...actions);
43
+ *
44
+ * // The returned function will:
45
+ * // 1. Extract product IDs from the actions' payloads.
46
+ * // 2. Construct the offer request URL.
47
+ * // 3. Make an HTTP request to fetch the offers.
48
+ * // 4. Dispatch appropriate actions to the store based on the response.
49
+ * // 5. Handle errors and dispatch error actions if necessary.
50
+ * // 6. Finally, dispatch a `fetchDone` action to signal completion.
51
+ */
52
+ function createOfferRequestHandler(store) {
53
+ return async function doOfferRequest(action) {
54
+ const state = store.getState();
55
+
56
+ const offerRequestUrl = mapStateToOfferURL(state, {
57
+ p: toProductId(safeProductId(action.payload.product)),
58
+ ...action.payload.searchParams
59
+ });
60
+
61
+ try {
62
+ const httpResponse = await fetch(offerRequestUrl);
63
+ const jsonResponse = await httpResponse.json();
64
+
65
+ store.dispatch({
66
+ type: RECEIVE_OFFER,
67
+ payload: {
68
+ ...jsonResponse,
69
+ offer: action.payload.offer
70
+ }
71
+ });
72
+ } catch (err) {
73
+ console.log(err);
74
+ store.dispatch({
75
+ type: FETCH_RESPONSE_ERROR,
76
+ payload: err
77
+ });
78
+ } finally {
79
+ store.dispatch({
80
+ type: RECEIVE_FETCH,
81
+ payload: action
82
+ });
83
+ }
84
+ };
85
+ }
86
+
87
+ export function offerRequestMiddleware(store) {
88
+ const doOfferRequests = createOfferRequestHandler(store);
89
+
90
+ return next => action => {
91
+ next(action);
92
+ if (action.type === REQUEST_OFFER) {
93
+ doOfferRequests(action);
94
+ }
95
+ };
96
+ }
@@ -271,7 +271,7 @@ export const productOffer = (state = {}, action) => {
271
271
  ...state,
272
272
  ...Object.entries(action.payload.autoship)
273
273
  .map(([key]) => ({ [key]: Object.keys(action.payload.modifiers) }))
274
- .reduce((acc, object) => ({ ...acc, ...object }))
274
+ .reduce((acc, object) => ({ ...acc, ...object }), {})
275
275
  };
276
276
  case constants.CHECKOUT:
277
277
  return {};
package/src/core/store.js CHANGED
@@ -4,6 +4,7 @@ import thunk from 'redux-thunk';
4
4
  import { loadState } from './localStorage';
5
5
  import { dispatchMiddleware, localStorageMiddleware, offerEvents } from './middleware';
6
6
  import { waitUntilOffersReady } from './waitUntilOffersReady';
7
+ import { offerRequestMiddleware } from './offerRequest';
7
8
 
8
9
  export function makeStore(reducer, ...extraMiddlewares) {
9
10
  if (window.og && window.og.store) return window.og.store;
@@ -17,7 +18,13 @@ export function makeStore(reducer, ...extraMiddlewares) {
17
18
  })
18
19
  : compose;
19
20
 
20
- const middlewares = [waitUntilOffersReady, thunk, dispatchMiddleware, offerEvents];
21
+ const middlewares = [
22
+ waitUntilOffersReady,
23
+ thunk, // remove thisone after migration, it will save some KB
24
+ offerRequestMiddleware,
25
+ dispatchMiddleware,
26
+ offerEvents
27
+ ];
21
28
 
22
29
  let initial = {};
23
30
 
@@ -30,7 +37,13 @@ export function makeStore(reducer, ...extraMiddlewares) {
30
37
  }
31
38
  }
32
39
 
33
- const enhancer = composeEnhancers(applyMiddleware(...middlewares, ...extraMiddlewares.filter(it => it)));
40
+ const sortedMiddlewares = extraMiddlewares
41
+ .filter(it => (it?.position || 0) < 0)
42
+ .concat(middlewares)
43
+ .concat(extraMiddlewares.filter(it => it && (it?.position || 0) >= 0));
44
+
45
+ const enhancer = composeEnhancers(applyMiddleware(...sortedMiddlewares));
46
+
34
47
  const store = createStore(reducer, initial, enhancer);
35
48
 
36
49
  window.og = window.og || {};