@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.
@@ -168,6 +168,10 @@ export const COMBINED_SMS_TEMPLATE_VAR_REGEX = /\{\{[^}]+\}\}|\{\#[^#]*\#\}/g;
168
168
  export const MUSTACHE_VAR_TOKEN_FULL_STRING_REGEX = /^\{\{[^}]+\}\}$/;
169
169
  /** Full-string check: one DLT hash token. */
170
170
  export const DLT_HASH_VAR_TOKEN_FULL_STRING_REGEX = /^\{\#[^#]*\#\}$/;
171
+ /** Full-string with capture group: inner name for `{{ name }}`-style tokens (whitespace-tolerant). */
172
+ export const MUSTACHE_TOKEN_INNER_NAME_REGEX = /^\{\{\s*([^}]+?)\s*\}\}$/;
173
+ /** Full-string with capture group: inner name/body for `{# name #}` DLT tokens (whitespace-tolerant). */
174
+ export const DLT_HASH_TOKEN_INNER_NAME_REGEX = /^\{#\s*(.*?)\s*#\}$/;
171
175
  /** Global with capture group: inner name inside `{{name}}`. */
172
176
  export const MUSTACHE_VAR_NAME_CAPTURE_REGEX = /\{\{([^}]+)\}\}/g;
173
177
  /** Global with capture group: inner body inside `{#body#}`. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.329",
4
+ "version": "8.0.330-alpha.0",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -539,4 +539,22 @@ export const isValidMobile = (mobile) => PHONE_REGEX.test(mobile);
539
539
  export const formatPhoneNumber = (phone) => {
540
540
  if (!phone) return '';
541
541
  return String(phone).replace(/[^\d]/g, '');
542
- };
542
+ };
543
+
544
+ /**
545
+ * TRAI sender IDs on persisted RCS SMS fallback: may live on the merged object, under
546
+ * `templateConfigs`, or (legacy) `templateConfigs.header`. Same resolution order as
547
+ * `createPayload` in `Rcs/index.js`.
548
+ */
549
+ export function extractRegisteredSenderIdsFromSmsFallbackRecord(record) {
550
+ if (!record || typeof record !== 'object') return null;
551
+ const tc = record.templateConfigs && typeof record.templateConfigs === 'object'
552
+ ? record.templateConfigs
553
+ : {};
554
+ const candidates = [record.registeredSenderIds, tc.registeredSenderIds, tc.header];
555
+ for (let i = 0; i < candidates.length; i += 1) {
556
+ const a = candidates[i];
557
+ if (Array.isArray(a) && a.length > 0) return a;
558
+ }
559
+ return null;
560
+ }
@@ -7,7 +7,9 @@ import {
7
7
  COMBINED_SMS_TEMPLATE_VAR_REGEX,
8
8
  DEFAULT_MUSTACHE_VAR_REGEX,
9
9
  DLT_HASH_VAR_TOKEN_FULL_STRING_REGEX,
10
+ DLT_HASH_TOKEN_INNER_NAME_REGEX,
10
11
  DLT_VAR_BODY_CAPTURE_REGEX,
12
+ MUSTACHE_TOKEN_INNER_NAME_REGEX,
11
13
  MUSTACHE_VAR_NAME_CAPTURE_REGEX,
12
14
  MUSTACHE_VAR_TOKEN_FULL_STRING_REGEX,
13
15
  } from '../constants/unified';
@@ -115,13 +117,36 @@ export const extractTemplateVariables = (templateStr = '', captureRegex) => {
115
117
  return variables;
116
118
  };
117
119
 
