@ordergroove/offers 2.44.0 → 2.44.1-alpha-PR-1167-2.36
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/dist/bundle-report.html +42 -39
- package/dist/offers.js +69 -69
- package/dist/offers.js.map +4 -4
- package/package.json +2 -2
- package/src/components/FrequencyStatus.js +14 -10
- package/src/components/Offer.js +33 -14
- package/src/components/OptinButton.js +2 -2
- package/src/components/OptinSelect.js +6 -5
- package/src/components/OptinStatus.js +16 -9
- package/src/components/Price.js +3 -3
- package/src/components/SelectFrequency.js +11 -6
- package/src/components/TestWizard.js +45 -41
- package/src/components/UpsellModal.js +9 -3
- package/src/components/__tests__/Offer.spec.js +0 -19
- package/src/components/__tests__/OptinStatus.spec.js +17 -4
- package/src/core/__tests__/actions.spec.js +47 -1
- package/src/core/__tests__/base.spec.js +0 -77
- package/src/core/__tests__/experiments.spec.js +0 -3
- package/src/core/__tests__/offerRequest.spec.js +2 -1
- package/src/core/__tests__/selectors.spec.js +7 -7
- package/src/core/actions-preview.js +6 -3
- package/src/core/actions.js +22 -13
- package/src/core/base.js +0 -23
- package/src/core/offerRequest.js +1 -1
- package/src/core/{reducer.js → reducer.ts} +30 -10
- package/src/core/selectors.ts +215 -0
- package/src/core/types/api.ts +71 -0
- package/src/core/types/reducer.ts +94 -0
- package/src/core/types/utility.ts +1 -0
- package/src/core/utils.ts +32 -15
- package/src/make-api.js +1 -1
- package/src/shopify/__tests__/reducers/config.spec.js +603 -0
- package/src/shopify/__tests__/shopifyReducer.spec.js +69 -744
- package/src/shopify/__tests__/utils.spec.js +24 -1
- package/src/shopify/reducers/config.ts +185 -0
- package/src/shopify/shopifyMiddleware.ts +2 -9
- package/src/shopify/{shopifyReducer.js → shopifyReducer.ts} +50 -195
- package/src/shopify/utils.ts +25 -0
- package/src/core/selectors.js +0 -192
- package/src/types.ts +0 -16
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
makeOptedinSelector,
|
|
4
4
|
isSameProduct,
|
|
5
5
|
templatesSelector,
|
|
6
|
-
|
|
6
|
+
makeProductSpecificDefaultFrequencySelector,
|
|
7
7
|
makeProductPrepaidShipmentOptionsSelector
|
|
8
8
|
} from '../selectors';
|
|
9
9
|
import { stringifyFrequency } from '../api';
|
|
@@ -61,25 +61,25 @@ describe('templatesSelector', () => {
|
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
describe('
|
|
64
|
+
describe('makeProductSpecificDefaultFrequencySelector', () => {
|
|
65
65
|
it('should return memoized function', () => {
|
|
66
|
-
const firsCall =
|
|
66
|
+
const firsCall = makeProductSpecificDefaultFrequencySelector(123);
|
|
67
67
|
expect(firsCall).toEqual(jasmine.any(Function));
|
|
68
|
-
expect(firsCall).toBe(
|
|
68
|
+
expect(firsCall).toBe(makeProductSpecificDefaultFrequencySelector(123));
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
it('should return null by default', () => {
|
|
72
|
-
const selectProductDefaultFrequency =
|
|
72
|
+
const selectProductDefaultFrequency = makeProductSpecificDefaultFrequencySelector(123);
|
|
73
73
|
expect(selectProductDefaultFrequency({})).toEqual(null);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
it('should select the product default frequency if there is one', () => {
|
|
77
|
-
const selectProductDefaultFrequency =
|
|
77
|
+
const selectProductDefaultFrequency = makeProductSpecificDefaultFrequencySelector(123);
|
|
78
78
|
expect(selectProductDefaultFrequency({ defaultFrequencies: { 123: '2_w' } })).toEqual('2_w');
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
it('should select the product default frequency if there is one (object frequency)', () => {
|
|
82
|
-
const selectProductDefaultFrequency =
|
|
82
|
+
const selectProductDefaultFrequency = makeProductSpecificDefaultFrequencySelector(123);
|
|
83
83
|
const frequency = { every: '2', period: 'w' };
|
|
84
84
|
expect(selectProductDefaultFrequency({ defaultFrequencies: { 123: frequency } })).toEqual(
|
|
85
85
|
stringifyFrequency(frequency)
|
|
@@ -58,7 +58,8 @@ export const setPreviewStandardOffer = (isPreview, productId, offer) =>
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
|
-
offer
|
|
61
|
+
offer,
|
|
62
|
+
productId
|
|
62
63
|
)
|
|
63
64
|
);
|
|
64
65
|
};
|
|
@@ -94,7 +95,8 @@ export const setPreviewUpsellOffer = (isPreview, productId, offer) =>
|
|
|
94
95
|
autoship_by_default: { [productId]: false },
|
|
95
96
|
modifiers: {}
|
|
96
97
|
},
|
|
97
|
-
offer
|
|
98
|
+
offer,
|
|
99
|
+
productId
|
|
98
100
|
)
|
|
99
101
|
);
|
|
100
102
|
await dispatch(
|
|
@@ -194,7 +196,8 @@ export const setPreviewPrepaid = (isPreview, productId, offer) =>
|
|
|
194
196
|
}
|
|
195
197
|
}
|
|
196
198
|
},
|
|
197
|
-
offer
|
|
199
|
+
offer,
|
|
200
|
+
productId
|
|
198
201
|
)
|
|
199
202
|
);
|
|
200
203
|
await dispatch({
|
package/src/core/actions.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { resolveAuth } from '@ordergroove/auth';
|
|
2
2
|
import * as constants from './constants';
|
|
3
3
|
import { api } from './api';
|
|
4
|
-
import { safeOgFrequency
|
|
4
|
+
import { safeOgFrequency } from './utils';
|
|
5
|
+
import { makeFrequencyForPrepaidShipmentsSelector, makeProductFrequenciesSelector } from './selectors';
|
|
5
6
|
|
|
6
7
|
export const optinProduct = (product, frequency, offer) => ({
|
|
7
8
|
type: constants.OPTIN_PRODUCT,
|
|
@@ -23,10 +24,14 @@ export const productChangeFrequency = (product, frequency, offer) => ({
|
|
|
23
24
|
payload: { product, frequency, offer }
|
|
24
25
|
});
|
|
25
26
|
|
|
26
|
-
export const productChangePrepaidShipments = (product, prepaidShipments, offer) => ({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
export const productChangePrepaidShipments = (product, prepaidShipments, offer) => (dispatch, getState) => {
|
|
28
|
+
// this is a thunk so that we access the state for the selector
|
|
29
|
+
const frequency = makeFrequencyForPrepaidShipmentsSelector(product, prepaidShipments)(getState());
|
|
30
|
+
dispatch({
|
|
31
|
+
type: constants.PRODUCT_CHANGE_PREPAID_SHIPMENTS,
|
|
32
|
+
payload: { product, prepaidShipments, offer, frequency }
|
|
33
|
+
});
|
|
34
|
+
};
|
|
30
35
|
|
|
31
36
|
export const cartProductKeyHasChanged = (oldCartProductKey, newCartProductKey) => ({
|
|
32
37
|
type: constants.CART_PRODUCT_KEY_HAS_CHANGED,
|
|
@@ -183,10 +188,13 @@ export const requestSessionId = () => (dispatch, getState) => {
|
|
|
183
188
|
return sessionId;
|
|
184
189
|
};
|
|
185
190
|
|
|
186
|
-
export const receiveOffer = (response, offer) => ({
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
export const receiveOffer = (response, offer, productId) => (dispatch, getState) => {
|
|
192
|
+
const frequencyConfig = makeProductFrequenciesSelector(productId)(getState());
|
|
193
|
+
dispatch({
|
|
194
|
+
type: constants.RECEIVE_OFFER,
|
|
195
|
+
payload: { ...response, offer, frequencyConfig }
|
|
196
|
+
});
|
|
197
|
+
};
|
|
190
198
|
|
|
191
199
|
export const fetchResponseError = err => ({
|
|
192
200
|
type: constants.FETCH_RESPONSE_ERROR,
|
|
@@ -226,18 +234,19 @@ export const receiveConvertOneTime = (response, product) => ({
|
|
|
226
234
|
|
|
227
235
|
export const createIu = (product, order, quantity, subscribed = false, initialFrequency = null) =>
|
|
228
236
|
function createIuThunk(dispatch, getState) {
|
|
237
|
+
const state = getState();
|
|
229
238
|
const {
|
|
230
239
|
auth,
|
|
231
|
-
config,
|
|
232
240
|
environment: { legoUrl },
|
|
233
241
|
previewUpsellOffer,
|
|
234
242
|
offerId: offer,
|
|
235
243
|
sessionId
|
|
236
|
-
} =
|
|
244
|
+
} = state;
|
|
237
245
|
|
|
238
246
|
if (!auth) return dispatch(unauthorized('No auth set.'));
|
|
239
247
|
|
|
240
|
-
const
|
|
248
|
+
const { frequencies, frequenciesEveryPeriod } = makeProductFrequenciesSelector(product.id)(state);
|
|
249
|
+
const frequency = safeOgFrequency(initialFrequency, frequencies, frequenciesEveryPeriod);
|
|
241
250
|
|
|
242
251
|
const requestAction = requestCreateOneTime(product, order, quantity, offer);
|
|
243
252
|
|
|
@@ -301,7 +310,7 @@ export const setProductToSubscribe = (product, productToSubscribe) => ({
|
|
|
301
310
|
});
|
|
302
311
|
|
|
303
312
|
/**
|
|
304
|
-
* @param {import('../types').MerchantSettings} settings
|
|
313
|
+
* @param {import('../core/types/api').MerchantSettings} settings
|
|
305
314
|
*/
|
|
306
315
|
export const receiveMerchantSettings = settings => ({
|
|
307
316
|
type: constants.RECEIVE_MERCHANT_SETTINGS,
|
package/src/core/base.js
CHANGED
|
@@ -1,30 +1,7 @@
|
|
|
1
1
|
import { LitElement } from 'lit-element';
|
|
2
|
-
import { kebabCase } from './selectors';
|
|
3
2
|
|
|
4
3
|
export const withTemplate = Base =>
|
|
5
4
|
class extends Base {
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
8
|
-
* @param {*} key html attribute key name
|
|
9
|
-
* @param {*} optional configKey key name in configuration object
|
|
10
|
-
*/
|
|
11
|
-
getOption(key, configKey = key) {
|
|
12
|
-
const attrName = kebabCase(key);
|
|
13
|
-
if (this.hasAttribute(attrName)) {
|
|
14
|
-
const attr = this.getAttribute(attrName);
|
|
15
|
-
if (attr.toString().toLowerCase() === 'true') return true;
|
|
16
|
-
if (attr.toString().toLowerCase() === 'false') return false;
|
|
17
|
-
return attr;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (this.template && this.template.config && typeof this.template.config[configKey] !== 'undefined')
|
|
21
|
-
return this.template.config[configKey];
|
|
22
|
-
|
|
23
|
-
if (this.config && typeof this.config[configKey] !== 'undefined') return this.config[configKey];
|
|
24
|
-
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
5
|
applyTemplate(template) {
|
|
29
6
|
this.template = template;
|
|
30
7
|
// update innerHTML if change (performance)
|
package/src/core/offerRequest.js
CHANGED
|
@@ -24,7 +24,7 @@ export function offerRequestMiddleware(store) {
|
|
|
24
24
|
action.payload.searchParams
|
|
25
25
|
)
|
|
26
26
|
.then(
|
|
27
|
-
response => store.dispatch(receiveOffer(response, action.payload.offer)),
|
|
27
|
+
response => store.dispatch(receiveOffer(response, action.payload.offer, productId)),
|
|
28
28
|
err => store.dispatch(fetchResponseError(err))
|
|
29
29
|
)
|
|
30
30
|
.finally(() => store.dispatch(fetchDone(action)));
|
|
@@ -6,7 +6,21 @@ import { getObjectStructuredProductPlans } from './adapters';
|
|
|
6
6
|
import { safeProductId, getMatchingProductIfExists } from './utils';
|
|
7
7
|
import { experimentsReducer } from './experiments';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import {
|
|
10
|
+
AutoshipByDefaultState,
|
|
11
|
+
AutoshipEligibleState,
|
|
12
|
+
ConfigState,
|
|
13
|
+
IncentiveObject,
|
|
14
|
+
IncentivesState,
|
|
15
|
+
NextUpcomingOrderState,
|
|
16
|
+
OptedInState,
|
|
17
|
+
OptedOutState,
|
|
18
|
+
PrepaidShipmentsSelectedState,
|
|
19
|
+
ReceiveOfferPayload
|
|
20
|
+
} from './types/reducer';
|
|
21
|
+
import { EmptyObject } from './types/utility';
|
|
22
|
+
|
|
23
|
+
export const optedin = (state: OptedInState = [], action): OptedInState => {
|
|
10
24
|
switch (action.type) {
|
|
11
25
|
case constants.LOCAL_STORAGE_CLEAR:
|
|
12
26
|
return [];
|
|
@@ -49,7 +63,7 @@ export const optedin = (state = [], action) => {
|
|
|
49
63
|
}
|
|
50
64
|
};
|
|
51
65
|
|
|
52
|
-
export const optedout = (state = [], action) => {
|
|
66
|
+
export const optedout = (state: OptedOutState = [], action): OptedOutState => {
|
|
53
67
|
switch (action.type) {
|
|
54
68
|
case constants.LOCAL_STORAGE_CLEAR:
|
|
55
69
|
return [];
|
|
@@ -77,7 +91,7 @@ export const optedout = (state = [], action) => {
|
|
|
77
91
|
}
|
|
78
92
|
};
|
|
79
93
|
|
|
80
|
-
export const nextUpcomingOrder = (state = {}, { type, payload }) => {
|
|
94
|
+
export const nextUpcomingOrder = (state: NextUpcomingOrderState = {}, { type, payload }): NextUpcomingOrderState => {
|
|
81
95
|
switch (type) {
|
|
82
96
|
case constants.RECEIVE_ORDERS:
|
|
83
97
|
return payload && payload.count > 0
|
|
@@ -107,7 +121,7 @@ export const nextUpcomingOrder = (state = {}, { type, payload }) => {
|
|
|
107
121
|
}
|
|
108
122
|
};
|
|
109
123
|
|
|
110
|
-
export const autoshipEligible = (state = {}, action) => {
|
|
124
|
+
export const autoshipEligible = (state: AutoshipEligibleState = {}, action): AutoshipEligibleState => {
|
|
111
125
|
switch (action.type) {
|
|
112
126
|
case constants.RECEIVE_OFFER:
|
|
113
127
|
return {
|
|
@@ -153,7 +167,10 @@ const mapIncentive = (incentive, incentiveDisplay) => {
|
|
|
153
167
|
}));
|
|
154
168
|
};
|
|
155
169
|
|
|
156
|
-
export const incentives = (
|
|
170
|
+
export const incentives = (
|
|
171
|
+
state: IncentivesState = {},
|
|
172
|
+
action: { type: string; payload: ReceiveOfferPayload }
|
|
173
|
+
): IncentivesState => {
|
|
157
174
|
switch (action.type) {
|
|
158
175
|
case constants.RECEIVE_OFFER:
|
|
159
176
|
return {
|
|
@@ -164,7 +181,7 @@ export const incentives = (state = {}, action) => {
|
|
|
164
181
|
[uniqueProductId]: Object.entries(action.payload.incentives)
|
|
165
182
|
.filter(([productId]) => productId === uniqueProductId)
|
|
166
183
|
.reduce(
|
|
167
|
-
(incentiveObj, [, { initial, ongoing }]) => ({
|
|
184
|
+
(incentiveObj: IncentiveObject | EmptyObject, [, { initial, ongoing }]) => ({
|
|
168
185
|
...incentiveObj,
|
|
169
186
|
initial: [
|
|
170
187
|
...(incentiveObj.initial || []),
|
|
@@ -392,11 +409,11 @@ export const locale = (
|
|
|
392
409
|
};
|
|
393
410
|
|
|
394
411
|
export const config = (
|
|
395
|
-
state = {
|
|
412
|
+
state: ConfigState = {
|
|
396
413
|
offerType: 'radio'
|
|
397
414
|
},
|
|
398
415
|
action
|
|
399
|
-
) => {
|
|
416
|
+
): ConfigState => {
|
|
400
417
|
switch (action.type) {
|
|
401
418
|
case constants.SET_CONFIG:
|
|
402
419
|
return {
|
|
@@ -447,7 +464,7 @@ export const previewPrepaidOffer = (state = false, action) => {
|
|
|
447
464
|
}
|
|
448
465
|
};
|
|
449
466
|
|
|
450
|
-
export const autoshipByDefault = (state =
|
|
467
|
+
export const autoshipByDefault = (state: AutoshipByDefaultState = {}, action): AutoshipByDefaultState => {
|
|
451
468
|
switch (action.type) {
|
|
452
469
|
case constants.RECEIVE_OFFER:
|
|
453
470
|
return {
|
|
@@ -492,7 +509,10 @@ export const productPlans = (state = {}, action) => {
|
|
|
492
509
|
}
|
|
493
510
|
};
|
|
494
511
|
|
|
495
|
-
export const prepaidShipmentsSelected = (
|
|
512
|
+
export const prepaidShipmentsSelected = (
|
|
513
|
+
state: PrepaidShipmentsSelectedState = {},
|
|
514
|
+
action
|
|
515
|
+
): PrepaidShipmentsSelectedState => {
|
|
496
516
|
switch (action.type) {
|
|
497
517
|
// Given that, in the cart, products will have a composed id (<productId>:<cartId>) and that every time
|
|
498
518
|
// a product changes in the cart we need to sync these changes back with the eComm platform, this operation
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { createSelector } from 'reselect';
|
|
2
|
+
import memoize from 'lodash.memoize';
|
|
3
|
+
import { stringifyFrequency } from './api';
|
|
4
|
+
import platform from '../platform';
|
|
5
|
+
import { mapFrequencyToSellingPlan, safeProductId } from './utils';
|
|
6
|
+
import { OfferElement, State } from './types/reducer';
|
|
7
|
+
|
|
8
|
+
memoize.Cache = Map;
|
|
9
|
+
|
|
10
|
+
type BaseProduct = {
|
|
11
|
+
id: string;
|
|
12
|
+
components?: string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function arraysEqual<T>(a: T[], b: T[]) {
|
|
16
|
+
if (a === b) return true;
|
|
17
|
+
if (a === null || b === null) return false;
|
|
18
|
+
if (a.length !== b.length) return false;
|
|
19
|
+
|
|
20
|
+
// If you don't care about the order of the elements inside
|
|
21
|
+
// the array, you should sort both arrays here.
|
|
22
|
+
// Please note that calling sort on an array will modify that array.
|
|
23
|
+
// you might want to clone your array first.
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < a.length; ++i) {
|
|
26
|
+
if (a[i] !== b[i]) return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveFrequency(sellingPlans: string[], frequenciesEveryPeriod: string[], frequency) {
|
|
32
|
+
const ogFrequency = stringifyFrequency(frequency);
|
|
33
|
+
if (!platform.shopify_selling_plans) return ogFrequency;
|
|
34
|
+
return mapFrequencyToSellingPlan(sellingPlans, frequenciesEveryPeriod, ogFrequency);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const isSameProduct = <T extends BaseProduct, S extends BaseProduct>(a: T, b: S) => {
|
|
38
|
+
if ((a as BaseProduct) === b) return true;
|
|
39
|
+
if (typeof a === 'object' && typeof b === 'object' && a && b) {
|
|
40
|
+
if (a.id === b.id) {
|
|
41
|
+
if (!(Array.isArray(a.components) && Array.isArray(b.components))) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (arraysEqual((a.components || []).sort(), (b.components || []).sort())) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Returns a list of opted in products id from the state
|
|
54
|
+
* @param {object} state
|
|
55
|
+
*/
|
|
56
|
+
export const optedinSelector = (state: State) => state.optedin || [];
|
|
57
|
+
|
|
58
|
+
const optedoutSelector = (state: State) => state.optedout || [];
|
|
59
|
+
|
|
60
|
+
export const autoshipSelector = (state: State) => state.autoshipByDefault || {};
|
|
61
|
+
|
|
62
|
+
const defaultFrequenciesSelector = (state: State) => state.defaultFrequencies || {};
|
|
63
|
+
|
|
64
|
+
const prepaidSellingPlansSelector = (state: State) => state?.config?.prepaidSellingPlans || [];
|
|
65
|
+
const prepaidShipmentsSelectedSelector = (state: State) => state?.prepaidShipmentsSelected || {};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Creates a function with state arguments that return the true when
|
|
69
|
+
* productId is in the optedin array or not in optedout or autoship by default
|
|
70
|
+
*/
|
|
71
|
+
export const makeOptedinSelector = memoize(
|
|
72
|
+
(product: BaseProduct) =>
|
|
73
|
+
createSelector(optedinSelector, optedoutSelector, autoshipSelector, (optedin, optedout, autoshipByDefault) => {
|
|
74
|
+
const entry = optedin.find(b => isSameProduct(product, b));
|
|
75
|
+
if (entry) {
|
|
76
|
+
return entry;
|
|
77
|
+
}
|
|
78
|
+
if (optedout.find(b => isSameProduct(product, b))) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (product && autoshipByDefault[product.id]) {
|
|
82
|
+
return { id: product.id };
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}),
|
|
86
|
+
product => JSON.stringify(product)
|
|
87
|
+
);
|
|
88
|
+
/**
|
|
89
|
+
* Creates a function with state arguments that return the true when
|
|
90
|
+
* productId is in the optedin array
|
|
91
|
+
*/
|
|
92
|
+
export const makeSubscribedSelector = memoize(
|
|
93
|
+
(product: BaseProduct) =>
|
|
94
|
+
createSelector(optedinSelector, optedin => {
|
|
95
|
+
const entry = optedin.find(b => isSameProduct(product, b));
|
|
96
|
+
if (entry) {
|
|
97
|
+
return entry;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}),
|
|
101
|
+
product => JSON.stringify(product)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
export const makePrepaidSubscribedSelector = memoize(
|
|
105
|
+
(product: BaseProduct) =>
|
|
106
|
+
createSelector(optedinSelector, optedin => optedin.some(b => isSameProduct(product, b) && b.prepaidShipments)),
|
|
107
|
+
product => JSON.stringify(product)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
export const makePrepaidShipmentsSelectedSelector = memoize(
|
|
111
|
+
(product: BaseProduct) =>
|
|
112
|
+
createSelector(
|
|
113
|
+
prepaidShipmentsSelectedSelector,
|
|
114
|
+
prepaidShipmentsSelected => prepaidShipmentsSelected[product.id] || null
|
|
115
|
+
),
|
|
116
|
+
product => JSON.stringify(product)
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Creates a function with state arguments that return the true when
|
|
121
|
+
* productId is in the optedout array
|
|
122
|
+
*/
|
|
123
|
+
export const makeOptedoutSelector = memoize((product: BaseProduct) =>
|
|
124
|
+
createSelector(optedoutSelector, optedout => optedout.find(b => isSameProduct(product, b)))
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
export const makeProductFrequencyOptedInSelector = memoize(
|
|
128
|
+
(product: BaseProduct) =>
|
|
129
|
+
createSelector(
|
|
130
|
+
makeOptedinSelector(product),
|
|
131
|
+
productOptin => (productOptin && 'frequency' in productOptin && productOptin.frequency) || null
|
|
132
|
+
),
|
|
133
|
+
product => JSON.stringify(product)
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
export const makeProductPrepaidShipmentsOptedInSelector = memoize(
|
|
137
|
+
(product: BaseProduct) =>
|
|
138
|
+
createSelector(
|
|
139
|
+
makeOptedinSelector(product),
|
|
140
|
+
productOptin => (productOptin && 'prepaidShipments' in productOptin && productOptin.prepaidShipments) || null
|
|
141
|
+
),
|
|
142
|
+
product => JSON.stringify(product)
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
export const makeProductPrepaidShipmentOptionsSelector = memoize((productId: string) =>
|
|
146
|
+
createSelector(prepaidSellingPlansSelector, prepaidSellingPlans => {
|
|
147
|
+
const shipmentsList =
|
|
148
|
+
prepaidSellingPlans[safeProductId(productId)]?.map(({ numberShipments }) => numberShipments) || [];
|
|
149
|
+
return shipmentsList.sort((a, b) => a - b);
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* If the product has a product-specific default frequency, return that frequency
|
|
155
|
+
*/
|
|
156
|
+
export const makeProductSpecificDefaultFrequencySelector = memoize((productId: string) =>
|
|
157
|
+
createSelector(
|
|
158
|
+
defaultFrequenciesSelector,
|
|
159
|
+
makeProductFrequenciesSelector(productId),
|
|
160
|
+
(defaultFrequencies, { frequencies: sellingPlans = [], frequenciesEveryPeriod = [] }) =>
|
|
161
|
+
(defaultFrequencies[safeProductId(productId)] &&
|
|
162
|
+
resolveFrequency(sellingPlans, frequenciesEveryPeriod, defaultFrequencies[safeProductId(productId)])) ||
|
|
163
|
+
null
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
export const makeProductFrequencyOptionsSelector = memoize((productId: string) =>
|
|
168
|
+
createSelector(makeProductFrequenciesSelector(productId), productFrequencies => productFrequencies.frequencies)
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
export const makeProductDefaultFrequencySelector = memoize((productId: string) =>
|
|
172
|
+
createSelector(makeProductFrequenciesSelector(productId), productFrequencies => productFrequencies.defaultFrequency)
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
export const makeProductFrequenciesSelector = memoize((productId: string) =>
|
|
176
|
+
createSelector(
|
|
177
|
+
(state: State) => state?.config?.productFrequencies || {},
|
|
178
|
+
productFrequencies => {
|
|
179
|
+
return productFrequencies[safeProductId(productId)] || {};
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// this selector is only called when an action is dispatched, so we don't need to memoize
|
|
185
|
+
// other selectors are called whenever the Redux state is updated
|
|
186
|
+
export const makeFrequencyForPrepaidShipmentsSelector = (product: BaseProduct, prepaidShipments: number) =>
|
|
187
|
+
createSelector(
|
|
188
|
+
prepaidSellingPlansSelector,
|
|
189
|
+
makeProductFrequenciesSelector(product.id),
|
|
190
|
+
(prepaidSellingPlans, { frequencies }) => {
|
|
191
|
+
if (prepaidShipments) {
|
|
192
|
+
const productId = safeProductId(product.id);
|
|
193
|
+
const plan = prepaidSellingPlans[productId]?.find(p => p.numberShipments === prepaidShipments);
|
|
194
|
+
return plan ? plan.sellingPlan : null;
|
|
195
|
+
}
|
|
196
|
+
return frequencies[0];
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Convert a string from camel case to kebab case.
|
|
202
|
+
*/
|
|
203
|
+
export const kebabCase = (string: string) => {
|
|
204
|
+
return string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const getFallbackValue = (element: HTMLElement & { offer: OfferElement }, key: string, defaultValue?) =>
|
|
208
|
+
(element && element.hasAttribute && element.hasAttribute(kebabCase(key)) && element[key]) ||
|
|
209
|
+
(element.offer && typeof (element.offer[key] !== 'undefined') && element.offer[key]) ||
|
|
210
|
+
defaultValue;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Returns a list of opted in products id from the state
|
|
214
|
+
*/
|
|
215
|
+
export const templatesSelector = (state: State) => ({ templates: state.templates || [] });
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type Order = {
|
|
2
|
+
merchant: string;
|
|
3
|
+
customer: string;
|
|
4
|
+
payment: string;
|
|
5
|
+
shipping_address: string;
|
|
6
|
+
public_id: string;
|
|
7
|
+
sub_total: string;
|
|
8
|
+
tax_total: string;
|
|
9
|
+
shipping_total: string;
|
|
10
|
+
discount_total: string;
|
|
11
|
+
total: string;
|
|
12
|
+
created: string;
|
|
13
|
+
updated: string;
|
|
14
|
+
place: string;
|
|
15
|
+
cancelled: string | null;
|
|
16
|
+
tries: number;
|
|
17
|
+
generic_error_count: number;
|
|
18
|
+
status: number;
|
|
19
|
+
type: number;
|
|
20
|
+
order_merchant_id: string | null;
|
|
21
|
+
rejected_message: string | null;
|
|
22
|
+
extra_data: unknown;
|
|
23
|
+
locked: boolean;
|
|
24
|
+
oos_free_shipping: boolean;
|
|
25
|
+
currency_code: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type Incentive = {
|
|
29
|
+
object: string;
|
|
30
|
+
field: string;
|
|
31
|
+
type: string;
|
|
32
|
+
value: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type ExperimentVariant = {
|
|
36
|
+
public_id: string;
|
|
37
|
+
parameters: any;
|
|
38
|
+
weight: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type ExperimentConfig = {
|
|
42
|
+
public_id: string;
|
|
43
|
+
variants: ExperimentVariant[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export interface MerchantSettings {
|
|
47
|
+
currency_code: string;
|
|
48
|
+
multicurrency_enabled: boolean;
|
|
49
|
+
experiments?: ExperimentConfig;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type OfferResponse = {
|
|
53
|
+
result: string;
|
|
54
|
+
module_view: {
|
|
55
|
+
regular: string;
|
|
56
|
+
};
|
|
57
|
+
modifiers: Record<string, { coupon_code: string }>;
|
|
58
|
+
autoship: Record<string, boolean>;
|
|
59
|
+
in_stock: Record<string, boolean>;
|
|
60
|
+
autoship_by_default: Record<string, boolean>;
|
|
61
|
+
default_frequencies: Record<string, { every: number; every_period: number }>;
|
|
62
|
+
eligibility_groups: Record<string, string[]>;
|
|
63
|
+
incentives: Record<
|
|
64
|
+
string,
|
|
65
|
+
{
|
|
66
|
+
ongoing: string[];
|
|
67
|
+
initial: string[];
|
|
68
|
+
}
|
|
69
|
+
>;
|
|
70
|
+
incentives_display: Record<string, Incentive>;
|
|
71
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Order, Incentive as ApiIncentive, MerchantSettings, OfferResponse } from './api';
|
|
2
|
+
import { type Offer } from '../../components/Offer';
|
|
3
|
+
import { ShopifyCart, ShopifyProductEntity } from '../../shopify/types/shopify';
|
|
4
|
+
import reducer from '../reducer';
|
|
5
|
+
|
|
6
|
+
export type State = ReturnType<typeof reducer>;
|
|
7
|
+
|
|
8
|
+
// state types
|
|
9
|
+
|
|
10
|
+
export type NextUpcomingOrderState = Partial<
|
|
11
|
+
Order & {
|
|
12
|
+
place: Date;
|
|
13
|
+
products: string[];
|
|
14
|
+
}
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
export type IncentivesState = Record<string, IncentiveObject>;
|
|
18
|
+
|
|
19
|
+
type Incentive = ApiIncentive & {
|
|
20
|
+
id: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type IncentiveObject = {
|
|
24
|
+
initial: Incentive[];
|
|
25
|
+
ongoing: Incentive[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ConfigState = Partial<{
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated use productFrequencies instead
|
|
31
|
+
*/
|
|
32
|
+
frequencies: string[];
|
|
33
|
+
offerType: string;
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated use productFrequencies instead
|
|
36
|
+
*/
|
|
37
|
+
frequenciesEveryPeriod: string[];
|
|
38
|
+
merchantSettings: MerchantSettings;
|
|
39
|
+
/**
|
|
40
|
+
* @deprecated use productFrequencies instead
|
|
41
|
+
*/
|
|
42
|
+
defaultFrequency: string;
|
|
43
|
+
/**
|
|
44
|
+
* @deprecated use productFrequencies instead
|
|
45
|
+
*/
|
|
46
|
+
frequenciesText: string[];
|
|
47
|
+
prepaidSellingPlans: Record<string, { numberShipments: number; sellingPlan: string }[]>;
|
|
48
|
+
storeCurrency: string;
|
|
49
|
+
productFrequencies: Record<string, ProductFrequencyConfig>;
|
|
50
|
+
}>;
|
|
51
|
+
|
|
52
|
+
type ProductFrequencyConfig = {
|
|
53
|
+
frequencies?: string[];
|
|
54
|
+
frequenciesEveryPeriod?: string[];
|
|
55
|
+
frequenciesText?: string[];
|
|
56
|
+
defaultFrequency?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type AutoshipEligibleState = Record<string, boolean>;
|
|
60
|
+
|
|
61
|
+
export type PrepaidShipmentsSelectedState = Record<string, number>;
|
|
62
|
+
|
|
63
|
+
export type OptInItem = { id: string; frequency: string; prepaidShipments?: number; components?: string[] };
|
|
64
|
+
|
|
65
|
+
export type OptedInState = OptInItem[];
|
|
66
|
+
|
|
67
|
+
export type OptedOutState = { id: string }[];
|
|
68
|
+
|
|
69
|
+
export type AutoshipByDefaultState = Record<string, boolean>;
|
|
70
|
+
|
|
71
|
+
// payload types
|
|
72
|
+
|
|
73
|
+
export type ReceiveOfferPayload = OfferResponse & {
|
|
74
|
+
offer: OfferElement;
|
|
75
|
+
frequencyConfig: {
|
|
76
|
+
frequencies?: string[];
|
|
77
|
+
frequenciesEveryPeriod?: string[];
|
|
78
|
+
frequenciesText?: string[];
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type OfferElement = InstanceType<typeof Offer> & { config: ConfigState };
|
|
83
|
+
|
|
84
|
+
export type ReceiveProductPlansPayload = Record<string, string[]>;
|
|
85
|
+
|
|
86
|
+
export type SetupProductPayload = {
|
|
87
|
+
product: ShopifyProductEntity;
|
|
88
|
+
offer: OfferElement;
|
|
89
|
+
currency: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type SetupCartPayload = ShopifyCart;
|
|
93
|
+
|
|
94
|
+
export type ReceiveMerchantSettingsPayload = MerchantSettings;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type EmptyObject = Record<string, never>;
|