@ordergroove/offers 2.48.8 → 2.49.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/CHANGELOG.md +20 -0
- package/dist/bundle-report.html +42 -36
- package/dist/examples.js +33 -33
- package/dist/examples.js.map +2 -2
- package/dist/offers.js +43 -37
- package/dist/offers.js.map +4 -4
- package/examples/index.js +5 -0
- package/package.json +2 -2
- package/src/__tests__/offers.spec.js +1 -0
- package/src/components/BenefitMessages.js +38 -0
- package/src/components/__tests__/BenefitMessages.spec.js +303 -0
- package/src/components/__tests__/OG.fspec.js +1 -0
- package/src/core/__tests__/reducer.spec.js +4 -0
- package/src/core/actions-preview.js +20 -1
- package/src/core/actions.js +5 -0
- package/src/core/constants.js +1 -0
- package/src/core/reducer.ts +13 -1
- package/src/core/selectors.ts +57 -0
- package/src/core/types/reducer.ts +5 -1
- package/src/index.js +1 -0
- package/src/make-api.js +12 -0
- package/src/shopify/shopifyReducer.ts +3 -1
package/examples/index.js
CHANGED
|
@@ -383,6 +383,11 @@ Add item to order on
|
|
|
383
383
|
];
|
|
384
384
|
|
|
385
385
|
const componentExamples = [
|
|
386
|
+
{
|
|
387
|
+
name: 'og-benefit-messages',
|
|
388
|
+
selector: 'og-offer[location="benefit-messages"]',
|
|
389
|
+
markup: `<og-benefit-messages></og-benefit-messages>`
|
|
390
|
+
},
|
|
386
391
|
{
|
|
387
392
|
name: 'og-optin-button',
|
|
388
393
|
selector: 'og-offer[location=og-optin-button]',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ordergroove/offers",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.49.0",
|
|
4
4
|
"description": "offer state component",
|
|
5
5
|
"author": "Eugenio Lattanzio <eugenio63@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/ordergroove/plush-toys#readme",
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"@ordergroove/offers-templates": "^0.10.4",
|
|
51
51
|
"@types/lodash.memoize": "^4.1.9"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "7b58b4c90e38ed68f42a40be308ecea2b6589d9e"
|
|
54
54
|
}
|
|
@@ -20,6 +20,7 @@ describe('Offers API', () => {
|
|
|
20
20
|
register: jasmine.any(Function),
|
|
21
21
|
resolveSettings: jasmine.any(Function),
|
|
22
22
|
setAuthUrl: jasmine.any(Function),
|
|
23
|
+
setBenefitMessages: jasmine.any(Function),
|
|
23
24
|
setEnvironment: jasmine.any(Function),
|
|
24
25
|
setLocale: jasmine.any(Function),
|
|
25
26
|
setMerchantId: jasmine.any(Function),
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { LitElement, html } from 'lit-element';
|
|
2
|
+
import { unsafeHTML } from 'lit-html/directives/unsafe-html';
|
|
3
|
+
import { connect } from '../core/connect';
|
|
4
|
+
import { withProduct } from '../core/resolveProperties';
|
|
5
|
+
import { makeBenefitMessagesSelector } from '../core/selectors';
|
|
6
|
+
|
|
7
|
+
export class BenefitMessages extends withProduct(LitElement) {
|
|
8
|
+
static get properties() {
|
|
9
|
+
return {
|
|
10
|
+
...super.properties,
|
|
11
|
+
messages: { type: Array, attribute: false }
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
createRenderRoot() {
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
render() {
|
|
20
|
+
if (!this.messages?.length) return html``;
|
|
21
|
+
// Messages are expected to be pre-sanitized by SSPC, so we can safely render them as HTML.
|
|
22
|
+
return html`
|
|
23
|
+
<ul class="og-benefit-messages">
|
|
24
|
+
${this.messages.map(
|
|
25
|
+
msg => html`
|
|
26
|
+
<li>${unsafeHTML(msg)}</li>
|
|
27
|
+
`
|
|
28
|
+
)}
|
|
29
|
+
</ul>
|
|
30
|
+
`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const mapStateToProps = (state, ownProps) => makeBenefitMessagesSelector(ownProps?.product)(state);
|
|
35
|
+
|
|
36
|
+
export const ConnectedBenefitMessages = connect(mapStateToProps)(BenefitMessages);
|
|
37
|
+
|
|
38
|
+
export default ConnectedBenefitMessages;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { BenefitMessages, mapStateToProps } from '../BenefitMessages';
|
|
2
|
+
import { appendToBody } from './utils';
|
|
3
|
+
|
|
4
|
+
customElements.define('og-benefit-messages-test', BenefitMessages);
|
|
5
|
+
|
|
6
|
+
describe('BenefitMessages', () => {
|
|
7
|
+
it('renders one li per message inside a ul', async () => {
|
|
8
|
+
const el = new BenefitMessages();
|
|
9
|
+
el.messages = ['Free shipping', 'Cancel anytime', '5% off every order'];
|
|
10
|
+
await appendToBody(el);
|
|
11
|
+
|
|
12
|
+
const ul = el.querySelector('ul.og-benefit-messages');
|
|
13
|
+
expect(ul).toBeTruthy();
|
|
14
|
+
const items = el.querySelectorAll('li');
|
|
15
|
+
expect(items.length).toBe(3);
|
|
16
|
+
expect(items[0].textContent.trim()).toBe('Free shipping');
|
|
17
|
+
expect(items[1].textContent.trim()).toBe('Cancel anytime');
|
|
18
|
+
expect(items[2].textContent.trim()).toBe('5% off every order');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders nothing when messages is an empty array', async () => {
|
|
22
|
+
const el = new BenefitMessages();
|
|
23
|
+
el.messages = [];
|
|
24
|
+
await appendToBody(el);
|
|
25
|
+
|
|
26
|
+
expect(el.querySelector('ul')).toBeNull();
|
|
27
|
+
expect(el.querySelectorAll('li').length).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders nothing when messages is undefined', async () => {
|
|
31
|
+
const el = new BenefitMessages();
|
|
32
|
+
await appendToBody(el);
|
|
33
|
+
|
|
34
|
+
expect(el.querySelector('ul')).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders HTML markup from messages (sanitized by backend)', async () => {
|
|
38
|
+
const el = new BenefitMessages();
|
|
39
|
+
el.messages = ['Get <strong>10% off</strong> every order'];
|
|
40
|
+
await appendToBody(el);
|
|
41
|
+
|
|
42
|
+
const li = el.querySelector('li');
|
|
43
|
+
expect(li).toBeTruthy();
|
|
44
|
+
expect(li.querySelector('strong')).toBeTruthy();
|
|
45
|
+
expect(li.textContent.trim()).toBe('Get 10% off every order');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('re-renders correctly after disconnect and reconnect', async () => {
|
|
49
|
+
const el = new BenefitMessages();
|
|
50
|
+
el.messages = ['First'];
|
|
51
|
+
await appendToBody(el);
|
|
52
|
+
expect(el.querySelector('li').textContent.trim()).toBe('First');
|
|
53
|
+
|
|
54
|
+
el.remove();
|
|
55
|
+
|
|
56
|
+
el.messages = ['After remount A', 'After remount B'];
|
|
57
|
+
await appendToBody(el);
|
|
58
|
+
|
|
59
|
+
const items = el.querySelectorAll('li');
|
|
60
|
+
expect(items.length).toBe(2);
|
|
61
|
+
expect(items[0].textContent.trim()).toBe('After remount A');
|
|
62
|
+
expect(items[1].textContent.trim()).toBe('After remount B');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('BenefitMessages mapStateToProps', () => {
|
|
67
|
+
const en = msg => ({ 'en-US': msg });
|
|
68
|
+
const state = ({ incentivesForProduct, benefitMessages, productId = 'prod-1' } = {}) => ({
|
|
69
|
+
incentives: incentivesForProduct ? { [productId]: incentivesForProduct } : {},
|
|
70
|
+
benefitMessages: benefitMessages || {}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns messages for the product, initial first then ongoing', () => {
|
|
74
|
+
expect(
|
|
75
|
+
mapStateToProps(
|
|
76
|
+
state({
|
|
77
|
+
incentivesForProduct: {
|
|
78
|
+
initial: [{ enhanced: true, id: 'inc-init' }],
|
|
79
|
+
ongoing: [{ enhanced: true, id: 'inc-ongoing' }]
|
|
80
|
+
},
|
|
81
|
+
benefitMessages: {
|
|
82
|
+
'inc-init': en('First-order bonus'),
|
|
83
|
+
'inc-ongoing': en('Every-order discount')
|
|
84
|
+
}
|
|
85
|
+
}),
|
|
86
|
+
{ product: { id: 'prod-1' } }
|
|
87
|
+
)
|
|
88
|
+
).toEqual({ messages: ['First-order bonus', 'Every-order discount'] });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('falls back to en-US when browser locale has no matching entry', () => {
|
|
92
|
+
Object.defineProperty(navigator, 'language', { value: 'en-US', configurable: true });
|
|
93
|
+
expect(
|
|
94
|
+
mapStateToProps(
|
|
95
|
+
state({
|
|
96
|
+
incentivesForProduct: {
|
|
97
|
+
initial: [{ enhanced: true, id: 'inc-1' }],
|
|
98
|
+
ongoing: []
|
|
99
|
+
},
|
|
100
|
+
benefitMessages: {
|
|
101
|
+
'inc-1': { 'en-US': 'Get 10% off!', 'es-ES': 'Obtén 10% extra!' }
|
|
102
|
+
}
|
|
103
|
+
}),
|
|
104
|
+
{ product: { id: 'prod-1' } }
|
|
105
|
+
)
|
|
106
|
+
).toEqual({ messages: ['Get 10% off!'] });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('prefers browser locale message over en-US when it matches', () => {
|
|
110
|
+
Object.defineProperty(navigator, 'language', { value: 'es-ES', configurable: true });
|
|
111
|
+
expect(
|
|
112
|
+
mapStateToProps(
|
|
113
|
+
state({
|
|
114
|
+
incentivesForProduct: {
|
|
115
|
+
initial: [{ enhanced: true, id: 'inc-1' }],
|
|
116
|
+
ongoing: []
|
|
117
|
+
},
|
|
118
|
+
benefitMessages: {
|
|
119
|
+
'inc-1': { 'en-US': 'Get 10% off!', 'es-ES': 'Obtén 10% extra!' }
|
|
120
|
+
}
|
|
121
|
+
}),
|
|
122
|
+
{ product: { id: 'prod-1' } }
|
|
123
|
+
)
|
|
124
|
+
).toEqual({ messages: ['Obtén 10% extra!'] });
|
|
125
|
+
Object.defineProperty(navigator, 'language', { value: 'en-US', configurable: true });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('skips incentives with no entry for the current locale and no en-US fallback', () => {
|
|
129
|
+
Object.defineProperty(navigator, 'language', { value: 'en-US', configurable: true });
|
|
130
|
+
expect(
|
|
131
|
+
mapStateToProps(
|
|
132
|
+
state({
|
|
133
|
+
incentivesForProduct: {
|
|
134
|
+
initial: [
|
|
135
|
+
{ enhanced: true, id: 'inc-no-en' },
|
|
136
|
+
{ enhanced: true, id: 'inc-with-en' }
|
|
137
|
+
],
|
|
138
|
+
ongoing: []
|
|
139
|
+
},
|
|
140
|
+
benefitMessages: {
|
|
141
|
+
'inc-no-en': { 'es-ES': 'Solo español' },
|
|
142
|
+
'inc-with-en': en('Has en-US')
|
|
143
|
+
}
|
|
144
|
+
}),
|
|
145
|
+
{ product: { id: 'prod-1' } }
|
|
146
|
+
)
|
|
147
|
+
).toEqual({ messages: ['Has en-US'] });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('dedupes by incentive id when the same id appears in both initial and ongoing', () => {
|
|
151
|
+
expect(
|
|
152
|
+
mapStateToProps(
|
|
153
|
+
state({
|
|
154
|
+
incentivesForProduct: {
|
|
155
|
+
initial: [{ enhanced: true, id: 'inc-shared' }],
|
|
156
|
+
ongoing: [
|
|
157
|
+
{ enhanced: true, id: 'inc-shared' },
|
|
158
|
+
{ enhanced: true, id: 'inc-extra' }
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
benefitMessages: {
|
|
162
|
+
'inc-shared': en('Shared'),
|
|
163
|
+
'inc-extra': en('Extra')
|
|
164
|
+
}
|
|
165
|
+
}),
|
|
166
|
+
{ product: { id: 'prod-1' } }
|
|
167
|
+
)
|
|
168
|
+
).toEqual({ messages: ['Shared', 'Extra'] });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('dedupes by message string when distinct incentive ids resolve to the same message', () => {
|
|
172
|
+
expect(
|
|
173
|
+
mapStateToProps(
|
|
174
|
+
state({
|
|
175
|
+
incentivesForProduct: {
|
|
176
|
+
initial: [
|
|
177
|
+
{ enhanced: true, id: 'inc-a' },
|
|
178
|
+
{ enhanced: true, id: 'inc-b' }
|
|
179
|
+
],
|
|
180
|
+
ongoing: [{ enhanced: true, id: 'inc-c' }]
|
|
181
|
+
},
|
|
182
|
+
benefitMessages: {
|
|
183
|
+
'inc-a': en('Free shipping'),
|
|
184
|
+
'inc-b': en('Free shipping'),
|
|
185
|
+
'inc-c': en('Cancel anytime')
|
|
186
|
+
}
|
|
187
|
+
}),
|
|
188
|
+
{ product: { id: 'prod-1' } }
|
|
189
|
+
)
|
|
190
|
+
).toEqual({ messages: ['Free shipping', 'Cancel anytime'] });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('skips incentives that have no configured message', () => {
|
|
194
|
+
expect(
|
|
195
|
+
mapStateToProps(
|
|
196
|
+
state({
|
|
197
|
+
incentivesForProduct: {
|
|
198
|
+
initial: [
|
|
199
|
+
{ enhanced: true, id: 'inc-1' },
|
|
200
|
+
{ enhanced: true, id: 'inc-2' }
|
|
201
|
+
],
|
|
202
|
+
ongoing: [{ enhanced: true, id: 'inc-3' }]
|
|
203
|
+
},
|
|
204
|
+
benefitMessages: {
|
|
205
|
+
'inc-2': en('Only this one is configured')
|
|
206
|
+
}
|
|
207
|
+
}),
|
|
208
|
+
{ product: { id: 'prod-1' } }
|
|
209
|
+
)
|
|
210
|
+
).toEqual({ messages: ['Only this one is configured'] });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('handles missing initial array (only ongoing)', () => {
|
|
214
|
+
expect(
|
|
215
|
+
mapStateToProps(
|
|
216
|
+
state({
|
|
217
|
+
incentivesForProduct: { ongoing: [{ enhanced: true, id: 'inc-1' }] },
|
|
218
|
+
benefitMessages: { 'inc-1': en('Ongoing only') }
|
|
219
|
+
}),
|
|
220
|
+
{ product: { id: 'prod-1' } }
|
|
221
|
+
)
|
|
222
|
+
).toEqual({ messages: ['Ongoing only'] });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('handles missing ongoing array (only initial)', () => {
|
|
226
|
+
expect(
|
|
227
|
+
mapStateToProps(
|
|
228
|
+
state({
|
|
229
|
+
incentivesForProduct: { initial: [{ enhanced: true, id: 'inc-1' }] },
|
|
230
|
+
benefitMessages: { 'inc-1': en('Initial only') }
|
|
231
|
+
}),
|
|
232
|
+
{ product: { id: 'prod-1' } }
|
|
233
|
+
)
|
|
234
|
+
).toEqual({ messages: ['Initial only'] });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('returns [] when both initial and ongoing are empty', () => {
|
|
238
|
+
expect(
|
|
239
|
+
mapStateToProps(
|
|
240
|
+
state({
|
|
241
|
+
incentivesForProduct: { initial: [], ongoing: [] },
|
|
242
|
+
benefitMessages: { 'inc-1': en('Should not appear') }
|
|
243
|
+
}),
|
|
244
|
+
{ product: { id: 'prod-1' } }
|
|
245
|
+
)
|
|
246
|
+
).toEqual({ messages: [] });
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('returns [] when the product has no incentives entry', () => {
|
|
250
|
+
expect(
|
|
251
|
+
mapStateToProps(
|
|
252
|
+
{ incentives: {}, benefitMessages: { 'inc-1': en('Should not appear') } },
|
|
253
|
+
{ product: { id: 'prod-1' } }
|
|
254
|
+
)
|
|
255
|
+
).toEqual({ messages: [] });
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('returns [] when state has no benefitMessages key', () => {
|
|
259
|
+
expect(
|
|
260
|
+
mapStateToProps(
|
|
261
|
+
{ incentives: { 'prod-1': { initial: [{ enhanced: true, id: 'inc-1' }], ongoing: [] } } },
|
|
262
|
+
{ product: { id: 'prod-1' } }
|
|
263
|
+
)
|
|
264
|
+
).toEqual({ messages: [] });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('returns [] when no incentive has a matching message', () => {
|
|
268
|
+
expect(
|
|
269
|
+
mapStateToProps(
|
|
270
|
+
state({
|
|
271
|
+
incentivesForProduct: {
|
|
272
|
+
initial: [{ enhanced: true, id: 'inc-1' }],
|
|
273
|
+
ongoing: [{ enhanced: true, id: 'inc-2' }]
|
|
274
|
+
},
|
|
275
|
+
benefitMessages: { 'inc-other': en('Unrelated') }
|
|
276
|
+
}),
|
|
277
|
+
{ product: { id: 'prod-1' } }
|
|
278
|
+
)
|
|
279
|
+
).toEqual({ messages: [] });
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('returns [] when ownProps has no product', () => {
|
|
283
|
+
expect(mapStateToProps({ benefitMessages: { 'inc-1': en('m') } }, {})).toEqual({ messages: [] });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('skips incentives that were not in incentives_display_enhanced', () => {
|
|
287
|
+
expect(
|
|
288
|
+
mapStateToProps(
|
|
289
|
+
state({
|
|
290
|
+
incentivesForProduct: {
|
|
291
|
+
initial: [{ id: 'inc-non-enhanced' }, { enhanced: true, id: 'inc-enhanced' }],
|
|
292
|
+
ongoing: []
|
|
293
|
+
},
|
|
294
|
+
benefitMessages: {
|
|
295
|
+
'inc-non-enhanced': en('Should not appear'),
|
|
296
|
+
'inc-enhanced': en('Should appear')
|
|
297
|
+
}
|
|
298
|
+
}),
|
|
299
|
+
{ product: { id: 'prod-1' } }
|
|
300
|
+
)
|
|
301
|
+
).toEqual({ messages: ['Should appear'] });
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -68,6 +68,7 @@ describe('og.offers', function () {
|
|
|
68
68
|
register: jasmine.any(Function),
|
|
69
69
|
resolveSettings: jasmine.any(Function),
|
|
70
70
|
setAuthUrl: jasmine.any(Function),
|
|
71
|
+
setBenefitMessages: jasmine.any(Function),
|
|
71
72
|
setEnvironment: jasmine.any(Function),
|
|
72
73
|
setLocale: jasmine.any(Function),
|
|
73
74
|
setMerchantId: jasmine.any(Function),
|
|
@@ -1140,6 +1140,7 @@ describe('reducers', () => {
|
|
|
1140
1140
|
type: 'Discount Percent',
|
|
1141
1141
|
value: 10.0,
|
|
1142
1142
|
id: '8285f16f826e4cf29b49f073e45e32ab',
|
|
1143
|
+
enhanced: true,
|
|
1143
1144
|
threshold_field: null,
|
|
1144
1145
|
threshold_value: null,
|
|
1145
1146
|
criteria: {
|
|
@@ -1154,6 +1155,7 @@ describe('reducers', () => {
|
|
|
1154
1155
|
type: 'Discount Percent',
|
|
1155
1156
|
value: 20.0,
|
|
1156
1157
|
id: 'threshold-id',
|
|
1158
|
+
enhanced: true,
|
|
1157
1159
|
threshold_field: 'order',
|
|
1158
1160
|
threshold_value: '10.00',
|
|
1159
1161
|
criteria: {
|
|
@@ -1168,6 +1170,7 @@ describe('reducers', () => {
|
|
|
1168
1170
|
type: 'Discount Amount',
|
|
1169
1171
|
value: 5.0,
|
|
1170
1172
|
id: '10205e185a5c4ccf83de75459241623a',
|
|
1173
|
+
enhanced: true,
|
|
1171
1174
|
threshold_field: null,
|
|
1172
1175
|
threshold_value: null,
|
|
1173
1176
|
criteria: {
|
|
@@ -1184,6 +1187,7 @@ describe('reducers', () => {
|
|
|
1184
1187
|
type: 'Discount Percent',
|
|
1185
1188
|
value: 11.0,
|
|
1186
1189
|
id: '0f98a321d29e47d2a17fe3a8a7522d26',
|
|
1190
|
+
enhanced: true,
|
|
1187
1191
|
threshold_field: null,
|
|
1188
1192
|
threshold_value: null,
|
|
1189
1193
|
criteria: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { receiveOffer, receiveOrders, authorize, unauthorized, optinProduct } from './actions';
|
|
1
|
+
import { receiveOffer, receiveOrders, authorize, unauthorized, optinProduct, setBenefitMessages } from './actions';
|
|
2
2
|
import { getObjectStructuredProductPlans } from './adapters';
|
|
3
3
|
import * as constants from './constants';
|
|
4
4
|
|
|
@@ -11,6 +11,13 @@ export const setPreviewStandardOffer = (isPreview, productId, offer) =>
|
|
|
11
11
|
await dispatch({
|
|
12
12
|
type: constants.UNAUTHORIZED
|
|
13
13
|
});
|
|
14
|
+
await dispatch(
|
|
15
|
+
setBenefitMessages({
|
|
16
|
+
'47c01e9aacbe40389b5c7325d79091aa': { 'en-US': 'Coffee products with 15% off' },
|
|
17
|
+
e6534b9d877f41e586c37b7d8abc3a58: { 'en-US': 'Get a free gift on your 3rd order' },
|
|
18
|
+
f35e842710b24929922db4a529eecd40: { 'en-US': 'Free shipping for your recurring orders' }
|
|
19
|
+
})
|
|
20
|
+
);
|
|
14
21
|
await dispatch(
|
|
15
22
|
receiveOffer(
|
|
16
23
|
{
|
|
@@ -83,6 +90,12 @@ export const setPreviewUpsellOffer = (isPreview, productId, offer) =>
|
|
|
83
90
|
|
|
84
91
|
const { merchantId } = getState();
|
|
85
92
|
if (isPreview) {
|
|
93
|
+
await dispatch(
|
|
94
|
+
setBenefitMessages({
|
|
95
|
+
'47c01e9aacbe40389b5c7325d79091aa': { 'en-US': 'Coffee products with 15% off' },
|
|
96
|
+
e6534b9d877f41e586c37b7d8abc3a58: { 'en-US': 'Get a free gift on your 3rd order' }
|
|
97
|
+
})
|
|
98
|
+
);
|
|
86
99
|
await dispatch(
|
|
87
100
|
receiveOffer(
|
|
88
101
|
{
|
|
@@ -149,6 +162,12 @@ export const setPreviewPrepaid = (isPreview, productId, offer) =>
|
|
|
149
162
|
await dispatch({
|
|
150
163
|
type: constants.UNAUTHORIZED
|
|
151
164
|
});
|
|
165
|
+
await dispatch(
|
|
166
|
+
setBenefitMessages({
|
|
167
|
+
'47c01e9aacbe40389b5c7325d79091aa': { 'en-US': 'Coffee products with 15% off' },
|
|
168
|
+
e6534b9d877f41e586c37b7d8abc3a58: { 'en-US': 'Get a free gift on your 3rd order' }
|
|
169
|
+
})
|
|
170
|
+
);
|
|
152
171
|
await dispatch(
|
|
153
172
|
receiveOffer(
|
|
154
173
|
{
|
package/src/core/actions.js
CHANGED
|
@@ -296,6 +296,11 @@ export const setConfig = payload => ({
|
|
|
296
296
|
payload
|
|
297
297
|
});
|
|
298
298
|
|
|
299
|
+
export const setBenefitMessages = payload => ({
|
|
300
|
+
type: constants.SET_BENEFIT_MESSAGES,
|
|
301
|
+
payload
|
|
302
|
+
});
|
|
303
|
+
|
|
299
304
|
export const addTemplate = (selector, markup, config) => ({
|
|
300
305
|
type: constants.ADD_TEMPLATE,
|
|
301
306
|
payload: { selector, markup, config }
|
package/src/core/constants.js
CHANGED
|
@@ -32,6 +32,7 @@ export const CHECKOUT = 'CHECKOUT';
|
|
|
32
32
|
export const RECEIVE_FETCH = 'RECEIVE_FETCH';
|
|
33
33
|
export const SET_LOCALE = 'SET_LOCALE';
|
|
34
34
|
export const SET_CONFIG = 'SET_CONFIG';
|
|
35
|
+
export const SET_BENEFIT_MESSAGES = 'SET_BENEFIT_MESSAGES';
|
|
35
36
|
export const SET_PREVIEW_STANDARD_OFFER = 'SET_PREVIEW_STANDARD_OFFER';
|
|
36
37
|
export const SET_PREVIEW_UPSELL_OFFER = 'SET_PREVIEW_UPSELL_OFFER';
|
|
37
38
|
export const SET_PREVIEW_PREPAID_OFFER = 'SET_PREVIEW_PREPAID_OFFER';
|
package/src/core/reducer.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
EnvironmentState,
|
|
12
12
|
AutoshipByDefaultState,
|
|
13
13
|
AutoshipEligibleState,
|
|
14
|
+
BenefitMessagesState,
|
|
14
15
|
ConfigState,
|
|
15
16
|
Incentive,
|
|
16
17
|
IncentiveObject,
|
|
@@ -178,6 +179,7 @@ const mapIncentive = (
|
|
|
178
179
|
// for standard incentives, include the criteria so we know which kind of incentive (e.g. PSI, prepaid, etc)
|
|
179
180
|
...(enhanced
|
|
180
181
|
? {
|
|
182
|
+
enhanced: true,
|
|
181
183
|
criteria: enhanced.criteria
|
|
182
184
|
? enhanced.criteria
|
|
183
185
|
: // when there is no criteria in the enhanced incentive, it means it's a program wide incentive
|
|
@@ -572,6 +574,15 @@ export const prepaidShipmentsSelected = (
|
|
|
572
574
|
|
|
573
575
|
export const price = (state: PriceState = {}, _action) => state;
|
|
574
576
|
|
|
577
|
+
export const benefitMessages = (state: BenefitMessagesState = {}, action): BenefitMessagesState => {
|
|
578
|
+
switch (action.type) {
|
|
579
|
+
case constants.SET_BENEFIT_MESSAGES:
|
|
580
|
+
return { ...(action.payload || {}) };
|
|
581
|
+
default:
|
|
582
|
+
return state;
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
575
586
|
export default combineReducers({
|
|
576
587
|
optedin,
|
|
577
588
|
optedout,
|
|
@@ -601,5 +612,6 @@ export default combineReducers({
|
|
|
601
612
|
templates,
|
|
602
613
|
productPlans,
|
|
603
614
|
prepaidShipmentsSelected,
|
|
604
|
-
price
|
|
615
|
+
price,
|
|
616
|
+
benefitMessages
|
|
605
617
|
});
|
package/src/core/selectors.ts
CHANGED
|
@@ -317,3 +317,60 @@ export const isShopifyDiscountFunctionInUseSelector = (state: State) => {
|
|
|
317
317
|
|
|
318
318
|
return plans.length > 0 && plans.every(plan => plan.hasPriceAdjustments === false || plan.prepaidShipments);
|
|
319
319
|
};
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Pick benefit message to be rendered in PDP. Preferences are:
|
|
323
|
+
* 1. Message matching the browser's locale exactly (eg "es-MX")
|
|
324
|
+
* 2. Message matching any locale with the same language prefix (eg "es-ES" for "es-MX")
|
|
325
|
+
* 3. US English message ("en-US")
|
|
326
|
+
* Returns null when none are present — the incentive is then skipped.
|
|
327
|
+
*/
|
|
328
|
+
const resolveLocaleMessage = (localeMap: Record<string, string>): string | null => {
|
|
329
|
+
if (!localeMap || typeof localeMap !== 'object') return null;
|
|
330
|
+
|
|
331
|
+
const enUS = 'en-US';
|
|
332
|
+
const browserLocale = navigator?.language || enUS;
|
|
333
|
+
const langPrefix = browserLocale.split('-')[0];
|
|
334
|
+
|
|
335
|
+
const partialMatch = Object.keys(localeMap).find(key => key !== browserLocale && key.split('-')[0] === langPrefix);
|
|
336
|
+
|
|
337
|
+
const msg = localeMap[browserLocale] || localeMap[partialMatch] || localeMap[enUS];
|
|
338
|
+
return typeof msg === 'string' && msg.length > 0 ? msg : null;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Walks the product's applicable incentives (initial first, then ongoing) and
|
|
343
|
+
* returns the deduped list of messages — one per unique incentive id that
|
|
344
|
+
* (a) was present in the offer response's `incentives_display_enhanced` and
|
|
345
|
+
* (b) has a configured benefit message in the active locale. Returns { messages: [] }
|
|
346
|
+
* when no qualifying incentive has a matching message.
|
|
347
|
+
*/
|
|
348
|
+
export const makeBenefitMessagesSelector = memoize(
|
|
349
|
+
(product: BaseProduct) =>
|
|
350
|
+
createSelector(
|
|
351
|
+
(state: State) => (state.incentives || {})[safeProductId(product?.id)],
|
|
352
|
+
(state: State) => state.benefitMessages || {},
|
|
353
|
+
(productIncentives, benefitMap) => {
|
|
354
|
+
if (!productIncentives) return { messages: [] as string[] };
|
|
355
|
+
|
|
356
|
+
const isPreview = window?.og?.previewMode;
|
|
357
|
+
const seenIds = new Set<string>();
|
|
358
|
+
const seenMessages = new Set<string>();
|
|
359
|
+
|
|
360
|
+
[productIncentives.initial, productIncentives.ongoing].forEach(list => {
|
|
361
|
+
(list || []).forEach(incentive => {
|
|
362
|
+
if (!isPreview && !incentive?.enhanced) return;
|
|
363
|
+
const id = incentive.id;
|
|
364
|
+
if (!id || seenIds.has(id)) return;
|
|
365
|
+
seenIds.add(id);
|
|
366
|
+
const msg = resolveLocaleMessage(benefitMap[id]);
|
|
367
|
+
if (!msg) return;
|
|
368
|
+
seenMessages.add(msg);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
return { messages: [...seenMessages] };
|
|
373
|
+
}
|
|
374
|
+
),
|
|
375
|
+
product => JSON.stringify(product)
|
|
376
|
+
);
|
|
@@ -25,11 +25,15 @@ export type NextUpcomingOrderState = Partial<
|
|
|
25
25
|
|
|
26
26
|
export type IncentivesState = Record<string, IncentiveObject>;
|
|
27
27
|
|
|
28
|
+
export type BenefitMessagesState = Record<string, Record<string, string>>;
|
|
29
|
+
|
|
28
30
|
export type Incentive = ApiIncentive & {
|
|
29
31
|
id: string;
|
|
30
32
|
/**
|
|
31
|
-
*
|
|
33
|
+
* True when the incentive id was present in `incentives_display_enhanced`
|
|
34
|
+
* on the offer response. The fields below are only populated in that case.
|
|
32
35
|
*/
|
|
36
|
+
enhanced?: boolean;
|
|
33
37
|
threshold_field?: string | null;
|
|
34
38
|
threshold_value?: string | null;
|
|
35
39
|
criteria?: {
|
package/src/index.js
CHANGED
|
@@ -28,6 +28,7 @@ export const previewMode = offers.previewMode;
|
|
|
28
28
|
export const register = offers.register;
|
|
29
29
|
export const resolveSettings = offers.resolveSettings;
|
|
30
30
|
export const setAuthUrl = offers.setAuthUrl;
|
|
31
|
+
export const setBenefitMessages = offers.setBenefitMessages;
|
|
31
32
|
export const setEnvironment = offers.setEnvironment;
|
|
32
33
|
export const setLocale = offers.setLocale;
|
|
33
34
|
export const setMerchantId = offers.setMerchantId;
|
package/src/make-api.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ConnectedOptinToggle } from './components/OptinToggle';
|
|
|
12
12
|
import { ConnectedOptinStatus } from './components/OptinStatus';
|
|
13
13
|
import { ConnectedText } from './components/Text';
|
|
14
14
|
import { ConnectedIncentiveText } from './components/IncentiveText';
|
|
15
|
+
import { ConnectedBenefitMessages } from './components/BenefitMessages';
|
|
15
16
|
import { ConnectedSelectFrequency } from './components/SelectFrequency';
|
|
16
17
|
import { ConnectedNextUpcomingOrder } from './components/NextUpcomingOrder';
|
|
17
18
|
import { ConnectedOffer } from './components/Offer';
|
|
@@ -38,6 +39,7 @@ export default function makeApi(store) {
|
|
|
38
39
|
customElements.define('og-when', ConnectedWhen);
|
|
39
40
|
customElements.define('og-text', ConnectedText);
|
|
40
41
|
customElements.define('og-incentive-text', ConnectedIncentiveText);
|
|
42
|
+
customElements.define('og-benefit-messages', ConnectedBenefitMessages);
|
|
41
43
|
customElements.define('og-offer', ConnectedOffer);
|
|
42
44
|
customElements.define('og-select-frequency', ConnectedSelectFrequency);
|
|
43
45
|
customElements.define('og-optout-button', ConnectedOptoutButton);
|
|
@@ -121,6 +123,16 @@ export default function makeApi(store) {
|
|
|
121
123
|
store.dispatch(actions.setLocale(locale));
|
|
122
124
|
return this;
|
|
123
125
|
},
|
|
126
|
+
/**
|
|
127
|
+
* Set benefit messages keyed by incentive public id. Each value is a
|
|
128
|
+
* locale map (IETF tag → message text) for the incentive; the consumer
|
|
129
|
+
* picks the appropriate locale at render time. Replaces the entire map.
|
|
130
|
+
* @param {{ [incentivePublicId: string]: { [locale: string]: string } }} messages
|
|
131
|
+
*/
|
|
132
|
+
setBenefitMessages(messages) {
|
|
133
|
+
store.dispatch(actions.setBenefitMessages(messages));
|
|
134
|
+
return this;
|
|
135
|
+
},
|
|
124
136
|
addTemplate(tagName, content, configOption) {
|
|
125
137
|
store.dispatch(actions.addTemplate(tagName, content, configOption));
|
|
126
138
|
return this;
|
|
@@ -5,6 +5,7 @@ import baseReducer, {
|
|
|
5
5
|
autoshipByDefault,
|
|
6
6
|
auth,
|
|
7
7
|
authUrl,
|
|
8
|
+
benefitMessages,
|
|
8
9
|
defaultFrequencies,
|
|
9
10
|
eligibilityGroups,
|
|
10
11
|
environment,
|
|
@@ -433,7 +434,8 @@ const reducer = combineReducers({
|
|
|
433
434
|
productToSubscribe,
|
|
434
435
|
sessionId,
|
|
435
436
|
templates,
|
|
436
|
-
prepaidShipmentsSelected
|
|
437
|
+
prepaidShipmentsSelected,
|
|
438
|
+
benefitMessages
|
|
437
439
|
});
|
|
438
440
|
|
|
439
441
|
export default function shopifyReducer(state, action) {
|