120
+ /**
121
+ * Looks up the inner name of a `{{name}}` or `{#name#}` token in a flat key→value map.
122
+ * Handles both exact matches and dot-path suffixes (e.g. `tag.FORMAT_1` → name `FORMAT_1`).
123
+ * Returns the resolved string value, or `undefined` if not found / blank.
124
+ */
125
+ function resolveUserVarForToken(segment, userVarMap) {
126
+ if (!userVarMap || typeof userVarMap !== 'object') return undefined;
127
+ const mMustache = segment.match(MUSTACHE_TOKEN_INNER_NAME_REGEX);
128
+ const mDlt = segment.match(DLT_HASH_TOKEN_INNER_NAME_REGEX);
129
+ const innerName = ((mMustache ? mMustache[1] : (mDlt ? mDlt[1] : '')) ?? '').trim();
130
+ if (!innerName) return undefined;
131
+ const direct = userVarMap[innerName];
132
+ if (direct != null && String(direct).trim() !== '') return String(direct);
133
+ const hit = Object.keys(userVarMap).find((k) => k === innerName || k.endsWith(`.${innerName}`));
134
+ if (hit != null && userVarMap[hit] != null && String(userVarMap[hit]).trim() !== '') return String(userVarMap[hit]);
135
+ return undefined;
136
+ }
137
+
118
138
  /**
119
139
  * SMS / DLT template preview: replace `{{…}}` / `{#…#}` tokens using `varMapData` keys `${token}_${index}`.
120
140
  * Used by SmsTraiEdit (RCS SMS fallback) and UnifiedPreview fallback SMS bubble.
121
141
  * DLT `{#…#}`: empty / unset slot values show the raw token (matches DLT preview UX); mustache `{{…}}` still
122
142
  * resolves to empty when the slot is cleared.
143
+ *
144
+ * @param {string} templateStr
145
+ * @param {Object} varMapData - Slot map (`${token}_${index}` → value)
146
+ * @param {Object|null} [userVarMap] - Optional Test & Preview custom values; used as fallback when a slot
147
+ * is absent or blank. When omitted the function behaves identically to its original two-argument form.
123
148
  */
124
- export const getFallbackResolvedContent = (templateStr = '', varMapData = {}) => {
149
+ export const getFallbackResolvedContent = (templateStr = '', varMapData = {}, userVarMap = null) => {
125
150
  const fallbackVarSlotMap = varMapData ?? {};
126
151
  const templateSegments = splitTemplateVarString(templateStr, COMBINED_SMS_TEMPLATE_VAR_REGEX);
127
152
  return templateSegments
@@ -132,13 +157,17 @@ export const getFallbackResolvedContent = (templateStr = '', varMapData = {}) =>
132
157
  if (Object.prototype.hasOwnProperty.call(fallbackVarSlotMap, slotKey)) {
133
158
  const slotValue = fallbackVarSlotMap[slotKey];
134
159
  if (isDltHashVarToken(segment)) {
135
- if (slotValue == null) return segment;
136
- const str = String(slotValue);
137
- if (str.trim() === '') return segment;
138
- return str;
160
+ if (slotValue == null || String(slotValue).trim() === '') {
161
+ if (userVarMap) { const uv = resolveUserVarForToken(segment, userVarMap); if (uv !== undefined) return uv; }
162
+ return segment;
163
+ }
164
+ return String(slotValue);
139
165
  }
140
- return slotValue == null ? '' : String(slotValue);
166
+ if (slotValue != null && String(slotValue).trim() !== '') return String(slotValue);
167
+ if (userVarMap) { const uv = resolveUserVarForToken(segment, userVarMap); if (uv !== undefined) return uv; }
168
+ return '';
141
169
  }
170
+ if (userVarMap) { const uv = resolveUserVarForToken(segment, userVarMap); if (uv !== undefined) return uv; }
142
171
  if (isDltHashVarToken(segment)) return segment;
143
172
  return '';
144
173
  })
@@ -157,4 +157,48 @@ describe('templateVarUtils', () => {
157
157
  expect(extractTemplateVariables('{{x}} {{y}}', globalRe)).toEqual(['x', 'y']);
158
158
  });
159
159
  });
