@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
@@ -43,6 +43,7 @@ import {EXTERNAL_URL, SITE_URL, WEBPUSH_MEDIA_TYPES} from '../WebPush/constants'
43
43
  import { IMAGE, VIDEO } from '../Facebook/Advertisement/constant';
44
44
  import { RCS_STATUSES } from '../Rcs/constants';
45
45
  import { mapRcsCardContentForConsumerWithResolvedTags } from '../Rcs/utils';
46
+ import { pickRcsCardVarMappedEntries } from '../Rcs/rcsLibraryHydrationUtils';
46
47
  import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
47
48
  import { CREATIVE } from '../Facebook/constants';
48
49
  import { LOYALTY } from '../App/constants';
@@ -743,7 +744,6 @@ export class Creatives extends React.Component {
743
744
  smsFallBackContent = {},
744
745
  creativeName = "",
745
746
  channel = constants.RCS,
746
- accountId = "",
747
747
  rcsCardVarMapped,
748
748
  } = templateData || {};
749
749
  const { isFullMode: isFullModeForRcsPayload } = this.props;
@@ -765,8 +765,13 @@ export class Creatives extends React.Component {
765
765
  rootMirrorCardVarMapped != null && typeof rootMirrorCardVarMapped === 'object'
766
766
  ? rootMirrorCardVarMapped
767
767
  : {};
768
- const mergedFromRootAndNested = { ...rootRecord, ...nestedRecord };
769
- return Object.keys(mergedFromRootAndNested).length > 0 ? mergedFromRootAndNested : null;
768
+ const mergedFromRootAndNested = {
769
+ ...pickRcsCardVarMappedEntries(rootRecord),
770
+ ...pickRcsCardVarMappedEntries(nestedRecord),
771
+ };
772
+ return Object.keys(mergedFromRootAndNested).length > 0
773
+ ? mergedFromRootAndNested
774
+ : null;
770
775
  })();
771
776
  // Campaigns (embedded): do not duplicate `cardVarMapped` as root `rcsCardVarMapped` on send —
772
777
  // slot map stays on `versions…cardContent[0].cardVarMapped` only. Library full mode keeps root mirror.
