@capillarytech/creatives-library 8.0.328 → 8.0.330-alpha.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.
Files changed (46) hide show
  1. package/constants/unified.js +4 -0
  2. package/package.json +1 -1
  3. package/services/api.js +17 -0
  4. package/services/tests/api.test.js +85 -0
  5. package/utils/commonUtils.js +28 -0
  6. package/utils/tagValidations.js +2 -3
  7. package/utils/templateVarUtils.js +35 -6
  8. package/utils/tests/commonUtil.test.js +169 -0
  9. package/utils/tests/tagValidations.test.js +1 -1
  10. package/utils/tests/templateVarUtils.test.js +44 -0
  11. package/v2Components/CommonTestAndPreview/AddTestCustomer.js +42 -0
  12. package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +155 -0
  13. package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +93 -0
  14. package/v2Components/CommonTestAndPreview/SendTestMessage.js +79 -51
  15. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +134 -34
  16. package/v2Components/CommonTestAndPreview/actions.js +10 -0
  17. package/v2Components/CommonTestAndPreview/constants.js +15 -1
  18. package/v2Components/CommonTestAndPreview/index.js +364 -72
  19. package/v2Components/CommonTestAndPreview/messages.js +106 -0
  20. package/v2Components/CommonTestAndPreview/reducer.js +10 -0
  21. package/v2Components/CommonTestAndPreview/tests/AddTestCustomer.test.js +66 -0
  22. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +648 -0
  23. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +24 -0
  24. package/v2Components/CommonTestAndPreview/tests/CustomerCreationModal.test.js +174 -0
  25. package/v2Components/CommonTestAndPreview/tests/ExistingCustomerModal.test.js +114 -0
  26. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +52 -0
  27. package/v2Components/CommonTestAndPreview/tests/constants.test.js +31 -1
  28. package/v2Components/CommonTestAndPreview/tests/index.test.js +36 -0
  29. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +71 -0
  30. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +17 -0
  31. package/v2Components/SmsFallback/smsFallbackUtils.js +14 -3
  32. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +16 -0
  33. package/v2Components/TestAndPreviewSlidebox/index.js +5 -0
  34. package/v2Containers/CreativesContainer/index.js +15 -10
  35. package/v2Containers/Rcs/constants.js +6 -2
  36. package/v2Containers/Rcs/index.js +219 -91
  37. package/v2Containers/Rcs/messages.js +2 -1
  38. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +20 -0
  39. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +2370 -1758
  40. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +67 -0
  41. package/v2Containers/Rcs/tests/utils.test.js +56 -0
  42. package/v2Containers/Rcs/utils.js +53 -6
  43. package/v2Containers/SmsTrai/Edit/index.js +27 -0
  44. package/v2Containers/SmsTrai/Edit/messages.js +5 -0
  45. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +357 -324
  46. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +5586 -5212
@@ -8,6 +8,8 @@ import {
8
8
  syncCardVarMappedSemanticsFromSlots,
9
9
  hasMeaningfulSmsFallbackShape,
10
10
  getLibrarySmsFallbackApiBaselineFromTemplateData,
11
+ extractRegisteredSenderIdsFromSmsFallbackRecord,
12
+ pickRcsCardVarMappedEntries,
11
13
  } from '../rcsLibraryHydrationUtils';
12
14
  import { rcsVarRegex } from '../constants';
13
15
 
@@ -103,6 +105,41 @@ describe('rcsLibraryHydrationUtils', () => {
103
105
  });
104
106
  });
105
107
 