160
+
161
+ describe('getFallbackResolvedContent — userVarMap (third param)', () => {
162
+ it('falls back to userVarMap for mustache slot absent from varMapData', () => {
163
+ expect(getFallbackResolvedContent('Hi {{name}}', {}, { name: 'Alice' })).toBe('Hi Alice');
164
+ });
165
+
166
+ it('falls back to userVarMap for DLT slot absent from varMapData', () => {
167
+ expect(getFallbackResolvedContent('{#city#}', {}, { city: 'Mumbai' })).toBe('Mumbai');
168
+ });
169
+
170
+ it('falls back to userVarMap when mustache slot is present but blank', () => {
171
+ expect(getFallbackResolvedContent('{{x}}', { '{{x}}_0': '' }, { x: 'val' })).toBe('val');
172
+ });
173
+
174
+ it('falls back to userVarMap when DLT slot is present but blank', () => {
175
+ expect(getFallbackResolvedContent('{#x#}', { '{#x#}_0': '' }, { x: 'val' })).toBe('val');
176
+ });
177
+
178
+ it('slot wins over userVarMap when mustache slot is non-empty', () => {
179
+ expect(getFallbackResolvedContent('{{x}}', { '{{x}}_0': 'slot-val' }, { x: 'user-val' })).toBe('slot-val');
180
+ });
181
+
182
+ it('slot wins over userVarMap when DLT slot is non-empty', () => {
183
+ expect(getFallbackResolvedContent('{#x#}', { '{#x#}_0': 'slot' }, { x: 'user' })).toBe('slot');
184
+ });
185
+
186
+ it('resolves dot-path suffix key from userVarMap', () => {
187
+ expect(
188
+ getFallbackResolvedContent('{{FORMAT_1}}', {}, { 'expiry.FORMAT_1': 'Dec 2025' }),
189
+ ).toBe('Dec 2025');
190
+ });
191
+
192
+ it('userVarMap null behaves identically to the two-argument form', () => {
193
+ expect(getFallbackResolvedContent('{{a}}', { '{{a}}_0': 'x' }, null)).toBe('x');
194
+ });
195
+
196
+ it('blank userVarMap value leaves mustache token as empty string', () => {
197
+ expect(getFallbackResolvedContent('{{a}}', {}, { a: '' })).toBe('');
198
+ });
199
+
200
+ it('blank userVarMap value leaves DLT token as raw token', () => {
201
+ expect(getFallbackResolvedContent('{#a#}', {}, { a: '' })).toBe('{#a#}');
202
+ });
203
+ });
160
204
  });
@@ -97,6 +97,7 @@ import {
97
97
  extractPreviewFromLiquidResponse,
98
98
  getSmsFallbackTextForTagExtraction,
99
99
  } from './previewApiUtils';
100
+ import { pickFirstSmsFallbackTemplateString } from '../../v2Containers/Rcs/rcsLibraryHydrationUtils';
100
101
 
101
102
  import { isValidEmail, isValidMobile, formatPhoneNumber } from '../../utils/commonUtils';
102
103
  import { getMembersLookup } from '../../services/api';
