@capillarytech/creatives-library 8.0.329 → 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.
@@ -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';
@@ -386,6 +389,12 @@ export const Rcs = (props) => {
386
389
 
387
390
  const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
388
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
+
389
398
  /** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
390
399
  const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
391
400
  const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
@@ -418,6 +427,7 @@ export const Rcs = (props) => {
418
427
  key,
419
428
  globalSlot,
420
429
  isEditLike,
430
+ rcsSpanningSemanticVarNames.has(key),
421
431
  );
422
432
  if (isNil(v) || String(v)?.trim?.() === '') return elem;
423
433
  return String(v);
@@ -484,6 +494,7 @@ export const Rcs = (props) => {
484
494
  isFullMode,
485
495
  isEditFlow,
486
496
  cardVarMapped,
497
+ rcsSpanningSemanticVarNames,
487
498
  ]);
488
499
 
489
500
  const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
@@ -514,7 +525,7 @@ export const Rcs = (props) => {
514
525
  const nextVarMap = {};
515
526
  let varOrdinal = 0;
516
527
  arr.forEach((elem, idx) => {
517
- // RCS placeholders are alphanumeric/underscore (e.g. {{user_name}}), not numeric-only
528
+ // Mustache tokens: {{1}}, {{user_name}}, {{tag.FORMAT_1}}, etc.
518
529
  if (rcsVarTestRegex.test(elem)) {
519
530
  const id = `${elem}_${idx}`;
520
531
  const varName = getVarNameFromToken(elem);
@@ -525,6 +536,7 @@ export const Rcs = (props) => {
525
536
  varName,
526
537
  globalSlot,
527
538
  isEditLike,
539
+ rcsSpanningSemanticVarNames.has(varName),
528
540
  );
529
541
  nextVarMap[id] = mappedValue;
530
542
  }
@@ -534,7 +546,7 @@ export const Rcs = (props) => {
534
546
 
535
547
  initField(templateTitle, setTitleVarMappedData, 0);
536
548
  initField(templateDesc, setDescVarMappedData, titleTokenCount);
537
- }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode]);
549
+ }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames]);
538
550
 
539
551
  useEffect(() => {
540
552
  if (!isEditFlow && isFullMode) {
@@ -780,12 +792,17 @@ export const Rcs = (props) => {
780
792
  typeof smsFallbackContent.unicodeValidity === 'boolean'
781
793
  ? smsFallbackContent.unicodeValidity
782
794
  : (typeof base['unicode-validity'] === 'boolean' ? base['unicode-validity'] : true);
795
+ const registeredSenderIdsFromApi =
796
+ extractRegisteredSenderIdsFromSmsFallbackRecord(smsFallbackContent);
783
797
  const nextSmsState = {
784
798
  templateName: smsFallbackContent.smsTemplateName || '',
785
799
  content: fallbackMessage,
786
800
  templateContent: fallbackMessage,
787
801
  unicodeValidity: unicodeFromApi,
788
802
  ...(hasVarMapped && { rcsSmsFallbackVarMapped: varMappedFromPayload }),
803
+ ...(Array.isArray(registeredSenderIdsFromApi) && registeredSenderIdsFromApi.length > 0
804
+ ? { registeredSenderIds: registeredSenderIdsFromApi }
805
+ : {}),
789
806
  };
790
807
  const hydrationKey = JSON.stringify({
791
808
  creativeKey: details._id || details.name || details.creativeName || '',
@@ -793,6 +810,10 @@ export const Rcs = (props) => {
793
810
  content: nextSmsState.content,
794
811
  unicodeValidity: nextSmsState.unicodeValidity,
795
812
  varMapped: nextSmsState.rcsSmsFallbackVarMapped || {},
813
+ senderIds:
814
+ Array.isArray(registeredSenderIdsFromApi)
815
+ ? registeredSenderIdsFromApi.join('\u001f')
816
+ : '',
796
817
  });
797
818
  if (
798
819
  isFullMode
@@ -892,58 +913,87 @@ export const Rcs = (props) => {
892
913
  return templateStr.replace(re, `{{${tagName}}}`);
893
914
  };
894
915
 
895
- const onTagSelect = (data, areaId, field) => {
896
- if (!areaId) return;
897
- const sep = areaId.lastIndexOf('_');
898
- if (sep === -1) return;
899
- const slotSuffix = areaId.slice(sep + 1);
900
- if (slotSuffix === '' || isNaN(Number(slotSuffix))) return;
901
- const token = areaId.slice(0, sep);
902
- const variableName = getVarNameFromToken(token);
903
- if (!variableName) return;
904
- const isNumericSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(variableName));
905
- const fieldStr = field === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
906
- const fieldType = field === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
907
- const globalSlotForArea = getGlobalSlotIndexForRcsFieldId(areaId, fieldStr, fieldType);
908
-
909
- setCardVarMapped((prev) => {
910
- const base = (prev?.[variableName] ?? '').toString();
911
- const nextVal = `${base}{{${data}}}`;
912
- const next = { ...(prev || {}) };
913
- if (isNumericSlot) {
914
- delete next[variableName];
915
- 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;
916
949
  } else {
917
- // Use the global slot index key only writing by semantic name (variableName)
918
- // would contaminate any other field that shares the same var name (e.g. {{gt}}
919
- // in both title and description), causing the label value to bleed across fields.
920
- if (globalSlotForArea !== null && globalSlotForArea !== undefined) {
921
- next[String(globalSlotForArea + 1)] = nextVal;
922
- // Same reasoning as handleRcsVarChange: delete the semantic key so
923
- // resolveCardVarMappedSlotValue uses only the numeric slot and the
924
- // value cannot bleed across fields that share the same var name.
925
- 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;
926
960
  } else {
927
- next[variableName] = nextVal;
961
+ updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
928
962
  }
929
963
  }
930
- return next;
964
+ return updatedCardVarMapped;
931
965
  });
932
966
 
933
- if (isNumericSlot && (field === RCS_TAG_AREA_FIELD_TITLE || field === RCS_TAG_AREA_FIELD_DESC)) {
934
- if (field === RCS_TAG_AREA_FIELD_TITLE) {
935
- setTemplateTitle((prev) => {
936
- const nextStr = replaceNumericPlaceholderWithTagInTemplate(prev || '', variableName, data);
937
- if (nextStr === prev) return prev;
938
- setTemplateTitleError(variableErrorHandling(nextStr));
939
- 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;
940
983
  });
941
984
  } else {
942
- setTemplateDesc((prev) => {
943
- const nextStr = replaceNumericPlaceholderWithTagInTemplate(prev || '', variableName, data);
944
- if (nextStr === prev) return prev;
945
- setTemplateDescError(variableErrorHandling(nextStr));
946
- 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;
947
997
  });
948
998
  }
949
999
  }
@@ -1101,7 +1151,8 @@ export const Rcs = (props) => {
1101
1151
  if(!isFullMode){
1102
1152
  return false;
1103
1153
  }
1104
- 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)) {
1105
1156
  return formatMessage(messages.unknownCharactersError);
1106
1157
  }
1107
1158
  }
@@ -1214,6 +1265,7 @@ const onTitleAddVar = () => {
1214
1265
  varName,
1215
1266
  globalSlot,
1216
1267
  isEditLike,
1268
+ rcsSpanningSemanticVarNames.has(varName),
1217
1269
  );
1218
1270
  }
1219
1271
  });
@@ -1222,42 +1274,37 @@ const onTitleAddVar = () => {
1222
1274
 
1223
1275
  const titleVarSegmentValueMapById = useMemo(
1224
1276
  () => getRcsValueMap(templateTitle, TITLE_TEXT),
1225
- [templateTitle, cardVarMapped, isEditFlow, isFullMode],
1277
+ [templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
1226
1278
  );
1227
1279
  const descriptionVarSegmentValueMapById = useMemo(
1228
1280
  () => getRcsValueMap(templateDesc, MESSAGE_TEXT),
1229
- [templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode],
1281
+ [templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
1230
1282
  );
1231
1283
 
1232
- const handleRcsVarChange = (id, value, type) => {
1233
- const sep = id.lastIndexOf('_');
1234
- if (sep === -1) return;
1235
- const token = id.slice(0, sep);
1236
- 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);
1237
1289
  if (variableName === undefined || variableName === null || variableName === '') return;
1238
1290
  const isInvalidValue = value?.trim() === '';
1239
1291
  const coercedSlotValue = isInvalidValue ? '' : value;
1240
- const fieldStr = type === TITLE_TEXT ? templateTitle : templateDesc;
1241
- const globalSlot = getGlobalSlotIndexForRcsFieldId(id, fieldStr, type);
1242
- setCardVarMapped((previousVarMap) => {
1243
- const nextVarMap = { ...previousVarMap };
1244
- if (globalSlot !== null && globalSlot !== undefined) {
1245
- // Write by global slot index only — title and description can share the
1246
- // same var name (e.g. {{gt}}), and writing by semantic name would cause
1247
- // the description slot to resolve the title's value and vice-versa.
1248
- const numericKey = String(globalSlot + 1);
1249
- nextVarMap[numericKey] = coercedSlotValue;
1250
- // Remove any stale semantic key so resolveCardVarMappedSlotValue never
1251
- // falls back to it. Guard: when variableName is already the numeric slot
1252
- // key (e.g. {{1}} at slot 0 both equal "1"), skip the delete or it
1253
- // would erase the value we just wrote.
1254
- if (variableName !== numericKey) {
1255
- delete nextVarMap[variableName];
1256
- }
1257
- } else {
1258
- 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;
1259
1306
  }
1260
- return nextVarMap;
1307
+ return updatedCardVarMapped;
1261
1308
  });
1262
1309
  };
1263
1310
 
@@ -1375,6 +1422,7 @@ const onTitleAddVar = () => {
1375
1422
  </CapRow>
1376
1423
  <CapRow className="rcs-create-template-message-input">
1377
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. */}
1378
1426
  {(isEditFlow || !isFullMode)
1379
1427
  ? (
1380
1428
  <VarSegmentMessageEditor
@@ -1990,23 +2038,25 @@ const onTitleAddVar = () => {
1990
2038
  ...(templateTitle?.match(rcsVarRegex) ?? []),
1991
2039
  ...(templateDesc?.match(rcsVarRegex) ?? []),
1992
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.
1993
2047
  const persistedSlotVarMap = {};
1994
- const seenSemanticVarNames = new Set();
1995
2048
  templateVarTokens.forEach((token, slotIndexZeroBased) => {
1996
2049
  const varName = getVarNameFromToken(token);
1997
2050
  if (!varName) return;
1998
2051
  const resolvedRawValue = resolveCardVarMappedSlotValue(
1999
- cardVarMapped,
2052
+ cardVarMappedForRcsCardOnly,
2000
2053
  varName,
2001
2054
  slotIndexZeroBased,
2002
2055
  isSlotMappingMode,
2056
+ rcsSpanningSemanticVarNames.has(varName),
2003
2057
  );
2004
2058
  const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
2005
2059
  persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
2006
- if (!seenSemanticVarNames.has(varName)) {
2007
- seenSemanticVarNames.add(varName);
2008
- persistedSlotVarMap[varName] = sanitizedSlotValue;
2009
- }
2010
2060
  });
2011
2061
  return { cardVarMapped: persistedSlotVarMap };
2012
2062
  })()),
