@ordergroove/offers 2.43.0 → 2.44.1-alpha-PR-1166-3.30
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/CHANGELOG.md +11 -0
- package/dist/bundle-report.html +55 -53
- package/dist/offers.js +38 -38
- package/dist/offers.js.map +4 -4
- package/package.json +2 -2
- package/src/components/FrequencyStatus.js +13 -10
- package/src/components/Offer.js +33 -14
- package/src/components/OptinButton.js +2 -2
- package/src/components/OptinSelect.js +5 -5
- package/src/components/OptinStatus.js +15 -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__/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/experiments.js +2 -4
- package/src/core/offerRequest.js +1 -1
- package/src/core/{reducer.js → reducer.ts} +30 -10
- package/src/core/{selectors.js → selectors.ts} +73 -57
- package/src/core/types/api.ts +71 -0
- package/src/core/types/reducer.ts +95 -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__/productPlan.spec.js +18 -8
- package/src/shopify/__tests__/reducers/config.spec.js +497 -0
- package/src/shopify/__tests__/shopifyReducer.spec.js +68 -611
- package/src/shopify/__tests__/utils.spec.js +68 -1
- package/src/shopify/reducers/config.ts +223 -0
- package/src/shopify/reducers/productPlans.ts +3 -10
- package/src/shopify/shopifyMiddleware.ts +2 -9
- package/src/shopify/{shopifyReducer.js → shopifyReducer.ts} +52 -221
- package/src/shopify/types/shopify.ts +1 -1
- package/src/shopify/utils.ts +70 -0
- package/src/types.ts +0 -16
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { money, percentage } from '../utils';
|
|
1
|
+
import { getPayAsYouGoSellingPlanGroups, money, percentage, textToFreq } from '../utils';
|
|
2
2
|
|
|
3
3
|
describe('Shopify Utils', () => {
|
|
4
4
|
describe('Money', () => {
|
|
@@ -26,4 +26,71 @@ describe('Shopify Utils', () => {
|
|
|
26
26
|
expect(formattedPrice).toBe('10%');
|
|
27
27
|
});
|
|
28
28
|
});
|
|
29
|
+
|
|
30
|
+
describe('textToFreq', () => {
|
|
31
|
+
it('textToFreq should return freq', () => {
|
|
32
|
+
expect(textToFreq('DAY')).toEqual('1_1');
|
|
33
|
+
expect(textToFreq('DAYS')).toEqual('1_1');
|
|
34
|
+
expect(textToFreq('DAYLY')).toEqual('1_1');
|
|
35
|
+
expect(textToFreq('1 DAY')).toEqual('1_1');
|
|
36
|
+
expect(textToFreq('2 DAYS')).toEqual('2_1');
|
|
37
|
+
expect(textToFreq('2 day(s)')).toEqual('2_1');
|
|
38
|
+
|
|
39
|
+
expect(textToFreq('week')).toEqual('1_2');
|
|
40
|
+
expect(textToFreq('weekly')).toEqual('1_2');
|
|
41
|
+
expect(textToFreq('1 week')).toEqual('1_2');
|
|
42
|
+
expect(textToFreq('2 weeks')).toEqual('2_2');
|
|
43
|
+
expect(textToFreq('2 week(s)')).toEqual('2_2');
|
|
44
|
+
|
|
45
|
+
expect(textToFreq('MONTH')).toEqual('1_3');
|
|
46
|
+
expect(textToFreq('MONTHLY')).toEqual('1_3');
|
|
47
|
+
expect(textToFreq('1 month')).toEqual('1_3');
|
|
48
|
+
expect(textToFreq('2 months')).toEqual('2_3');
|
|
49
|
+
expect(textToFreq('2 month(s)')).toEqual('2_3');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('selling plan queries', () => {
|
|
55
|
+
describe('getPayAsYouGoSellingPlanGroups', () => {
|
|
56
|
+
it('returns default group with app ID', () => {
|
|
57
|
+
expect(
|
|
58
|
+
getPayAsYouGoSellingPlanGroups([{ name: 'Subscribe and Save', app_id: 'ordergroove-subscribe-and-save' }])
|
|
59
|
+
.length
|
|
60
|
+
).toBe(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns default group without app ID', () => {
|
|
64
|
+
expect(getPayAsYouGoSellingPlanGroups([{ name: 'Subscribe and Save', app_id: null }]).length).toBe(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns PSI selling plan groups', () => {
|
|
68
|
+
expect(
|
|
69
|
+
getPayAsYouGoSellingPlanGroups([{ name: 'incentive-group', app_id: 'ordergroove-subscribe-and-save' }]).length
|
|
70
|
+
).toBe(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('does not return prepaid selling plan groups', () => {
|
|
74
|
+
expect(
|
|
75
|
+
getPayAsYouGoSellingPlanGroups([{ name: 'Prepaid-1', app_id: 'ordergroove-prepaid-and-save' }]).length
|
|
76
|
+
).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('returns PSFL group with app ID', () => {
|
|
80
|
+
expect(
|
|
81
|
+
getPayAsYouGoSellingPlanGroups([
|
|
82
|
+
{
|
|
83
|
+
// name probably still starts with og_psfl
|
|
84
|
+
// but changing it so that the app_id branch is hit
|
|
85
|
+
name: 'changed-psfl-name',
|
|
86
|
+
app_id: 'ordergroove-product-specific-frequency-list'
|
|
87
|
+
}
|
|
88
|
+
]).length
|
|
89
|
+
).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns PSFL group without app ID', () => {
|
|
93
|
+
expect(getPayAsYouGoSellingPlanGroups([{ name: 'og_psfl_1w', app_id: null }]).length).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
29
96
|
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import * as constants from '../../core/constants';
|
|
2
|
+
import { getFirstSellingPlan, isOgFrequency, mapFrequencyToSellingPlan, safeProductId } from '../../core/utils';
|
|
3
|
+
import type {
|
|
4
|
+
ConfigState,
|
|
5
|
+
ReceiveMerchantSettingsPayload,
|
|
6
|
+
ReceiveOfferPayload,
|
|
7
|
+
ReceiveProductPlansPayload,
|
|
8
|
+
SetupProductPayload
|
|
9
|
+
} from '../../core/types/reducer';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
getPayAsYouGoSellingPlanGroup,
|
|
13
|
+
isProductSpecificFrequencySellingPlanGroup,
|
|
14
|
+
sellingPlansToEveryPeriod,
|
|
15
|
+
sellingPlansToFrequencies,
|
|
16
|
+
getPrepaidShipments
|
|
17
|
+
} from '../utils';
|
|
18
|
+
import { ShopifySellingPlanGroupsEntity, ShopifyVariantsEntity } from '../types/shopify';
|
|
19
|
+
|
|
20
|
+
const config = (
|
|
21
|
+
state: ConfigState = {
|
|
22
|
+
frequencies: [],
|
|
23
|
+
offerType: 'radio',
|
|
24
|
+
frequenciesEveryPeriod: [],
|
|
25
|
+
productFrequencies: {}
|
|
26
|
+
},
|
|
27
|
+
action
|
|
28
|
+
): ConfigState => {
|
|
29
|
+
if (constants.RECEIVE_PRODUCT_PLANS === action.type) {
|
|
30
|
+
const frequencies = [
|
|
31
|
+
...new Set(
|
|
32
|
+
Object.values(action.payload as ReceiveProductPlansPayload)
|
|
33
|
+
.map(Object.keys)
|
|
34
|
+
.flat()
|
|
35
|
+
)
|
|
36
|
+
];
|
|
37
|
+
return {
|
|
38
|
+
...state,
|
|
39
|
+
frequencies
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (constants.SETUP_PRODUCT === action.type) {
|
|
44
|
+
const {
|
|
45
|
+
payload: { product, currency }
|
|
46
|
+
} = action as { payload: SetupProductPayload };
|
|
47
|
+
let configToAdd: ConfigState = {};
|
|
48
|
+
|
|
49
|
+
// get the frequency values for the old config structure
|
|
50
|
+
// leaving for now for backwards compatibility, but they should eventually be removed in favor of productFrequencies
|
|
51
|
+
let globalFrequencies = getFrequencies(product.selling_plan_groups, state);
|
|
52
|
+
if (globalFrequencies?.frequencies?.length) {
|
|
53
|
+
configToAdd = {
|
|
54
|
+
...globalFrequencies,
|
|
55
|
+
hasProductSpecificFrequencies:
|
|
56
|
+
state.hasProductSpecificFrequencies ||
|
|
57
|
+
isProductSpecificFrequencySellingPlanGroup(getPayAsYouGoSellingPlanGroup(product.selling_plan_groups))
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let productFrequencies = product.variants?.reduce(
|
|
62
|
+
(acc, variant) => reduceSellingPlansToFrequencies(acc, variant, product.selling_plan_groups, state),
|
|
63
|
+
{}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
configToAdd = {
|
|
67
|
+
...configToAdd,
|
|
68
|
+
|
|
69
|
+
productFrequencies: {
|
|
70
|
+
...state.productFrequencies,
|
|
71
|
+
...productFrequencies
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// prepaid selling plans
|
|
76
|
+
const prepaidSellingPlanGroups = product?.selling_plan_groups.filter(group => /^Prepaid-.*/.test(group.name));
|
|
77
|
+
if (prepaidSellingPlanGroups.length) {
|
|
78
|
+
configToAdd = {
|
|
79
|
+
...configToAdd,
|
|
80
|
+
prepaidSellingPlans: { ...state.prepaidSellingPlans, ...getPrepaidSellingPlans(prepaidSellingPlanGroups) }
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
...state,
|
|
85
|
+
...configToAdd,
|
|
86
|
+
storeCurrency: currency
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (constants.RECEIVE_OFFER === action.type) {
|
|
91
|
+
const {
|
|
92
|
+
payload: { offer: offerEl }
|
|
93
|
+
} = action as { payload: ReceiveOfferPayload };
|
|
94
|
+
const { defaultFrequency, product } = offerEl || {};
|
|
95
|
+
const { frequencies: sellingPlans, frequenciesEveryPeriod, prepaidSellingPlans = {} } = state;
|
|
96
|
+
|
|
97
|
+
// productFrequencies does not have entries for the cart ID
|
|
98
|
+
// eligible frequencies apply to cart entries for the product
|
|
99
|
+
const productId = safeProductId(product?.id);
|
|
100
|
+
const currentProductFrequencies = state.productFrequencies[productId];
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
...state,
|
|
104
|
+
// populate this field for backwards compatibility
|
|
105
|
+
// this should eventually be removed in favor of productFrequencies
|
|
106
|
+
defaultFrequency: getUpdatedDefaultFrequency(
|
|
107
|
+
productId,
|
|
108
|
+
defaultFrequency,
|
|
109
|
+
prepaidSellingPlans,
|
|
110
|
+
sellingPlans,
|
|
111
|
+
frequenciesEveryPeriod
|
|
112
|
+
),
|
|
113
|
+
productFrequencies: {
|
|
114
|
+
...state.productFrequencies,
|
|
115
|
+
[productId]: {
|
|
116
|
+
...currentProductFrequencies,
|
|
117
|
+
defaultFrequency: getUpdatedDefaultFrequency(
|
|
118
|
+
productId,
|
|
119
|
+
defaultFrequency,
|
|
120
|
+
prepaidSellingPlans,
|
|
121
|
+
currentProductFrequencies?.frequencies,
|
|
122
|
+
currentProductFrequencies?.frequenciesEveryPeriod
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (constants.RECEIVE_MERCHANT_SETTINGS === action.type) {
|
|
130
|
+
return {
|
|
131
|
+
...state,
|
|
132
|
+
merchantSettings: {
|
|
133
|
+
...(action.payload as ReceiveMerchantSettingsPayload)
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return state;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
function getFrequencies(
|
|
142
|
+
productSellingPlanGroups: ShopifySellingPlanGroupsEntity[],
|
|
143
|
+
state: { defaultFrequency?: string }
|
|
144
|
+
) {
|
|
145
|
+
const sellingPlanGroup = getPayAsYouGoSellingPlanGroup(productSellingPlanGroups);
|
|
146
|
+
const frequencies = sellingPlansToFrequencies(sellingPlanGroup);
|
|
147
|
+
if (frequencies?.length) {
|
|
148
|
+
const frequenciesEveryPeriod = sellingPlansToEveryPeriod(sellingPlanGroup);
|
|
149
|
+
const frequenciesText = sellingPlanGroup.options?.[0]?.values || frequencies;
|
|
150
|
+
let defaultFrequency = state?.defaultFrequency;
|
|
151
|
+
|
|
152
|
+
if (defaultFrequency && isOgFrequency(defaultFrequency)) {
|
|
153
|
+
defaultFrequency =
|
|
154
|
+
mapFrequencyToSellingPlan(frequencies, frequenciesEveryPeriod, defaultFrequency) ||
|
|
155
|
+
getFirstSellingPlan(frequencies) ||
|
|
156
|
+
defaultFrequency;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
frequencies,
|
|
160
|
+
frequenciesEveryPeriod,
|
|
161
|
+
frequenciesText,
|
|
162
|
+
...(defaultFrequency ? { defaultFrequency } : {})
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function reduceSellingPlansToFrequencies(
|
|
169
|
+
acc: ConfigState['productFrequencies'],
|
|
170
|
+
variant: ShopifyVariantsEntity,
|
|
171
|
+
sellingPlanGroups: ShopifySellingPlanGroupsEntity[],
|
|
172
|
+
state: ConfigState
|
|
173
|
+
) {
|
|
174
|
+
const sellingPlanGroupIdsForProduct = variant.selling_plan_allocations.map(all => all.selling_plan_group_id);
|
|
175
|
+
const applicableSellingPlanGroups = sellingPlanGroups.filter(group =>
|
|
176
|
+
sellingPlanGroupIdsForProduct.includes(group.id)
|
|
177
|
+
);
|
|
178
|
+
const frequencies = getFrequencies(applicableSellingPlanGroups, state.productFrequencies[variant.id]);
|
|
179
|
+
if (frequencies) {
|
|
180
|
+
acc[variant.id] = frequencies;
|
|
181
|
+
}
|
|
182
|
+
return acc;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getUpdatedDefaultFrequency(
|
|
186
|
+
productId: string,
|
|
187
|
+
offerElementDefaultFrequency: string,
|
|
188
|
+
prepaidSellingPlans: ConfigState['prepaidSellingPlans'],
|
|
189
|
+
frequencies: string[] | undefined = [],
|
|
190
|
+
frequenciesEveryPeriod: string[] | undefined = []
|
|
191
|
+
) {
|
|
192
|
+
// We don't want to be setting the default frequency to a prepaid selling plan
|
|
193
|
+
if (prepaidSellingPlans[productId]?.some(({ sellingPlan }) => sellingPlan === offerElementDefaultFrequency)) {
|
|
194
|
+
return getFirstSellingPlan(frequencies) || offerElementDefaultFrequency;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!isOgFrequency(offerElementDefaultFrequency)) {
|
|
198
|
+
return offerElementDefaultFrequency;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
mapFrequencyToSellingPlan(frequencies, frequenciesEveryPeriod, offerElementDefaultFrequency) ||
|
|
203
|
+
getFirstSellingPlan(frequencies) ||
|
|
204
|
+
offerElementDefaultFrequency
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getPrepaidSellingPlans(prepaidSellingPlanGroups) {
|
|
209
|
+
return prepaidSellingPlanGroups.reduce((acc, cur) => {
|
|
210
|
+
const variant = cur.name.split('-')[1];
|
|
211
|
+
|
|
212
|
+
const sellingPlanInfo = cur.selling_plans.map(sellingPlanObject => {
|
|
213
|
+
return {
|
|
214
|
+
numberShipments: getPrepaidShipments(sellingPlanObject),
|
|
215
|
+
sellingPlan: String(sellingPlanObject.id)
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return { ...acc, [variant]: sellingPlanInfo };
|
|
220
|
+
}, {});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export default config;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { money, percentage } from '../utils';
|
|
1
|
+
import { getPayAsYouGoSellingPlan, money, percentage } from '../utils';
|
|
2
2
|
import { ShopifyProductEntity, ShopifySellingPlanAllocationsEntity, ShopifySellingPlansEntity } from '../types/shopify';
|
|
3
3
|
import { ProductPlanEntity } from '../types/productPlan';
|
|
4
4
|
|
|
@@ -92,8 +92,6 @@ export const addPrepaidPriceAndSavings = (
|
|
|
92
92
|
return productPlan;
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
-
export const DEFAULT_PAY_AS_YOU_GO_GROUP_NAME = 'Subscribe and Save';
|
|
96
|
-
|
|
97
95
|
export const mapSellingPlanToDiscount = (
|
|
98
96
|
allocation: ShopifySellingPlanAllocationsEntity,
|
|
99
97
|
sellingPlans: ShopifySellingPlansEntity[],
|
|
@@ -112,9 +110,7 @@ export const mapSellingPlanToDiscount = (
|
|
|
112
110
|
};
|
|
113
111
|
|
|
114
112
|
if (isPrepaidAllocation(allocation)) {
|
|
115
|
-
const payAsYouGoPlan = sellingPlans
|
|
116
|
-
plan => plan.group_name === DEFAULT_PAY_AS_YOU_GO_GROUP_NAME && plan.options.length === 1
|
|
117
|
-
);
|
|
113
|
+
const payAsYouGoPlan = getPayAsYouGoSellingPlan(sellingPlans);
|
|
118
114
|
return addPrepaidPriceAndSavings(allocation, productPlan, payAsYouGoPlan, currency);
|
|
119
115
|
}
|
|
120
116
|
|
|
@@ -130,9 +126,6 @@ export const sellingPlanAllocationsReducer = (
|
|
|
130
126
|
|
|
131
127
|
export const getSellingPlans = (product: ShopifyProductEntity) =>
|
|
132
128
|
product.selling_plan_groups.reduce<ShopifySellingPlansEntity[]>(
|
|
133
|
-
(allGroups, group) => [
|
|
134
|
-
...allGroups,
|
|
135
|
-
...group.selling_plans.map(selling_plan => ({ ...selling_plan, group_name: group.name }))
|
|
136
|
-
],
|
|
129
|
+
(allGroups, group) => [...allGroups, ...group.selling_plans.map(selling_plan => ({ ...selling_plan, group }))],
|
|
137
130
|
[]
|
|
138
131
|
);
|
|
@@ -18,6 +18,7 @@ import { makeSubscribedSelector } from '../core/selectors';
|
|
|
18
18
|
import { getOrCreateHidden, safeProductId } from '../core/utils';
|
|
19
19
|
import { getTrackingKey } from './shopifyTrackingMiddleware';
|
|
20
20
|
import { ShopifyCart, ShopifyProductEntity } from './types/shopify';
|
|
21
|
+
import { SetupProductPayload, SetupCartPayload, OfferElement } from '../core/types/reducer';
|
|
21
22
|
|
|
22
23
|
const SHOPIFY_ROOT = window.Shopify?.routes?.root || '/';
|
|
23
24
|
const CART_PAGE_URL = '/cart';
|
|
@@ -25,14 +26,6 @@ const CART_JS_URL = `${SHOPIFY_ROOT}cart.js`;
|
|
|
25
26
|
const CART_CHANGE_URL = `${SHOPIFY_ROOT}cart/change.js`;
|
|
26
27
|
const PRODUCTS_URL = `${SHOPIFY_ROOT}products/`;
|
|
27
28
|
|
|
28
|
-
type SetupProductPayload = {
|
|
29
|
-
product: ShopifyProductEntity;
|
|
30
|
-
offer: any;
|
|
31
|
-
currency: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type SetupCartPayload = ShopifyCart;
|
|
35
|
-
|
|
36
29
|
/**
|
|
37
30
|
* List of section DOM elements to update via section-rendering api https://shopify.dev/api/section-rendering
|
|
38
31
|
*/
|
|
@@ -344,7 +337,7 @@ export function getSubscribedFrequency(productId, store) {
|
|
|
344
337
|
*
|
|
345
338
|
* @param store
|
|
346
339
|
*/
|
|
347
|
-
function synchronizeSellingPlan(store: any, offerElement?:
|
|
340
|
+
function synchronizeSellingPlan(store: any, offerElement?: OfferElement) {
|
|
348
341
|
if (offerElement?.isCart) return; // hidden inputs are used when product page, not cart.
|
|
349
342
|
if (!offerElement?.shouldEnableOffer) return; // do not set a selling plan if we're hiding the offer
|
|
350
343
|
|