@capillarytech/creatives-library 8.0.358 → 8.0.359-alpha.1

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 (125) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/tests/api.test.js +35 -20
  4. package/utils/commonUtils.js +19 -1
  5. package/utils/rcsPayloadUtils.js +92 -0
  6. package/utils/templateVarUtils.js +201 -0
  7. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  8. package/utils/tests/templateVarUtils.test.js +204 -0
  9. package/v2Components/CapActionButton/constants.js +7 -0
  10. package/v2Components/CapActionButton/index.js +166 -108
  11. package/v2Components/CapActionButton/index.scss +157 -6
  12. package/v2Components/CapActionButton/messages.js +19 -3
  13. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  14. package/v2Components/CapImageUpload/index.js +2 -2
  15. package/v2Components/CapTagList/index.js +10 -0
  16. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +72 -49
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  23. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +157 -15
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +346 -76
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +150 -4
  27. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  28. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  29. package/v2Components/CommonTestAndPreview/index.js +810 -222
  30. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  31. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  32. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  33. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  34. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  35. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  37. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  38. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  39. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  40. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  41. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  42. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  43. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  44. package/v2Components/FormBuilder/index.js +5 -4
  45. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  46. package/v2Components/SmsFallback/constants.js +73 -0
  47. package/v2Components/SmsFallback/index.js +956 -0
  48. package/v2Components/SmsFallback/index.scss +265 -0
  49. package/v2Components/SmsFallback/messages.js +78 -0
  50. package/v2Components/SmsFallback/smsFallbackUtils.js +119 -0
  51. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  52. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  53. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  54. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +223 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  56. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  57. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  58. package/v2Components/TemplatePreview/_templatePreview.scss +37 -22
  59. package/v2Components/TemplatePreview/constants.js +2 -0
  60. package/v2Components/TemplatePreview/index.js +143 -31
  61. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  62. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  63. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  64. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  65. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  66. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  67. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  68. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +17 -0
  69. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  70. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  71. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  72. package/v2Containers/CreativesContainer/constants.js +9 -0
  73. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  74. package/v2Containers/CreativesContainer/index.js +322 -103
  75. package/v2Containers/CreativesContainer/index.scss +83 -1
  76. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  77. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +79 -34
  78. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  79. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  80. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  81. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  82. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  83. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  84. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  85. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  86. package/v2Containers/Rcs/constants.js +120 -11
  87. package/v2Containers/Rcs/index.js +2577 -812
  88. package/v2Containers/Rcs/index.scss +281 -8
  89. package/v2Containers/Rcs/messages.js +34 -3
  90. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  91. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98036 -70145
  92. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  93. package/v2Containers/Rcs/tests/index.test.js +152 -121
  94. package/v2Containers/Rcs/tests/mockData.js +38 -0
  95. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  96. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  97. package/v2Containers/Rcs/utils.js +478 -11
  98. package/v2Containers/Sms/Create/index.js +106 -40
  99. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  100. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  101. package/v2Containers/SmsTrai/Create/index.js +9 -4
  102. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  103. package/v2Containers/SmsTrai/Edit/index.js +640 -130
  104. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  105. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  106. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  107. package/v2Containers/SmsWrapper/index.js +37 -8
  108. package/v2Containers/TagList/index.js +6 -0
  109. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  110. package/v2Containers/Templates/_templates.scss +166 -9
  111. package/v2Containers/Templates/actions.js +11 -0
  112. package/v2Containers/Templates/constants.js +2 -0
  113. package/v2Containers/Templates/index.js +121 -53
  114. package/v2Containers/Templates/sagas.js +56 -12
  115. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  116. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1062 -1017
  117. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  118. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  119. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  120. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  121. package/v2Containers/TemplatesV2/index.js +86 -23
  122. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  123. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  124. package/v2Containers/Whatsapp/index.js +3 -20
  125. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -31,7 +31,8 @@ import CustomValuesEditor from './CustomValuesEditor';
31
31
  import SendTestMessage from './SendTestMessage';
32
32
  import PreviewSection from './PreviewSection';
33
33
 
34
- // Import constants
34
+ import * as Api from '../../services/api';
35
+ import { extractTemplateVariables } from '../../utils/templateVarUtils';
35
36
  import AddTestCustomerButton from './AddTestCustomer';
36
37
  import ExistingCustomerModal from './ExistingCustomerModal';
37
38
  // Import constants
@@ -81,12 +82,24 @@ import {
81
82
  IMAGE,
82
83
  VIDEO,
83
84
  URL,
85
+ PREVIEW_TAB_RCS,
86
+ PREVIEW_TAB_SMS_FALLBACK,
84
87
  CHANNELS_USING_ANDROID_PREVIEW_DEVICE,
85
88
  DAYS,
89
+ RCS_TEST_META_CONTENT_TYPE_RICHCARD,
90
+ RCS_TEST_META_CARD_TYPE_STANDALONE,
91
+ RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
92
+ RCS_TEST_META_CARD_WIDTH_SMALL,
93
+ SMS_MUSTACHE_TAG_PATTERN,
86
94
  } from './constants';
87
-
88
- // Import utilities
89
95
  import { getCdnUrl } from '../../utils/cdnTransformation';
96
+ import {
97
+ normalizePreviewApiPayload,
98
+ extractPreviewFromLiquidResponse,
99
+ getSmsFallbackTextForTagExtraction,
100
+ } from './previewApiUtils';
101
+ import { pickFirstSmsFallbackTemplateString } from '../../v2Containers/Rcs/rcsLibraryHydrationUtils';
102
+
90
103
  import { isValidEmail, isValidMobile, formatPhoneNumber } from '../../utils/commonUtils';
91
104
  import { getMembersLookup } from '../../services/api';
92
105
 
@@ -109,6 +122,85 @@ const filterUsableGsmSendersForDomain = (domain, gsmSenders, { skipDomainNameEch
109
122
  });
110
123
  };
111
124
 
125
+ /** Preview payload from Redux may be an Immutable Map — normalize for React state. */
126
+ const toPlainPreviewData = (data) => {
127
+ if (data == null) return null;
128
+ const plain = typeof data.toJS === 'function' ? data.toJS() : data;
129
+ return normalizePreviewApiPayload(plain);
130
+ };
131
+
132
+ /**
133
+ * Merge existing customValues with tag keys from categorized groups.
134
+ * Each group is { required, optional } (arrays of tag objects with fullPath).
135
+ * Preserves existing values; adds '' for any tag key not yet present.
136
+ * Reusable for RCS+fallback and any flow that needs to ensure customValues has keys for tags.
137
+ */
138
+ const mergeCustomValuesWithTagKeys = (prev, ...categorizedGroups) => {
139
+ const next = { ...(prev || {}) };
140
+ categorizedGroups.forEach((group) => {
141
+ [...(group.required || []), ...(group.optional || [])].forEach((tag) => {
142
+ const key = tag?.fullPath;
143
+ if (key && next[key] === undefined) next[key] = '';
144
+ });
145
+ });
146
+ return next;
147
+ };
148
+
149
+ /** True when `body` contains `{{name}}` mustache tokens (user-fillable personalization tags).
150
+ * DLT `{#name#}` slots are pre-bound template variables and are intentionally excluded. */
151
+ const smsTemplateHasMustacheTags = (body) =>
152
+ typeof body === 'string' && SMS_MUSTACHE_TAG_PATTERN.test(body);
153
+
154
+ /**
155
+ * Build tag rows from `{{…}}` mustache tokens only — DLT `{#…#}` slots are excluded because
156
+ * they are pre-bound template variables, not user-fillable personalization tags.
157
+ * Passing a mustache-only captureRegex to extractTemplateVariables skips the DLT branch.
158
+ * A non-global regex is used so ensureGlobalRegexForExecLoop creates a fresh instance on each call.
159
+ */
160
+ const buildSyntheticSmsMustacheTags = (body = '') => {
161
+ if (!body || typeof body !== 'string') return [];
162
+ return extractTemplateVariables(body, /\{\{([^}]+)\}\}/).map((name) => ({
163
+ name,
164
+ metaData: { userDriven: false },
165
+ children: [],
166
+ }));
167
+ };
168
+
169
+ /** RCS createMessageMeta: media shape (mediaUrl, thumbnailUrl, height string). */
170
+ const normalizeRcsTestCardMedia = (media) => {
171
+ if (!media || typeof media !== 'object') return undefined;
172
+ const mediaUrl =
173
+ media.mediaUrl != null && String(media.mediaUrl).trim() !== ''
174
+ ? String(media.mediaUrl)
175
+ : media.url != null && String(media.url).trim() !== ''
176
+ ? String(media.url)
177
+ : '';
178
+ const thumbnailUrl = media.thumbnailUrl != null ? String(media.thumbnailUrl) : '';
179
+ const height = media.height != null ? String(media.height) : undefined;
180
+ const out = { mediaUrl, thumbnailUrl };
181
+ if (height) out.height = height;
182
+ return out;
183
+ };
184
+
185
+ /** RCS createMessageMeta: suggestion shape (index, type, text, phoneNumber, url, postback). */
186
+ const mapRcsSuggestionForTestMeta = (suggestionRow, index) => ({
187
+ index,
188
+ type: suggestionRow?.type ?? '',
189
+ text: suggestionRow?.text != null ? String(suggestionRow.text) : '',
190
+ phoneNumber:
191
+ suggestionRow?.phoneNumber != null
192
+ ? String(suggestionRow.phoneNumber)
193
+ : suggestionRow?.phone_number != null
194
+ ? String(suggestionRow.phone_number)
195
+ : '',
196
+ url: suggestionRow?.url !== undefined ? suggestionRow.url : null,
197
+ postback:
198
+ suggestionRow?.postback != null
199
+ ? String(suggestionRow.postback)
200
+ : suggestionRow?.text != null
201
+ ? String(suggestionRow.text)
202
+ : '',
203
+ });
112
204
 
113
205
  /**
114
206
  * CapTreeSelect and group resolution use strict equality; API data may mix numeric and string ids.
@@ -134,7 +226,7 @@ const testEntityIdsEqual = (a, b) => {
134
226
  */
