@ordergroove/offers 2.40.3 → 2.40.4-alpha-PR-1091-5.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.3",
3
+ "version": "2.40.4-alpha-PR-1091-5.0+abf9741c",
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": "46f1b45441b87946e459a29d8f503895e4e3f0de"
52
+ "gitHead": "abf9741c5b1b86199eade5ed2f12215835ef54c5"
53
53
  }
@@ -1,5 +1,6 @@
1
1
  import * as offersAll from '../index';
2
2
  import { api } from '../core/api';
3
+ import { REQUEST_OFFER } from '../core/constants';
3
4
 
4
5
  const offers = offersAll.offers;
5
6
 
@@ -71,16 +72,13 @@ describe('Offers', () => {
71
72
  });
72
73
 
73
74
  describe('offers.resolveSettings', () => {
74
- it('should fetch offer for single product in pdp', () => {
75
+ it('should fetch offer for single product in pdp', async () => {
75
76
  offers.resolveSettings('0e5de2bedc5e11e3a2e4bc764e106cf4', 'staging', { product: '123' }, mockStore);
76
- expect(fetchOfferSpy).toHaveBeenCalledWith(
77
- 'https://staging.offers.ordergroove.com',
78
- '0e5de2bedc5e11e3a2e4bc764e106cf4',
79
- 'xyz',
80
- '123',
81
- 'pdp',
82
- theState
83
- );
77
+ expect(mockStore.dispatch).toHaveBeenCalledTimes(1);
78
+ expect(mockStore.dispatch).toHaveBeenCalledWith({
79
+ type: REQUEST_OFFER,
80
+ payload: { product: '123', module: 'pdp', offer: undefined }
81
+ });
84
82
  });
85
83
 
86
84
  it('should fetch offer for each product in cart', () => {
@@ -90,23 +88,14 @@ describe('Offers', () => {
90
88
  { cart: { products: ['123', '456'] } },
91
89
  mockStore
92
90
  );
93
- expect(fetchOfferSpy).toHaveBeenCalledTimes(2);
94
91
 
95
- expect(fetchOfferSpy.calls.argsFor(0)).toEqual([
96
- 'https://staging.offers.ordergroove.com',
97
- '0e5de2bedc5e11e3a2e4bc764e106cf4',
98
- 'xyz',
99
- '123',
100
- 'pdp',
101
- theState
92
+ expect(mockStore.dispatch).toHaveBeenCalledTimes(2);
93
+
94
+ expect(mockStore.dispatch.calls.argsFor(0)).toEqual([
95
+ { type: REQUEST_OFFER, payload: { product: '123', module: 'pdp', offer: undefined } }
102
96
  ]);
103
- expect(fetchOfferSpy.calls.argsFor(1)).toEqual([
104
- 'https://staging.offers.ordergroove.com',
105
- '0e5de2bedc5e11e3a2e4bc764e106cf4',
106
- 'xyz',
107
- '456',
108
- 'pdp',
109
- theState
97
+ expect(mockStore.dispatch.calls.argsFor(1)).toEqual([
98
+ { type: REQUEST_OFFER, payload: { product: '456', module: 'pdp', offer: undefined } }
110
99
  ]);
111
100
  });
112
101
 
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  import { experimentsMiddleware, experimentsReducer, getAssignedExperimentVariant } from '../experiments';
2
- import { CREATED_SESSION_ID, RECEIVE_MERCHANT_SETTINGS } from '../constants';
2
+ import { CREATED_SESSION_ID, READY, RECEIVE_MERCHANT_SETTINGS } from '../constants';
3
3
  import { createSessionId } from '../actions';
4
4
  import { createStore, combineReducers, applyMiddleware } from 'redux';
5
5
  import { sessionId } from '../reducer';
@@ -34,7 +34,9 @@ describe('experiments', () => {
34
34
 
35
35
  store.dispatch({ type: CREATED_SESSION_ID, payload: 'abc' });
36
36
  store.dispatch({ type: RECEIVE_MERCHANT_SETTINGS, payload: { experiments } });
37
+ store.dispatch({ type: READY });
37
38
 
39
+ await new Promise(r => setTimeout(r, 10));
38
40
  const {
39
41
  experiments: { currentVariant }
40
42
  } = store.getState();
@@ -56,7 +58,9 @@ describe('experiments', () => {
56
58
 
57
59
  store.dispatch({ type: CREATED_SESSION_ID, payload: 'cda' });
58
60
  store.dispatch({ type: RECEIVE_MERCHANT_SETTINGS, payload: { experiments } });
61
+ store.dispatch({ type: READY });
59
62
 
63
+ await new Promise(r => setTimeout(r, 10));
60
64
  const {
61
65
  experiments: { currentVariant }
62
66
  } = store.getState();
@@ -0,0 +1,78 @@
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
+ 'http://localhost/offer/abc-merchant/pdp?session_id=x.y.z&page_type=1&p=%5B%22123%22%5D&module_view=%5B%22regular%22%5D',
57
+
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({ error: true });
67
+ store.dispatch(requestOffer('1234'));
68
+ await new Promise(r => setTimeout(r, 100));
69
+ expect(fetch).toHaveBeenCalledTimes(1);
70
+ expect(fetch).toHaveBeenCalledWith(
71
+ 'http://localhost/offer/abc-merchant/pdp?session_id=x.y.z&page_type=1&p=%5B%221234%22%5D&module_view=%5B%22regular%22%5D',
72
+
73
+ {}
74
+ );
75
+
76
+ expect(store.getState().lastError).toEqual({ type: FETCH_RESPONSE_ERROR, payload: { error: true } });
77
+ });
78
+ });
@@ -198,7 +198,9 @@ export const requestOffer = (product, module = constants.DEFAULT_OFFER_MODULE, o
198
198
  payload: { product, module, offer }
199
199
  });
200
200
 
201
- export const fetchOffer = (product, module = constants.DEFAULT_OFFER_MODULE, offerElement) =>
201
+ export const fetchOffer = requestOffer;
202
+
203
+ export const _fetchOffer = (product, module = constants.DEFAULT_OFFER_MODULE, offerElement) =>
202
204
  function fetchOfferThunk(dispatch, getState) {
203
205
  const state = getState();
204
206
  const {
package/src/core/api.js CHANGED
@@ -66,7 +66,7 @@ export const toProductId = product =>
66
66
 
67
67
  export const fetchOffer = memoize(
68
68
  withFetchJson(
69
- withHost((merchantId, sessionId, product, module = 'pdp', state = {}) => {
69
+ withHost((merchantId, sessionId, product, module = 'pdp', searchParams = {}) => {
70
70
  if (!merchantId) throw Error('merchantId required');
71
71
  if (!sessionId) throw Error('sessionId required');
72
72
  if (!product) throw Error('product required');
@@ -75,18 +75,10 @@ export const fetchOffer = memoize(
75
75
  ['session_id', sessionId],
76
76
  ['page_type', 1],
77
77
  ['p', toProductId(product)],
78
- ['module_view', JSON.stringify(['regular'])]
78
+ ['module_view', JSON.stringify(['regular'])],
79
+ ...Object.entries(searchParams)
79
80
  ];
80
81
 
81
- // NOTE: This block is only relevant for merchants actively running an AB test.
82
- // For all other merchants, experiments.variants will be null.
83
- const { experiments } = state;
84
- const variant = experiments?.variants?.at(experiments.currentVariant);
85
-
86
- if (variant) {
87
- query.push(['variant', variant.public_id]);
88
- }
89
-
90
82
  return [`/offer/${merchantId}/${module}?${toQuery(query)}`];
91
83
  })
92
84
  ),
@@ -1,5 +1,6 @@
1
- import { RECEIVE_MERCHANT_SETTINGS, SET_EXPERIMENT_VARIANT } from './constants';
1
+ import { READY, RECEIVE_MERCHANT_SETTINGS, REQUEST_OFFER, SET_EXPERIMENT_VARIANT } from './constants';
2
2
  import murmur from 'murmurhash-js';
3
+ import { waitFor } from './waitUntilOffersReady';
3
4
 
4
5
  /**
5
6
  * Returns the index of a variant based on the provided key and variants.
@@ -82,22 +83,39 @@ export function getAssignedExperimentVariant(experimentSettings, sessionId) {
82
83
  * @return {function} A function that takes the next middleware function and an action as arguments.
83
84
  */
84
85
  export function experimentsMiddleware(store) {
85
- return next => action => {
86
- const response = next(action);
87
- if (action.type !== RECEIVE_MERCHANT_SETTINGS) {
88
- return response;
89
- }
86
+ const [waitForReady, resolveReady] = waitFor();
87
+
88
+ let variant, experimentSettings;
89
+
90
+ return next => async action => {
91
+ if (action.type === READY) {
92
+ resolveReady();
93
+ } else if (action.type === RECEIVE_MERCHANT_SETTINGS) {
94
+ await waitForReady;
95
+ experimentSettings = action.payload.experiments;
90
96
 
91
- const { experiments: experimentSettings, sessionId } = store.getState();
92
- const variant = getAssignedExperimentVariant(experimentSettings, sessionId);
97
+ const { sessionId } = store.getState();
93
98
 
94
- if (variant) {
95
- store.dispatch({
96
- type: SET_EXPERIMENT_VARIANT,
97
- payload: variant
98
- });
99
+ variant = getAssignedExperimentVariant(experimentSettings, sessionId);
100
+
101
+ if (variant) {
102
+ store.dispatch({
103
+ type: SET_EXPERIMENT_VARIANT,
104
+ payload: variant
105
+ });
106
+ }
107
+ } else if (action.type === REQUEST_OFFER) {
108
+ await waitForReady;
109
+ if (variant) {
110
+ action.payload.searchParams = {
111
+ ...action.payload.searchParams,
112
+ variant: variant.public_id
113
+ };
114
+ }
99
115
  }
100
116
 
101
- return response;
117
+ next(action);
102
118
  };
103
119
  }
120
+
121
+ experimentsMiddleware.position = -1;
@@ -0,0 +1,34 @@
1
+ import { fetchDone, fetchResponseError, receiveOffer } from './actions';
2
+ import api from './api';
3
+ import { DEFAULT_OFFER_MODULE, REQUEST_OFFER } from './constants';
4
+ import { safeProductId } from './utils';
5
+
6
+ export function offerRequestMiddleware(store) {
7
+ return next => action => {
8
+ if (action.type === REQUEST_OFFER) {
9
+ const {
10
+ merchantId,
11
+ sessionId,
12
+ environment: { apiUrl }
13
+ } = store.getState();
14
+
15
+ const productId = safeProductId(action.payload.product);
16
+ if (productId)
17
+ api
18
+ .fetchOffer(
19
+ apiUrl,
20
+ merchantId,
21
+ sessionId,
22
+ productId,
23
+ action.payload.module || DEFAULT_OFFER_MODULE,
24
+ action.payload.searchParams
25
+ )
26
+ .then(
27
+ response => store.dispatch(receiveOffer(response, action.payload.offer)),
28
+ err => store.dispatch(fetchResponseError(err))
29
+ )
30
+ .finally(() => store.dispatch(fetchDone(action)));
31
+ }
32
+ return next(action);
33
+ };
34
+ }
@@ -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 || {};
package/src/make-api.js CHANGED
@@ -146,15 +146,11 @@ export default function makeApi(store) {
146
146
  } else if (settings.cart && Array.isArray(settings.cart.products)) {
147
147
  products = products.concat(settings.cart.products);
148
148
  }
149
- const { apiUrl } = environment({}, actions.setEnvironment(env));
150
149
 
151
150
  const state = storeInstance.getState();
152
151
  const { sessionId } = state;
153
152
  if (sessionId) {
154
- products.forEach(product => {
155
- const id = safeProductId(product);
156
- api.fetchOffer(apiUrl, merchantId, sessionId, `${id}`, DEFAULT_OFFER_MODULE, state);
157
- });
153
+ products.forEach(product => storeInstance.dispatch(actions.requestOffer(product)));
158
154
  }
159
155
 
160
156
  if (settings.product_discounts && typeof settings.product_discounts === 'object') {