@ordergroove/offers 2.48.6 → 2.48.7-alpha-PR-1482-3.31

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ordergroove/offers",
3
- "version": "2.48.6",
3
+ "version": "2.48.7-alpha-PR-1482-3.31+dbac0c59c",
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": "f76cf94b1c295431c581a1c46f6f1180f920de76"
53
+ "gitHead": "dbac0c59cdfab98862e3f758ab96d6872c48cf28"
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,66 @@
1
+ import { LitElement, html } from 'lit-element';
2
+ import { connect } from '../core/connect';
3
+ import { withProduct } from '../core/resolveProperties';
4
+ import { safeProductId } from '../core/utils';
5
+
6
+ export class BenefitMessages extends withProduct(LitElement) {
7
+ static get properties() {
8
+ return {
9
+ ...super.properties,
10
+ messages: { type: Array, attribute: false }
11
+ };
12
+ }
13
+
14
+ createRenderRoot() {
15
+ return this;
16
+ }
17
+
18
+ render() {
19
+ if (!this.messages?.length) return html``;
20
+ return html`
21
+ <ul class="og-benefit-messages">
22
+ ${this.messages.map(
23
+ msg => html`
24
+ <li>${msg}</li>
25
+ `
26
+ )}
27
+ </ul>
28
+ `;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Walks the product's applicable incentives (initial first, then ongoing) and
34
+ * returns the deduped list of messages — one per unique incentive id that
35
+ * (a) was present in the offer response's `incentives_display_enhanced` and
36
+ * (b) has a configured benefit message. Either array (initial / ongoing) may
37
+ * be absent. Returns an empty array when no qualifying incentive has a
38
+ * matching message.
39
+ */
40
+ export const mapStateToProps = (state, ownProps) => {
41
+ const productId = safeProductId(ownProps?.product?.id);
42
+ const productIncentives = (state.incentives || {})[productId];
43
+ const benefitMap = state.benefitMessages || {};
44
+
45
+ if (!productIncentives) return { messages: [] };
46
+
47
+ const seen = new Set();
48
+ const messages = [];
49
+
50
+ [productIncentives.initial, productIncentives.ongoing].forEach(list => {
51
+ (list || []).forEach(incentive => {
52
+ if (!incentive?.enhanced) return;
53
+ const id = incentive.id;
54
+ if (!id || seen.has(id)) return;
55
+ seen.add(id);
56
+ const msg = benefitMap[id];
57
+ if (msg) messages.push(msg);
58
+ });
59
+ });
60
+
61
+ return { messages };
62
+ };
63
+
64
+ export const ConnectedBenefitMessages = connect(mapStateToProps)(BenefitMessages);
65
+
66
+ export default ConnectedBenefitMessages;
@@ -0,0 +1,221 @@
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('escapes message strings as text (no HTML interpolation)', async () => {
38
+ const el = new BenefitMessages();
39
+ el.messages = ['<img src=x onerror="alert(1)">'];
40
+ await appendToBody(el);
41
+
42
+ const li = el.querySelector('li');
43
+ expect(li).toBeTruthy();
44
+ expect(li.querySelector('img')).toBeNull();
45
+ expect(li.textContent.trim()).toBe('<img src=x onerror="alert(1)">');
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 state = ({ incentivesForProduct, benefitMessages, productId = 'prod-1' } = {}) => ({
68
+ incentives: incentivesForProduct ? { [productId]: incentivesForProduct } : {},
69
+ benefitMessages: benefitMessages || {}
70
+ });
71
+
72
+ it('returns messages for the product, initial first then ongoing', () => {
73
+ expect(
74
+ mapStateToProps(
75
+ state({
76
+ incentivesForProduct: {
77
+ initial: [{ enhanced: true, id: 'inc-init' }],
78
+ ongoing: [{ enhanced: true, id: 'inc-ongoing' }]
79
+ },
80
+ benefitMessages: {
81
+ 'inc-init': 'First-order bonus',
82
+ 'inc-ongoing': 'Every-order discount'
83
+ }
84
+ }),
85
+ { product: { id: 'prod-1' } }
86
+ )
87
+ ).toEqual({ messages: ['First-order bonus', 'Every-order discount'] });
88
+ });
89
+
90
+ it('dedupes by incentive id when the same id appears in both initial and ongoing', () => {
91
+ expect(
92
+ mapStateToProps(
93
+ state({
94
+ incentivesForProduct: {
95
+ initial: [{ enhanced: true, id: 'inc-shared' }],
96
+ ongoing: [
97
+ { enhanced: true, id: 'inc-shared' },
98
+ { enhanced: true, id: 'inc-extra' }
99
+ ]
100
+ },
101
+ benefitMessages: {
102
+ 'inc-shared': 'Shared',
103
+ 'inc-extra': 'Extra'
104
+ }
105
+ }),
106
+ { product: { id: 'prod-1' } }
107
+ )
108
+ ).toEqual({ messages: ['Shared', 'Extra'] });
109
+ });
110
+
111
+ it('skips incentives that have no configured message', () => {
112
+ expect(
113
+ mapStateToProps(
114
+ state({
115
+ incentivesForProduct: {
116
+ initial: [
117
+ { enhanced: true, id: 'inc-1' },
118
+ { enhanced: true, id: 'inc-2' }
119
+ ],
120
+ ongoing: [{ enhanced: true, id: 'inc-3' }]
121
+ },
122
+ benefitMessages: {
123
+ 'inc-2': 'Only this one is configured'
124
+ }
125
+ }),
126
+ { product: { id: 'prod-1' } }
127
+ )
128
+ ).toEqual({ messages: ['Only this one is configured'] });
129
+ });
130
+
131
+ it('handles missing initial array (only ongoing)', () => {
132
+ expect(
133
+ mapStateToProps(
134
+ state({
135
+ incentivesForProduct: { ongoing: [{ enhanced: true, id: 'inc-1' }] },
136
+ benefitMessages: { 'inc-1': 'Ongoing only' }
137
+ }),
138
+ { product: { id: 'prod-1' } }
139
+ )
140
+ ).toEqual({ messages: ['Ongoing only'] });
141
+ });
142
+
143
+ it('handles missing ongoing array (only initial)', () => {
144
+ expect(
145
+ mapStateToProps(
146
+ state({
147
+ incentivesForProduct: { initial: [{ enhanced: true, id: 'inc-1' }] },
148
+ benefitMessages: { 'inc-1': 'Initial only' }
149
+ }),
150
+ { product: { id: 'prod-1' } }
151
+ )
152
+ ).toEqual({ messages: ['Initial only'] });
153
+ });
154
+
155
+ it('returns [] when both initial and ongoing are empty', () => {
156
+ expect(
157
+ mapStateToProps(
158
+ state({
159
+ incentivesForProduct: { initial: [], ongoing: [] },
160
+ benefitMessages: { 'inc-1': 'Should not appear' }
161
+ }),
162
+ { product: { id: 'prod-1' } }
163
+ )
164
+ ).toEqual({ messages: [] });
165
+ });
166
+
167
+ it('returns [] when the product has no incentives entry', () => {
168
+ expect(
169
+ mapStateToProps(
170
+ { incentives: {}, benefitMessages: { 'inc-1': 'Should not appear' } },
171
+ { product: { id: 'prod-1' } }
172
+ )
173
+ ).toEqual({ messages: [] });
174
+ });
175
+
176
+ it('returns [] when state has no benefitMessages key', () => {
177
+ expect(
178
+ mapStateToProps(
179
+ { incentives: { 'prod-1': { initial: [{ enhanced: true, id: 'inc-1' }], ongoing: [] } } },
180
+ { product: { id: 'prod-1' } }
181
+ )
182
+ ).toEqual({ messages: [] });
183
+ });
184
+
185
+ it('returns [] when no incentive has a matching message', () => {
186
+ expect(
187
+ mapStateToProps(
188
+ state({
189
+ incentivesForProduct: {
190
+ initial: [{ enhanced: true, id: 'inc-1' }],
191
+ ongoing: [{ enhanced: true, id: 'inc-2' }]
192
+ },
193
+ benefitMessages: { 'inc-other': 'Unrelated' }
194
+ }),
195
+ { product: { id: 'prod-1' } }
196
+ )
197
+ ).toEqual({ messages: [] });
198
+ });
199
+
200
+ it('returns [] when ownProps has no product', () => {
201
+ expect(mapStateToProps({ benefitMessages: { 'inc-1': 'm' } }, {})).toEqual({ messages: [] });
202
+ });
203
+
204
+ it('skips incentives that were not in incentives_display_enhanced', () => {
205
+ expect(
206
+ mapStateToProps(
207
+ state({
208
+ incentivesForProduct: {
209
+ initial: [{ id: 'inc-non-enhanced' }, { enhanced: true, id: 'inc-enhanced' }],
210
+ ongoing: []
211
+ },
212
+ benefitMessages: {
213
+ 'inc-non-enhanced': 'Should not appear',
214
+ 'inc-enhanced': 'Should appear'
215
+ }
216
+ }),
217
+ { product: { id: 'prod-1' } }
218
+ )
219
+ ).toEqual({ messages: ['Should appear'] });
220
+ });
221
+ });
@@ -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: {
@@ -296,6 +296,17 @@ export const setConfig = payload => ({
296
296
  payload
297
297
  });
298
298
 
299
+ /**
300
+ * Set benefit messages keyed by incentive public id. Each value is the
301
+ * already-locale-resolved message text to display when the incentive applies
302
+ * to the product being rendered. Replaces the entire map on each call.
303
+ * @param {{ [incentivePublicId: string]: string }} payload
304
+ */
305
+ export const setBenefitMessages = payload => ({
306
+ type: constants.SET_BENEFIT_MESSAGES,
307
+ payload
308
+ });
309
+
299
310
  export const addTemplate = (selector, markup, config) => ({
300
311
  type: constants.ADD_TEMPLATE,
301
312
  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';
@@ -9,6 +9,7 @@ import { experimentsReducer } from './experiments';
9
9
  import {
10
10
  AutoshipByDefaultState,
11
11
  AutoshipEligibleState,
12
+ BenefitMessagesState,
12
13
  ConfigState,
13
14
  Incentive,
14
15
  IncentiveObject,
@@ -176,6 +177,7 @@ const mapIncentive = (
176
177
  // for standard incentives, include the criteria so we know which kind of incentive (e.g. PSI, prepaid, etc)
177
178
  ...(enhanced
178
179
  ? {
180
+ enhanced: true,
179
181
  criteria: enhanced.criteria
180
182
  ? enhanced.criteria
181
183
  : // when there is no criteria in the enhanced incentive, it means it's a program wide incentive
@@ -570,6 +572,15 @@ export const prepaidShipmentsSelected = (
570
572
 
571
573
  export const price = (state: PriceState = {}, _action) => state;
572
574
 
575
+ export const benefitMessages = (state: BenefitMessagesState = {}, action): BenefitMessagesState => {
576
+ switch (action.type) {
577
+ case constants.SET_BENEFIT_MESSAGES:
578
+ return { ...(action.payload || {}) };
579
+ default:
580
+ return state;
581
+ }
582
+ };
583
+
573
584
  export default combineReducers({
574
585
  optedin,
575
586
  optedout,
@@ -599,5 +610,6 @@ export default combineReducers({
599
610
  templates,
600
611
  productPlans,
601
612
  prepaidShipmentsSelected,
602
- price
613
+ price,
614
+ benefitMessages
603
615
  });
@@ -17,11 +17,15 @@ export type NextUpcomingOrderState = Partial<
17
17
 
18
18
  export type IncentivesState = Record<string, IncentiveObject>;
19
19
 
20
+ export type BenefitMessagesState = Record<string, string>;
21
+
20
22
  export type Incentive = ApiIncentive & {
21
23
  id: string;
22
24
  /**
23
- * all these fields are undefined when the offer profile is not standardized
25
+ * True when the incentive id was present in `incentives_display_enhanced`
26
+ * on the offer response. The fields below are only populated in that case.
24
27
  */
28
+ enhanced?: boolean;
25
29
  threshold_field?: string | null;
26
30
  threshold_value?: string | null;
27
31
  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 the
128
+ * already-locale-resolved message text to display when the incentive
129
+ * applies to the product being rendered. Replaces the entire map.
130
+ * @param {{ [incentivePublicId: 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) {