@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/bundle-report.html +55 -53
  3. package/dist/offers.js +38 -38
  4. package/dist/offers.js.map +4 -4
  5. package/package.json +2 -2
  6. package/src/components/FrequencyStatus.js +13 -10
  7. package/src/components/Offer.js +33 -14
  8. package/src/components/OptinButton.js +2 -2
  9. package/src/components/OptinSelect.js +5 -5
  10. package/src/components/OptinStatus.js +15 -9
  11. package/src/components/Price.js +3 -3
  12. package/src/components/SelectFrequency.js +11 -6
  13. package/src/components/TestWizard.js +45 -41
  14. package/src/components/UpsellModal.js +9 -3
  15. package/src/components/__tests__/Offer.spec.js +0 -19
  16. package/src/components/__tests__/OptinStatus.spec.js +17 -4
  17. package/src/core/__tests__/actions.spec.js +47 -1
  18. package/src/core/__tests__/base.spec.js +0 -77
  19. package/src/core/__tests__/offerRequest.spec.js +2 -1
  20. package/src/core/__tests__/selectors.spec.js +7 -7
  21. package/src/core/actions-preview.js +6 -3
  22. package/src/core/actions.js +22 -13
  23. package/src/core/base.js +0 -23
  24. package/src/core/experiments.js +2 -4
  25. package/src/core/offerRequest.js +1 -1
  26. package/src/core/{reducer.js → reducer.ts} +30 -10
  27. package/src/core/{selectors.js → selectors.ts} +73 -57
  28. package/src/core/types/api.ts +71 -0
  29. package/src/core/types/reducer.ts +95 -0
  30. package/src/core/types/utility.ts +1 -0
  31. package/src/core/utils.ts +32 -15
  32. package/src/make-api.js +1 -1
  33. package/src/shopify/__tests__/productPlan.spec.js +18 -8
  34. package/src/shopify/__tests__/reducers/config.spec.js +497 -0
  35. package/src/shopify/__tests__/shopifyReducer.spec.js +68 -611
  36. package/src/shopify/__tests__/utils.spec.js +68 -1
  37. package/src/shopify/reducers/config.ts +223 -0
  38. package/src/shopify/reducers/productPlans.ts +3 -10
  39. package/src/shopify/shopifyMiddleware.ts +2 -9
  40. package/src/shopify/{shopifyReducer.js → shopifyReducer.ts} +52 -221
  41. package/src/shopify/types/shopify.ts +1 -1
  42. package/src/shopify/utils.ts +70 -0
  43. 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.find(
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?: HTMLElement) {
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