@@ -2023,6 +2073,53 @@ const onTitleAddVar = () => {
2023
2073
  || smsFallbackForPayload.message
2024
2074
  || smsFallbackForPayload.smsContent
2025
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;
2026
2123
  return {
2027
2124
  smsFallBackContent: {
2028
2125
  smsTemplateName: smsFallbackForPayload.templateName || '',
@@ -2036,6 +2133,9 @@ const onTitleAddVar = () => {
2036
2133
  && Object.keys(smsFallbackForPayload.rcsSmsFallbackVarMapped).length > 0 && {
2037
2134
  [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackForPayload.rcsSmsFallbackVarMapped,
2038
2135
  }),
2136
+ ...(smsFallbackTemplateConfigs && {
2137
+ templateConfigs: smsFallbackTemplateConfigs,
2138
+ }),
2039
2139
  },
2040
2140
  };
2041
2141
  })()),
@@ -2058,13 +2158,36 @@ const onTitleAddVar = () => {
2058
2158
  (wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
2059
2159
  ? String(wecrmAccountId)
2060
2160
  : accountId;
2061
- const rcsForTest = {
2161
+ const isSlotMappingModeForPreview = isEditFlow || !isFullMode;
2162
+ let rcsForTest = {
2062
2163
  ...rcs,
2063
2164
  rcsContent: {
2064
2165
  ...rcs.rcsContent,
2065
2166
  ...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
2066
2167
  },
2067
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
+ }
2068
2191
  const out = {
2069
2192
  versions: {
2070
2193
  base: {
@@ -2235,7 +2358,13 @@ const onTitleAddVar = () => {
2235
2358
  return true;
2236
2359
  }
2237
2360
  return orderedVarNames.some((name, globalIdx) => {
2238
- const v = resolveCardVarMappedSlotValue(cardVarMapped, name, globalIdx, true);
2361
+ const v = resolveCardVarMappedSlotValue(
2362
+ cardVarMapped,
2363
+ name,
2364
+ globalIdx,
2365
+ true,
2366
+ rcsSpanningSemanticVarNames.has(name),
2367
+ );
2239
2368
  const s = v == null ? '' : String(v);
2240
2369
  return s.trim() === '';
2241
2370
  });
@@ -2554,13 +2683,19 @@ const onTitleAddVar = () => {
2554
2683
  content={testAndPreviewContent}
2555
2684
  currentChannel={RCS}
2556
2685
  orgUnitId={orgUnitId}
2686
+ rcsTestPreviewOptions={{ isLibraryMode: !isFullMode }}
2557
2687
  smsFallbackContent={
2558
2688
  smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
2559
2689
  ? {
2560
2690
  templateContent:
2561
2691
  smsFallbackData.templateContent || smsFallbackData.content || '',
2562
2692
  templateName: smsFallbackData.templateName || '',
2563
- [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),
2564
2699
  }
2565
2700
  : null
2566
2701
  }
@@ -466,7 +466,8 @@ export default defineMessages({
466
466
  },
467
467
  unknownCharactersError: {
468
468
  id: `${prefix}.unknownCharactersError`,
469
- defaultMessage: 'Only alphanumeric characters and underscore are allowed in custom param. Spaces/other special characters are not allowed.',
469
+ defaultMessage:
470
+ 'Only letters, numbers, underscores, and dots are allowed in custom param (e.g. tag.FORMAT_1). Spaces and other special characters are not allowed.',
470
471
  },
471
472
  emptyVariableError: {
472
473
  id: `${prefix}.emptyVariableError`,
@@ -1,6 +1,24 @@
1
1
  import isEmpty from 'lodash/isEmpty';
2
2
  import get from 'lodash/get';
3
3
  import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
4
+ import {
5
+ RCS_NUMERIC_VAR_NAME_REGEX,
6
+ RCS_CARD_VAR_MAPPED_SEMANTIC_KEY_REGEX,
7
+ } from './constants';
8
+
9
+ /** RCS card `cardVarMapped` / `rcsCardVarMapped` only (SMS fallback slot keys stay on `smsFallBackContent`). */
10
+ export function pickRcsCardVarMappedEntries(record) {
11
+ if (record == null || typeof record !== 'object' || Array.isArray(record)) return {};
12
+ return Object.fromEntries(
13
+ Object.entries(record).filter(([k]) => {
14
+ const key = String(k);
15
+ return (
16
+ RCS_NUMERIC_VAR_NAME_REGEX.test(key)
17
+ || RCS_CARD_VAR_MAPPED_SEMANTIC_KEY_REGEX.test(key)
18
+ );
19
+ }),
20
+ );
21
+ }
4
22
 
5
23
  /**
6
24
  * Nested `versions…smsFallBackContent` and root `smsFallBackContent` (CreativesContainer mirror)
@@ -88,6 +106,8 @@ export function getLibrarySmsFallbackApiBaselineFromTemplateData(templateData) {
88
106
  };
89
107
  }
90
108
 
109
+ export { extractRegisteredSenderIdsFromSmsFallbackRecord } from '../../utils/commonUtils';
110
+
91
111
  export function pickFirstSmsFallbackTemplateString(sms = {}) {
92
112
  if (!sms || typeof sms !== 'object') return '';
93
113
  const keys = [