@@ -785,7 +790,6 @@ export class Creatives extends React.Component {
785
790
  RCS: {
786
791
  rcsContent: {
787
792
  ...rcsContent,
788
- ...(accountId && !isFullMode && { accountId }),
789
793
  cardContent: [
790
794
  {
791
795
  ...cardContent,
@@ -1237,7 +1241,7 @@ export class Creatives extends React.Component {
1237
1241
  };
1238
1242
  }
1239
1243
  break;
1240
- case constants.FACEBOOK:
1244
+ case constants.FACEBOOK: {
1241
1245
  if (template.value) {
1242
1246
  const FacebookAd = template?.value?.versions?.base?.content?.FacebookAd;
1243
1247
  const { type } = FacebookAd[0];
@@ -1281,8 +1285,9 @@ export class Creatives extends React.Component {
1281
1285
  selectedMarketingObjective: template.value.selectedMarketingObjective,
1282
1286
  };
1283
1287
  }
1288
+ }
1284
1289
  break;
1285
- case constants.RCS:
1290
+ case constants.RCS: {
1286
1291
  if (template.value) {
1287
1292
  const { isFullMode: isFullModeForRcsConsumerPayload } = this.props;
1288
1293
  const { name = "", versions = {} } = template.value || {};
@@ -1330,7 +1335,6 @@ export class Creatives extends React.Component {
1330
1335
  contentType = "",
1331
1336
  cardType = "",
1332
1337
  cardSettings = {},
1333
- accountId = "",
1334
1338
  } = get(versions, 'base.content.RCS.rcsContent', {});
1335
1339
  const rootRcsCardVarMappedFromSubmit = get(template, 'value.rcsCardVarMapped');
1336
1340
  const firstCardFromSubmit = Array.isArray(cardContentFromSubmit)
@@ -1350,7 +1354,7 @@ export class Creatives extends React.Component {
1350
1354
  const cardVarMappedFromFirstRcsCard =
1351
1355
  firstCardFromSubmit?.cardVarMapped != null
1352
1356
  && typeof firstCardFromSubmit.cardVarMapped === 'object'
1353
- ? firstCardFromSubmit.cardVarMapped
1357
+ ? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
1354
1358
  : null;
1355
1359
  const includeRootRcsCardVarMappedOnConsumerPayload =
1356
1360
  cardVarMappedFromFirstRcsCard
@@ -1360,7 +1364,6 @@ export class Creatives extends React.Component {
1360
1364
  channel,
1361
1365
  creativeName: name,
1362
1366
  rcsContent,
1363
- accountId: accountId,
1364
1367
  ...(includeRootRcsCardVarMappedOnConsumerPayload
1365
1368
  ? { rcsCardVarMapped: cardVarMappedFromFirstRcsCard }
1366
1369
  : {}),
@@ -1382,8 +1385,9 @@ export class Creatives extends React.Component {
1382
1385
  };
1383
1386
  }
1384
1387
  }
1388
+ }
1385
1389
  break;
1386
- case constants.ZALO:
1390
+ case constants.ZALO: {
1387
1391
  if (template.value) {
1388
1392
  templateData = {
1389
1393
  ...template.value,
@@ -1392,6 +1396,7 @@ export class Creatives extends React.Component {
1392
1396
  delete templateData.type;
1393
1397
  }
1394
1398
  }
1399
+ }
1395
1400
  break;
1396
1401
  case constants.WEBPUSH: {
1397
1402
  if (template.value) {
@@ -90,13 +90,17 @@ export const RCS_MEDIA_TYPES = {
90
90
  VIDEO: 'VIDEO',
91
91
  };
92
92
 
93
- export const rcsVarRegex = /\{\{\w+\}\}/g;
94
- export const rcsVarTestRegex = /\{\{\w+\}\}/;
93
+ /** Match `{{}}` placeholders including Liquid-style names (e.g. `tag.FORMAT_1`). Must align with split + `isAnyTemplateVarToken` / SMS fallback. */
94
+ export const rcsVarRegex = /\{\{[^}]+\}\}/g;
95
+ /** True when the whole string is a single RCS/mustache token (used when testing segment tokens). */
96
+ export const rcsVarTestRegex = /^\{\{[^}]+\}\}$/;
95
97
 
96
98
  /** Matches all `{{N}}` numeric-index variable tokens in a template string (global). */
97
99
  export const RCS_NUMERIC_VAR_TOKEN_REGEX = /\{\{(\d+)\}\}/g;
98
100
  /** `cardVarMapped` slot keys that are numeric only (legacy ordering). */
99
101
  export const RCS_NUMERIC_VAR_NAME_REGEX = /^\d+$/;
102
+ /** Semantic Liquid-style keys on RCS `cardVarMapped` (same class as `{{…}}` inner names in the editor). */
103
+ export const RCS_CARD_VAR_MAPPED_SEMANTIC_KEY_REGEX = /^[\w.]+$/;
100
104
  /** Escape `RegExp` metacharacters when building a pattern from user/tag text. */
101
105
  export const RCS_REGEX_META_CHARS_PATTERN = /[.*+?^${}()|[\]\\]/g;
102
106
  /** Entire string is a single `{{tag}}` token (value still a placeholder). */
@@ -44,10 +44,12 @@ import {
44
44
  normalizeLibraryLoadedTitleDesc,
45
45
  mergeRcsSmsFallBackContentFromDetails,
46
46
  mergeRcsSmsFallbackVarMapLayers,
47
+ extractRegisteredSenderIdsFromSmsFallbackRecord,
47
48
  pickFirstSmsFallbackTemplateString,
48
49
  syncCardVarMappedSemanticsFromSlots,
49
50
  hasMeaningfulSmsFallbackShape,
50
51
  getLibrarySmsFallbackApiBaselineFromTemplateData,
52
+ pickRcsCardVarMappedEntries,
51
53
  } from './rcsLibraryHydrationUtils';
52
54
  import {
53
55
  RCS,
@@ -118,6 +120,7 @@ import {
118
120
  getTemplateStatusType,
119
121
  normalizeCardVarMapped,
120
122
  coalesceCardVarMappedToTemplate,
123
+ getRcsSemanticVarNamesSpanningTitleAndDesc,
121
124
  resolveCardVarMappedSlotValue,
122
125
  sanitizeCardVarMappedValue,
123
126
  } from './utils';
@@ -261,7 +264,6 @@ export const Rcs = (props) => {
261
264
  const [accessToken, setAccessToken] = useState('');
262
265
  const [hostName, setHostName] = useState('');
263
266
  const [accountName, setAccountName] = useState('');
264
- const [rcsAccount, setRcsAccount] = useState('');
265
267
  useEffect(() => {
266
268
  const accountObj = accountData.selectedRcsAccount || {};
267
269
  if (!isEmpty(accountObj)) {
@@ -277,14 +279,12 @@ export const Rcs = (props) => {
277
279
  setAccessToken(configs.accessToken || '');
278
280
  setHostName(accountObj.hostName || '');
279
281
  setAccountName(accountObj.name || '');
280
- setRcsAccount(accountObj.id || '');
281
282
  } else {
282
283
  setAccountId('');
283
284
  setWecrmAccountId('');
284
285
  setAccessToken('');
285
286
  setHostName('');
286
287
  setAccountName('');
287
- setRcsAccount('');
288
288
  }
289
289
  }, [accountData.selectedRcsAccount]);
290
290
 
@@ -389,6 +389,12 @@ export const Rcs = (props) => {
389
389
 
390
390
  const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
391
391
 
392
+ /** Same `{{tag}}` in both title and description must not share one semantic map entry. */
393
+ const rcsSpanningSemanticVarNames = useMemo(
394
+ () => getRcsSemanticVarNamesSpanningTitleAndDesc(templateTitle, templateDesc, rcsVarRegex),
395
+ [templateTitle, templateDesc],
396
+ );
397
+
392
398
  /** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
393
399
  const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
394
400
  const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
@@ -421,6 +427,7 @@ export const Rcs = (props) => {
421
427
  key,
422
428
  globalSlot,
423
429
  isEditLike,
430
+ rcsSpanningSemanticVarNames.has(key),
424
431
  );
425
432
  if (isNil(v) || String(v)?.trim?.() === '') return elem;
426
433
  return String(v);
@@ -487,6 +494,7 @@ export const Rcs = (props) => {
487
494
  isFullMode,
488
495
  isEditFlow,
489
496
  cardVarMapped,
497
+ rcsSpanningSemanticVarNames,
490
498
  ]);
491
499
 
492
500
  const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
@@ -517,7 +525,7 @@ export const Rcs = (props) => {
517
525
  const nextVarMap = {};
518
526
  let varOrdinal = 0;
519
527
  arr.forEach((elem, idx) => {
520
- // RCS placeholders are alphanumeric/underscore (e.g. {{user_name}}), not numeric-only
528
+ // Mustache tokens: {{1}}, {{user_name}}, {{tag.FORMAT_1}}, etc.
521
529
  if (rcsVarTestRegex.test(elem)) {
522
530
  const id = `${elem}_${idx}`;
523
531
  const varName = getVarNameFromToken(elem);
@@ -528,6 +536,7 @@ export const Rcs = (props) => {
528
536
  varName,
529
537
  globalSlot,
530
538
  isEditLike,
539
+ rcsSpanningSemanticVarNames.has(varName),
531
540
  );
532
541
  nextVarMap[id] = mappedValue;
533
542
  }
@@ -537,7 +546,7 @@ export const Rcs = (props) => {
537
546
 
538
547
  initField(templateTitle, setTitleVarMappedData, 0);
539
548
  initField(templateDesc, setDescVarMappedData, titleTokenCount);
540
- }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode]);
549
+ }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames]);
541
550
 
542
551
  useEffect(() => {
543
552
  if (!isEditFlow && isFullMode) {
@@ -752,10 +761,7 @@ export const Rcs = (props) => {
752
761
  get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '')
753
762
  || get(details, 'rcsContent.cardSettings', '');
754
763
  setMediaData(mediaData, mediaType, cardSettings);
755
- if (details?.edit) {
756
- const rcsAccountId = get(details, 'versions.base.content.RCS.rcsContent.accountId', '');
757
- setRcsAccount(rcsAccountId);
758
- }
764
+
759
765
  const smsFallbackContent = mergeRcsSmsFallBackContentFromDetails(details);
760
766
  const base = get(smsFallbackContent, 'versions.base', {});
761
767
  const updatedEditor = base['updated-sms-editor'] ?? base['sms-editor'];
@@ -786,12 +792,17 @@ export const Rcs = (props) => {
786
792
  typeof smsFallbackContent.unicodeValidity === 'boolean'
787
793
  ? smsFallbackContent.unicodeValidity
788
794
  : (typeof base['unicode-validity'] === 'boolean' ? base['unicode-validity'] : true);
795
+ const registeredSenderIdsFromApi =
796
+ extractRegisteredSenderIdsFromSmsFallbackRecord(smsFallbackContent);
789
797
  const nextSmsState = {
790
798
  templateName: smsFallbackContent.smsTemplateName || '',
791
799
  content: fallbackMessage,
792
800
  templateContent: fallbackMessage,
793
801
  unicodeValidity: unicodeFromApi,
794
802
  ...(hasVarMapped && { rcsSmsFallbackVarMapped: varMappedFromPayload }),
803
+ ...(Array.isArray(registeredSenderIdsFromApi) && registeredSenderIdsFromApi.length > 0
804
+ ? { registeredSenderIds: registeredSenderIdsFromApi }
805
+ : {}),
795
806
  };
796
807
  const hydrationKey = JSON.stringify({
797
808
  creativeKey: details._id || details.name || details.creativeName || '',
@@ -799,6 +810,10 @@ export const Rcs = (props) => {
799
810
  content: nextSmsState.content,
800
811
  unicodeValidity: nextSmsState.unicodeValidity,
801
812
  varMapped: nextSmsState.rcsSmsFallbackVarMapped || {},
813
+ senderIds:
814
+ Array.isArray(registeredSenderIdsFromApi)
815
+ ? registeredSenderIdsFromApi.join('\u001f')
816
+ : '',
802
817
  });
803
818
  if (
804
819
  isFullMode
@@ -898,58 +913,87 @@ export const Rcs = (props) => {
898
913
  return templateStr.replace(re, `{{${tagName}}}`);
899
914
  };
900
915
 
901
- const onTagSelect = (data, areaId, field) => {
902
- if (!areaId) return;
903
- const sep = areaId.lastIndexOf('_');
904
- if (sep === -1) return;
905
- const slotSuffix = areaId.slice(sep + 1);
906
- if (slotSuffix === '' || isNaN(Number(slotSuffix))) return;
907
- const token = areaId.slice(0, sep);
908
- const variableName = getVarNameFromToken(token);
909
- if (!variableName) return;
910
- const isNumericSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(variableName));
911
- const fieldStr = field === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
912
- const fieldType = field === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
913
- const globalSlotForArea = getGlobalSlotIndexForRcsFieldId(areaId, fieldStr, fieldType);
914
-
915
- setCardVarMapped((prev) => {
916
- const base = (prev?.[variableName] ?? '').toString();
917
- const nextVal = `${base}{{${data}}}`;
918
- const next = { ...(prev || {}) };
919
- if (isNumericSlot) {
920
- delete next[variableName];
921
- next[data] = nextVal;
916
+ const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
917
+ if (!varSegmentCompositeDomId) return;
918
+ const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
919
+ if (underscoreIndexInCompositeId === -1) return;
920
+ const segmentIndexSuffix = varSegmentCompositeDomId.slice(underscoreIndexInCompositeId + 1);
921
+ if (segmentIndexSuffix === '' || isNaN(Number(segmentIndexSuffix))) return;
922
+ const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
923
+ const semanticOrNumericVarName = getVarNameFromToken(mustacheTokenFromCompositeId);
924
+ if (!semanticOrNumericVarName) return;
925
+ const isNumericPlaceholderSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(semanticOrNumericVarName));
926
+ const templateStringForField =
927
+ tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
928
+ const titleOrMessageFieldType =
929
+ tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
930
+ const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
931
+ varSegmentCompositeDomId,
932
+ templateStringForField,
933
+ titleOrMessageFieldType,
934
+ );
935
+ const cardVarMappedNumericSlotKey =
936
+ globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined
937
+ ? String(globalVarSlotIndexZeroBased + 1)
938
+ : null;
939
+
940
+ setCardVarMapped((previousCardVarMapped) => {
941
+ const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
942
+ if (isNumericPlaceholderSlot) {
943
+ const existingValueBeforeAppend = (
944
+ previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
945
+ ).toString();
946
+ const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
947
+ delete updatedCardVarMapped[semanticOrNumericVarName];
948
+ updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
922
949
  } else {
923
- // Use the global slot index key only writing by semantic name (variableName)
924
- // would contaminate any other field that shares the same var name (e.g. {{gt}}
925
- // in both title and description), causing the label value to bleed across fields.
926
- if (globalSlotForArea !== null && globalSlotForArea !== undefined) {
927
- next[String(globalSlotForArea + 1)] = nextVal;
928
- // Same reasoning as handleRcsVarChange: delete the semantic key so
929
- // resolveCardVarMappedSlotValue uses only the numeric slot and the
930
- // value cannot bleed across fields that share the same var name.
931
- delete next[variableName];
950
+ // Same semantic token (e.g. {{adv}}) in title and body must not share one map key for
951
+ // "existing value" that appends the new tag onto the other field. Match handleRcsVarChange:
952
+ // read/write the global numeric slot only and drop the shared semantic key.
953
+ const existingValueBeforeAppend = cardVarMappedNumericSlotKey
954
+ ? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
955
+ : String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
956
+ const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
957
+ delete updatedCardVarMapped[semanticOrNumericVarName];
958
+ if (cardVarMappedNumericSlotKey) {
959
+ updatedCardVarMapped[cardVarMappedNumericSlotKey] = mappedValueAfterAppendingTag;
932
960
  } else {
933
- next[variableName] = nextVal;
961
+ updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
934
962
  }
935
963
  }
936
- return next;
964
+ return updatedCardVarMapped;
937
965
  });
938
966
 
939
- if (isNumericSlot && (field === RCS_TAG_AREA_FIELD_TITLE || field === RCS_TAG_AREA_FIELD_DESC)) {
940
- if (field === RCS_TAG_AREA_FIELD_TITLE) {
941
- setTemplateTitle((prev) => {
942
- const nextStr = replaceNumericPlaceholderWithTagInTemplate(prev || '', variableName, data);
943
- if (nextStr === prev) return prev;
944
- setTemplateTitleError(variableErrorHandling(nextStr));
945
- return nextStr;
967
+ if (
968
+ isNumericPlaceholderSlot
969
+ && (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC)
970
+ ) {
971
+ if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
972
+ setTemplateTitle((previousTitle) => {
973
+ const titleAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
974
+ previousTitle || '',
975
+ semanticOrNumericVarName,
976
+ selectedTagNameFromPicker,
977
+ );
978
+ if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
979
+ setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
980
+ // Remount segment editor: tag insert replaces {{n}} with e.g. {{tag.FORMAT_1}} — slot ids change; avoids stale UI vs manual typing in full-mode TextArea
981
+ setRcsVarSegmentEditorRemountKey((k) => k + 1);
982
+ return titleAfterReplacingNumericPlaceholder;
946
983
  });
947
984
  } else {
948
- setTemplateDesc((prev) => {
949
- const nextStr = replaceNumericPlaceholderWithTagInTemplate(prev || '', variableName, data);
950
- if (nextStr === prev) return prev;
951
- setTemplateDescError(variableErrorHandling(nextStr));
952
- return nextStr;
985
+ setTemplateDesc((previousDescription) => {
986
+ const descriptionAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
987
+ previousDescription || '',
988
+ semanticOrNumericVarName,
989
+ selectedTagNameFromPicker,
990
+ );
991
+ if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
992
+ return previousDescription;
993
+ }
994
+ setTemplateDescError(variableErrorHandling(descriptionAfterReplacingNumericPlaceholder));
995
+ setRcsVarSegmentEditorRemountKey((k) => k + 1);
996
+ return descriptionAfterReplacingNumericPlaceholder;
953
997
  });
954
998
  }
955
999
  }
@@ -1107,7 +1151,8 @@ export const Rcs = (props) => {
1107
1151
  if(!isFullMode){
1108
1152
  return false;
1109
1153
  }
1110
- if (!/^\w+$/.test(paramName)) {
1154
+ // Allow Liquid-style param names: letters, digits, underscore, dots (e.g. dynamic_expiry_date_after_3_days.FORMAT_1)
1155
+ if (!/^[\w.]+$/.test(paramName)) {
1111
1156
  return formatMessage(messages.unknownCharactersError);
1112
1157
  }
1113
1158
  }
@@ -1220,6 +1265,7 @@ const onTitleAddVar = () => {
1220
1265
  varName,
1221
1266
  globalSlot,
1222
1267
  isEditLike,
1268
+ rcsSpanningSemanticVarNames.has(varName),
1223
1269
  );
1224
1270
  }
1225
1271
  });
@@ -1228,42 +1274,37 @@ const onTitleAddVar = () => {
1228
1274
 
1229
1275
  const titleVarSegmentValueMapById = useMemo(
1230
1276
  () => getRcsValueMap(templateTitle, TITLE_TEXT),
1231
- [templateTitle, cardVarMapped, isEditFlow, isFullMode],
1277
+ [templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
1232
1278
  );
1233
1279
  const descriptionVarSegmentValueMapById = useMemo(
1234
1280
  () => getRcsValueMap(templateDesc, MESSAGE_TEXT),
1235
- [templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode],
1281
+ [templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
1236
1282
  );
1237
1283
 
1238
- const handleRcsVarChange = (id, value, type) => {
1239
- const sep = id.lastIndexOf('_');
1240
- if (sep === -1) return;
1241
- const token = id.slice(0, sep);
1242
- const variableName = getVarNameFromToken(token);
1284
+ const handleRcsVarChange = (varSegmentCompositeDomId, value, type) => {
1285
+ const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
1286
+ if (underscoreIndexInCompositeId === -1) return;
1287
+ const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
1288
+ const variableName = getVarNameFromToken(mustacheTokenFromCompositeId);
1243
1289
  if (variableName === undefined || variableName === null || variableName === '') return;
1244
1290
  const isInvalidValue = value?.trim() === '';
1245
1291
  const coercedSlotValue = isInvalidValue ? '' : value;
1246
- const fieldStr = type === TITLE_TEXT ? templateTitle : templateDesc;
1247
- const globalSlot = getGlobalSlotIndexForRcsFieldId(id, fieldStr, type);
1248
- setCardVarMapped((previousVarMap) => {
1249
- const nextVarMap = { ...previousVarMap };
1250
- if (globalSlot !== null && globalSlot !== undefined) {
1251
- // Write by global slot index only — title and description can share the
1252
- // same var name (e.g. {{gt}}), and writing by semantic name would cause
1253
- // the description slot to resolve the title's value and vice-versa.
1254
- const numericKey = String(globalSlot + 1);
1255
- nextVarMap[numericKey] = coercedSlotValue;
1256
- // Remove any stale semantic key so resolveCardVarMappedSlotValue never
1257
- // falls back to it. Guard: when variableName is already the numeric slot
1258
- // key (e.g. {{1}} at slot 0 both equal "1"), skip the delete or it
1259
- // would erase the value we just wrote.
1260
- if (variableName !== numericKey) {
1261
- delete nextVarMap[variableName];
1262
- }
1263
- } else {
1264
- nextVarMap[variableName] = coercedSlotValue;
1292
+ const templateStringForField = type === TITLE_TEXT ? templateTitle : templateDesc;
1293
+ const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
1294
+ varSegmentCompositeDomId,
1295
+ templateStringForField,
1296
+ type,
1297
+ );
1298
+ setCardVarMapped((previousCardVarMapped) => {
1299
+ const updatedCardVarMapped = { ...previousCardVarMapped };
1300
+ // Remove stale semantic key: keeping it causes every other slot sharing the same
1301
+ // variable name (e.g. {{adv}} in both title and description) to read the same value
1302
+ // via the semantic-key fallback in resolveCardVarMappedSlotValue.
1303
+ delete updatedCardVarMapped[variableName];
1304
+ if (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined) {
1305
+ updatedCardVarMapped[String(globalVarSlotIndexZeroBased + 1)] = coercedSlotValue;
1265
1306
  }
1266
- return nextVarMap;
1307
+ return updatedCardVarMapped;
1267
1308
  });
1268
1309
  };
1269
1310
 
@@ -1381,6 +1422,7 @@ const onTitleAddVar = () => {
1381
1422
  </CapRow>
1382
1423
  <CapRow className="rcs-create-template-message-input">
1383
1424
  <div className="rcs_text_area_wrapper">
1425
+ {/* Edit/library: segmented inputs (split on {{…}}). Full-mode create: single TextArea below — manual entry there never hits segment split. TagList replaces {{n}} in template string here. */}
1384
1426
  {(isEditFlow || !isFullMode)
1385
1427
  ? (
1386
1428
  <VarSegmentMessageEditor
@@ -1971,7 +2013,6 @@ const onTitleAddVar = () => {
1971
2013
  content: {
1972
2014
  RCS: {
1973
2015
  rcsContent: {
1974
- ...(rcsAccount && !isFullMode && { accountId: rcsAccount }),
1975
2016
  cardType: STANDALONE,
1976
2017
  cardSettings: {
1977
2018
  cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
@@ -1997,23 +2038,25 @@ const onTitleAddVar = () => {
1997
2038
  ...(templateTitle?.match(rcsVarRegex) ?? []),
1998
2039
  ...(templateDesc?.match(rcsVarRegex) ?? []),
1999
2040
  ];
2041
+ const cardVarMappedForRcsCardOnly = pickRcsCardVarMappedEntries(
2042
+ cardVarMapped,
2043
+ );
2044
+ // Persist numeric slot keys only ("1","2",…) — avoids duplicating the same value under
2045
+ // semantic names (e.g. both "1" and dynamic_expiry_date_after_3_days.FORMAT_1). Hydration
2046
+ // and coalesceCardVarMappedToTemplate still resolve from numeric keys + template tokens.
2000
2047
  const persistedSlotVarMap = {};
2001
- const seenSemanticVarNames = new Set();
2002
2048
  templateVarTokens.forEach((token, slotIndexZeroBased) => {
2003
2049
  const varName = getVarNameFromToken(token);
2004
2050
  if (!varName) return;
2005
2051
  const resolvedRawValue = resolveCardVarMappedSlotValue(
2006
- cardVarMapped,
2052
+ cardVarMappedForRcsCardOnly,
2007
2053
  varName,
2008
2054
  slotIndexZeroBased,
2009
2055
  isSlotMappingMode,
2056
+ rcsSpanningSemanticVarNames.has(varName),
2010
2057
  );
2011
2058
  const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
2012
2059
  persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
2013
- if (!seenSemanticVarNames.has(varName)) {
2014
- seenSemanticVarNames.add(varName);
2015
- persistedSlotVarMap[varName] = sanitizedSlotValue;
2016
- }
2017
2060
  });
2018
2061
  return { cardVarMapped: persistedSlotVarMap };
2019
2062
  })()),
@@ -2030,6 +2073,53 @@ const onTitleAddVar = () => {
2030
2073
  || smsFallbackForPayload.message
2031
2074
  || smsFallbackForPayload.smsContent
2032
2075
  || '';
2076
+ /**
2077
+ * Campaigns `getTraiSenderIds` / Iris read `smsFallBackContent.templateConfigs.registeredSenderIds`.
2078
+ * Library `smsFallbackForPayload` omits ids — use merged state (`smsFallbackMerged`) like test preview.
2079
+ */
2080
+ const m = smsFallbackMerged || {};
2081
+ const tcSibling = m.templateConfigs && typeof m.templateConfigs === 'object'
2082
+ ? m.templateConfigs
2083
+ : {};
2084
+ const smsFallbackTemplateId =
2085
+ (m.smsTemplateId != null && String(m.smsTemplateId).trim() !== ''
2086
+ ? String(m.smsTemplateId)
2087
+ : '')
2088
+ || (tcSibling.templateId != null && String(tcSibling.templateId).trim() !== ''
2089
+ ? String(tcSibling.templateId)
2090
+ : '');
2091
+ const smsFallbackTemplateStr =
2092
+ pickFirstSmsFallbackTemplateString(m)
2093
+ || (typeof m.templateContent === 'string' ? m.templateContent : '')
2094
+ || (typeof tcSibling.template === 'string' ? tcSibling.template : '')
2095
+ || '';
2096
+ const smsFallbackTemplateName =
2097
+ m.templateName
2098
+ || m.smsTemplateName
2099
+ || tcSibling.templateName
2100
+ || tcSibling.name
2101
+ || '';
2102
+ const registeredSenderIdsForPayload = Array.isArray(m.registeredSenderIds)
2103
+ ? m.registeredSenderIds
2104
+ : Array.isArray(tcSibling.registeredSenderIds)
2105
+ ? tcSibling.registeredSenderIds
2106
+ : Array.isArray(tcSibling.header)
2107
+ ? tcSibling.header
2108
+ : null;
2109
+ const hasRegisteredSenderIds = Array.isArray(registeredSenderIdsForPayload);
2110
+ const smsFallbackTemplateConfigs =
2111
+ smsFallbackTemplateId || hasRegisteredSenderIds
2112
+ ? {
2113
+ ...(smsFallbackTemplateId && { templateId: smsFallbackTemplateId }),
2114
+ ...(smsFallbackTemplateStr && { template: smsFallbackTemplateStr }),
2115
+ ...(smsFallbackTemplateName && {
2116
+ templateName: smsFallbackTemplateName,
2117
+ }),
2118
+ ...(hasRegisteredSenderIds && {
2119
+ registeredSenderIds: registeredSenderIdsForPayload,
2120
+ }),
2121
+ }
2122
+ : null;
2033
2123
  return {
2034
2124
  smsFallBackContent: {
2035
2125
  smsTemplateName: smsFallbackForPayload.templateName || '',
@@ -2043,6 +2133,9 @@ const onTitleAddVar = () => {
2043
2133
  && Object.keys(smsFallbackForPayload.rcsSmsFallbackVarMapped).length > 0 && {
2044
2134
  [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackForPayload.rcsSmsFallbackVarMapped,
2045
2135
  }),
2136
+ ...(smsFallbackTemplateConfigs && {
2137
+ templateConfigs: smsFallbackTemplateConfigs,
2138
+ }),
2046
2139
  },
2047
2140
  };
2048
2141
  })()),
@@ -2065,13 +2158,36 @@ const onTitleAddVar = () => {
2065
2158
  (wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
2066
2159
  ? String(wecrmAccountId)
2067
2160
  : accountId;
2068
- const rcsForTest = {
2161
+ const isSlotMappingModeForPreview = isEditFlow || !isFullMode;
2162
+ let rcsForTest = {
2069
2163
  ...rcs,
2070
2164
  rcsContent: {
2071
2165
  ...rcs.rcsContent,
2072
2166
  ...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
2073
2167
  },
2074
2168
  };
2169
+ /** Approval payload keeps numeric-only `cardVarMapped`; preview APIs still need semantic keys. */
2170
+ if (isSlotMappingModeForPreview) {
2171
+ const cardContent = rcsForTest.rcsContent?.cardContent;
2172
+ if (Array.isArray(cardContent) && cardContent[0]) {
2173
+ const fullCardVarMapped = coalesceCardVarMappedToTemplate(
2174
+ pickRcsCardVarMappedEntries(cardVarMapped),
2175
+ templateTitle,
2176
+ templateDesc,
2177
+ rcsVarRegex,
2178
+ );
2179
+ rcsForTest = {
2180
+ ...rcsForTest,
2181
+ rcsContent: {
2182
+ ...rcsForTest.rcsContent,
2183
+ cardContent: [
2184
+ { ...cardContent[0], cardVarMapped: fullCardVarMapped },
2185
+ ...cardContent.slice(1),
2186
+ ],
2187
+ },
2188
+ };
2189
+ }
2190
+ }
2075
2191
  const out = {
2076
2192
  versions: {
2077
2193
  base: {
@@ -2242,7 +2358,13 @@ const onTitleAddVar = () => {
2242
2358
  return true;
2243
2359
  }
2244
2360
  return orderedVarNames.some((name, globalIdx) => {
2245
- const v = resolveCardVarMappedSlotValue(cardVarMapped, name, globalIdx, true);
2361
+ const v = resolveCardVarMappedSlotValue(
2362
+ cardVarMapped,
2363
+ name,
2364
+ globalIdx,
2365
+ true,
2366
+ rcsSpanningSemanticVarNames.has(name),
2367
+ );
2246
2368
  const s = v == null ? '' : String(v);
2247
2369
  return s.trim() === '';
2248
2370
  });
@@ -2561,13 +2683,19 @@ const onTitleAddVar = () => {
2561
2683
  content={testAndPreviewContent}
2562
2684
  currentChannel={RCS}
2563
2685
  orgUnitId={orgUnitId}
2686
+ rcsTestPreviewOptions={{ isLibraryMode: !isFullMode }}
2564
2687
  smsFallbackContent={
2565
2688
  smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
2566
2689
  ? {
2567
2690
  templateContent:
2568
2691
  smsFallbackData.templateContent || smsFallbackData.content || '',
2569
2692
  templateName: smsFallbackData.templateName || '',
2570
- [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackData?.rcsSmsFallbackVarMapped ?? {},
2693
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: !isFullMode
2694
+ ? mergeRcsSmsFallbackVarMapLayers(
2695
+ getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
2696
+ smsFallbackData,
2697
+ )
2698
+ : mergeRcsSmsFallbackVarMapLayers({}, smsFallbackData),
2571
2699
  }
2572
2700
  : null
2573
2701
  }