@@ -1039,78 +1040,77 @@ const CommonTestAndPreview = (props) => {
1039
1040
  * rcsMessageContent: { channel, accountId?, rcsRichCardContent: { contentType, cardType, cardSettings, cardContent }, smsFallBackContent? }
1040
1041
  * Then rcsDeliverySettings, executionParams, clientName last.
1041
1042
  */
1042
- const buildRcsTestMessagePayload = (formDataObj, _contentStr, customValuesObj, deliverySettingsOverride, basePayload, rcsExtra = {}) => {
1043
- const plainCustom =
1044
- customValuesObj != null && typeof customValuesObj.toJS === 'function'
1045
- ? customValuesObj.toJS()
1046
- : (customValuesObj || {});
1047
- const userVarMap = Object.fromEntries(
1048
- Object.entries(plainCustom).filter(([, v]) => v != null && String(v).trim() !== '')
1049
- );
1050
- const rcsData = formDataObj?.versions?.base?.content?.RCS ?? formDataObj?.content?.RCS ?? {};
1051
- const rcsContent = rcsData?.rcsContent || {};
1052
- const smsFallback = rcsData?.smsFallBackContent || {};
1053
- let cardContentList = [];
1054
- if (Array.isArray(rcsContent?.cardContent)) {
1055
- cardContentList = rcsContent.cardContent;
1056
- } else if (rcsContent?.cardContent) {
1057
- cardContentList = [rcsContent.cardContent];
1043
+ const buildRcsTestMessagePayload = (
1044
+ creativeFormData,
1045
+ _unusedEditorContentString,
1046
+ _customValuesObj,
1047
+ deliverySettingsOverride,
1048
+ basePayload,
1049
+ _rcsTestMetaExtras = {},
1050
+ ) => {
1051
+ const rcsSectionFromForm =
1052
+ creativeFormData?.versions?.base?.content?.RCS ?? creativeFormData?.content?.RCS ?? {};
1053
+ const rcsContentFromForm = rcsSectionFromForm?.rcsContent || {};
1054
+ const smsFallbackFromCreativeForm = rcsSectionFromForm?.smsFallBackContent || {};
1055
+ let rcsCardPayloadList = [];
1056
+ if (Array.isArray(rcsContentFromForm?.cardContent)) {
1057
+ rcsCardPayloadList = rcsContentFromForm.cardContent;
1058
+ } else if (rcsContentFromForm?.cardContent) {
1059
+ rcsCardPayloadList = [rcsContentFromForm.cardContent];
1058
1060
  }
1059
- // Merge test customValues into cardVarMapped (template snapshot + user-entered RCS + fallback SMS tags).
1060
- const cardContent = cardContentList.map((rcsCard) => {
1061
- const baseMap = rcsCard.cardVarMapped || {};
1062
- const mergedCardVarMapped =
1063
- Object.keys(userVarMap).length > 0 ? { ...baseMap, ...userVarMap } : baseMap;
1064
- const mediaNorm = rcsCard?.media ? normalizeRcsTestCardMedia(rcsCard.media) : undefined;
1065
- const suggestionsRaw = Array.isArray(rcsCard?.suggestions) ? rcsCard.suggestions : [];
1066
- const suggestionsMapped = suggestionsRaw.map((suggestionItem, i) =>
1067
- mapRcsSuggestionForTestMeta(suggestionItem, i));
1061
+ // Raw title/description with template tags; SMS fallback uses tagged template fields (pickFirst…).
1062
+ const cardContentForTestMetaApi = rcsCardPayloadList.map((singleRcsCardPayload) => {
1063
+ const normalizedCardMediaForTestApi = singleRcsCardPayload?.media
1064
+ ? normalizeRcsTestCardMedia(singleRcsCardPayload.media)
1065
+ : undefined;
1066
+ const suggestionsFromCard = Array.isArray(singleRcsCardPayload?.suggestions)
1067
+ ? singleRcsCardPayload.suggestions
1068
+ : [];
1069
+ const suggestionsFormattedForTestMeta = suggestionsFromCard.map((suggestionItem, index) =>
1070
+ mapRcsSuggestionForTestMeta(suggestionItem, index));
1068
1071
  return {
1069
- title: rcsCard?.title ?? '',
1070
- description: rcsCard?.description ?? '',
1071
- mediaType: rcsCard?.mediaType ?? MEDIA_TYPE_TEXT,
1072
- ...(mediaNorm && { media: mediaNorm }),
1073
- ...(Object.keys(mergedCardVarMapped).length > 0 && { cardVarMapped: mergedCardVarMapped }),
1074
- ...(suggestionsMapped.length > 0 && { suggestions: suggestionsMapped }),
1072
+ title: singleRcsCardPayload?.title ?? '',
1073
+ description: singleRcsCardPayload?.description ?? '',
1074
+ mediaType: singleRcsCardPayload?.mediaType ?? MEDIA_TYPE_TEXT,
1075
+ ...(normalizedCardMediaForTestApi && { media: normalizedCardMediaForTestApi }),
1076
+ ...(suggestionsFormattedForTestMeta.length > 0 && {
1077
+ suggestions: suggestionsFormattedForTestMeta,
1078
+ }),
1075
1079
  };
1076
1080
  });
1077
- // Prefer parent `smsFallbackContent` snapshot (rcsExtra) — includes VarSegment-resolved body via
1078
- // getSmsFallbackTextForTagExtraction. Nested formData.smsFallBackContent is often stale vs live editor.
1079
- const rcsExtraFallbackTemplate = rcsExtra?.smsFallbackTemplateContent;
1080
- const hasResolvedFallbackBodyFromRcsExtra =
1081
- rcsExtraFallbackTemplate != null
1082
- && String(rcsExtraFallbackTemplate).trim() !== '';
1083
- const smsMessageRaw = hasResolvedFallbackBodyFromRcsExtra
1084
- ? String(rcsExtraFallbackTemplate)
1085
- : (smsFallback?.smsContent ?? smsFallback?.message ?? '');
1081
+ const smsFallbackTaggedTemplateBody = pickFirstSmsFallbackTemplateString(
1082
+ smsFallbackFromCreativeForm,
1083
+ ) || '';
1086
1084
  const smsSenderFromDelivery = deliverySettingsOverride?.cdmaSenderId?.includes('|')
1087
1085
  ? deliverySettingsOverride.cdmaSenderId.split('|')[1]
1088
1086
  : deliverySettingsOverride?.cdmaSenderId;
1089
1087
  const deliveryFallbackSmsId =
1090
1088
  typeof smsSenderFromDelivery === 'string' ? smsSenderFromDelivery.trim() : '';
1091
1089
  const creativeFallbackSmsId =
1092
- smsFallback?.senderId != null ? String(smsFallback.senderId).trim() : '';
1090
+ smsFallbackFromCreativeForm?.senderId != null
1091
+ ? String(smsFallbackFromCreativeForm.senderId).trim()
1092
+ : '';
1093
1093
  const fallbackSmsSenderIdForChannel = deliveryFallbackSmsId || creativeFallbackSmsId || '';
1094
1094
 
1095
1095
  const smsFallBackContent =
1096
- smsMessageRaw.trim() !== ''
1097
- ? { message: smsMessageRaw }
1096
+ smsFallbackTaggedTemplateBody.trim() !== ''
1097
+ ? { message: smsFallbackTaggedTemplateBody }
1098
1098
  : undefined;
1099
1099
 
1100
1100
  // accountId: WeCRM account id (not sourceAccountIdentifier) for createMessageMeta
1101
1101
  const accountIdForMeta =
1102
- rcsContent?.accountId != null && String(rcsContent.accountId).trim() !== ''
1103
- ? String(rcsContent.accountId)
1102
+ rcsContentFromForm?.accountId != null && String(rcsContentFromForm.accountId).trim() !== ''
1103
+ ? String(rcsContentFromForm.accountId)
1104
1104
  : undefined;
1105
1105
 
1106
1106
  const rcsRichCardContent = {
1107
1107
  contentType: RCS_TEST_META_CONTENT_TYPE_RICHCARD,
1108
- cardType: rcsContent?.cardType ?? RCS_TEST_META_CARD_TYPE_STANDALONE,
1109
- cardSettings: rcsContent?.cardSettings ?? {
1108
+ cardType: rcsContentFromForm?.cardType ?? RCS_TEST_META_CARD_TYPE_STANDALONE,
1109
+ cardSettings: rcsContentFromForm?.cardSettings ?? {
1110
1110
  cardOrientation: RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
1111
1111
  cardWidth: RCS_TEST_META_CARD_WIDTH_SMALL,
1112
1112
  },
1113
- ...(cardContent.length > 0 && { cardContent }),
1113
+ ...(cardContentForTestMetaApi.length > 0 && { cardContent: cardContentForTestMetaApi }),
1114
1114
  };
1115
1115
 
1116
1116
  const rcsMessageContent = {
@@ -3299,15 +3299,7 @@ const CommonTestAndPreview = (props) => {
3299
3299
  uniqueUserIds,
3300
3300
  previewData,
3301
3301
  deliveryOverride,
3302
- channel === CHANNELS.RCS
3303
- ? {
3304
- smsFallbackTemplateContent:
3305
- smsFallbackTextForTagExtraction
3306
- || smsFallbackContent?.templateContent
3307
- || smsFallbackContent?.content
3308
- || '',
3309
- }
3310
- : {},
3302
+ {},
3311
3303
  );
3312
3304
 
3313
3305
  actions.createMessageMetaRequested(
@@ -1,5 +1,6 @@
1
1
  import get from 'lodash/get';
2
2
  import { getFallbackResolvedContentForCardDisplay } from '../../utils/templateVarUtils';
3
+ import { extractRegisteredSenderIdsFromSmsFallbackRecord } from '../../utils/commonUtils';
3
4
  import { SMS_CATEGORY_FILTERS } from './constants';
4
5
 
5
6
  export function buildFallbackDataFromTemplate(template) {
@@ -16,9 +17,14 @@ export function buildFallbackDataFromTemplate(template) {
16
17
  const content = smsEditor;
17
18
  const headerList = Array.isArray(base.header) ? base.header : [];
18
19
  const senderId = (headerList[0]) || base.senderId || '';
20
+ const templateName =
21
+ template.name
22
+ || base.template_name
23
+ || base['template-name']
24
+ || '';
19
25
  return {
20
26
  smsTemplateId: template._id || '',
21
- templateName: template.name || '',
27
+ templateName,
22
28
  content,
23
29
  templateContent,
24
30
  senderId,
@@ -35,14 +41,19 @@ export function mapFallbackValueToEditTemplateData(source) {
35
41
  rcsSmsFallbackVarMappedFromSource != null
36
42
  && typeof rcsSmsFallbackVarMappedFromSource === 'object'
37
43
  && Object.keys(rcsSmsFallbackVarMappedFromSource).length > 0;
44
+ const registeredSenderIdsForHeader =
45
+ extractRegisteredSenderIdsFromSmsFallbackRecord(source);
38
46
  return {
39
47
  _id: source?.smsTemplateId,
40
48
  name: source?.templateName,
41
49
  versions: {
42
50
  base: {
43
51
  'sms-editor': source?.templateContent || source?.content,
44
- ...(Array.isArray(source?.registeredSenderIds) && source.registeredSenderIds.length
45
- ? { header: source.registeredSenderIds }
52
+ ...(source?.templateName != null && String(source.templateName).trim() !== ''
53
+ ? { template_name: String(source.templateName).trim() }
54
+ : {}),
55
+ ...(Array.isArray(registeredSenderIdsForHeader) && registeredSenderIdsForHeader.length
56
+ ? { header: registeredSenderIdsForHeader }
46
57
  : {}),
47
58
  ...(typeof source?.unicodeValidity === 'boolean'
48
59
  ? { 'unicode-validity': source.unicodeValidity }
@@ -50,6 +50,21 @@ describe('smsFallbackUtils', () => {
50
50
  };
51
51
  expect(buildFallbackDataFromTemplate(template).unicodeValidity).toBe(true);
52
52
  });
53
+
54
+ it('uses versions.base.template_name when root name is empty (DLT detail shape)', () => {
55
+ const template = {
56
+ _id: 'dlt1',
57
+ name: '',
58
+ versions: {
59
+ base: {
60
+ template_name: 'DLT Registered Name',
61
+ 'sms-editor': 'Hi',
62
+ header: ['H1'],
63
+ },
64
+ },
65
+ };
66
+ expect(buildFallbackDataFromTemplate(template).templateName).toBe('DLT Registered Name');
67
+ });
53
68
  });
54
69
 
55
70
  describe('getSmsFallbackCardDisplayContent', () => {
@@ -101,6 +116,7 @@ describe('smsFallbackUtils', () => {
101
116
  versions: {
102
117
  base: {
103
118
  'sms-editor': 'body',
119
+ template_name: 'N',
104
120
  header: ['h1'],
105
121
  'unicode-validity': false,
106
122
  },
@@ -84,6 +84,10 @@ TestAndPreviewSlidebox.propTypes = {
84
84
  templateName: PropTypes.string,
85
85
  [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: PropTypes.object,
86
86
  }),
87
+ /** Passed to CommonTestAndPreview for RCS test-meta resolution (slot semantics vs full-mode). */
88
+ rcsTestPreviewOptions: PropTypes.shape({
89
+ isLibraryMode: PropTypes.bool,
90
+ }),
87
91
  // Redux props are passed through
88
92
  actions: PropTypes.object.isRequired,
89
93
  extractedTags: PropTypes.array.isRequired,
@@ -115,6 +119,7 @@ TestAndPreviewSlidebox.defaultProps = {
115
119
  currentTab: 1,
116
120
  messageMetaConfigId: null,
117
121
  prefilledValues: {},
122
+ rcsTestPreviewOptions: undefined,
118
123
  senderDetailsByChannel: {},
119
124
  wecrmAccounts: [],
120
125
  isLoadingSenderDetails: false,
@@ -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';
@@ -764,8 +765,13 @@ export class Creatives extends React.Component {
764
765
  rootMirrorCardVarMapped != null && typeof rootMirrorCardVarMapped === 'object'
765
766
  ? rootMirrorCardVarMapped
766
767
  : {};
767
- const mergedFromRootAndNested = { ...rootRecord, ...nestedRecord };
768
- 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;
769
775
  })();
770
776
  // Campaigns (embedded): do not duplicate `cardVarMapped` as root `rcsCardVarMapped` on send —
771
777
  // slot map stays on `versions…cardContent[0].cardVarMapped` only. Library full mode keeps root mirror.
@@ -1348,7 +1354,7 @@ export class Creatives extends React.Component {
1348
1354
  const cardVarMappedFromFirstRcsCard =
1349
1355
  firstCardFromSubmit?.cardVarMapped != null
1350
1356
  && typeof firstCardFromSubmit.cardVarMapped === 'object'
1351
- ? firstCardFromSubmit.cardVarMapped
1357
+ ? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
1352
1358
  : null;
1353
1359
  const includeRootRcsCardVarMappedOnConsumerPayload =
1354
1360
  cardVarMappedFromFirstRcsCard
@@ -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). */