135
227
  const CommonTestAndPreview = (props) => {
136
228
  const {
137
- intl: { formatMessage },
229
+ intl: { formatMessage, locale: userLocale = 'en' },
138
230
  show,
139
231
  onClose,
140
232
  channel, // The channel: 'EMAIL', 'SMS', 'RCS', etc.
@@ -170,13 +262,23 @@ const CommonTestAndPreview = (props) => {
170
262
  ...additionalProps
171
263
  } = props;
172
264
 
265
+ const smsFallbackContent = additionalProps?.smsFallbackContent;
266
+ const smsFallbackTextForTagExtraction = useMemo(
267
+ () => getSmsFallbackTextForTagExtraction(smsFallbackContent),
268
+ [smsFallbackContent],
269
+ );
173
270
  // ============================================
174
271
  // STATE MANAGEMENT
175
272
  // ============================================
176
273
  const [selectedCustomer, setSelectedCustomer] = useState(null);
177
274
  const [requiredTags, setRequiredTags] = useState([]);
178
275
  const [optionalTags, setOptionalTags] = useState([]);
276
+ const [smsFallbackExtractedTags, setSmsFallbackExtractedTags] = useState([]);
277
+ const [smsFallbackRequiredTags, setSmsFallbackRequiredTags] = useState([]);
278
+ const [smsFallbackOptionalTags, setSmsFallbackOptionalTags] = useState([]);
279
+ const [isExtractingSmsFallbackTags, setIsExtractingSmsFallbackTags] = useState(false);
179
280
  const [customValues, setCustomValues] = useState({});
281
+ const previewCustomValuesRef = useRef({});
180
282
  const [showJSON, setShowJSON] = useState(false);
181
283
  const [tagsExtracted, setTagsExtracted] = useState(false);
182
284
 
@@ -187,6 +289,8 @@ const CommonTestAndPreview = (props) => {
187
289
  const [customerData, setCustomerData] = useState({ name: '', email: '', mobile: '', customerId: '' });
188
290
 
189
291
  const [previewDevice, setPreviewDevice] = useState(initialDevice);
292
+ const [activePreviewTab, setActivePreviewTab] = useState(PREVIEW_TAB_RCS);
293
+ const [smsFallbackPreviewText, setSmsFallbackPreviewText] = useState(undefined);
190
294
  // Track if a preview call has been made (to know when to use previewDataHtml vs raw content)
191
295
  const [hasPreviewCallBeenMade, setHasPreviewCallBeenMade] = useState(false);
192
296
  const [previewDataHtml, setPreviewDataHtml] = useState(() => {
@@ -219,15 +323,22 @@ const CommonTestAndPreview = (props) => {
219
323
  [CHANNELS.WHATSAPP]: {
220
324
  domainId: null, senderMobNum: '', sourceAccountIdentifier: '',
221
325
  },
326
+ [CHANNELS.RCS]: {
327
+ domainId: null,
328
+ domainGatewayMapId: null,
329
+ gsmSenderId: '',
330
+ smsFallbackDomainId: null,
331
+ cdmaSenderId: '', // gsmSenderId = RCS sender (domainId|senderId), cdmaSenderId = SMS fallback
332
+ },
222
333
  });
223
334
 
224
- const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP];
335
+ const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS];
225
336
  const formDataForSendTest = formData ?? (content && typeof content === 'object' && !Array.isArray(content) ? content : formData);
226
337
  const smsTemplateConfigs = formDataForSendTest?.templateConfigs || {};
227
338
  const smsTraiDltEnabled = !!smsTemplateConfigs?.traiDltEnabled;
228
339
  const registeredSenderIds = smsTemplateConfigs?.registeredSenderIds || [];
229
340
 
230
- // Fetch sender details and WeCRM accounts when Test & Preview opens for SMS/Email/WhatsApp
341
+ // Fetch sender details and WeCRM accounts when Test & Preview opens (SMS, Email, WhatsApp, RCS — same process)
231
342
  useEffect(() => {
232
343
  if (!show || !channel) {
233
344
  return;
@@ -236,39 +347,99 @@ const CommonTestAndPreview = (props) => {
236
347
  if (actions.getSenderDetailsRequested) {
237
348
  actions.getSenderDetailsRequested({ channel, orgUnitId: orgUnitId ?? -1 });
238
349
  }
350
+ // SMS domains/senders are needed for RCS delivery UI (fallback row + slidebox) whenever RCS is open — not only when fallback body exists.
351
+ if (channel === CHANNELS.RCS && actions.getSenderDetailsRequested) {
352
+ actions.getSenderDetailsRequested({ channel: CHANNELS.SMS, orgUnitId: orgUnitId ?? -1 });
353
+ }
239
354
  if (channel === CHANNELS.WHATSAPP && actions.getWeCrmAccountsRequested) {
240
355
  actions.getWeCrmAccountsRequested({ sourceName: CHANNELS.WHATSAPP });
241
356
  }
242
357
  }
243
358
  }, [show, channel, orgUnitId, actions]);
244
359
 
245
- useEffect(() => {
246
- if (!show) {
247
- setCustomerModal([false, '']);
248
- setSearchValue('');
249
- setCustomerData({ name: '', email: '', mobile: '', customerId: '' });
250
- }
251
- }, [show]);
252
-
253
360
  const findDefault = (arr) => (arr && arr.find((x) => x.default)) || (arr && arr[0]) || {};
254
361
 
255
362
  // Auto-set default delivery setting when sender details load (campaigns-style: first domain + default/first sender)
256
363
  useEffect(() => {
257
364
  if (!channel || !channelsWithDeliverySettings.includes(channel)) return;
365
+
366
+ if (channel === CHANNELS.RCS) {
367
+ const rcsDomainRows = senderDetailsByChannel?.[CHANNELS.RCS] || [];
368
+ const smsFallbackDomainRows = senderDetailsByChannel?.[CHANNELS.SMS] || [];
369
+ if (!rcsDomainRows.length) return;
370
+
371
+ const currentRcsDeliverySettings = testPreviewDeliverySettings?.[CHANNELS.RCS] || {};
372
+ const isRcsGsmSenderUnset = !currentRcsDeliverySettings?.gsmSenderId;
373
+ const isSmsFallbackSenderUnset = !currentRcsDeliverySettings?.cdmaSenderId;
374
+ const isRcsDeliveryFullyUnset = isRcsGsmSenderUnset && isSmsFallbackSenderUnset;
375
+ const shouldOnlyFillSmsFallbackSender =
376
+ !isRcsGsmSenderUnset && isSmsFallbackSenderUnset && smsFallbackDomainRows.length > 0;
377
+
378
+ if (!isRcsDeliveryFullyUnset && !shouldOnlyFillSmsFallbackSender) return;
379
+
380
+ const firstRcsDomain = rcsDomainRows[0];
381
+ const firstSmsFallbackDomain = smsFallbackDomainRows[0];
382
+ const usableRcsGsmSenders = filterUsableGsmSendersForDomain(
383
+ firstRcsDomain,
384
+ firstRcsDomain?.gsmSenders,
385
+ { skipDomainNameEchoFilter: true },
386
+ );
387
+ const usableSmsFallbackGsmSenders = firstSmsFallbackDomain
388
+ ? filterUsableGsmSendersForDomain(firstSmsFallbackDomain, firstSmsFallbackDomain?.gsmSenders)
389
+ : [];
390
+ const defaultRcsGsmSender = usableRcsGsmSenders[0];
391
+ const defaultSmsFallbackGsmSender = usableSmsFallbackGsmSenders[0];
392
+ const rcsSenderCompositeValue =
393
+ firstRcsDomain?.domainId != null && defaultRcsGsmSender?.value != null
394
+ ? `${firstRcsDomain.domainId}|${defaultRcsGsmSender.value}`
395
+ : (defaultRcsGsmSender?.value || '');
396
+ const smsFallbackSenderCompositeValue =
397
+ firstSmsFallbackDomain?.domainId != null && defaultSmsFallbackGsmSender?.value != null
398
+ ? `${firstSmsFallbackDomain.domainId}|${defaultSmsFallbackGsmSender.value}`
399
+ : (defaultSmsFallbackGsmSender?.value || '');
400
+
401
+ setTestPreviewDeliverySettings((prev) => {
402
+ const previousRcsSettings = prev?.[CHANNELS.RCS] || {};
403
+ if (shouldOnlyFillSmsFallbackSender) {
404
+ if (!smsFallbackSenderCompositeValue) return prev;
405
+ return {
406
+ ...prev,
407
+ [CHANNELS.RCS]: {
408
+ ...previousRcsSettings,
409
+ smsFallbackDomainId: firstSmsFallbackDomain?.domainId ?? null,
410
+ cdmaSenderId: smsFallbackSenderCompositeValue,
411
+ },
412
+ };
413
+ }
414
+ return {
415
+ ...prev,
416
+ [CHANNELS.RCS]: {
417
+ domainId: firstRcsDomain?.domainId ?? null,
418
+ domainGatewayMapId: firstRcsDomain?.dgmId ?? null,
419
+ gsmSenderId: rcsSenderCompositeValue,
420
+ smsFallbackDomainId: firstSmsFallbackDomain?.domainId ?? null,
421
+ cdmaSenderId: smsFallbackSenderCompositeValue,
422
+ },
423
+ };
424
+ });
425
+ return;
426
+ }
427
+
258
428
  const domains = senderDetailsByChannel[channel];
259
429
  if (!domains || domains.length === 0) return;
260
430
  const {
261
- domainId = '', gsmSenderId = '', senderEmail = '', senderMobNum = '',
431
+ domainId = '', gsmSenderId = '', cdmaSenderId = '', senderEmail = '', senderMobNum = '',
262
432
  } = testPreviewDeliverySettings[channel] || {};
263
- const isEmptySelection = !domainId && !gsmSenderId && !senderEmail && !senderMobNum;
433
+ const isEmptySelection = !domainId && !gsmSenderId && !cdmaSenderId && !senderEmail && !senderMobNum;
264
434
  if (!isEmptySelection) return;
265
435
 
266
436
  const whatsappAccountFromForm = channel === CHANNELS.WHATSAPP ? formData?.accountName : undefined;
267
437
  const matchedWhatsappAccount = whatsappAccountFromForm
268
438
  ? (wecrmAccounts || []).find((account) => account?.name === whatsappAccountFromForm)
269
439
  : null;
270
- const smsDomains = channel === CHANNELS.SMS && smsTraiDltEnabled
271
- ? domains.filter((domain) => (domain.gsmSenders || []).some((gsm) => registeredSenderIds.includes(gsm.value)))
440
+ const smsDomains = channel === CHANNELS.SMS && smsTraiDltEnabled && registeredSenderIds?.length
441
+ ? domains.filter((domain) => (domain?.gsmSenders || []).some((gsm) =>
442
+ registeredSenderIds?.includes(gsm?.value)))
272
443
  : domains;
273
444
  const [defaultDomain] = domains;
274
445
  const [firstSmsDomain] = smsDomains;
@@ -283,8 +454,8 @@ const CommonTestAndPreview = (props) => {
283
454
  const next = { ...prev };
284
455
  if (channel === CHANNELS.SMS) {
285
456
  const smsGsmSenders = smsTraiDltEnabled
286
- ? (firstDomain.gsmSenders || []).filter((gsm) => registeredSenderIds.includes(gsm.value))
287
- : firstDomain.gsmSenders;
457
+ ? (firstDomain?.gsmSenders || []).filter((gsm) => registeredSenderIds?.includes(gsm?.value))
458
+ : firstDomain?.gsmSenders;
288
459
  next[channel] = {
289
460
  domainId: firstDomain.domainId,
290
461
  domainGatewayMapId: firstDomain.dgmId,
@@ -317,10 +488,29 @@ const CommonTestAndPreview = (props) => {
317
488
  // MEMOIZED VALUES
318
489
  // ============================================
319
490
 
491
+ const allTags = useMemo(
492
+ () => [...requiredTags, ...optionalTags, ...smsFallbackRequiredTags, ...smsFallbackOptionalTags],
493
+ [requiredTags, optionalTags, smsFallbackRequiredTags, smsFallbackOptionalTags]
494
+ );
495
+
496
+ const allRequiredTags = useMemo(
497
+ () => [...requiredTags, ...smsFallbackRequiredTags],
498
+ [requiredTags, smsFallbackRequiredTags]
499
+ );
500
+
501
+ const buildEmptyValues = useCallback(
502
+ () => allTags.reduce((acc, tag) => {
503
+ const key = tag?.fullPath;
504
+ if (key) acc[key] = '';
505
+ return acc;
506
+ }, {}),
507
+ [allTags]
508
+ );
509
+
320
510
  // Check if update preview button should be disabled
321
511
  const isUpdatePreviewDisabled = useMemo(() => (
322
- requiredTags.some((tag) => !customValues[tag.fullPath])
323
- ), [requiredTags, customValues]);
512
+ allRequiredTags.some((tag) => !customValues[tag.fullPath])
513
+ ), [allRequiredTags, customValues]);
324
514
 
325
515
  // Get current content based on channel and editor type
326
516
  const getCurrentContent = useMemo(() => {
@@ -364,6 +554,13 @@ const CommonTestAndPreview = (props) => {
364
554
  return currentTabData.base['sms-editor'];
365
555
  }
366
556
  }
557
+ // DLT / Test & Preview shape: { templateConfigs: { template, templateId, ... } }
558
+ if (formData.templateConfigs?.template) {
559
+ const smsDltTemplateValue = formData.templateConfigs.template;
560
+ if (typeof smsDltTemplateValue === 'string') return smsDltTemplateValue;
561
+ if (Array.isArray(smsDltTemplateValue)) return smsDltTemplateValue.join('');
562
+ return '';
563
+ }
367
564
  }
368
565
 
369
566
  // SMS channel fallback - if formData is not available, use content directly
@@ -409,7 +606,70 @@ const CommonTestAndPreview = (props) => {
409
606
  return content || '';
410
607
  }, [channel, formData, currentTab, beeContent, content, beeInstance]);
411
608
 
412
- // Build test entities tree data
609
+ const leftPanelExtractedTags = useMemo(() => {
610
+ if (channel === CHANNELS.SMS) {
611
+ const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
612
+ if (!smsTemplateHasMustacheTags(smsEditorBody)) return [];
613
+ const extractTagsFromApi = extractedTags ?? [];
614
+ if (extractTagsFromApi.length > 0) return extractTagsFromApi;
615
+ return buildSyntheticSmsMustacheTags(smsEditorBody);
616
+ }
617
+ const hasFallbackSmsBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
618
+ if (channel === CHANNELS.RCS && hasFallbackSmsBody) {
619
+ const rcsPrimaryTags = extractedTags ?? [];
620
+ const fallbackSmsTextForTags = smsFallbackTextForTagExtraction ?? '';
621
+ const fallbackSmsTagRows = smsTemplateHasMustacheTags(fallbackSmsTextForTags)
622
+ ? (smsFallbackExtractedTags?.length > 0
623
+ ? smsFallbackExtractedTags
624
+ : buildSyntheticSmsMustacheTags(fallbackSmsTextForTags))
625
+ : [];
626
+ const mergedRcsAndFallbackTags = [...rcsPrimaryTags, ...fallbackSmsTagRows];
627
+ if (mergedRcsAndFallbackTags.length > 0) return mergedRcsAndFallbackTags;
628
+ return buildSyntheticSmsMustacheTags(fallbackSmsTextForTags);
629
+ }
630
+ return extractedTags ?? [];
631
+ }, [
632
+ channel,
633
+ extractedTags,
634
+ getCurrentContent,
635
+ smsFallbackContent,
636
+ smsFallbackExtractedTags,
637
+ smsFallbackTextForTagExtraction,
638
+ ]);
639
+
640
+ const isRcsSmsFallbackPreviewEnabled =
641
+ channel === CHANNELS.RCS
642
+ && !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
643
+ // Only treat as SMS when user is on the Fallback SMS tab — not whenever fallback exists (RCS tab needs RCS preview API).
644
+ const isSmsFallbackTabActive = isRcsSmsFallbackPreviewEnabled && activePreviewTab === PREVIEW_TAB_SMS_FALLBACK;
645
+ const activeChannelForActions = isSmsFallbackTabActive ? CHANNELS.SMS : channel;
646
+ // VarSegment slot values live in rcsSmsFallbackVarMapped; raw templateContent alone is stale for /preview Body.
647
+ const resolvedSmsFallbackBodyForPreviewTab =
648
+ smsFallbackTextForTagExtraction
649
+ || smsFallbackContent?.templateContent
650
+ || smsFallbackContent?.content
651
+ || '';
652
+ const activeContentForActions = isSmsFallbackTabActive
653
+ ? resolvedSmsFallbackBodyForPreviewTab
654
+ : getCurrentContent;
655
+
656
+ /**
657
+ * SMS fallback pane must show /preview API result when user updated preview on that tab (plain text).
658
+ * Skip when resolvedBody is RCS-shaped (e.g. user last previewed on RCS tab).
659
+ */
660
+ // smsFallbackPreviewText is the single source of truth for the resolved SMS fallback preview.
661
+ // It is set only by syncSmsFallbackPreview (called from handleUpdatePreview and the
662
+ // prefilled-values effect) and reset to undefined on discard / slidebox close.
663
+ // Using previewDataHtml as a fallback is unsafe because that state is shared with the primary
664
+ // RCS preview and can contain stale SMS or RCS content.
665
+ const smsFallbackResolvedText = useMemo(() => {
666
+ const hasFallbackBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
667
+ if (channel !== CHANNELS.RCS || !hasFallbackBody) return undefined;
668
+ if (smsFallbackPreviewText != null) return smsFallbackPreviewText;
669
+ return undefined;
670
+ }, [channel, smsFallbackContent, smsFallbackPreviewText]);
671
+
672
+ // Build test entities tree data from testCustomers prop
413
673
  // Build test entities tree data from testCustomers prop (includes customers added via addTestCustomer action)
414
674
  const testEntitiesTreeData = useMemo(() => {
415
675
  const groupsNode = {
@@ -418,7 +678,7 @@ const CommonTestAndPreview = (props) => {
418
678
  selectable: false,
419
679
  children: testGroups?.map((group) => ({
420
680
  title: group?.groupName,
421
- value: 'group:' + normalizeTestEntityId(group?.groupId),
681
+ value: normalizeTestEntityId(group?.groupId),
422
682
  })),
423
683
  };
424
684
 
@@ -428,7 +688,7 @@ const CommonTestAndPreview = (props) => {
428
688
  selectable: false,
429
689
  children: testCustomers?.map((customer) => ({
430
690
  title: customer?.name?.trim() || customer?.email?.trim() || customer?.mobile?.trim() || customer?.userId || customer?.customerId,
431
- value: 'customer:' + normalizeTestEntityId(customer?.userId ?? customer?.customerId),
691
+ value: normalizeTestEntityId(customer?.userId ?? customer?.customerId),
432
692
  })) || [],
433
693
  };
434
694
 
@@ -508,11 +768,10 @@ const CommonTestAndPreview = (props) => {
508
768
  email: customerData?.email || '',
509
769
  mobile: customerData?.mobile || '',
510
770
  });
511
- const prefixedAddedId = 'customer:' + normalizedAddedId;
512
771
  setSelectedTestEntities((prev) => (
513
- prev.some((id) => id === prefixedAddedId)
772
+ prev.some((id) => testEntityIdsEqual(id, normalizedAddedId))
514
773
  ? prev
515
- : [...prev, prefixedAddedId]
774
+ : [...prev, normalizedAddedId]
516
775
  ));
517
776
  }
518
777
  handleCloseCustomerModal();
@@ -576,13 +835,33 @@ const CommonTestAndPreview = (props) => {
576
835
  whatsappContent: formDataObj?.whatsappContent,
577
836
  };
578
837
 
579
- case CHANNELS.RCS:
838
+ case CHANNELS.RCS: {
839
+ // For carousel, replace {{N}} tokens in carouselData with resolved semantic vars from
840
+ // formData.cardContent so the preview API can resolve {{member.firstName}} etc.
841
+ const formDataCardContent =
842
+ formDataObj?.versions?.base?.content?.RCS?.rcsContent?.cardContent
843
+ ?? formDataObj?.content?.RCS?.rcsContent?.cardContent;
844
+ let parsedRcs = {};
845
+ try { parsedRcs = JSON.parse(contentStr); } catch (e) { parsedRcs = typeof contentStr === 'object' ? contentStr : {}; }
846
+ if (Array.isArray(parsedRcs.carouselData) && Array.isArray(formDataCardContent) && formDataCardContent.length > 0) {
847
+ const resolvedCarouselData = parsedRcs.carouselData.map((card, idx) => {
848
+ const formCard = formDataCardContent[idx] || {};
849
+ return {
850
+ ...card,
851
+ title: formCard.title || card.title,
852
+ bodyText: formCard.description || card.bodyText,
853
+ };
854
+ });
855
+ return {
856
+ ...basePayload,
857
+ messageBody: JSON.stringify({ ...parsedRcs, carouselData: resolvedCarouselData }),
858
+ };
859
+ }
580
860
  return {
581
861
  ...basePayload,
582
862
  messageBody: contentStr,
583
- // messageTitle: resolveTagsInText(formDataObj?.rcsTitle || '', customValuesObj),
584
- // rcsDesc: formDataObj?.rcsDesc,
585
863
  };
864
+ }
586
865
 
587
866
  case CHANNELS.INAPP:
588
867
  return {
@@ -622,6 +901,37 @@ const CommonTestAndPreview = (props) => {
622
901
  }
623
902
  };
624
903
 
904
+ /**
905
+ * When RCS has SMS fallback, refresh fallback preview text via the same Liquid /preview API
906
+ * (separate call with SMS channel + fallback template body). Used after primary preview updates.
907
+ */
908
+ const syncSmsFallbackPreview = async (customValuesForResolve, selectedCustomerObj) => {
909
+ const fallbackBodyForLiquidPreview =
910
+ getSmsFallbackTextForTagExtraction(smsFallbackContent)
911
+ || smsFallbackContent?.templateContent
912
+ || smsFallbackContent?.content
913
+ || '';
914
+ if (channel !== CHANNELS.RCS || !String(fallbackBodyForLiquidPreview).trim()) return;
915
+ try {
916
+ const smsFallbackPayload = preparePreviewPayload(
917
+ CHANNELS.SMS,
918
+ formData || {},
919
+ fallbackBodyForLiquidPreview,
920
+ customValuesForResolve,
921
+ selectedCustomerObj
922
+ );
923
+ const fallbackResponse = await Api.updateEmailPreview(smsFallbackPayload);
924
+ const fallbackPreview = extractPreviewFromLiquidResponse(fallbackResponse);
925
+ setSmsFallbackPreviewText(
926
+ typeof fallbackPreview?.resolvedBody === 'string'
927
+ ? fallbackPreview.resolvedBody
928
+ : undefined
929
+ );
930
+ } catch (e) {
931
+ /* keep existing smsFallbackPreviewText on failure */
932
+ }
933
+ };
934
+
625
935
  /**
626
936
  * Prepare payload for tag extraction based on channel
627
937
  */
@@ -645,11 +955,32 @@ const CommonTestAndPreview = (props) => {
645
955
  templateContent: contentStr,
646
956
  };
647
957
 
648
- case CHANNELS.RCS:
958
+ case CHANNELS.RCS: {
959
+ // Carousel templates don't have rcsTitle/rcsDesc — their content lives in cardContent[].
960
+ // Use the resolved card strings from formDataObj (resolved by resolveTemplateWithMap in
961
+ // testPreviewFormData) so the extraction API receives actual Capillary tag expressions
962
+ // instead of numeric {{1}}/{{2}} placeholders.
963
+ const rcsCardContentArr =
964
+ formDataObj?.versions?.base?.content?.RCS?.rcsContent?.cardContent
965
+ ?? formDataObj?.content?.RCS?.rcsContent?.cardContent;
966
+ if (Array.isArray(rcsCardContentArr) && rcsCardContentArr.length > 0) {
967
+ const carouselTagText = rcsCardContentArr
968
+ .map((c) => `${c.title || ''} ${c.description || ''}`)
969
+ .join(' ')
970
+ .trim();
971
+ if (carouselTagText) {
972
+ return {
973
+ templateSubject: formDataObj?.rcsTitle || '',
974
+ templateContent: carouselTagText,
975
+ };
976
+ }
977
+ }
978
+ // Standalone (non-carousel) card: use rcsTitle/rcsDesc directly.
649
979
  return {
650
980
  templateSubject: formDataObj?.rcsTitle || '',
651
981
  templateContent: formDataObj?.rcsDesc || contentStr,
652
982
  };
983
+ }
653
984
 
654
985
  case CHANNELS.INAPP:
655
986
  return {
@@ -722,7 +1053,7 @@ const CommonTestAndPreview = (props) => {
722
1053
  } = carousel || {};
723
1054
  const buttonData = buttons.map((button, index) => {
724
1055
  const {
725
- type, text, phone_number, urlType, url,
1056
+ type, text, phone_number: phoneNumber, urlType, url,
726
1057
  } = button || {};
727
1058
  const buttonObj = {
728
1059
  type,
@@ -730,7 +1061,7 @@ const CommonTestAndPreview = (props) => {
730
1061
  index,
731
1062
  };
732
1063
  if (type === PHONE_NUMBER) {
733
- buttonObj.phoneNumber = phone_number;
1064
+ buttonObj.phoneNumber = phoneNumber;
734
1065
  }
735
1066
  if (type === URL) {
736
1067
  buttonObj.url = url;
@@ -759,7 +1090,159 @@ const CommonTestAndPreview = (props) => {
759
1090
  };
760
1091
  });
761
1092
 
762
- const prepareTestMessagePayload = (channelType, formDataObj, contentStr, customValuesObj, recipientDetails, previewDataObj, deliverySettingsOverride) => {
1093
+ /**
1094
+ * Build createMessageMeta payload for RCS (test message).
1095
+ * rcsMessageContent: { channel, accountId?, rcsRichCardContent: { contentType, cardType, cardSettings, cardContent }, smsFallBackContent? }
1096
+ * Then rcsDeliverySettings, executionParams, clientName last.
1097
+ */
1098
+ const buildRcsTestMessagePayload = (
1099
+ creativeFormData,
1100
+ _unusedEditorContentString,
1101
+ _customValuesObj,
1102
+ deliverySettingsOverride,
1103
+ basePayload,
1104
+ _rcsTestMetaExtras = {},
1105
+ ) => {
1106
+ const rcsSectionFromForm =
1107
+ creativeFormData?.versions?.base?.content?.RCS ?? creativeFormData?.content?.RCS ?? {};
1108
+ const rcsContentFromForm = rcsSectionFromForm?.rcsContent || {};
1109
+ const smsFallbackFromCreativeForm = rcsSectionFromForm?.smsFallBackContent || {};
1110
+ let rcsCardPayloadList = [];
1111
+ if (Array.isArray(rcsContentFromForm?.cardContent)) {
1112
+ rcsCardPayloadList = rcsContentFromForm.cardContent;
1113
+ } else if (rcsContentFromForm?.cardContent) {
1114
+ rcsCardPayloadList = [rcsContentFromForm.cardContent];
1115
+ }
1116
+ // Raw title/description with template tags; SMS fallback uses tagged template fields (pickFirst…).
1117
+ const cardContentForTestMetaApi = rcsCardPayloadList.map((singleRcsCardPayload) => {
1118
+ const normalizedCardMediaForTestApi = singleRcsCardPayload?.media
1119
+ ? normalizeRcsTestCardMedia(singleRcsCardPayload.media)
1120
+ : undefined;
1121
+ const suggestionsFromCard = Array.isArray(singleRcsCardPayload?.suggestions)
1122
+ ? singleRcsCardPayload.suggestions
1123
+ : [];
1124
+ const suggestionsFormattedForTestMeta = suggestionsFromCard.map((suggestionItem, index) =>
1125
+ mapRcsSuggestionForTestMeta(suggestionItem, index));
1126
+ return {
1127
+ title: singleRcsCardPayload?.title ?? '',
1128
+ description: singleRcsCardPayload?.description ?? '',
1129
+ mediaType: singleRcsCardPayload?.mediaType ?? MEDIA_TYPE_TEXT,
1130
+ ...(normalizedCardMediaForTestApi && { media: normalizedCardMediaForTestApi }),
1131
+ ...(suggestionsFormattedForTestMeta.length > 0 && {
1132
+ suggestions: suggestionsFormattedForTestMeta,
1133
+ }),
1134
+ };
1135
+ });
1136
+ // Use the component-level smsFallbackContent prop (has rcsSmsFallbackVarMapped) so DLT
1137
+ // {#var#} slots are converted to {{tagName}} mustache tags before sending to createMessageMeta.
1138
+ const smsFallbackTaggedTemplateBody =
1139
+ getSmsFallbackTextForTagExtraction(smsFallbackContent)
1140
+ || pickFirstSmsFallbackTemplateString(smsFallbackFromCreativeForm)
1141
+ || '';
1142
+ const smsSenderFromDelivery = deliverySettingsOverride?.cdmaSenderId?.includes('|')
1143
+ ? deliverySettingsOverride.cdmaSenderId.split('|')[1]
1144
+ : deliverySettingsOverride?.cdmaSenderId;
1145
+ const deliveryFallbackSmsId =
1146
+ typeof smsSenderFromDelivery === 'string' ? smsSenderFromDelivery.trim() : '';
1147
+ const creativeFallbackSmsId =
1148
+ smsFallbackFromCreativeForm?.senderId != null
1149
+ ? String(smsFallbackFromCreativeForm.senderId).trim()
1150
+ : '';
1151
+ const fallbackSmsSenderIdForChannel = deliveryFallbackSmsId || creativeFallbackSmsId || '';
1152
+
1153
+ // For DLT orgs: include templateConfigs inside smsFallBackContent so createMessageMeta
1154
+ // receives the DLT template ID, registered sender IDs, and body alongside the message.
1155
+ // Priority: smsFallBackContent.templateConfigs from the RCS payload (clean API shape,
1156
+ // built by createPayload) → fallback to root-level formData.templateConfigs (strip the
1157
+ // UI-only traiDltEnabled flag before sending to the API).
1158
+ const smsFallbackTemplateConfigsForApi = (() => {
1159
+ if (
1160
+ smsFallbackFromCreativeForm?.templateConfigs
1161
+ && typeof smsFallbackFromCreativeForm.templateConfigs === 'object'
1162
+ ) {
1163
+ return smsFallbackFromCreativeForm.templateConfigs;
1164
+ }
1165
+ const rootTc = creativeFormData?.templateConfigs;
1166
+ if (rootTc && typeof rootTc === 'object') {
1167
+ // eslint-disable-next-line no-unused-vars
1168
+ const { traiDltEnabled: _uiFlag, ...tcForApi } = rootTc;
1169
+ return Object.keys(tcForApi).length > 0 ? tcForApi : null;
1170
+ }
1171
+ return null;
1172
+ })();
1173
+
1174
+ const smsFallBackContent =
1175
+ smsFallbackTaggedTemplateBody.trim() !== ''
1176
+ ? {
1177
+ message: smsFallbackTaggedTemplateBody,
1178
+ ...(smsFallbackTemplateConfigsForApi && {
1179
+ templateConfigs: smsFallbackTemplateConfigsForApi,
1180
+ }),
1181
+ }
1182
+ : undefined;
1183
+
1184
+ // accountId: WeCRM account id (not sourceAccountIdentifier) for createMessageMeta
1185
+ const accountIdForMeta =
1186
+ rcsContentFromForm?.accountId != null && String(rcsContentFromForm.accountId).trim() !== ''
1187
+ ? String(rcsContentFromForm.accountId)
1188
+ : undefined;
1189
+
1190
+ const rcsRichCardContent = {
1191
+ contentType: RCS_TEST_META_CONTENT_TYPE_RICHCARD,
1192
+ cardType: rcsContentFromForm?.cardType ?? RCS_TEST_META_CARD_TYPE_STANDALONE,
1193
+ cardSettings: rcsContentFromForm?.cardSettings ?? {
1194
+ cardOrientation: RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
1195
+ cardWidth: RCS_TEST_META_CARD_WIDTH_SMALL,
1196
+ },
1197
+ ...(cardContentForTestMetaApi.length > 0 && { cardContent: cardContentForTestMetaApi }),
1198
+ };
1199
+
1200
+ const rcsMessageContent = {
1201
+ channel: CHANNELS.RCS,
1202
+ ...(accountIdForMeta && { accountId: accountIdForMeta }),
1203
+ rcsRichCardContent,
1204
+ ...(smsFallBackContent && { smsFallBackContent }),
1205
+ };
1206
+ const rcsComposite = deliverySettingsOverride?.gsmSenderId ?? '';
1207
+ const [rcsDomainId, rcsSenderId] = rcsComposite.includes('|') ? rcsComposite.split('|') : ['', rcsComposite];
1208
+ const rcsDeliverySettings = {
1209
+ channelSettings: {
1210
+ channel: CHANNELS.RCS,
1211
+ rcsSender: (rcsSenderId || deliverySettingsOverride?.rcsSender) ?? '',
1212
+ domainId:
1213
+ rcsDomainId !== '' && rcsDomainId !== undefined && !Number.isNaN(Number(rcsDomainId))
1214
+ ? Number(rcsDomainId)
1215
+ : (deliverySettingsOverride?.domainId ?? 0),
1216
+ fallbackSmsSenderId: fallbackSmsSenderIdForChannel,
1217
+ },
1218
+ additionalSettings: {
1219
+ useTinyUrl: false,
1220
+ encryptUrl: false,
1221
+ linkTrackingEnabled: false,
1222
+ bypassControlUser: false,
1223
+ userSubscriptionDisabled: false,
1224
+ },
1225
+ };
1226
+ const { clientName: baseClientName = CLIENT_NAME_CREATIVES, ...restBase } = basePayload;
1227
+ return {
1228
+ ...restBase,
1229
+ rcsMessageContent,
1230
+ rcsDeliverySettings,
1231
+ executionParams: {},
1232
+ clientName: baseClientName,
1233
+ };
1234
+ };
1235
+
1236
+ const prepareTestMessagePayload = (
1237
+ channelType,
1238
+ formDataObj,
1239
+ contentStr,
1240
+ customValuesObj,
1241
+ recipientDetails,
1242
+ previewDataObj,
1243
+ deliverySettingsOverride,
1244
+ rcsExtra = {},
1245
+ ) => {
763
1246
  // Base payload structure common to all channels
764
1247
  const basePayload = {
765
1248
  ouId: -1,
@@ -840,12 +1323,12 @@ const CommonTestAndPreview = (props) => {
840
1323
  },
841
1324
  smsDeliverySettings: {
842
1325
  channelSettings: {
1326
+ channel: CHANNELS.SMS,
843
1327
  gsmSenderId: deliverySettingsOverride?.gsmSenderId ?? '',
844
1328
  domainId: deliverySettingsOverride?.domainId ?? null,
845
1329
  domainGatewayMapId: deliverySettingsOverride?.domainGatewayMapId ?? '',
846
1330
  targetNdnc: false,
847
1331
  cdmaSenderId: deliverySettingsOverride?.cdmaSenderId ?? '',
848
- channel: CHANNELS.SMS,
849
1332
  },
850
1333
  additionalSettings: {
851
1334
  useTinyUrl: false,
@@ -989,7 +1472,7 @@ const CommonTestAndPreview = (props) => {
989
1472
  return {
990
1473
  ...basePayload,
991
1474
  whatsappMessageContent: {
992
- messageBody: templateEditorValue || '',
1475
+ messageBody: resolvedMessageBody || templateEditorValue || '',
993
1476
  accountId: formDataObj?.accountId || '',
994
1477
  sourceAccountIdentifier: sourceAccountIdentifier || formDataObj?.sourceAccountIdentifier || '',
995
1478
  accountName: formDataObj?.accountName || '',
@@ -1014,16 +1497,7 @@ const CommonTestAndPreview = (props) => {
1014
1497
  }
1015
1498
 
1016
1499
  case CHANNELS.RCS:
1017
- return {
1018
- ...basePayload,
1019
- rcsMessageContent: {
1020
- channel: CHANNELS.RCS,
1021
- messageBody: contentStr,
1022
- rcsType: additionalProps?.rcsType,
1023
- rcsImageSrc: formDataObj?.rcsImageSrc,
1024
- rcsSuggestions: formDataObj?.rcsSuggestions,
1025
- },
1026
- };
1500
+ return buildRcsTestMessagePayload(formDataObj, contentStr, customValuesObj, deliverySettingsOverride, basePayload, rcsExtra);
1027
1501
 
1028
1502
  case CHANNELS.INAPP: {
1029
1503
  // InApp payload structure similar to MobilePush
@@ -1862,6 +2336,46 @@ const CommonTestAndPreview = (props) => {
1862
2336
  *
1863
2337
  * IMPORTANT: Use raw content/formData initially, only use previewDataHtml if preview call was made
1864
2338
  */
2339
+ // Memoized RCS carousel content: only recomputes when the API response or raw template changes,
2340
+ // NOT on every customValues keystroke. Uses previewCustomValuesRef captured at Update Preview time.
2341
+ const rcsCarouselContentObj = useMemo(() => {
2342
+ if (channel !== CHANNELS.RCS) return null;
2343
+ let resolvedContent = null;
2344
+ if (hasPreviewCallBeenMade && previewDataHtml?.resolvedBody) {
2345
+ resolvedContent = previewDataHtml.resolvedBody;
2346
+ if (typeof resolvedContent === 'string') {
2347
+ try { resolvedContent = JSON.parse(resolvedContent); } catch (e) { resolvedContent = null; }
2348
+ }
2349
+ }
2350
+ if (resolvedContent && typeof resolvedContent === 'object') return resolvedContent;
2351
+
2352
+ let rawRcs = content;
2353
+ if (typeof rawRcs === 'string') {
2354
+ try { rawRcs = JSON.parse(rawRcs); } catch (e) { rawRcs = content; }
2355
+ }
2356
+ const snapCustomValues = previewCustomValuesRef.current;
2357
+ const hasEnteredValues = snapCustomValues && Object.keys(snapCustomValues).some((k) => snapCustomValues[k]);
2358
+ if (hasPreviewCallBeenMade && hasEnteredValues && Array.isArray(rawRcs?.carouselData)) {
2359
+ const formDataCardContent =
2360
+ formData?.versions?.base?.content?.RCS?.rcsContent?.cardContent
2361
+ ?? formData?.content?.RCS?.rcsContent?.cardContent
2362
+ ?? [];
2363
+ return {
2364
+ ...rawRcs,
2365
+ carouselData: rawRcs.carouselData.map((card, idx) => {
2366
+ const formCard = formDataCardContent[idx] || {};
2367
+ return {
2368
+ ...card,
2369
+ title: resolveTagsInText(formCard.title || card.title || '', snapCustomValues),
2370
+ bodyText: resolveTagsInText(formCard.description || card.bodyText || '', snapCustomValues),
2371
+ };
2372
+ }),
2373
+ };
2374
+ }
2375
+ return rawRcs || {};
2376
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2377
+ }, [channel, content, hasPreviewCallBeenMade, previewDataHtml, formData]);
2378
+
1865
2379
  const prepareUnifiedPreviewProps = () => {
1866
2380
  // Prepare content based on channel
1867
2381
  let contentObj = {};
@@ -1923,42 +2437,14 @@ const CommonTestAndPreview = (props) => {
1923
2437
  contentObj = parsedContent || {};
1924
2438
  }
1925
2439
  } else if (channel === CHANNELS.RCS) {
1926
- // For RCS, content is an object with rcsTitle, rcsDesc, rcsImageSrc, suggestions, etc.
1927
- // Content comes from RCS component state, passed via content prop
1928
- let resolvedContent = null;
1929
-
1930
- // Only use previewDataHtml if preview call was made
1931
- if (hasPreviewCallBeenMade && previewDataHtml?.resolvedBody) {
1932
- resolvedContent = previewDataHtml.resolvedBody;
1933
-
1934
- // Handle case where resolvedBody might be a JSON string
1935
- if (typeof resolvedContent === 'string') {
1936
- try {
1937
- resolvedContent = JSON.parse(resolvedContent);
1938
- } catch (e) {
1939
- // If parsing fails, treat as null so we fall back to content
1940
- resolvedContent = null;
1941
- }
1942
- }
1943
- }
1944
-
1945
- // Parse content if it's a string
2440
+ // rcsCarouselContentObj is a useMemo that handles both the API-response path and the
2441
+ // client-side fallback. It is stable across keystrokes (only recomputes when
2442
+ // hasPreviewCallBeenMade / previewDataHtml / content / formData change).
1946
2443
  let parsedRcsContent = content;
1947
2444
  if (typeof content === 'string') {
1948
- try {
1949
- parsedRcsContent = JSON.parse(content);
1950
- } catch (e) {
1951
- parsedRcsContent = content;
1952
- }
1953
- }
1954
-
1955
- // Use resolvedContent if available (from preview call), otherwise use raw content
1956
- if (resolvedContent && typeof resolvedContent === 'object') {
1957
- contentObj = { ...resolvedContent };
1958
- } else {
1959
- // Use raw content if no preview call was made or resolvedContent is not available
1960
- contentObj = parsedRcsContent || {};
2445
+ try { parsedRcsContent = JSON.parse(content); } catch (e) { parsedRcsContent = content; }
1961
2446
  }
2447
+ contentObj = rcsCarouselContentObj || parsedRcsContent || {};
1962
2448
  } else if (channel === CHANNELS.INAPP) {
1963
2449
  // For InApp, content is an object with androidContent and iosContent (similar to MobilePush)
1964
2450
  // Content comes from InApp component state, passed via content prop
@@ -2161,6 +2647,10 @@ const CommonTestAndPreview = (props) => {
2161
2647
  formatMessage,
2162
2648
  lastModified: formData?.lastModified,
2163
2649
  updatedByName: formData?.updatedByName,
2650
+ smsFallbackContent: isRcsSmsFallbackPreviewEnabled ? smsFallbackContent : null,
2651
+ smsFallbackResolvedText,
2652
+ activePreviewTab,
2653
+ onPreviewTabChange: setActivePreviewTab,
2164
2654
  };
2165
2655
  };
2166
2656
 
@@ -2200,7 +2690,12 @@ const CommonTestAndPreview = (props) => {
2200
2690
  }, [show, beeInstance, currentTab, channel]);
2201
2691
 
2202
2692
  /**
2203
- * Initial data load when slidebox opens
2693
+ * Initial data load when slidebox opens.
2694
+ * EXTRACT TAGS CALL SITES (on open/edit RCS):
2695
+ * 1. Here (non-email): actions.extractTagsRequested() at line ~2161 when show is true.
2696
+ * 2. RCS SMS fallback useEffect below: Api.extractTagsWithMetaData() for fallback message.
2697
+ * 3. handleExtractTags() (user clicks "Enter custom values for tags") – not from effects.
2698
+ * The "Process extracted tags" effect only processes API results and must not call extract again.
2204
2699
  */
2205
2700
  useEffect(() => {
2206
2701
  if (show) {
@@ -2285,7 +2780,61 @@ const CommonTestAndPreview = (props) => {
2285
2780
  actions.getTestGroupsRequested();
2286
2781
  }
2287
2782
  }
2288
- }, [show, beeInstance, currentTab, channel]);
2783
+ // getCurrentContent: RCS applies cardVarMapped → placeholder resolution; re-extract when it changes.
2784
+ }, [show, beeInstance, currentTab, channel, getCurrentContent]);
2785
+
2786
+ /**
2787
+ * RCS with SMS fallback: extract tags for fallback SMS content as well
2788
+ * (so we can show a separate "Fallback SMS tags" section in left panel).
2789
+ */
2790
+ useEffect(() => {
2791
+ let cancelled = false;
2792
+
2793
+ if (!show || channel !== CHANNELS.RCS) {
2794
+ return () => {
2795
+ cancelled = true;
2796
+ };
2797
+ }
2798
+
2799
+ if (!smsFallbackContent?.templateContent && !smsFallbackContent?.content) {
2800
+ setSmsFallbackExtractedTags([]);
2801
+ setSmsFallbackRequiredTags([]);
2802
+ setSmsFallbackOptionalTags([]);
2803
+ return () => {
2804
+ cancelled = true;
2805
+ };
2806
+ }
2807
+
2808
+ setIsExtractingSmsFallbackTags(true);
2809
+ (async () => {
2810
+ try {
2811
+ const fallbackBodyForExtractApi = getSmsFallbackTextForTagExtraction(smsFallbackContent);
2812
+ const payload = {
2813
+ messageTitle: '',
2814
+ messageBody: fallbackBodyForExtractApi || '',
2815
+ };
2816
+ const response = await Api.extractTagsWithMetaData(payload); //not using saga action here because we dont store fallbacksms related data in store but only in useState since this is only used in RCS SMS fallback
2817
+ let smsFallbackTagTree = response?.data ?? [];
2818
+ if (!Array.isArray(smsFallbackTagTree)) smsFallbackTagTree = [];
2819
+ if (!smsTemplateHasMustacheTags(fallbackBodyForExtractApi)) {
2820
+ smsFallbackTagTree = [];
2821
+ } else if (smsFallbackTagTree.length === 0) {
2822
+ smsFallbackTagTree = buildSyntheticSmsMustacheTags(fallbackBodyForExtractApi);
2823
+ }
2824
+ if (cancelled) return;
2825
+ setSmsFallbackExtractedTags(smsFallbackTagTree);
2826
+ } catch (e) {
2827
+ if (cancelled) return;
2828
+ setSmsFallbackExtractedTags([]);
2829
+ } finally {
2830
+ if (!cancelled) setIsExtractingSmsFallbackTags(false);
2831
+ }
2832
+ })();
2833
+
2834
+ return () => {
2835
+ cancelled = true;
2836
+ };
2837
+ }, [show, channel, smsFallbackContent]);
2289
2838
 
2290
2839
  /**
2291
2840
  * Email-specific: Handle content updates for both BEE and CKEditor
@@ -2337,15 +2886,22 @@ const CommonTestAndPreview = (props) => {
2337
2886
  setSelectedCustomer(null);
2338
2887
  setRequiredTags([]);
2339
2888
  setOptionalTags([]);
2889
+ setSmsFallbackExtractedTags([]);
2890
+ setSmsFallbackRequiredTags([]);
2891
+ setSmsFallbackOptionalTags([]);
2892
+ setIsExtractingSmsFallbackTags(false);
2340
2893
  setCustomValues({});
2341
2894
  setShowJSON(false);
2342
2895
  setTagsExtracted(false);
2343
2896
  setPreviewDevice(DESKTOP);
2897
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2898
+ setSmsFallbackPreviewText(undefined);
2344
2899
  setSelectedTestEntities([]);
2345
2900
  actions.clearPrefilledValues();
2346
2901
  } else {
2347
2902
  // Reset device to initialDevice when opening (Android for mobile channels, Desktop for others)
2348
2903
  setPreviewDevice(initialDevice);
2904
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2349
2905
  }
2350
2906
  }, [show, initialDevice]);
2351
2907
 
@@ -2354,79 +2910,10 @@ const CommonTestAndPreview = (props) => {
2354
2910
  */
2355
2911
  useEffect(() => {
2356
2912
  if (previewData) {
2357
- setPreviewDataHtml(previewData);
2913
+ setPreviewDataHtml(toPlainPreviewData(previewData));
2358
2914
  }
2359
2915
  }, [previewData]);
2360
2916
 
2361
- /**
2362
- * Process extracted tags and categorize them
2363
- */
2364
- useEffect(() => {
2365
- // Categorize tags into required and optional
2366
- const required = [];
2367
- const optional = [];
2368
- let hasPersonalizationTags = false;
2369
-
2370
- if (extractedTags?.length > 0) {
2371
- const processTag = (tag, parentPath = '') => {
2372
- const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
2373
-
2374
- // Skip unsubscribe tag for input fields
2375
- if (tag?.name === UNSUBSCRIBE_TAG_NAME) {
2376
- return;
2377
- }
2378
-
2379
- hasPersonalizationTags = true;
2380
-
2381
- if (tag?.metaData?.userDriven === false) {
2382
- required.push({
2383
- ...tag,
2384
- fullPath: currentPath,
2385
- });
2386
- } else if (tag?.metaData?.userDriven === true) {
2387
- optional.push({
2388
- ...tag,
2389
- fullPath: currentPath,
2390
- });
2391
- }
2392
-
2393
- if (tag?.children?.length > 0) {
2394
- tag.children.forEach((child) => processTag(child, currentPath));
2395
- }
2396
- };
2397
-
2398
- extractedTags.forEach((tag) => processTag(tag));
2399
-
2400
- if (hasPersonalizationTags) {
2401
- setRequiredTags(required);
2402
- setOptionalTags(optional);
2403
- setTagsExtracted(true); // Mark tags as extracted and processed
2404
-
2405
- // Initialize custom values for required tags
2406
- const initialValues = {};
2407
- required.forEach((tag) => {
2408
- initialValues[tag?.fullPath] = '';
2409
- });
2410
- optional.forEach((tag) => {
2411
- initialValues[tag?.fullPath] = '';
2412
- });
2413
- setCustomValues(initialValues);
2414
- } else {
2415
- // Reset all tag-related state if no personalization tags
2416
- setRequiredTags([]);
2417
- setOptionalTags([]);
2418
- setCustomValues({});
2419
- setTagsExtracted(false);
2420
- }
2421
- } else {
2422
- // Reset all tag-related state if no tags
2423
- setRequiredTags([]);
2424
- setOptionalTags([]);
2425
- setCustomValues({});
2426
- setTagsExtracted(false);
2427
- }
2428
- }, [extractedTags]);
2429
-
2430
2917
  /**
2431
2918
  * Handle customer selection and fetch prefilled values
2432
2919
  */
@@ -2434,17 +2921,15 @@ const CommonTestAndPreview = (props) => {
2434
2921
  if (selectedCustomer && config.enableCustomerSearch !== false) {
2435
2922
  setTagsExtracted(true); // Auto-open custom values editor
2436
2923
 
2437
- // Get all available tags
2438
- const allTags = [...requiredTags, ...optionalTags];
2439
2924
  const requiredTagObj = {};
2440
- requiredTags.forEach((tag) => {
2925
+ allRequiredTags.forEach((tag) => {
2441
2926
  requiredTagObj[tag?.fullPath] = '';
2442
2927
  });
2443
2928
  if (allTags.length > 0) {
2444
2929
  const payload = preparePreviewPayload(
2445
- channel,
2930
+ activeChannelForActions,
2446
2931
  formData || {},
2447
- getCurrentContent,
2932
+ activeContentForActions,
2448
2933
  {
2449
2934
  ...requiredTagObj,
2450
2935
  },
@@ -2453,7 +2938,7 @@ const CommonTestAndPreview = (props) => {
2453
2938
  actions.getPrefilledValuesRequested(payload);
2454
2939
  }
2455
2940
  }
2456
- }, [selectedCustomer]);
2941
+ }, [selectedCustomer, allTags.length, activeChannelForActions, activePreviewTab]);
2457
2942
 
2458
2943
  /**
2459
2944
  * Update custom values with prefilled values from API
@@ -2464,24 +2949,29 @@ const CommonTestAndPreview = (props) => {
2464
2949
  if (prefilledValues && selectedCustomer) {
2465
2950
  // Always replace all values with prefilled values
2466
2951
  const updatedValues = {};
2467
- [...requiredTags, ...optionalTags].forEach((tag) => {
2952
+ allTags.forEach((tag) => {
2468
2953
  updatedValues[tag?.fullPath] = prefilledValues[tag?.fullPath] || '';
2469
2954
  });
2470
2955
 
2471
2956
  setCustomValues(updatedValues);
2472
2957
 
2473
2958
  // Update preview with prefilled values (this is a valid preview call trigger)
2959
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped.
2960
+ // SMS fallback preview is kept in sync by syncSmsFallbackPreview via smsFallbackPreviewText.
2961
+ const previewChannelForPrefill = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
2962
+ const previewContentForPrefill = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2474
2963
  const payload = preparePreviewPayload(
2475
- channel,
2964
+ previewChannelForPrefill,
2476
2965
  formData || {},
2477
- getCurrentContent,
2966
+ previewContentForPrefill,
2478
2967
  updatedValues,
2479
2968
  selectedCustomer
2480
2969
  );
2481
2970
  actions.updatePreviewRequested(payload);
2482
2971
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2972
+ void syncSmsFallbackPreview(updatedValues, selectedCustomer);
2483
2973
  }
2484
- }, [JSON.stringify(prefilledValues), selectedCustomer]);
2974
+ }, [JSON.stringify(prefilledValues), selectedCustomer, activeChannelForActions, activePreviewTab]);
2485
2975
 
2486
2976
  /**
2487
2977
  * Map channel constants to display names (lowercase for message)
@@ -2575,11 +3065,7 @@ const CommonTestAndPreview = (props) => {
2575
3065
  setTagsExtracted(true); // Auto-open custom values editor
2576
3066
 
2577
3067
  // Clear any existing values while waiting for prefilled values
2578
- const emptyValues = {};
2579
- [...requiredTags, ...optionalTags].forEach((tag) => {
2580
- emptyValues[tag?.fullPath] = '';
2581
- });
2582
- setCustomValues(emptyValues);
3068
+ setCustomValues(buildEmptyValues());
2583
3069
  };
2584
3070
 
2585
3071
  /**
@@ -2593,11 +3079,7 @@ const CommonTestAndPreview = (props) => {
2593
3079
  actions.clearPreviewErrors();
2594
3080
 
2595
3081
  // Initialize empty values for all tags
2596
- const emptyValues = {};
2597
- [...requiredTags, ...optionalTags].forEach((tag) => {
2598
- emptyValues[tag?.fullPath] = '';
2599
- });
2600
- setCustomValues(emptyValues);
3082
+ setCustomValues(buildEmptyValues());
2601
3083
 
2602
3084
  // Don't make preview call when clearing selection - just reset to raw content
2603
3085
  // Preview will be shown using raw formData/content
@@ -2633,17 +3115,20 @@ const CommonTestAndPreview = (props) => {
2633
3115
  */
2634
3116
  const handleDiscardCustomValues = () => {
2635
3117
  // Initialize empty values for all tags
2636
- const emptyValues = {};
2637
- [...requiredTags, ...optionalTags].forEach((tag) => {
2638
- emptyValues[tag?.fullPath] = '';
2639
- });
3118
+ const emptyValues = buildEmptyValues();
2640
3119
  setCustomValues(emptyValues);
2641
3120
 
3121
+ // Reset SMS fallback preview so it shows raw template (with {{tags}} visible) after discard
3122
+ setSmsFallbackPreviewText(undefined);
3123
+
2642
3124
  // Update preview with empty values (this is a valid preview call trigger)
3125
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped.
3126
+ const previewChannelForDiscard = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
3127
+ const previewContentForDiscard = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2643
3128
  const payload = preparePreviewPayload(
2644
- channel,
3129
+ previewChannelForDiscard,
2645
3130
  formData || {},
2646
- getCurrentContent,
3131
+ previewContentForDiscard,
2647
3132
  emptyValues,
2648
3133
  selectedCustomer
2649
3134
  );
@@ -2657,14 +3142,24 @@ const CommonTestAndPreview = (props) => {
2657
3142
  */
2658
3143
  const handleUpdatePreview = async () => {
2659
3144
  try {
3145
+ // Capture customValues at click time so the carousel preview only updates here,
3146
+ // not on every subsequent keystroke.
3147
+ previewCustomValuesRef.current = customValues;
3148
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped,
3149
+ // even when the user triggers update from the SMS fallback tab.
3150
+ // SMS fallback preview is kept in sync by syncSmsFallbackPreview via smsFallbackPreviewText.
3151
+ const previewChannel = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
3152
+ const previewContent = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2660
3153
  const payload = preparePreviewPayload(
2661
- channel,
3154
+ previewChannel,
2662
3155
  formData || {},
2663
- getCurrentContent,
3156
+ previewContent,
2664
3157
  customValues,
2665
3158
  selectedCustomer
2666
3159
  );
2667
3160
  await actions.updatePreviewRequested(payload);
3161
+
3162
+ await syncSmsFallbackPreview(customValues, selectedCustomer);
2668
3163
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2669
3164
  } catch (error) {
2670
3165
  CapNotification.error({
@@ -2674,25 +3169,115 @@ const CommonTestAndPreview = (props) => {
2674
3169
  };
2675
3170
 
2676
3171
  /**
2677
- * Handle extract tags
3172
+ * Categorize extracted tags into required/optional.
2678
3173
  */
2679
- const handleExtractTags = () => {
2680
- // Get content based on channel
2681
- let contentToExtract = getCurrentContent;
3174
+ const categorizeTags = (tagsTree = []) => {
3175
+ const required = [];
3176
+ const optional = [];
3177
+ let hasPersonalizationTags = false;
3178
+ const processTag = (tag, parentPath = '') => {
3179
+ const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
3180
+
3181
+ // Skip unsubscribe tag for input fields
3182
+ if (tag?.name === UNSUBSCRIBE_TAG_NAME) return;
3183
+
3184
+ hasPersonalizationTags = true;
3185
+ const userDriven = tag?.metaData?.userDriven;
3186
+ if (userDriven === true) {
3187
+ optional.push({ ...tag, fullPath: currentPath });
3188
+ } else {
3189
+ // false or missing (SMS/DLT extract often omits metaData) → required for test values
3190
+ required.push({ ...tag, fullPath: currentPath });
3191
+ }
3192
+
3193
+ if (tag?.children?.length > 0) {
3194
+ tag.children.forEach((child) => processTag(child, currentPath));
3195
+ }
3196
+ };
3197
+
3198
+ (tagsTree || []).forEach((tag) => processTag(tag));
3199
+ return { required, optional, hasPersonalizationTags };
3200
+ };
3201
+
3202
+ /**
3203
+ * Apply tag extraction when content comes from RCS + SMS fallback (no API call).
3204
+ */
3205
+ const applyRcsSmsFallbackTagExtraction = () => {
3206
+ const rcsPrimaryCategorized = categorizeTags(extractedTags ?? []);
3207
+ const fallbackSmsResolvedForTags = smsFallbackTextForTagExtraction ?? '';
3208
+ let fallbackSmsTagTree = smsFallbackExtractedTags?.length > 0
3209
+ ? smsFallbackExtractedTags
3210
+ : buildSyntheticSmsMustacheTags(fallbackSmsResolvedForTags);
3211
+ if (!smsTemplateHasMustacheTags(fallbackSmsResolvedForTags)) {
3212
+ fallbackSmsTagTree = [];
3213
+ }
3214
+ const fallbackSmsCategorized = categorizeTags(fallbackSmsTagTree);
3215
+ setRequiredTags(rcsPrimaryCategorized.required);
3216
+ setOptionalTags(rcsPrimaryCategorized.optional);
3217
+ setSmsFallbackRequiredTags(fallbackSmsCategorized.required);
3218
+ setSmsFallbackOptionalTags(fallbackSmsCategorized.optional);
3219
+ setTagsExtracted(
3220
+ rcsPrimaryCategorized.hasPersonalizationTags || fallbackSmsCategorized.hasPersonalizationTags,
3221
+ );
3222
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, rcsPrimaryCategorized, fallbackSmsCategorized));
3223
+ };
2682
3224
 
3225
+ /**
3226
+ * When extract-tags API returns, map Redux `extractedTags` into required/optional so
3227
+ * CustomValuesEditor shows personalization fields (effect was previously commented out).
3228
+ * RCS + SMS fallback: merge primary + fallback tag trees when fallback template exists.
3229
+ */
3230
+ useEffect(() => {
3231
+ if (!show) return;
3232
+ const hasFallbackSmsTemplate = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
3233
+ if (channel === CHANNELS.RCS && hasFallbackSmsTemplate) {
3234
+ applyRcsSmsFallbackTagExtraction();
3235
+ return;
3236
+ }
3237
+ const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
3238
+ let smsTagSource = channel === CHANNELS.SMS && (!extractedTags || extractedTags.length === 0)
3239
+ ? buildSyntheticSmsMustacheTags(smsEditorBody)
3240
+ : (extractedTags ?? []);
3241
+ if (channel === CHANNELS.SMS && !smsTemplateHasMustacheTags(smsEditorBody)) {
3242
+ smsTagSource = [];
3243
+ }
3244
+ const { required, optional, hasPersonalizationTags } = categorizeTags(smsTagSource);
3245
+ setRequiredTags(required);
3246
+ setOptionalTags(optional);
3247
+ setTagsExtracted(hasPersonalizationTags);
3248
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, { required, optional }, { required: [], optional: [] }));
3249
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- applyRcsSmsFallbackTagExtraction closes over latest extractedTags/smsFallbackExtractedTags
3250
+ }, [show, extractedTags, channel, smsFallbackContent, smsFallbackExtractedTags, getCurrentContent, smsFallbackTextForTagExtraction]);
3251
+
3252
+ /**
3253
+ * Get content to run tag extraction on (channel-specific).
3254
+ */
3255
+ const getContentForTagExtraction = () => {
3256
+ let contentToExtract = activeContentForActions;
2683
3257
  if (channel === CHANNELS.EMAIL && formData) {
2684
3258
  const currentTabData = formData[currentTab - 1];
2685
3259
  const activeTab = currentTabData?.activeTab;
2686
3260
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
2687
3261
  contentToExtract = templateContent || contentToExtract;
2688
3262
  }
3263
+ return contentToExtract;
3264
+ };
2689
3265
 
2690
- // Check for personalization tags (excluding unsubscribe)
3266
+ /**
3267
+ * Handle extract tags
3268
+ */
3269
+ const handleExtractTags = () => {
3270
+ if (channel === CHANNELS.RCS) {
3271
+ applyRcsSmsFallbackTagExtraction();
3272
+ return;
3273
+ }
3274
+
3275
+ const contentToExtract = getContentForTagExtraction();
2691
3276
  const tags = contentToExtract.match(/{{[^}]+}}/g) || [];
2692
3277
  const hasPersonalizationTags = tags.some((tag) => !tag.includes(UNSUBSCRIBE_TAG_NAME));
3278
+ const onlyUnsubscribe = !hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME);
2693
3279
 
2694
- if (!hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME)) {
2695
- // If only unsubscribe tag is present, show noTagsExtracted message
3280
+ if (onlyUnsubscribe) {
2696
3281
  setTagsExtracted(false);
2697
3282
  setRequiredTags([]);
2698
3283
  setOptionalTags([]);
@@ -2700,10 +3285,9 @@ const CommonTestAndPreview = (props) => {
2700
3285
  return;
2701
3286
  }
2702
3287
 
2703
- // Extract tags
2704
3288
  setTagsExtracted(true);
2705
3289
  const { templateSubject, templateContent } = prepareTagExtractionPayload(
2706
- channel,
3290
+ activeChannelForActions,
2707
3291
  formData || {},
2708
3292
  contentToExtract
2709
3293
  );
@@ -2765,7 +3349,7 @@ const CommonTestAndPreview = (props) => {
2765
3349
  if (existingTestCustomer) {
2766
3350
  const entityId = existingTestCustomer.userId ?? existingTestCustomer.customerId;
2767
3351
  if (entityId != null) {
2768
- const id = 'customer:' + normalizeTestEntityId(entityId);
3352
+ const id = normalizeTestEntityId(entityId);
2769
3353
  setSelectedTestEntities((prev) => (
2770
3354
  prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
2771
3355
  ));
@@ -2802,7 +3386,7 @@ const CommonTestAndPreview = (props) => {
2802
3386
  (c) => String(c?.customerId) === customerIdFromLookup || String(c?.userId) === customerIdFromLookup
2803
3387
  );
2804
3388
  if (alreadyInTestListByCustomerId) {
2805
- const id = 'customer:' + normalizeTestEntityId(customerIdFromLookup);
3389
+ const id = normalizeTestEntityId(customerIdFromLookup);
2806
3390
  setSelectedTestEntities((prev) => (
2807
3391
  prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
2808
3392
  ));
@@ -2839,26 +3423,21 @@ const CommonTestAndPreview = (props) => {
2839
3423
  const handleSendTestMessage = () => {
2840
3424
  const allUserIds = [];
2841
3425
  selectedTestEntities.forEach((entityId) => {
2842
- if (String(entityId).startsWith('group:')) {
2843
- const rawId = String(entityId).slice('group:'.length);
2844
- const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, rawId));
2845
- if (group) {
2846
- allUserIds.push(...group.userIds);
2847
- }
3426
+ const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, entityId));
3427
+ if (group) {
3428
+ allUserIds.push(...group.userIds);
2848
3429
  } else {
2849
- const rawId = String(entityId).startsWith('customer:')
2850
- ? String(entityId).slice('customer:'.length)
2851
- : String(entityId);
2852
- allUserIds.push(Number(rawId));
3430
+ allUserIds.push(entityId);
2853
3431
  }
2854
3432
  });
2855
3433
  const uniqueUserIds = [...new Set(allUserIds)];
2856
3434
 
2857
- const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP].includes(channel)
3435
+ const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS].includes(channel)
2858
3436
  ? testPreviewDeliverySettings[channel]
2859
3437
  : null;
2860
3438
 
2861
- // Create initial payload based on channel
3439
+ // createMessageMeta must match the creative channel and full creative (RCS + SMS fallback in one meta).
3440
+ // Do not use activeChannelForActions / activeContentForActions — those follow the RCS vs Fallback SMS *preview* tab.
2862
3441
  const initialPayload = prepareTestMessagePayload(
2863
3442
  channel,
2864
3443
  formData || content || {},
@@ -2866,7 +3445,8 @@ const CommonTestAndPreview = (props) => {
2866
3445
  customValues,
2867
3446
  uniqueUserIds,
2868
3447
  previewData,
2869
- deliveryOverride
3448
+ deliveryOverride,
3449
+ {},
2870
3450
  );
2871
3451
 
2872
3452
  actions.createMessageMetaRequested(
@@ -2899,11 +3479,10 @@ const CommonTestAndPreview = (props) => {
2899
3479
  // ============================================
2900
3480
  // RENDER HELPER FUNCTIONS
2901
3481
  // ============================================
2902
-
2903
3482
  const renderLeftPanelContent = () => (
2904
3483
  <LeftPanelContent
2905
- isExtractingTags={isExtractingTags}
2906
- extractedTags={extractedTags}
3484
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
3485
+ extractedTags={leftPanelExtractedTags}
2907
3486
  selectedCustomer={selectedCustomer}
2908
3487
  handleCustomerSelect={handleCustomerSelect}
2909
3488
  handleSearchCustomer={handleSearchCustomer}
@@ -2921,15 +3500,29 @@ const CommonTestAndPreview = (props) => {
2921
3500
 
2922
3501
  const renderCustomValuesEditor = () => (
2923
3502
  <CustomValuesEditor
2924
- isExtractingTags={isExtractingTags}
3503
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
2925
3504
  isUpdatePreviewDisabled={isUpdatePreviewDisabled}
2926
3505
  showJSON={showJSON}
2927
3506
  setShowJSON={setShowJSON}
2928
3507
  customValues={customValues}
2929
3508
  handleJSONTextChange={handleJSONTextChange}
2930
- extractedTags={extractedTags}
2931
- requiredTags={requiredTags}
2932
- optionalTags={optionalTags}
3509
+ sections={[
3510
+ {
3511
+ key: channel,
3512
+ title:
3513
+ channel === CHANNELS.RCS
3514
+ ? messages.rcsTagsSectionTitle
3515
+ : messages[`${channel}TagsSectionTitle`],
3516
+ requiredTags,
3517
+ optionalTags,
3518
+ },
3519
+ {
3520
+ key: PREVIEW_TAB_SMS_FALLBACK,
3521
+ title: isRcsSmsFallbackPreviewEnabled ? messages.smsFallbackTagsSectionTitle : null,
3522
+ requiredTags: smsFallbackRequiredTags,
3523
+ optionalTags: smsFallbackOptionalTags,
3524
+ },
3525
+ ]}
2933
3526
  handleCustomValueChange={handleCustomValueChange}
2934
3527
  handleDiscardCustomValues={handleDiscardCustomValues}
2935
3528
  handleUpdatePreview={handleUpdatePreview}
@@ -2945,18 +3538,13 @@ const CommonTestAndPreview = (props) => {
2945
3538
  }));
2946
3539
  };
2947
3540
 
2948
- /** Trim pasted emails (trailing CR/LF). SMS: strip non-digits so pasted formatted numbers match isValidMobile / API. */
3541
+ /** Trim pasted emails (trailing CR/LF). Allow any input for SMS; valid-only check is in renderAddTestCustomerButton. */
2949
3542
  const handleTestCustomersSearch = useCallback((value) => {
2950
3543
  if (value == null || value === '') {
2951
3544
  setSearchValue('');
2952
3545
  return;
2953
3546
  }
2954
- const raw = String(value).trim();
2955
- if (channel === CHANNELS.SMS) {
2956
- setSearchValue(formatPhoneNumber(raw));
2957
- } else {
2958
- setSearchValue(raw);
2959
- }
3547
+ setSearchValue(String(value).trim());
2960
3548
  }, [channel]);
2961
3549
 
2962
3550
  const renderSendTestMessage = () => (
@@ -2974,12 +3562,13 @@ const CommonTestAndPreview = (props) => {
2974
3562
  renderAddTestCustomerButton={renderAddTestCustomerButton}
2975
3563
  formatMessage={formatMessage}
2976
3564
  deliverySettings={testPreviewDeliverySettings[channel]}
2977
- senderDetailsOptions={senderDetailsByChannel[channel]}
3565
+ senderDetailsByChannel={senderDetailsByChannel}
2978
3566
  wecrmAccounts={wecrmAccounts}
2979
3567
  onSaveDeliverySettings={handleSaveDeliverySettings}
2980
3568
  isLoadingSenderDetails={isLoadingSenderDetails}
2981
3569
  smsTraiDltEnabled={smsTraiDltEnabled}
2982
3570
  registeredSenderIds={registeredSenderIds}
3571
+ isChannelSmsFallbackPreviewEnabled={isRcsSmsFallbackPreviewEnabled}
2983
3572
  searchValue={searchValue}
2984
3573
  setSearchValue={handleTestCustomersSearch}
2985
3574
  />
@@ -2993,14 +3582,13 @@ const CommonTestAndPreview = (props) => {
2993
3582
 
2994
3583
  const renderAddTestCustomerButton = () => {
2995
3584
  const raw = (searchValue || '').trim();
2996
- const value = channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw;
2997
3585
  const showAddButton =
2998
3586
  [CHANNELS.EMAIL, CHANNELS.SMS].includes(channel) &&
2999
- (channel === CHANNELS.EMAIL ? isValidEmail(value) : isValidMobile(value));
3587
+ (channel === CHANNELS.EMAIL ? isValidEmail(raw) : isValidMobile(formatPhoneNumber(raw)));
3000
3588
  if (!showAddButton) return null;
3001
3589
  return (
3002
3590
  <AddTestCustomerButton
3003
- searchValue={value}
3591
+ searchValue={channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw}
3004
3592
  handleAddTestCustomer={handleAddTestCustomer}
3005
3593
  />
3006
3594
  );