@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/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.48.8",
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": "b59fd55f3c3771c7e8019cd42cb82f1064d284b2"
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
  {
@@ -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 }
@@ -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';
@@ -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
  });
@@ -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
- * all these fields are undefined when the offer profile is not standardized
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) {