@ordergroove/offers 2.26.10 → 2.27.1-alpha-PR-634-2.3

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 (58) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +34 -0
  3. package/build.js +3 -1
  4. package/dist/bundle-report.html +185 -116
  5. package/dist/examples.js +1 -0
  6. package/dist/examples.js.map +1 -1
  7. package/dist/offers.js +65 -76
  8. package/dist/offers.js.map +3 -3
  9. package/examples/cart.js +105 -0
  10. package/examples/index.html +2 -2
  11. package/examples/products/cheap-watch.js +183 -0
  12. package/examples/shopify-cart.html +26 -0
  13. package/examples/shopify-pdp.html +34 -0
  14. package/karma.conf.js +2 -1
  15. package/package.json +5 -5
  16. package/src/__tests__/offers.spec.js +35 -10
  17. package/src/components/FrequencyStatus.js +14 -11
  18. package/src/components/IncentiveText.js +2 -1
  19. package/src/components/Offer.js +14 -7
  20. package/src/components/OptinButton.js +1 -1
  21. package/src/components/OptinSelect.js +2 -2
  22. package/src/components/OptinToggle.js +2 -2
  23. package/src/components/OptoutButton.js +1 -1
  24. package/src/components/Price.js +7 -3
  25. package/src/components/Select.js +3 -13
  26. package/src/components/SelectFrequency.js +24 -6
  27. package/src/components/TestWizard.js +1 -1
  28. package/src/components/__tests__/OG.fspec.js +24 -0
  29. package/src/components/__tests__/Offer.spec.js +4 -4
  30. package/src/components/__tests__/OptinButton.spec.js +2 -2
  31. package/src/components/__tests__/OptinToggle.spec.js +2 -2
  32. package/src/components/__tests__/OptoutButton.spec.js +1 -1
  33. package/src/components/__tests__/SelectFrequency.fspec.js +1 -0
  34. package/src/components/__tests__/SelectFrequency.spec.js +1 -1
  35. package/src/components/__tests__/TestWizard.spec.js +2 -2
  36. package/src/components/__tests__/Text.spec.js +5 -1
  37. package/src/core/__tests__/actions.spec.js +6 -6
  38. package/src/core/actions.js +22 -17
  39. package/src/core/constants.js +21 -0
  40. package/src/core/descriptors.js +2 -1
  41. package/src/core/middleware.js +41 -1
  42. package/src/core/reducer.js +22 -21
  43. package/src/core/resolveProperties.js +2 -7
  44. package/src/core/selectors.js +1 -1
  45. package/src/core/store.js +17 -9
  46. package/src/core/utils.ts +67 -0
  47. package/src/index.js +46 -203
  48. package/src/make-api.js +195 -0
  49. package/src/platform.ts +9 -0
  50. package/src/shopify/__tests__/shopifyMiddleware.spec.js +126 -0
  51. package/src/shopify/__tests__/shopifyReducer.spec.js +489 -0
  52. package/src/shopify/shopifyBootstrap.ts +136 -0
  53. package/src/shopify/shopifyMiddleware.ts +336 -0
  54. package/src/shopify/shopifyReducer.js +254 -0
  55. package/tsconfig.json +35 -0
  56. package/examples/5starnutrition-main.js +0 -3
  57. package/examples/single-offer.html +0 -9
  58. package/src/init-test.js +0 -3