108
+ describe('extractRegisteredSenderIdsFromSmsFallbackRecord', () => {
109
+ it('returns null for missing / empty', () => {
110
+ expect(extractRegisteredSenderIdsFromSmsFallbackRecord(null)).toBe(null);
111
+ expect(extractRegisteredSenderIdsFromSmsFallbackRecord({})).toBe(null);
112
+ expect(
113
+ extractRegisteredSenderIdsFromSmsFallbackRecord({ registeredSenderIds: [] }),
114
+ ).toBe(null);
115
+ });
116
+
117
+ it('prefers top-level registeredSenderIds', () => {
118
+ expect(
119
+ extractRegisteredSenderIdsFromSmsFallbackRecord({
120
+ registeredSenderIds: ['A'],
121
+ templateConfigs: { registeredSenderIds: ['B'] },
122
+ }),
123
+ ).toEqual(['A']);
124
+ });
125
+
126
+ it('reads templateConfigs.registeredSenderIds when top-level absent', () => {
127
+ expect(
128
+ extractRegisteredSenderIdsFromSmsFallbackRecord({
129
+ templateConfigs: { registeredSenderIds: ['DLT1', 'DLT2'] },
130
+ }),
131
+ ).toEqual(['DLT1', 'DLT2']);
132
+ });
133
+
134
+ it('falls back to templateConfigs.header array', () => {
135
+ expect(
136
+ extractRegisteredSenderIdsFromSmsFallbackRecord({
137
+ templateConfigs: { header: ['H1'] },
138
+ }),
139
+ ).toEqual(['H1']);
140
+ });
141
+ });
142
+
106
143
  describe('pickFirstSmsFallbackTemplateString', () => {
107
144
  it('prefers smsTemplateContent over resolved message so DLT tokens stay in the string', () => {
108
145
  const sms = {
@@ -173,6 +210,7 @@ describe('rcsLibraryHydrationUtils', () => {
173
210
  it('is true when string contains {{word}} tokens', () => {
174
211
  expect(hasRcsVarTokens('{{1}} hi', rcsVarRegex)).toBe(true);
175
212
  expect(hasRcsVarTokens('{{user_id}}', rcsVarRegex)).toBe(true);
213
+ expect(hasRcsVarTokens('{{dynamic_expiry_date_after_3_days.FORMAT_1}}', rcsVarRegex)).toBe(true);
176
214
  });
177
215
  });
178
216
 
@@ -248,4 +286,33 @@ describe('rcsLibraryHydrationUtils', () => {
248
286
  expect(out.user_name).toBe('kept');
249
287
  });
250
288
  });
289
+
290
+ describe('pickRcsCardVarMappedEntries', () => {
291
+ it('returns {} for null, undefined, and array inputs', () => {
292
+ expect(pickRcsCardVarMappedEntries(null)).toEqual({});
293
+ expect(pickRcsCardVarMappedEntries(undefined)).toEqual({});
294
+ expect(pickRcsCardVarMappedEntries(['{#v#}_0', 'x'])).toEqual({});
295
+ });
296
+
297
+ it('keeps numeric slot keys', () => {
298
+ expect(pickRcsCardVarMappedEntries({ 1: 'a', 2: 'b' })).toEqual({ 1: 'a', 2: 'b' });
299
+ });
300
+
301
+ it('keeps semantic word keys', () => {
302
+ expect(pickRcsCardVarMappedEntries({ user_name: 'Alice', gt: '{{tag}}' })).toEqual({
303
+ user_name: 'Alice',
304
+ gt: '{{tag}}',
305
+ });
306
+ });
307
+
308
+ it('strips SMS fallback slot keys like {#var#}_0', () => {
309
+ const input = { 1: 'x', user_name: 'y', '{#promo#}_0': 'z', '{{name}}_1': 'w' };
310
+ const out = pickRcsCardVarMappedEntries(input);
311
+ expect(out).toEqual({ 1: 'x', user_name: 'y' });
312
+ });
313
+
314
+ it('returns {} for an empty object', () => {
315
+ expect(pickRcsCardVarMappedEntries({})).toEqual({});
316
+ });
317
+ });
251
318
  });
@@ -7,6 +7,7 @@ import {
7
7
  getTemplateStatusType,
8
8
  normalizeCardVarMapped,
9
9
  coalesceCardVarMappedToTemplate,
10
+ getRcsSemanticVarNamesSpanningTitleAndDesc,
10
11
  resolveCardVarMappedSlotValue,
11
12
  isRcsTextOnlyCardMediaType,
12
13
  mapRcsCardContentForConsumerWithResolvedTags,
@@ -215,7 +216,39 @@ describe('RCS utils', () => {
215
216
  });
216
217
  });
217
218
 