@@ -0,0 +1,126 @@
1
+ import fetchMock from 'fetch-mock';
2
+ import { CART_UPDATED_EVENT, OPTIN_PRODUCT, SETUP_CART } from '../../core/constants';
3
+ import { getOrCreateHidden, synchronizeCartOptin } from '../shopifyMiddleware';
4
+
5
+ function makeForm(addInput = true) {
6
+ const element = document.createElement('form');
7
+ if (addInput) {
8
+ element.innerHTML = `
9
+ <input value="123456" name="selling_plan" type="hidden">
10
+ `;
11
+ }
12
+ document.body.appendChild(element);
13
+ return element;
14
+ }
15
+
16
+ describe('getOrCreateHidden', () => {
17
+ it('should update the input value for an existing input', () => {
18
+ const formElement = makeForm(true);
19
+ getOrCreateHidden(formElement, 'selling_plan', '098765');
20
+ const newInput = formElement.querySelector(`[name="selling_plan"]`);
21
+ expect(newInput.getAttribute('value')).toEqual('098765');
22
+ });
23
+
24
+ it('should create an input with the correct value when a value is provided and no input exists', () => {
25
+ const formElement = makeForm(false);
26
+ getOrCreateHidden(formElement, 'selling_plan', '098765');
27
+ const newInput = formElement.querySelector(`[name="selling_plan"]`);
28
+ expect(newInput.getAttribute('value')).toEqual('098765');
29
+ });
30
+
31
+ it('should remove the input if it exists but an empty value is passed', () => {
32
+ const formElement = makeForm(true);
33
+ getOrCreateHidden(formElement, 'selling_plan', null);
34
+ const newInput = formElement.querySelector(`[name="selling_plan"]`);
35
+ expect(newInput).toBeNull();
36
+ });
37
+
38
+ it('should do nothing if there is no input and there is no value passed', () => {
39
+ const formElement = makeForm(false);
40
+ getOrCreateHidden(formElement, 'selling_plan', null);
41
+ const newInput = formElement.querySelector(`[name="selling_plan"]`);
42
+ expect(newInput).toBeNull();
43
+ });
44
+ });
45
+
46
+ describe('synchronizeCartOptin', () => {
47
+ let store, offer, frequency, product;
48
+ beforeEach(() => {
49
+ store = { dispatch: jasmine.createSpy('dispatch') };
50
+ offer = document.createElement('some-fake-og-offer');
51
+
52
+ offer.isCart = true;
53
+ offer.setAttribute('product', '38995975209111:original-hash');
54
+
55
+ frequency = '1234';
56
+ product = { id: '38995975209111:original-hash' };
57
+
58
+ fetchMock.once(
59
+ 'cart.js',
60
+ {
61
+ attributes: {},
62
+ items: [
63
+ {
64
+ quantity: 4,
65
+ key: '38995975209111:original-hash'
66
+ }
67
+ ]
68
+ },
69
+ { method: 'GET' }
70
+ );
71
+
72
+ fetchMock.once(
73
+ 'cart/change.js',
74
+ {
75
+ attributes: {},
76
+ items: [
77
+ {
78
+ quantity: 4,
79
+ key: '38995975209111:new-hash'
80
+ }
81
+ ]
82
+ },
83
+ { method: 'POST' }
84
+ );
85
+ });
86
+
87
+ afterEach(() => {
88
+ fetchMock.restore();
89
+ offer.remove();
90
+ });
91
+
92
+ it('should set new has product id', async () => {
93
+ await synchronizeCartOptin({ type: OPTIN_PRODUCT, payload: { offer, frequency, product } }, store);
94
+
95
+ expect(store.dispatch).toHaveBeenCalledWith({
96
+ type: SETUP_CART,
97
+ payload: { attributes: {}, items: [{ quantity: 4, key: '38995975209111:new-hash' }] }
98
+ });
99
+ expect(offer.getAttribute('product')).toEqual('38995975209111:new-hash');
100
+ });
101
+
102
+ it('should dispatch cart updated event', async () => {
103
+ let called;
104
+ document.addEventListener(CART_UPDATED_EVENT, ev => {
105
+ called = true;
106
+ });
107
+
108
+ document.body.appendChild(offer);
109
+
110
+ await synchronizeCartOptin({ type: OPTIN_PRODUCT, payload: { offer, frequency, product } }, store);
111
+
112
+ expect(called).toBeTruthy();
113
+ });
114
+
115
+ it('should pass closest section to shopify api', async () => {
116
+ const sectionDiv = document.createElement('div');
117
+ sectionDiv.id = 'shopify-section-123456789';
118
+ sectionDiv.classList.add('shopify-section');
119
+ sectionDiv.appendChild(offer);
120
+ document.body.appendChild(sectionDiv);
121
+
122
+ await synchronizeCartOptin({ type: OPTIN_PRODUCT, payload: { offer, frequency, product } }, store);
123
+ expect(fetchMock.lastOptions().body).toContain('"sections":["123456789"]');
124
+ sectionDiv.remove();
125
+ });
126
+ });
@@ -0,0 +1,489 @@
1
+ import * as constants from '../../core/constants';
2
+ import * as coreReducers from '../../core/reducer';
3
+ import {
4
+ autoshipEligible,
5
+ config,
6
+ inStock,
7
+ offer,
8
+ offerId,
9
+ optedin,
10
+ productOffer,
11
+ productPlans
12
+ } from '../shopifyReducer';
13
+
14
+ describe('autoshipEligible', () => {
15
+ it('should return true for each id given action RECEIVE_PRODUCT_PLANS', () => {
16
+ const actual = autoshipEligible(
17
+ {},
18
+ {
19
+ type: constants.RECEIVE_PRODUCT_PLANS,
20
+ payload: {
21
+ 'yum product id 1': {},
22
+ 'yum product id 2': {}
23
+ }
24
+ }
25
+ );
26
+ expect(actual).toEqual({ 'yum product id 1': true, 'yum product id 2': true });
27
+ });
28
+
29
+ it('should return null for each key in items given action SETUP_CART', () => {
30
+ const actual = autoshipEligible(
31
+ {},
32
+ {
33
+ type: constants.SETUP_CART,
34
+ payload: {
35
+ items: [
36
+ {
37
+ key: 'yum key 1'
38
+ },
39
+ {
40
+ key: 'yum key 2'
41
+ }
42
+ ]
43
+ }
44
+ }
45
+ );
46
+ expect(actual).toEqual({ 'yum key 1': null, 'yum key 2': null });
47
+ });
48
+
49
+ it('should return true for each product and variant given action SETUP_PRODUCT and products have selling plan allocations', () => {
50
+ const actual = autoshipEligible(
51
+ {},
52
+ {
53
+ type: constants.SETUP_PRODUCT,
54
+ payload: {
55
+ id: 'yum product id',
56
+ selling_plan_allocations: [{ 'yum key': 'yum value' }],
57
+ variants: [
58
+ {
59
+ id: 'yum variant id',
60
+ selling_plan_allocations: [{ 'yum key': 'yum value' }]
61
+ }
62
+ ]
63
+ }
64
+ }
65
+ );
66
+
67
+ expect(actual).toEqual({ 'yum product id': true, 'yum variant id': true });
68
+ });
69
+
70
+ it('should return false for each product and variant given action SETUP_PRODUCT and products have no selling plan allocations', () => {
71
+ const actual = autoshipEligible(
72
+ {},
73
+ {
74
+ type: constants.SETUP_PRODUCT,
75
+ payload: {
76
+ id: 'yum product id',
77
+ selling_plan_allocations: [],
78
+ variants: [
79
+ {
80
+ id: 'yum variant id',
81
+ selling_plan_allocations: []
82
+ }
83
+ ]
84
+ }
85
+ }
86
+ );
87
+
88
+ expect(actual).toEqual({ 'yum product id': false, 'yum variant id': false });
89
+ });
90
+
91
+ it('should return unmodified state given unsupported action', () => {
92
+ const actual = autoshipEligible(
93
+ { 'yum existing key': 'yum existing value' },
94
+ {
95
+ type: 'yum unsupported action',
96
+ payload: {}
97
+ }
98
+ );
99
+
100
+ expect(actual).toEqual({ 'yum existing key': 'yum existing value' });
101
+ });
102
+ });
103
+
104
+ describe('config', () => {
105
+ it('should return unique frequencies given action RECEIVE_PRODUCT_PLANS', () => {
106
+ const actual = config(
107
+ {},
108
+ {
109
+ type: constants.RECEIVE_PRODUCT_PLANS,
110
+ payload: {
111
+ 'product plan id': { '1_1': {}, '3_1': {} }
112
+ }
113
+ }
114
+ );
115
+
116
+ expect(actual).toEqual(
117
+ jasmine.objectContaining({
118
+ frequencies: ['1_1', '3_1']
119
+ })
120
+ );
121
+ });
122
+
123
+ it('should return first selling plan id as default frequency given action SETUP_PRODUCT', () => {
124
+ const actual = config(
125
+ {},
126
+ {
127
+ type: constants.SETUP_PRODUCT,
128
+ payload: {
129
+ selling_plan_groups: [
130
+ {
131
+ name: 'Subscribe and Save',
132
+ selling_plans: [
133
+ {
134
+ id: 'yum selling plan id 1'
135
+ },
136
+ {
137
+ id: 'yum selling plan id 2'
138
+ }
139
+ ]
140
+ }
141
+ ]
142
+ }
143
+ }
144
+ );
145
+
146
+ expect(actual).toEqual(
147
+ jasmine.objectContaining({
148
+ defaultFrequency: 'yum selling plan id 1'
149
+ })
150
+ );
151
+ });
152
+
153
+ it('should return selling plan ids as frequencies given action SETUP_PRODUCT', () => {
154
+ const actual = config(
155
+ {},
156
+ {
157
+ type: constants.SETUP_PRODUCT,
158
+ payload: {
159
+ selling_plan_groups: [
160
+ {
161
+ name: 'Subscribe and Save',
162
+ selling_plans: [
163
+ {
164
+ id: 'yum selling plan id 1'
165
+ },
166
+ {
167
+ id: 'yum selling plan id 2'
168
+ }
169
+ ]
170
+ }
171
+ ]
172
+ }
173
+ }
174
+ );
175
+
176
+ expect(actual).toEqual(
177
+ jasmine.objectContaining({
178
+ frequencies: ['yum selling plan id 1', 'yum selling plan id 2']
179
+ })
180
+ );
181
+ });
182
+
183
+ it('should return values of first OG selling plan group as frequencies text given action SETUP_PRODUCT', () => {
184
+ const actual = config(
185
+ {},
186
+ {
187
+ type: constants.SETUP_PRODUCT,
188
+ payload: {
189
+ selling_plan_groups: [
190
+ {
191
+ name: 'Old Selling Plan Group',
192
+ options: [{ values: 'old yum values' }],
193
+ selling_plans: [
194
+ {
195
+ id: 'old yum selling plan id'
196
+ }
197
+ ]
198
+ },
199
+ {
200
+ name: 'Subscribe and Save',
201
+ options: [{ values: 'yum values' }],
202
+ selling_plans: [
203
+ {
204
+ id: 'yum selling plan id'
205
+ }
206
+ ]
207
+ }
208
+ ]
209
+ }
210
+ }
211
+ );
212
+
213
+ expect(actual).toEqual(
214
+ jasmine.objectContaining({
215
+ frequenciesText: 'yum values'
216
+ })
217
+ );
218
+ });
219
+
220
+ it('should return unmodified state given unsupported action', () => {
221
+ const actual = config(
222
+ { 'yum existing key': 'yum existing value' },
223
+ {
224
+ type: 'yum unsupported action',
225
+ payload: {}
226
+ }
227
+ );
228
+
229
+ expect(actual).toEqual({ 'yum existing key': 'yum existing value' });
230
+ });
231
+ });
232
+
233
+ describe('inStock', () => {
234
+ it('should return true for each id given action RECEIVE_PRODUCT_PLANS', () => {
235
+ const actual = inStock(
236
+ {},
237
+ {
238
+ type: constants.RECEIVE_PRODUCT_PLANS,
239
+ payload: {
240
+ 'yum product id 1': {},
241
+ 'yum product id 2': {}
242
+ }
243
+ }
244
+ );
245
+ expect(actual).toEqual({ 'yum product id 1': true, 'yum product id 2': true });
246
+ });
247
+
248
+ it('should return null item key given action SETUP_CART', () => {
249
+ const actual = inStock(
250
+ {},
251
+ {
252
+ type: constants.SETUP_CART,
253
+ payload: {
254
+ items: [{ key: 'yum item key 1' }, { key: 'yum item key 2' }]
255
+ }
256
+ }
257
+ );
258
+ expect(actual).toEqual({ 'yum item key 1': null, 'yum item key 2': null });
259
+ });
260
+
261
+ it('should return true for each available product and variant given action SETUP_PRODUCT', () => {
262
+ const actual = inStock(
263
+ {},
264
+ {
265
+ type: constants.SETUP_PRODUCT,
266
+ payload: {
267
+ id: 'yum product id',
268
+ available: true,
269
+ variants: [
270
+ {
271
+ id: 'yum variant id',
272
+ available: true
273
+ }
274
+ ]
275
+ }
276
+ }
277
+ );
278
+
279
+ expect(actual).toEqual({
280
+ 'yum product id': true,
281
+ 'yum variant id': true
282
+ });
283
+ });
284
+
285
+ it('should return false for each unavailable product and variant given action SETUP_PRODUCT', () => {
286
+ const actual = inStock(
287
+ {},
288
+ {
289
+ type: constants.SETUP_PRODUCT,
290
+ payload: {
291
+ id: 'yum product id',
292
+ available: false,
293
+ variants: [
294
+ {
295
+ id: 'yum variant id',
296
+ available: false
297
+ }
298
+ ]
299
+ }
300
+ }
301
+ );
302
+
303
+ expect(actual).toEqual({
304
+ 'yum product id': false,
305
+ 'yum variant id': false
306
+ });
307
+ });
308
+
309
+ it('should return unmodified state given unsupported action', () => {
310
+ const actual = inStock(
311
+ { 'yum existing key': 'yum existing value' },
312
+ {
313
+ type: 'yum unsupported action',
314
+ payload: {}
315
+ }
316
+ );
317
+
318
+ expect(actual).toEqual({ 'yum existing key': 'yum existing value' });
319
+ });
320
+ });
321
+
322
+ describe('offer', () => {
323
+ it('should return unmodified state', () => {
324
+ const actual = offer({ 'yum existing key': 'yum existing value' });
325
+
326
+ expect(actual).toEqual({ 'yum existing key': 'yum existing value' });
327
+ });
328
+ });
329
+
330
+ describe('offerId', () => {
331
+ it('should return shopify constant', () => {
332
+ const actual = offerId();
333
+
334
+ expect(actual).toEqual('native-shopify-offer');
335
+ });
336
+ });
337
+
338
+ describe('optedin', () => {
339
+ it('should return optins given action SETUP_CART', () => {
340
+ const actual = optedin(
341
+ {},
342
+ {
343
+ type: constants.SETUP_CART,
344
+ payload: {
345
+ items: [
346
+ {
347
+ key: 'yum item key 1',
348
+ selling_plan_allocation: {
349
+ selling_plan: {
350
+ id: 'yum selling plan id 1'
351
+ }
352
+ }
353
+ },
354
+ {
355
+ key: 'yum item key 2',
356
+ selling_plan_allocation: {
357
+ selling_plan: {
358
+ id: 'yum selling plan id 2'
359
+ }
360
+ }
361
+ }
362
+ ]
363
+ }
364
+ }
365
+ );
366
+
367
+ expect(actual).toEqual([
368
+ {
369
+ id: 'yum item key 1',
370
+ frequency: 'yum selling plan id 1'
371
+ },
372
+ {
373
+ id: 'yum item key 2',
374
+ frequency: 'yum selling plan id 2'
375
+ }
376
+ ]);
377
+ });
378
+
379
+ it('should return unmodified state given unsupported action', () => {
380
+ const actual = optedin(
381
+ { 'yum existing key': 'yum existing value' },
382
+ {
383
+ type: 'yum unsupported action',
384
+ payload: {}
385
+ }
386
+ );
387
+
388
+ expect(actual).toEqual({ 'yum existing key': 'yum existing value' });
389
+ });
390
+ });
391
+
392
+ describe('productOffer', () => {
393
+ it('should return unmodified state', () => {
394
+ const actual = productOffer({ 'yum existing key': 'yum existing value' });
395
+
396
+ expect(actual).toEqual({ 'yum existing key': 'yum existing value' });
397
+ });
398
+ });
399
+
400
+ describe('productPlans', () => {
401
+ it('should product plans with formatted discounts given action SETUP_PRODUCT', () => {
402
+ const actual = productPlans(
403
+ {},
404
+ {
405
+ type: constants.SETUP_PRODUCT,
406
+ payload: {
407
+ id: 'yum product id',
408
+ selling_plan_allocations: [
409
+ {
410
+ selling_plan_id: 'yum selling plan id 1',
411
+ compare_at_price: 100,
412
+ price: 50,
413
+ price_adjustments: [
414
+ {
415
+ value: 50,
416
+ value_type: 'percentage'
417
+ }
418
+ ]
419
+ }
420
+ ],
421
+ variants: [
422
+ {
423
+ id: 'yum variant id 1',
424
+ selling_plan_allocations: [
425
+ {
426
+ selling_plan_id: 'yum selling plan id 2',
427
+ compare_at_price: 50,
428
+ price: 25,
429
+ price_adjustments: [
430
+ {
431
+ value: 25
432
+ }
433
+ ]
434
+ }
435
+ ]
436
+ },
437
+ {
438
+ id: 'yum variant id 2',
439
+ selling_plan_allocations: [
440
+ {
441
+ selling_plan_id: 'yum selling plan id 3',
442
+ compare_at_price: 10,
443
+ price: 8,
444
+ price_adjustments: []
445
+ }
446
+ ]
447
+ }
448
+ ]
449
+ }
450
+ }
451
+ );
452
+
453
+ expect(actual).toEqual({
454
+ 'yum product id': { 'yum selling plan id 1': ['$1.00', '50%', '$.50'] },
455
+ 'yum variant id 1': {
456
+ 'yum selling plan id 2': ['$.50', '$.25', '$.25']
457
+ },
458
+ 'yum variant id 2': { 'yum selling plan id 3': ['$.10', '$2', '$8'] }
459
+ });
460
+ });
461
+
462
+ it('should return payload given action RECEIVE_PRODUCT_PLANS', () => {
463
+ const actual = productPlans(
464
+ {},
465
+ {
466
+ type: constants.RECEIVE_PRODUCT_PLANS,
467
+ payload: {
468
+ 'yum key': 'yum value'
469
+ }
470
+ }
471
+ );
472
+
473
+ expect(actual).toEqual({
474
+ 'yum key': 'yum value'
475
+ });
476
+ });
477
+
478
+ it('should return unmodified state given unsupported action', () => {
479
+ const actual = productPlans(
480
+ { 'yum existing key': 'yum existing value' },
481
+ {
482
+ type: 'yum unsupported action',
483
+ payload: {}
484
+ }
485
+ );
486
+
487
+ expect(actual).toEqual({ 'yum existing key': 'yum existing value' });
488
+ });
489
+ });