219
+ describe('getRcsSemanticVarNamesSpanningTitleAndDesc', () => {
220
+ it('returns names that appear in both title and description', () => {
221
+ const set = getRcsSemanticVarNamesSpanningTitleAndDesc(
222
+ 'Hello {{adv}}',
223
+ 'Body {{adv}} x {{other}}',
224
+ rcsVarRegex,
225
+ );
226
+ expect(Array.from(set).sort()).toEqual(['adv']);
227
+ });
228
+
229
+ it('is empty when the same name is only repeated in the title', () => {
230
+ const set = getRcsSemanticVarNamesSpanningTitleAndDesc(
231
+ '{{user_name}} and {{user_name}}',
232
+ 'Hi',
233
+ rcsVarRegex,
234
+ );
235
+ expect(set.size).toBe(0);
236
+ });
237
+ });
238
+
218
239
  describe('coalesceCardVarMappedToTemplate', () => {
240
+ it('does not copy a shared semantic value into the second slot when the tag spans title and description', () => {
241
+ const out = coalesceCardVarMappedToTemplate(
242
+ { adv: 'shared' },
243
+ 'T {{adv}}',
244
+ 'D {{adv}}',
245
+ rcsVarRegex,
246
+ );
247
+ expect(out[1]).toBe('shared');
248
+ expect(out[2]).toBe('');
249
+ expect(out.adv).toBe('shared');
250
+ });
251
+
219
252
  it('maps slot 1 to first token name from title', () => {
220
253
  expect(
221
254
  coalesceCardVarMappedToTemplate(
@@ -332,6 +365,24 @@ describe('RCS utils', () => {
332
365
  ),
333
366
  ).toBe('{{loyalty_points}}');
334
367
  });
368
+
369
+ it('omitSemanticFallback: duplicate title+desc tag does not read shared semantic for either slot', () => {
370
+ expect(
371
+ resolveCardVarMappedSlotValue({ adv: '{{adv}}', 1: '', 2: '' }, 'adv', 0, false, true),
372
+ ).toBe('');
373
+ expect(
374
+ resolveCardVarMappedSlotValue({ adv: '{{adv}}', 1: 'A', 2: 'B' }, 'adv', 0, false, true),
375
+ ).toBe('A');
376
+ expect(
377
+ resolveCardVarMappedSlotValue({ adv: '{{adv}}', 1: 'A', 2: 'B' }, 'adv', 1, false, true),
378
+ ).toBe('B');
379
+ });
380
+
381
+ it('omitSemanticFallback: cleared semantic does not force empty when numeric slot has a value', () => {
382
+ expect(
383
+ resolveCardVarMappedSlotValue({ adv: '', 1: 'first', 2: 'second' }, 'adv', 1, false, true),
384
+ ).toBe('second');
385
+ });
335
386
  });
336
387
 
337
388
  describe('mapRcsCardContentForConsumerWithResolvedTags', () => {
@@ -374,6 +425,11 @@ describe('RCS utils', () => {
374
425
  expect(out[0].title).toBe('Hi {{customer_name}}');
375
426
  expect(out[0].description).toBe('Pts {{loyalty_points}}');
376
427
  });
428
+
429
+ it('passes through null or non-object items in the card array unchanged', () => {
430
+ const out = mapRcsCardContentForConsumerWithResolvedTags([null, 'string', 42], {}, false);
431
+ expect(out).toEqual([null, 'string', 42]);
432
+ });
377
433
  });
378
434
 
379
435
  describe('isRcsTextOnlyCardMediaType', () => {
@@ -22,6 +22,7 @@ import {
22
22
  COMBINED_SMS_TEMPLATE_VAR_REGEX,
23
23
  isAnyTemplateVarToken,
24
24
  } from '../../utils/templateVarUtils';
25
+ import { pickRcsCardVarMappedEntries } from './rcsLibraryHydrationUtils';
25
26
 
26
27
  export const getRcsStatusType = (status) => {
27
28
  switch (status) {
@@ -130,6 +131,27 @@ export function normalizeCardVarMapped(rawCardVarMapped, orderedTagNames) {
130
131
  return normalizedMap;
131
132
  }
132
133
 
134
+ /**
135
+ * Semantic names that appear in both title and description (e.g. `{{adv}}` in header and body).
136
+ * Those slots must not share one semantic `cardVarMapped` key — otherwise VarSegment inputs mirror.
137
+ */
138
+ export function getRcsSemanticVarNamesSpanningTitleAndDesc(
139
+ templateTitle,
140
+ templateDesc,
141
+ rcsVarRegex,
142
+ ) {
143
+ const getVarNameFromToken = (token = '') => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
144
+ const titleTokens = templateTitle?.match(rcsVarRegex) ?? [];
145
+ const descTokens = templateDesc?.match(rcsVarRegex) ?? [];
146
+ const titleNames = new Set(titleTokens.map(getVarNameFromToken).filter(Boolean));
147
+ const descNames = new Set(descTokens.map(getVarNameFromToken).filter(Boolean));
148
+ const spanning = new Set();
149
+ titleNames.forEach((n) => {
150
+ if (descNames.has(n)) spanning.add(n);
151
+ });
152
+ return spanning;
153
+ }
154
+
133
155
  /**
134
156
  * Rebuild `cardVarMapped` so keys match the current title/description tokens (title tokens first,
135
157
  * then description), in order. Values are taken from the matching key, else from legacy slot
@@ -151,15 +173,25 @@ export function coalesceCardVarMappedToTemplate(
151
173
  if (!templateVarTokens.length) {
152
174
  return { ...lookupSourceMap };
153
175
  }
176
+ const semanticNamesSpanningTitleAndDesc = getRcsSemanticVarNamesSpanningTitleAndDesc(
177
+ templateTitle,
178
+ templateDesc,
179
+ rcsVarRegex,
180
+ );
154
181
  const coalescedMap = { ...lookupSourceMap };
155
182
  const seenSemanticVarNames = new Set();
156
183
  templateVarTokens.forEach((token, slotIndexZeroBased) => {
157
184
  const semanticVarName = getVarNameFromToken(token);
158
185
  if (!semanticVarName) return;
159
186
  const numericSlotKey = String(slotIndexZeroBased + 1);
187
+ const isRepeatOfSemanticName = seenSemanticVarNames.has(semanticVarName);
188
+ const skipSharedSemanticLookup =
189
+ isRepeatOfSemanticName && semanticNamesSpanningTitleAndDesc.has(semanticVarName);
160
190
  let valueFromSource = lookupSourceMap[numericSlotKey];
161
191
  if (valueFromSource === undefined || valueFromSource === null) {
162
- valueFromSource = lookupSourceMap[semanticVarName];
192
+ if (!skipSharedSemanticLookup) {
193
+ valueFromSource = lookupSourceMap[semanticVarName];
194
+ }
163
195
  }
164
196
  if (valueFromSource === undefined || valueFromSource === null) {
165
197
  valueFromSource = lookupSourceMap[String(slotIndexZeroBased + 1)];
@@ -189,12 +221,17 @@ export function coalesceCardVarMappedToTemplate(
189
221
  * When a numeric slot is present but only whitespace / empty (common after hydration), do not
190
222
  * treat it as authoritative — fall through to the semantic key so preview and payload match the
191
223
  * tag the user selected (e.g. `1: ''` but `promotion_points: '{{newTag}}'`).
224
+ *
225
+ * @param {boolean} [omitSemanticFallback=false] When true, do not read `varName` on the map (and do
226
+ * not apply the early `semanticEmpty` short-circuit). Use when the same semantic name appears in
227
+ * both title and description so each global slot stays independent.
192
228
  */
193
229
  export function resolveCardVarMappedSlotValue(
194
230
  cardVarMapped,
195
231
  varName,
196
232
  globalSlotIndexZeroBased,
197
233
  isLibraryMode = false,
234
+ omitSemanticFallback = false,
198
235
  ) {
199
236
  const varMap = cardVarMapped ?? {};
200
237
  const slotKey = String(globalSlotIndexZeroBased + 1);
@@ -205,7 +242,7 @@ export function resolveCardVarMappedSlotValue(
205
242
  Object.prototype.hasOwnProperty.call(varMap, slotKey)
206
243
  && String(varMap[slotKey] ?? '').trim() !== '';
207
244
 
208
- if (semanticEmpty && !(isLibraryMode && slotNonEmpty)) {
245
+ if (semanticEmpty && !(isLibraryMode && slotNonEmpty) && !omitSemanticFallback) {
209
246
  return '';
210
247
  }
211
248
  let numericSlotValue = '';
@@ -217,7 +254,7 @@ export function resolveCardVarMappedSlotValue(
217
254
  if (numericSlotValue.trim() !== '') {
218
255
  return numericSlotValue;
219
256
  }
220
- if (Object.prototype.hasOwnProperty.call(varMap, varName)) {
257
+ if (!omitSemanticFallback && Object.prototype.hasOwnProperty.call(varMap, varName)) {
221
258
  return String(varMap[varName] ?? '');
222
259
  }
223
260
  return '';
@@ -247,6 +284,9 @@ export function resolveRcsCardPreviewStrings(
247
284
  const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
248
285
  const getVarNameFromToken = (token = '') =>
249
286
  token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
287
+ const semanticNamesSpanningTitleAndDesc = textOnlyCard
288
+ ? new Set()
289
+ : getRcsSemanticVarNamesSpanningTitleAndDesc(title, description, rcsVarRegex);
250
290
 
251
291
  const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
252
292
  if (!str) return '';
@@ -258,11 +298,13 @@ export function resolveRcsCardPreviewStrings(
258
298
  const key = getVarNameFromToken(elem);
259
299
  const globalSlot = slotOffset + varOrdinal;
260
300
  varOrdinal += 1;
301
+ const omitSemantic = semanticNamesSpanningTitleAndDesc.has(key);
261
302
  const v = resolveCardVarMappedSlotValue(
262
303
  cardVarMapped,
263
304
  key,
264
305
  globalSlot,
265
306
  isLibraryMode,
307
+ omitSemantic,
266
308
  );
267
309
  if (v == null || String(v).trim() === '') return elem;
268
310
  return String(v);
@@ -285,7 +327,8 @@ export function resolveRcsCardPreviewStrings(
285
327
  /**
286
328
  * Campaign consumer payload: replace each card's `title` / `description` with VarSegment-resolved
287
329
  * tag strings (same rules as {@link resolveRcsCardPreviewStrings}). Root `rcsCardVarMapped` merges
288
- * with per-card `cardVarMapped`; `cardVarMapped` on each card is left unchanged for round-trip.
330
+ * with per-card `cardVarMapped` for resolution; emitted `cardVarMapped` omits SMS-fallback slot keys
331
+ * (root/nested merged with {@link pickRcsCardVarMappedEntries} per side).
289
332
  */
290
333
  export function mapRcsCardContentForConsumerWithResolvedTags(
291
334
  cardContentArray,
@@ -304,7 +347,9 @@ export function mapRcsCardContentForConsumerWithResolvedTags(
304
347
  card.cardVarMapped != null && typeof card.cardVarMapped === 'object'
305
348
  ? card.cardVarMapped
306
349
  : {};
307
- const merged = { ...rootRecord, ...nested };
350
+ const rootClean = pickRcsCardVarMappedEntries(rootRecord);
351
+ const nestedClean = pickRcsCardVarMappedEntries(nested);
352
+ const merged = { ...rootClean, ...nestedClean };
308
353
  const textOnly = isRcsTextOnlyCardMediaType(card.mediaType);
309
354
  const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
310
355
  card.title ?? '',
@@ -313,10 +358,12 @@ export function mapRcsCardContentForConsumerWithResolvedTags(
313
358
  isLibraryMode,
314
359
  textOnly,
315
360
  );
361
+ const { cardVarMapped: _drop, ...cardRest } = card;
316
362
  return {
317
- ...card,
363
+ ...cardRest,
318
364
  title: rcsTitle,
319
365
  description: rcsDesc,
366
+ ...(Object.keys(nestedClean).length > 0 ? { cardVarMapped: nestedClean } : {}),
320
367
  };
321
368
  });
322
369
  }
@@ -1102,6 +1102,33 @@ export const SmsTraiEdit = (props) => {
1102
1102
  </CapLabelInline>
1103
1103
  </TraiEditTemplateDetails>
1104
1104
  )}
1105
+ {isRcsSmsFallback &&
1106
+ traiDataRef.current &&
1107
+ !isEmpty(traiDataRef.current) &&
1108
+ !loading && (
1109
+ <TraiEditTemplateDetails className="sms-trai-edit-rcs-fallback__template-meta">
1110
+ <CapLabelInline type="label1">
1111
+ {formatMessage(messages.templateNameLabel)}
1112
+ </CapLabelInline>
1113
+ <CapLabelInline type="label2">
1114
+ {get(traiDataRef.current, 'versions.base.template_name', '')
1115
+ || get(traiDataRef.current, 'name', '')}
1116
+ </CapLabelInline>
1117
+ {traiDltEnabled ? (
1118
+ <>
1119
+ <CapLabelInline type="label1">
1120
+ {formatMessage(messages.traiEditSeperator)}
1121
+ </CapLabelInline>
1122
+ <CapLabelInline type="label1">
1123
+ {formatMessage(messages.senderIdlabel)}
1124
+ </CapLabelInline>
1125
+ <CapLabelInline type="label2">
1126
+ {[...get(traiDataRef.current, 'versions.base.header', [])].join(', ')}
1127
+ </CapLabelInline>
1128
+ </>
1129
+ ) : null}
1130
+ </TraiEditTemplateDetails>
1131
+ )}
1105
1132
  <CapColumn span={shouldShowPreview ? 14 : 24}>
1106
1133
  <CapRow>
1107
1134
  <CapHeader
@@ -48,6 +48,11 @@ export default defineMessages({
48
48
  id: `${prefix}.templateLabel`,
49
49
  defaultMessage: 'Template',
50
50
  },
51
+ /** RCS → SMS fallback slidebox: label above selected template metadata. */
52
+ templateNameLabel: {
53
+ id: `${prefix}.templateNameLabel`,
54
+ defaultMessage: 'Template name',
55
+ },
51
56
  traiEditSeperator: {
52
57
  id: `${prefix}.traiEditSeperator`,
53
58
  defaultMessage: '|',