@capillarytech/creatives-library 9.0.15-alpha.0 → 9.0.15-alpha.3

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 (132) hide show
  1. package/constants/unified.js +29 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/commonUtils.js +19 -1
  6. package/utils/rcsPayloadUtils.js +92 -0
  7. package/utils/templateVarUtils.js +201 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +8 -1
  11. package/v2Components/CapActionButton/index.js +202 -144
  12. package/v2Components/CapActionButton/index.scss +232 -14
  13. package/v2Components/CapActionButton/messages.js +19 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +42 -18
  15. package/v2Components/CapImageUpload/index.js +2 -2
  16. package/v2Components/CapTagList/index.js +10 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +60 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +202 -22
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  28. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  29. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  30. package/v2Components/CommonTestAndPreview/index.js +761 -222
  31. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  32. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  33. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  34. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +284 -267
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  39. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  40. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  46. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  47. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  48. package/v2Components/SmsFallback/constants.js +94 -0
  49. package/v2Components/SmsFallback/index.js +958 -0
  50. package/v2Components/SmsFallback/index.scss +266 -0
  51. package/v2Components/SmsFallback/messages.js +78 -0
  52. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  53. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  54. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  58. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  59. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  60. package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
  61. package/v2Components/TemplatePreview/constants.js +2 -0
  62. package/v2Components/TemplatePreview/index.js +147 -32
  63. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  64. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  65. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  66. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  67. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  68. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  69. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  70. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  71. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  72. package/v2Containers/CommunicationFlow/index.js +3 -3
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  75. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +17 -0
  76. package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
  77. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  78. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  79. package/v2Containers/CreativesContainer/constants.js +11 -0
  80. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  81. package/v2Containers/CreativesContainer/index.js +323 -104
  82. package/v2Containers/CreativesContainer/index.scss +83 -1
  83. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  84. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +79 -34
  85. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  86. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  87. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +333 -90
  88. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  89. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  90. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  91. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  92. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  93. package/v2Containers/Rcs/constants.js +131 -11
  94. package/v2Containers/Rcs/index.js +2685 -832
  95. package/v2Containers/Rcs/index.scss +443 -8
  96. package/v2Containers/Rcs/messages.js +45 -6
  97. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +227 -0
  98. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74176 -39409
  99. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  100. package/v2Containers/Rcs/tests/index.test.js +151 -40
  101. package/v2Containers/Rcs/tests/mockData.js +38 -0
  102. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  103. package/v2Containers/Rcs/tests/utils.test.js +643 -30
  104. package/v2Containers/Rcs/utils.js +466 -12
  105. package/v2Containers/Sms/Create/index.js +115 -48
  106. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  107. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  108. package/v2Containers/SmsTrai/Create/index.js +9 -4
  109. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  110. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  111. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  112. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  113. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  114. package/v2Containers/SmsWrapper/index.js +37 -8
  115. package/v2Containers/TagList/index.js +6 -0
  116. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  117. package/v2Containers/Templates/_templates.scss +196 -12
  118. package/v2Containers/Templates/actions.js +11 -0
  119. package/v2Containers/Templates/constants.js +2 -0
  120. package/v2Containers/Templates/index.js +131 -59
  121. package/v2Containers/Templates/sagas.js +57 -13
  122. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  123. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  124. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  125. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  126. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  127. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  128. package/v2Containers/TemplatesV2/index.js +86 -23
  129. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  130. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  131. package/v2Containers/Whatsapp/index.js +3 -20
  132. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -31,7 +31,13 @@ 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 {
36
+ smsTemplateHasMustacheTags,
37
+ buildSyntheticSmsMustacheTags,
38
+ normalizeRcsTestCardMedia,
39
+ mapRcsSuggestionForTestMeta,
40
+ } from './utils';
35
41
  import AddTestCustomerButton from './AddTestCustomer';
36
42
  import ExistingCustomerModal from './ExistingCustomerModal';
37
43
  // Import constants
@@ -81,12 +87,23 @@ import {
81
87
  IMAGE,
82
88
  VIDEO,
83
89
  URL,
90
+ PREVIEW_TAB_RCS,
91
+ PREVIEW_TAB_SMS_FALLBACK,
84
92
  CHANNELS_USING_ANDROID_PREVIEW_DEVICE,
85
93
  DAYS,
94
+ RCS_TEST_META_CONTENT_TYPE_RICHCARD,
95
+ RCS_TEST_META_CARD_TYPE_STANDALONE,
96
+ RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
97
+ RCS_TEST_META_CARD_WIDTH_SMALL,
86
98
  } from './constants';
87
-
88
- // Import utilities
89
99
  import { getCdnUrl } from '../../utils/cdnTransformation';
100
+ import {
101
+ normalizePreviewApiPayload,
102
+ extractPreviewFromLiquidResponse,
103
+ getSmsFallbackTextForTagExtraction,
104
+ } from './previewApiUtils';
105
+ import { pickFirstSmsFallbackTemplateString } from '../../v2Containers/Rcs/rcsLibraryHydrationUtils';
106
+
90
107
  import { isValidEmail, isValidMobile, formatPhoneNumber } from '../../utils/commonUtils';
91
108
  import { getMembersLookup } from '../../services/api';
92
109
 
@@ -109,6 +126,32 @@ const filterUsableGsmSendersForDomain = (domain, gsmSenders, { skipDomainNameEch
109
126
  });
110
127
  };
111
128
 
129
+ /** Preview payload from Redux may be an Immutable Map — normalize for React state. */
130
+ const toPlainPreviewData = (data) => {
131
+ if (data == null) return null;
132
+ const plain = typeof data.toJS === 'function' ? data.toJS() : data;
133
+ return normalizePreviewApiPayload(plain);
134
+ };
135
+
136
+ /**
137
+ * Merge existing customValues with tag keys from categorized groups.
138
+ * Each group is { required, optional } (arrays of tag objects with fullPath).
139
+ * Preserves existing values; adds '' for any tag key not yet present.
140
+ * Reusable for RCS+fallback and any flow that needs to ensure customValues has keys for tags.
141
+ */
142
+ const mergeCustomValuesWithTagKeys = (prev, ...categorizedGroups) => {
143
+ const next = { ...(prev || {}) };
144
+ categorizedGroups.forEach((group) => {
145
+ [...(group.required || []), ...(group.optional || [])].forEach((tag) => {
146
+ const key = tag?.fullPath;
147
+ if (key && next[key] === undefined) next[key] = '';
148
+ });
149
+ });
150
+ return next;
151
+ };
152
+
153
+ /** True when `body` contains `{{name}}` mustache tokens (user-fillable personalization tags).
154
+ * DLT `{#name#}` slots are pre-bound template variables and are intentionally excluded. */
112
155
 
113
156
  /**
114
157
  * CapTreeSelect and group resolution use strict equality; API data may mix numeric and string ids.
@@ -134,7 +177,7 @@ const testEntityIdsEqual = (a, b) => {
134
177
  */
135
178
  const CommonTestAndPreview = (props) => {
136
179
  const {
137
- intl: { formatMessage },
180
+ intl: { formatMessage, locale: userLocale = 'en' },
138
181
  show,
139
182
  onClose,
140
183
  channel, // The channel: 'EMAIL', 'SMS', 'RCS', etc.
@@ -170,13 +213,23 @@ const CommonTestAndPreview = (props) => {
170
213
  ...additionalProps
171
214
  } = props;
172
215
 
216
+ const smsFallbackContent = additionalProps?.smsFallbackContent;
217
+ const smsFallbackTextForTagExtraction = useMemo(
218
+ () => getSmsFallbackTextForTagExtraction(smsFallbackContent),
219
+ [smsFallbackContent],
220
+ );
173
221
  // ============================================
174
222
  // STATE MANAGEMENT
175
223
  // ============================================
176
224
  const [selectedCustomer, setSelectedCustomer] = useState(null);
177
225
  const [requiredTags, setRequiredTags] = useState([]);
178
226
  const [optionalTags, setOptionalTags] = useState([]);
227
+ const [smsFallbackExtractedTags, setSmsFallbackExtractedTags] = useState([]);
228
+ const [smsFallbackRequiredTags, setSmsFallbackRequiredTags] = useState([]);
229
+ const [smsFallbackOptionalTags, setSmsFallbackOptionalTags] = useState([]);
230
+ const [isExtractingSmsFallbackTags, setIsExtractingSmsFallbackTags] = useState(false);
179
231
  const [customValues, setCustomValues] = useState({});
232
+ const previewCustomValuesRef = useRef({});
180
233
  const [showJSON, setShowJSON] = useState(false);
181
234
  const [tagsExtracted, setTagsExtracted] = useState(false);
182
235
 
@@ -187,6 +240,8 @@ const CommonTestAndPreview = (props) => {
187
240
  const [customerData, setCustomerData] = useState({ name: '', email: '', mobile: '', customerId: '' });
188
241
 
189
242
  const [previewDevice, setPreviewDevice] = useState(initialDevice);
243
+ const [activePreviewTab, setActivePreviewTab] = useState(PREVIEW_TAB_RCS);
244
+ const [smsFallbackPreviewText, setSmsFallbackPreviewText] = useState(undefined);
190
245
  // Track if a preview call has been made (to know when to use previewDataHtml vs raw content)
191
246
  const [hasPreviewCallBeenMade, setHasPreviewCallBeenMade] = useState(false);
192
247
  const [previewDataHtml, setPreviewDataHtml] = useState(() => {
@@ -219,15 +274,22 @@ const CommonTestAndPreview = (props) => {
219
274
  [CHANNELS.WHATSAPP]: {
220
275
  domainId: null, senderMobNum: '', sourceAccountIdentifier: '',
221
276
  },
277
+ [CHANNELS.RCS]: {
278
+ domainId: null,
279
+ domainGatewayMapId: null,
280
+ gsmSenderId: '',
281
+ smsFallbackDomainId: null,
282
+ cdmaSenderId: '', // gsmSenderId = RCS sender (domainId|senderId), cdmaSenderId = SMS fallback
283
+ },
222
284
  });
223
285
 
224
- const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP];
286
+ const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS];
225
287
  const formDataForSendTest = formData ?? (content && typeof content === 'object' && !Array.isArray(content) ? content : formData);
226
288
  const smsTemplateConfigs = formDataForSendTest?.templateConfigs || {};
227
289
  const smsTraiDltEnabled = !!smsTemplateConfigs?.traiDltEnabled;
228
290
  const registeredSenderIds = smsTemplateConfigs?.registeredSenderIds || [];
229
291
 
230
- // Fetch sender details and WeCRM accounts when Test & Preview opens for SMS/Email/WhatsApp
292
+ // Fetch sender details and WeCRM accounts when Test & Preview opens (SMS, Email, WhatsApp, RCS — same process)
231
293
  useEffect(() => {
232
294
  if (!show || !channel) {
233
295
  return;
@@ -236,39 +298,99 @@ const CommonTestAndPreview = (props) => {
236
298
  if (actions.getSenderDetailsRequested) {
237
299
  actions.getSenderDetailsRequested({ channel, orgUnitId: orgUnitId ?? -1 });
238
300
  }
301
+ // SMS domains/senders are needed for RCS delivery UI (fallback row + slidebox) whenever RCS is open — not only when fallback body exists.
302
+ if (channel === CHANNELS.RCS && actions.getSenderDetailsRequested) {
303
+ actions.getSenderDetailsRequested({ channel: CHANNELS.SMS, orgUnitId: orgUnitId ?? -1 });
304
+ }
239
305
  if (channel === CHANNELS.WHATSAPP && actions.getWeCrmAccountsRequested) {
240
306
  actions.getWeCrmAccountsRequested({ sourceName: CHANNELS.WHATSAPP });
241
307
  }
242
308
  }
243
309
  }, [show, channel, orgUnitId, actions]);
244
310
 
245
- useEffect(() => {
246
- if (!show) {
247
- setCustomerModal([false, '']);
248
- setSearchValue('');
249
- setCustomerData({ name: '', email: '', mobile: '', customerId: '' });
250
- }
251
- }, [show]);
252
-
253
311
  const findDefault = (arr) => (arr && arr.find((x) => x.default)) || (arr && arr[0]) || {};
254
312
 
255
313
  // Auto-set default delivery setting when sender details load (campaigns-style: first domain + default/first sender)
256
314
  useEffect(() => {
257
315
  if (!channel || !channelsWithDeliverySettings.includes(channel)) return;
316
+
317
+ if (channel === CHANNELS.RCS) {
318
+ const rcsDomainRows = senderDetailsByChannel?.[CHANNELS.RCS] || [];
319
+ const smsFallbackDomainRows = senderDetailsByChannel?.[CHANNELS.SMS] || [];
320
+ if (!rcsDomainRows.length) return;
321
+
322
+ const currentRcsDeliverySettings = testPreviewDeliverySettings?.[CHANNELS.RCS] || {};
323
+ const isRcsGsmSenderUnset = !currentRcsDeliverySettings?.gsmSenderId;
324
+ const isSmsFallbackSenderUnset = !currentRcsDeliverySettings?.cdmaSenderId;
325
+ const isRcsDeliveryFullyUnset = isRcsGsmSenderUnset && isSmsFallbackSenderUnset;
326
+ const shouldOnlyFillSmsFallbackSender =
327
+ !isRcsGsmSenderUnset && isSmsFallbackSenderUnset && smsFallbackDomainRows.length > 0;
328
+
329
+ if (!isRcsDeliveryFullyUnset && !shouldOnlyFillSmsFallbackSender) return;
330
+
331
+ const firstRcsDomain = rcsDomainRows[0];
332
+ const firstSmsFallbackDomain = smsFallbackDomainRows[0];
333
+ const usableRcsGsmSenders = filterUsableGsmSendersForDomain(
334
+ firstRcsDomain,
335
+ firstRcsDomain?.gsmSenders,
336
+ { skipDomainNameEchoFilter: true },
337
+ );
338
+ const usableSmsFallbackGsmSenders = firstSmsFallbackDomain
339
+ ? filterUsableGsmSendersForDomain(firstSmsFallbackDomain, firstSmsFallbackDomain?.gsmSenders)
340
+ : [];
341
+ const defaultRcsGsmSender = usableRcsGsmSenders[0];
342
+ const defaultSmsFallbackGsmSender = usableSmsFallbackGsmSenders[0];
343
+ const rcsSenderCompositeValue =
344
+ firstRcsDomain?.domainId != null && defaultRcsGsmSender?.value != null
345
+ ? `${firstRcsDomain.domainId}|${defaultRcsGsmSender.value}`
346
+ : (defaultRcsGsmSender?.value || '');
347
+ const smsFallbackSenderCompositeValue =
348
+ firstSmsFallbackDomain?.domainId != null && defaultSmsFallbackGsmSender?.value != null
349
+ ? `${firstSmsFallbackDomain.domainId}|${defaultSmsFallbackGsmSender.value}`
350
+ : (defaultSmsFallbackGsmSender?.value || '');
351
+
352
+ setTestPreviewDeliverySettings((prev) => {
353
+ const previousRcsSettings = prev?.[CHANNELS.RCS] || {};
354
+ if (shouldOnlyFillSmsFallbackSender) {
355
+ if (!smsFallbackSenderCompositeValue) return prev;
356
+ return {
357
+ ...prev,
358
+ [CHANNELS.RCS]: {
359
+ ...previousRcsSettings,
360
+ smsFallbackDomainId: firstSmsFallbackDomain?.domainId ?? null,
361
+ cdmaSenderId: smsFallbackSenderCompositeValue,
362
+ },
363
+ };
364
+ }
365
+ return {
366
+ ...prev,
367
+ [CHANNELS.RCS]: {
368
+ domainId: firstRcsDomain?.domainId ?? null,
369
+ domainGatewayMapId: firstRcsDomain?.dgmId ?? null,
370
+ gsmSenderId: rcsSenderCompositeValue,
371
+ smsFallbackDomainId: firstSmsFallbackDomain?.domainId ?? null,
372
+ cdmaSenderId: smsFallbackSenderCompositeValue,
373
+ },
374
+ };
375
+ });
376
+ return;
377
+ }
378
+
258
379
  const domains = senderDetailsByChannel[channel];
259
380
  if (!domains || domains.length === 0) return;
260
381
  const {
261
- domainId = '', gsmSenderId = '', senderEmail = '', senderMobNum = '',
382
+ domainId = '', gsmSenderId = '', cdmaSenderId = '', senderEmail = '', senderMobNum = '',
262
383
  } = testPreviewDeliverySettings[channel] || {};
263
- const isEmptySelection = !domainId && !gsmSenderId && !senderEmail && !senderMobNum;
384
+ const isEmptySelection = !domainId && !gsmSenderId && !cdmaSenderId && !senderEmail && !senderMobNum;
264
385
  if (!isEmptySelection) return;
265
386
 
266
387
  const whatsappAccountFromForm = channel === CHANNELS.WHATSAPP ? formData?.accountName : undefined;
267
388
  const matchedWhatsappAccount = whatsappAccountFromForm
268
389
  ? (wecrmAccounts || []).find((account) => account?.name === whatsappAccountFromForm)
269
390
  : null;
270
- const smsDomains = channel === CHANNELS.SMS && smsTraiDltEnabled
271
- ? domains.filter((domain) => (domain.gsmSenders || []).some((gsm) => registeredSenderIds.includes(gsm.value)))
391
+ const smsDomains = channel === CHANNELS.SMS && smsTraiDltEnabled && registeredSenderIds?.length
392
+ ? domains.filter((domain) => (domain?.gsmSenders || []).some((gsm) =>
393
+ registeredSenderIds?.includes(gsm?.value)))
272
394
  : domains;
273
395
  const [defaultDomain] = domains;
274
396
  const [firstSmsDomain] = smsDomains;
@@ -283,8 +405,8 @@ const CommonTestAndPreview = (props) => {
283
405
  const next = { ...prev };
284
406
  if (channel === CHANNELS.SMS) {
285
407
  const smsGsmSenders = smsTraiDltEnabled
286
- ? (firstDomain.gsmSenders || []).filter((gsm) => registeredSenderIds.includes(gsm.value))
287
- : firstDomain.gsmSenders;
408
+ ? (firstDomain?.gsmSenders || []).filter((gsm) => registeredSenderIds?.includes(gsm?.value))
409
+ : firstDomain?.gsmSenders;
288
410
  next[channel] = {
289
411
  domainId: firstDomain.domainId,
290
412
  domainGatewayMapId: firstDomain.dgmId,
@@ -317,10 +439,29 @@ const CommonTestAndPreview = (props) => {
317
439
  // MEMOIZED VALUES
318
440
  // ============================================
319
441
 
442
+ const allTags = useMemo(
443
+ () => [...requiredTags, ...optionalTags, ...smsFallbackRequiredTags, ...smsFallbackOptionalTags],
444
+ [requiredTags, optionalTags, smsFallbackRequiredTags, smsFallbackOptionalTags]
445
+ );
446
+
447
+ const allRequiredTags = useMemo(
448
+ () => [...requiredTags, ...smsFallbackRequiredTags],
449
+ [requiredTags, smsFallbackRequiredTags]
450
+ );
451
+
452
+ const buildEmptyValues = useCallback(
453
+ () => allTags.reduce((acc, tag) => {
454
+ const key = tag?.fullPath;
455
+ if (key) acc[key] = '';
456
+ return acc;
457
+ }, {}),
458
+ [allTags]
459
+ );
460
+
320
461
  // Check if update preview button should be disabled
321
462
  const isUpdatePreviewDisabled = useMemo(() => (
322
- requiredTags.some((tag) => !customValues[tag.fullPath])
323
- ), [requiredTags, customValues]);
463
+ allRequiredTags.some((tag) => !customValues[tag.fullPath])
464
+ ), [allRequiredTags, customValues]);
324
465
 
325
466
  // Get current content based on channel and editor type
326
467
  const getCurrentContent = useMemo(() => {
@@ -364,6 +505,13 @@ const CommonTestAndPreview = (props) => {
364
505
  return currentTabData.base['sms-editor'];
365
506
  }
366
507
  }
508
+ // DLT / Test & Preview shape: { templateConfigs: { template, templateId, ... } }
509
+ if (formData.templateConfigs?.template) {
510
+ const smsDltTemplateValue = formData.templateConfigs.template;
511
+ if (typeof smsDltTemplateValue === 'string') return smsDltTemplateValue;
512
+ if (Array.isArray(smsDltTemplateValue)) return smsDltTemplateValue.join('');
513
+ return '';
514
+ }
367
515
  }
368
516
 
369
517
  // SMS channel fallback - if formData is not available, use content directly
@@ -416,7 +564,70 @@ const CommonTestAndPreview = (props) => {
416
564
  return content || '';
417
565
  }, [channel, formData, currentTab, beeContent, content, beeInstance]);
418
566
 
419
- // Build test entities tree data
567
+ const leftPanelExtractedTags = useMemo(() => {
568
+ if (channel === CHANNELS.SMS) {
569
+ const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
570
+ if (!smsTemplateHasMustacheTags(smsEditorBody)) return [];
571
+ const extractTagsFromApi = extractedTags ?? [];
572
+ if (extractTagsFromApi.length > 0) return extractTagsFromApi;
573
+ return buildSyntheticSmsMustacheTags(smsEditorBody);
574
+ }
575
+ const hasFallbackSmsBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
576
+ if (channel === CHANNELS.RCS && hasFallbackSmsBody) {
577
+ const rcsPrimaryTags = extractedTags ?? [];
578
+ const fallbackSmsTextForTags = smsFallbackTextForTagExtraction ?? '';
579
+ const fallbackSmsTagRows = smsTemplateHasMustacheTags(fallbackSmsTextForTags)
580
+ ? (smsFallbackExtractedTags?.length > 0
581
+ ? smsFallbackExtractedTags
582
+ : buildSyntheticSmsMustacheTags(fallbackSmsTextForTags))
583
+ : [];
584
+ const mergedRcsAndFallbackTags = [...rcsPrimaryTags, ...fallbackSmsTagRows];
585
+ if (mergedRcsAndFallbackTags.length > 0) return mergedRcsAndFallbackTags;
586
+ return buildSyntheticSmsMustacheTags(fallbackSmsTextForTags);
587
+ }
588
+ return extractedTags ?? [];
589
+ }, [
590
+ channel,
591
+ extractedTags,
592
+ getCurrentContent,
593
+ smsFallbackContent,
594
+ smsFallbackExtractedTags,
595
+ smsFallbackTextForTagExtraction,
596
+ ]);
597
+
598
+ const isRcsSmsFallbackPreviewEnabled =
599
+ channel === CHANNELS.RCS
600
+ && !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
601
+ // Only treat as SMS when user is on the Fallback SMS tab — not whenever fallback exists (RCS tab needs RCS preview API).
602
+ const isSmsFallbackTabActive = isRcsSmsFallbackPreviewEnabled && activePreviewTab === PREVIEW_TAB_SMS_FALLBACK;
603
+ const activeChannelForActions = isSmsFallbackTabActive ? CHANNELS.SMS : channel;
604
+ // VarSegment slot values live in rcsSmsFallbackVarMapped; raw templateContent alone is stale for /preview Body.
605
+ const resolvedSmsFallbackBodyForPreviewTab =
606
+ smsFallbackTextForTagExtraction
607
+ || smsFallbackContent?.templateContent
608
+ || smsFallbackContent?.content
609
+ || '';
610
+ const activeContentForActions = isSmsFallbackTabActive
611
+ ? resolvedSmsFallbackBodyForPreviewTab
612
+ : getCurrentContent;
613
+
614
+ /**
615
+ * SMS fallback pane must show /preview API result when user updated preview on that tab (plain text).
616
+ * Skip when resolvedBody is RCS-shaped (e.g. user last previewed on RCS tab).
617
+ */
618
+ // smsFallbackPreviewText is the single source of truth for the resolved SMS fallback preview.
619
+ // It is set only by syncSmsFallbackPreview (called from handleUpdatePreview and the
620
+ // prefilled-values effect) and reset to undefined on discard / slidebox close.
621
+ // Using previewDataHtml as a fallback is unsafe because that state is shared with the primary
622
+ // RCS preview and can contain stale SMS or RCS content.
623
+ const smsFallbackResolvedText = useMemo(() => {
624
+ const hasFallbackBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
625
+ if (channel !== CHANNELS.RCS || !hasFallbackBody) return undefined;
626
+ if (smsFallbackPreviewText != null) return smsFallbackPreviewText;
627
+ return undefined;
628
+ }, [channel, smsFallbackContent, smsFallbackPreviewText]);
629
+
630
+ // Build test entities tree data from testCustomers prop
420
631
  // Build test entities tree data from testCustomers prop (includes customers added via addTestCustomer action)
421
632
  const testEntitiesTreeData = useMemo(() => {
422
633
  const groupsNode = {
@@ -425,7 +636,7 @@ const CommonTestAndPreview = (props) => {
425
636
  selectable: false,
426
637
  children: testGroups?.map((group) => ({
427
638
  title: group?.groupName,
428
- value: 'group:' + normalizeTestEntityId(group?.groupId),
639
+ value: normalizeTestEntityId(group?.groupId),
429
640
  })),
430
641
  };
431
642
 
@@ -435,7 +646,7 @@ const CommonTestAndPreview = (props) => {
435
646
  selectable: false,
436
647
  children: testCustomers?.map((customer) => ({
437
648
  title: customer?.name?.trim() || customer?.email?.trim() || customer?.mobile?.trim() || customer?.userId || customer?.customerId,
438
- value: 'customer:' + normalizeTestEntityId(customer?.userId ?? customer?.customerId),
649
+ value: normalizeTestEntityId(customer?.userId ?? customer?.customerId),
439
650
  })) || [],
440
651
  };
441
652
 
@@ -515,11 +726,10 @@ const CommonTestAndPreview = (props) => {
515
726
  email: customerData?.email || '',
516
727
  mobile: customerData?.mobile || '',
517
728
  });
518
- const prefixedAddedId = 'customer:' + normalizedAddedId;
519
729
  setSelectedTestEntities((prev) => (
520
- prev.some((id) => id === prefixedAddedId)
730
+ prev.some((id) => testEntityIdsEqual(id, normalizedAddedId))
521
731
  ? prev
522
- : [...prev, prefixedAddedId]
732
+ : [...prev, normalizedAddedId]
523
733
  ));
524
734
  }
525
735
  handleCloseCustomerModal();
@@ -583,13 +793,33 @@ const CommonTestAndPreview = (props) => {
583
793
  whatsappContent: formDataObj?.whatsappContent,
584
794
  };
585
795
 
586
- case CHANNELS.RCS:
796
+ case CHANNELS.RCS: {
797
+ // For carousel, replace {{N}} tokens in carouselData with resolved semantic vars from
798
+ // formData.cardContent so the preview API can resolve {{member.firstName}} etc.
799
+ const formDataCardContent =
800
+ formDataObj?.versions?.base?.content?.RCS?.rcsContent?.cardContent
801
+ ?? formDataObj?.content?.RCS?.rcsContent?.cardContent;
802
+ let parsedRcs = {};
803
+ try { parsedRcs = JSON.parse(contentStr); } catch (e) { parsedRcs = typeof contentStr === 'object' ? contentStr : {}; }
804
+ if (Array.isArray(parsedRcs.carouselData) && Array.isArray(formDataCardContent) && formDataCardContent.length > 0) {
805
+ const resolvedCarouselData = parsedRcs.carouselData.map((card, idx) => {
806
+ const formCard = formDataCardContent[idx] || {};
807
+ return {
808
+ ...card,
809
+ title: formCard.title || card.title,
810
+ bodyText: formCard.description || card.bodyText,
811
+ };
812
+ });
813
+ return {
814
+ ...basePayload,
815
+ messageBody: JSON.stringify({ ...parsedRcs, carouselData: resolvedCarouselData }),
816
+ };
817
+ }
587
818
  return {
588
819
  ...basePayload,
589
820
  messageBody: contentStr,
590
- // messageTitle: resolveTagsInText(formDataObj?.rcsTitle || '', customValuesObj),
591
- // rcsDesc: formDataObj?.rcsDesc,
592
821
  };
822
+ }
593
823
 
594
824
  case CHANNELS.INAPP:
595
825
  return {
@@ -629,6 +859,37 @@ const CommonTestAndPreview = (props) => {
629
859
  }
630
860
  };
631
861
 
862
+ /**
863
+ * When RCS has SMS fallback, refresh fallback preview text via the same Liquid /preview API
864
+ * (separate call with SMS channel + fallback template body). Used after primary preview updates.
865
+ */
866
+ const syncSmsFallbackPreview = async (customValuesForResolve, selectedCustomerObj) => {
867
+ const fallbackBodyForLiquidPreview =
868
+ getSmsFallbackTextForTagExtraction(smsFallbackContent)
869
+ || smsFallbackContent?.templateContent
870
+ || smsFallbackContent?.content
871
+ || '';
872
+ if (channel !== CHANNELS.RCS || !String(fallbackBodyForLiquidPreview).trim()) return;
873
+ try {
874
+ const smsFallbackPayload = preparePreviewPayload(
875
+ CHANNELS.SMS,
876
+ formData || {},
877
+ fallbackBodyForLiquidPreview,
878
+ customValuesForResolve,
879
+ selectedCustomerObj
880
+ );
881
+ const fallbackResponse = await Api.updateEmailPreview(smsFallbackPayload);
882
+ const fallbackPreview = extractPreviewFromLiquidResponse(fallbackResponse);
883
+ setSmsFallbackPreviewText(
884
+ typeof fallbackPreview?.resolvedBody === 'string'
885
+ ? fallbackPreview.resolvedBody
886
+ : undefined
887
+ );
888
+ } catch (e) {
889
+ /* keep existing smsFallbackPreviewText on failure */
890
+ }
891
+ };
892
+
632
893
  /**
633
894
  * Prepare payload for tag extraction based on channel
634
895
  */
@@ -652,11 +913,32 @@ const CommonTestAndPreview = (props) => {
652
913
  templateContent: contentStr,
653
914
  };
654
915
 
655
- case CHANNELS.RCS:
916
+ case CHANNELS.RCS: {
917
+ // Carousel templates don't have rcsTitle/rcsDesc — their content lives in cardContent[].
918
+ // Use the resolved card strings from formDataObj (resolved by resolveTemplateWithMap in
919
+ // testPreviewFormData) so the extraction API receives actual Capillary tag expressions
920
+ // instead of numeric {{1}}/{{2}} placeholders.
921
+ const rcsCardContentArr =
922
+ formDataObj?.versions?.base?.content?.RCS?.rcsContent?.cardContent
923
+ ?? formDataObj?.content?.RCS?.rcsContent?.cardContent;
924
+ if (Array.isArray(rcsCardContentArr) && rcsCardContentArr.length > 0) {
925
+ const carouselTagText = rcsCardContentArr
926
+ .map((c) => `${c.title || ''} ${c.description || ''}`)
927
+ .join(' ')
928
+ .trim();
929
+ if (carouselTagText) {
930
+ return {
931
+ templateSubject: formDataObj?.rcsTitle || '',
932
+ templateContent: carouselTagText,
933
+ };
934
+ }
935
+ }
936
+ // Standalone (non-carousel) card: use rcsTitle/rcsDesc directly.
656
937
  return {
657
938
  templateSubject: formDataObj?.rcsTitle || '',
658
939
  templateContent: formDataObj?.rcsDesc || contentStr,
659
940
  };
941
+ }
660
942
 
661
943
  case CHANNELS.INAPP:
662
944
  return {
@@ -729,7 +1011,7 @@ const CommonTestAndPreview = (props) => {
729
1011
  } = carousel || {};
730
1012
  const buttonData = buttons.map((button, index) => {
731
1013
  const {
732
- type, text, phone_number, urlType, url,
1014
+ type, text, phone_number: phoneNumber, urlType, url,
733
1015
  } = button || {};
734
1016
  const buttonObj = {
735
1017
  type,
@@ -737,7 +1019,7 @@ const CommonTestAndPreview = (props) => {
737
1019
  index,
738
1020
  };
739
1021
  if (type === PHONE_NUMBER) {
740
- buttonObj.phoneNumber = phone_number;
1022
+ buttonObj.phoneNumber = phoneNumber;
741
1023
  }
742
1024
  if (type === URL) {
743
1025
  buttonObj.url = url;
@@ -766,7 +1048,159 @@ const CommonTestAndPreview = (props) => {
766
1048
  };
767
1049
  });
768
1050
 
769
- const prepareTestMessagePayload = (channelType, formDataObj, contentStr, customValuesObj, recipientDetails, previewDataObj, deliverySettingsOverride) => {
1051
+ /**
1052
+ * Build createMessageMeta payload for RCS (test message).
1053
+ * rcsMessageContent: { channel, accountId?, rcsRichCardContent: { contentType, cardType, cardSettings, cardContent }, smsFallBackContent? }
1054
+ * Then rcsDeliverySettings, executionParams, clientName last.
1055
+ */
1056
+ const buildRcsTestMessagePayload = (
1057
+ creativeFormData,
1058
+ _unusedEditorContentString,
1059
+ _customValuesObj,
1060
+ deliverySettingsOverride,
1061
+ basePayload,
1062
+ _rcsTestMetaExtras = {},
1063
+ ) => {
1064
+ const rcsSectionFromForm =
1065
+ creativeFormData?.versions?.base?.content?.RCS ?? creativeFormData?.content?.RCS ?? {};
1066
+ const rcsContentFromForm = rcsSectionFromForm?.rcsContent || {};
1067
+ const smsFallbackFromCreativeForm = rcsSectionFromForm?.smsFallBackContent || {};
1068
+ let rcsCardPayloadList = [];
1069
+ if (Array.isArray(rcsContentFromForm?.cardContent)) {
1070
+ rcsCardPayloadList = rcsContentFromForm.cardContent;
1071
+ } else if (rcsContentFromForm?.cardContent) {
1072
+ rcsCardPayloadList = [rcsContentFromForm.cardContent];
1073
+ }
1074
+ // Raw title/description with template tags; SMS fallback uses tagged template fields (pickFirst…).
1075
+ const cardContentForTestMetaApi = rcsCardPayloadList.map((singleRcsCardPayload) => {
1076
+ const normalizedCardMediaForTestApi = singleRcsCardPayload?.media
1077
+ ? normalizeRcsTestCardMedia(singleRcsCardPayload.media)
1078
+ : undefined;
1079
+ const suggestionsFromCard = Array.isArray(singleRcsCardPayload?.suggestions)
1080
+ ? singleRcsCardPayload.suggestions
1081
+ : [];
1082
+ const suggestionsFormattedForTestMeta = suggestionsFromCard.map((suggestionItem, index) =>
1083
+ mapRcsSuggestionForTestMeta(suggestionItem, index));
1084
+ return {
1085
+ title: singleRcsCardPayload?.title ?? '',
1086
+ description: singleRcsCardPayload?.description ?? '',
1087
+ mediaType: singleRcsCardPayload?.mediaType ?? MEDIA_TYPE_TEXT,
1088
+ ...(normalizedCardMediaForTestApi && { media: normalizedCardMediaForTestApi }),
1089
+ ...(suggestionsFormattedForTestMeta.length > 0 && {
1090
+ suggestions: suggestionsFormattedForTestMeta,
1091
+ }),
1092
+ };
1093
+ });
1094
+ // Use the component-level smsFallbackContent prop (has rcsSmsFallbackVarMapped) so DLT
1095
+ // {#var#} slots are converted to {{tagName}} mustache tags before sending to createMessageMeta.
1096
+ const smsFallbackTaggedTemplateBody =
1097
+ getSmsFallbackTextForTagExtraction(smsFallbackContent)
1098
+ || pickFirstSmsFallbackTemplateString(smsFallbackFromCreativeForm)
1099
+ || '';
1100
+ const smsSenderFromDelivery = deliverySettingsOverride?.cdmaSenderId?.includes('|')
1101
+ ? deliverySettingsOverride.cdmaSenderId.split('|')[1]
1102
+ : deliverySettingsOverride?.cdmaSenderId;
1103
+ const deliveryFallbackSmsId =
1104
+ typeof smsSenderFromDelivery === 'string' ? smsSenderFromDelivery.trim() : '';
1105
+ const creativeFallbackSmsId =
1106
+ smsFallbackFromCreativeForm?.senderId != null
1107
+ ? String(smsFallbackFromCreativeForm.senderId).trim()
1108
+ : '';
1109
+ const fallbackSmsSenderIdForChannel = deliveryFallbackSmsId || creativeFallbackSmsId || '';
1110
+
1111
+ // For DLT orgs: include templateConfigs inside smsFallBackContent so createMessageMeta
1112
+ // receives the DLT template ID, registered sender IDs, and body alongside the message.
1113
+ // Priority: smsFallBackContent.templateConfigs from the RCS payload (clean API shape,
1114
+ // built by createPayload) → fallback to root-level formData.templateConfigs (strip the
1115
+ // UI-only traiDltEnabled flag before sending to the API).
1116
+ const smsFallbackTemplateConfigsForApi = (() => {
1117
+ if (
1118
+ smsFallbackFromCreativeForm?.templateConfigs
1119
+ && typeof smsFallbackFromCreativeForm.templateConfigs === 'object'
1120
+ ) {
1121
+ return smsFallbackFromCreativeForm.templateConfigs;
1122
+ }
1123
+ const rootTc = creativeFormData?.templateConfigs;
1124
+ if (rootTc && typeof rootTc === 'object') {
1125
+ // eslint-disable-next-line no-unused-vars
1126
+ const { traiDltEnabled: _uiFlag, ...tcForApi } = rootTc;
1127
+ return Object.keys(tcForApi).length > 0 ? tcForApi : null;
1128
+ }
1129
+ return null;
1130
+ })();
1131
+
1132
+ const smsFallBackContent =
1133
+ smsFallbackTaggedTemplateBody.trim() !== ''
1134
+ ? {
1135
+ message: smsFallbackTaggedTemplateBody,
1136
+ ...(smsFallbackTemplateConfigsForApi && {
1137
+ templateConfigs: smsFallbackTemplateConfigsForApi,
1138
+ }),
1139
+ }
1140
+ : undefined;
1141
+
1142
+ // accountId: WeCRM account id (not sourceAccountIdentifier) for createMessageMeta
1143
+ const accountIdForMeta =
1144
+ rcsContentFromForm?.accountId != null && String(rcsContentFromForm.accountId).trim() !== ''
1145
+ ? String(rcsContentFromForm.accountId)
1146
+ : undefined;
1147
+
1148
+ const rcsRichCardContent = {
1149
+ contentType: RCS_TEST_META_CONTENT_TYPE_RICHCARD,
1150
+ cardType: rcsContentFromForm?.cardType ?? RCS_TEST_META_CARD_TYPE_STANDALONE,
1151
+ cardSettings: rcsContentFromForm?.cardSettings ?? {
1152
+ cardOrientation: RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
1153
+ cardWidth: RCS_TEST_META_CARD_WIDTH_SMALL,
1154
+ },
1155
+ ...(cardContentForTestMetaApi.length > 0 && { cardContent: cardContentForTestMetaApi }),
1156
+ };
1157
+
1158
+ const rcsMessageContent = {
1159
+ channel: CHANNELS.RCS,
1160
+ ...(accountIdForMeta && { accountId: accountIdForMeta }),
1161
+ rcsRichCardContent,
1162
+ ...(smsFallBackContent && { smsFallBackContent }),
1163
+ };
1164
+ const rcsComposite = deliverySettingsOverride?.gsmSenderId ?? '';
1165
+ const [rcsDomainId, rcsSenderId] = rcsComposite.includes('|') ? rcsComposite.split('|') : ['', rcsComposite];
1166
+ const rcsDeliverySettings = {
1167
+ channelSettings: {
1168
+ channel: CHANNELS.RCS,
1169
+ rcsSender: (rcsSenderId || deliverySettingsOverride?.rcsSender) ?? '',
1170
+ domainId:
1171
+ rcsDomainId !== '' && rcsDomainId !== undefined && !Number.isNaN(Number(rcsDomainId))
1172
+ ? Number(rcsDomainId)
1173
+ : (deliverySettingsOverride?.domainId ?? 0),
1174
+ fallbackSmsSenderId: fallbackSmsSenderIdForChannel,
1175
+ },
1176
+ additionalSettings: {
1177
+ useTinyUrl: false,
1178
+ encryptUrl: false,
1179
+ linkTrackingEnabled: false,
1180
+ bypassControlUser: false,
1181
+ userSubscriptionDisabled: false,
1182
+ },
1183
+ };
1184
+ const { clientName: baseClientName = CLIENT_NAME_CREATIVES, ...restBase } = basePayload;
1185
+ return {
1186
+ ...restBase,
1187
+ rcsMessageContent,
1188
+ rcsDeliverySettings,
1189
+ executionParams: {},
1190
+ clientName: baseClientName,
1191
+ };
1192
+ };
1193
+
1194
+ const prepareTestMessagePayload = (
1195
+ channelType,
1196
+ formDataObj,
1197
+ contentStr,
1198
+ customValuesObj,
1199
+ recipientDetails,
1200
+ previewDataObj,
1201
+ deliverySettingsOverride,
1202
+ rcsExtra = {},
1203
+ ) => {
770
1204
  // Base payload structure common to all channels
771
1205
  const basePayload = {
772
1206
  ouId: -1,
@@ -847,12 +1281,12 @@ const CommonTestAndPreview = (props) => {
847
1281
  },
848
1282
  smsDeliverySettings: {
849
1283
  channelSettings: {
1284
+ channel: CHANNELS.SMS,
850
1285
  gsmSenderId: deliverySettingsOverride?.gsmSenderId ?? '',
851
1286
  domainId: deliverySettingsOverride?.domainId ?? null,
852
1287
  domainGatewayMapId: deliverySettingsOverride?.domainGatewayMapId ?? '',
853
1288
  targetNdnc: false,
854
1289
  cdmaSenderId: deliverySettingsOverride?.cdmaSenderId ?? '',
855
- channel: CHANNELS.SMS,
856
1290
  },
857
1291
  additionalSettings: {
858
1292
  useTinyUrl: false,
@@ -996,7 +1430,7 @@ const CommonTestAndPreview = (props) => {
996
1430
  return {
997
1431
  ...basePayload,
998
1432
  whatsappMessageContent: {
999
- messageBody: templateEditorValue || '',
1433
+ messageBody: resolvedMessageBody || templateEditorValue || '',
1000
1434
  accountId: formDataObj?.accountId || '',
1001
1435
  sourceAccountIdentifier: sourceAccountIdentifier || formDataObj?.sourceAccountIdentifier || '',
1002
1436
  accountName: formDataObj?.accountName || '',
@@ -1021,16 +1455,7 @@ const CommonTestAndPreview = (props) => {
1021
1455
  }
1022
1456
 
1023
1457
  case CHANNELS.RCS:
1024
- return {
1025
- ...basePayload,
1026
- rcsMessageContent: {
1027
- channel: CHANNELS.RCS,
1028
- messageBody: contentStr,
1029
- rcsType: additionalProps?.rcsType,
1030
- rcsImageSrc: formDataObj?.rcsImageSrc,
1031
- rcsSuggestions: formDataObj?.rcsSuggestions,
1032
- },
1033
- };
1458
+ return buildRcsTestMessagePayload(formDataObj, contentStr, customValuesObj, deliverySettingsOverride, basePayload, rcsExtra);
1034
1459
 
1035
1460
  case CHANNELS.INAPP: {
1036
1461
  // InApp payload structure similar to MobilePush
@@ -1869,6 +2294,46 @@ const CommonTestAndPreview = (props) => {
1869
2294
  *
1870
2295
  * IMPORTANT: Use raw content/formData initially, only use previewDataHtml if preview call was made
1871
2296
  */
2297
+ // Memoized RCS carousel content: only recomputes when the API response or raw template changes,
2298
+ // NOT on every customValues keystroke. Uses previewCustomValuesRef captured at Update Preview time.
2299
+ const rcsCarouselContentObj = useMemo(() => {
2300
+ if (channel !== CHANNELS.RCS) return null;
2301
+ let resolvedContent = null;
2302
+ if (hasPreviewCallBeenMade && previewDataHtml?.resolvedBody) {
2303
+ resolvedContent = previewDataHtml.resolvedBody;
2304
+ if (typeof resolvedContent === 'string') {
2305
+ try { resolvedContent = JSON.parse(resolvedContent); } catch (e) { resolvedContent = null; }
2306
+ }
2307
+ }
2308
+ if (resolvedContent && typeof resolvedContent === 'object') return resolvedContent;
2309
+
2310
+ let rawRcs = content;
2311
+ if (typeof rawRcs === 'string') {
2312
+ try { rawRcs = JSON.parse(rawRcs); } catch (e) { rawRcs = content; }
2313
+ }
2314
+ const snapCustomValues = previewCustomValuesRef.current;
2315
+ const hasEnteredValues = snapCustomValues && Object.keys(snapCustomValues).some((k) => snapCustomValues[k]);
2316
+ if (hasPreviewCallBeenMade && hasEnteredValues && Array.isArray(rawRcs?.carouselData)) {
2317
+ const formDataCardContent =
2318
+ formData?.versions?.base?.content?.RCS?.rcsContent?.cardContent
2319
+ ?? formData?.content?.RCS?.rcsContent?.cardContent
2320
+ ?? [];
2321
+ return {
2322
+ ...rawRcs,
2323
+ carouselData: rawRcs.carouselData.map((card, idx) => {
2324
+ const formCard = formDataCardContent[idx] || {};
2325
+ return {
2326
+ ...card,
2327
+ title: resolveTagsInText(formCard.title || card.title || '', snapCustomValues),
2328
+ bodyText: resolveTagsInText(formCard.description || card.bodyText || '', snapCustomValues),
2329
+ };
2330
+ }),
2331
+ };
2332
+ }
2333
+ return rawRcs || {};
2334
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2335
+ }, [channel, content, hasPreviewCallBeenMade, previewDataHtml, formData]);
2336
+
1872
2337
  const prepareUnifiedPreviewProps = () => {
1873
2338
  // Prepare content based on channel
1874
2339
  let contentObj = {};
@@ -1930,42 +2395,14 @@ const CommonTestAndPreview = (props) => {
1930
2395
  contentObj = parsedContent || {};
1931
2396
  }
1932
2397
  } else if (channel === CHANNELS.RCS) {
1933
- // For RCS, content is an object with rcsTitle, rcsDesc, rcsImageSrc, suggestions, etc.
1934
- // Content comes from RCS component state, passed via content prop
1935
- let resolvedContent = null;
1936
-
1937
- // Only use previewDataHtml if preview call was made
1938
- if (hasPreviewCallBeenMade && previewDataHtml?.resolvedBody) {
1939
- resolvedContent = previewDataHtml.resolvedBody;
1940
-
1941
- // Handle case where resolvedBody might be a JSON string
1942
- if (typeof resolvedContent === 'string') {
1943
- try {
1944
- resolvedContent = JSON.parse(resolvedContent);
1945
- } catch (e) {
1946
- // If parsing fails, treat as null so we fall back to content
1947
- resolvedContent = null;
1948
- }
1949
- }
1950
- }
1951
-
1952
- // Parse content if it's a string
2398
+ // rcsCarouselContentObj is a useMemo that handles both the API-response path and the
2399
+ // client-side fallback. It is stable across keystrokes (only recomputes when
2400
+ // hasPreviewCallBeenMade / previewDataHtml / content / formData change).
1953
2401
  let parsedRcsContent = content;
1954
2402
  if (typeof content === 'string') {
1955
- try {
1956
- parsedRcsContent = JSON.parse(content);
1957
- } catch (e) {
1958
- parsedRcsContent = content;
1959
- }
1960
- }
1961
-
1962
- // Use resolvedContent if available (from preview call), otherwise use raw content
1963
- if (resolvedContent && typeof resolvedContent === 'object') {
1964
- contentObj = { ...resolvedContent };
1965
- } else {
1966
- // Use raw content if no preview call was made or resolvedContent is not available
1967
- contentObj = parsedRcsContent || {};
2403
+ try { parsedRcsContent = JSON.parse(content); } catch (e) { parsedRcsContent = content; }
1968
2404
  }
2405
+ contentObj = rcsCarouselContentObj || parsedRcsContent || {};
1969
2406
  } else if (channel === CHANNELS.INAPP) {
1970
2407
  // For InApp, content is an object with androidContent and iosContent (similar to MobilePush)
1971
2408
  // Content comes from InApp component state, passed via content prop
@@ -2168,6 +2605,10 @@ const CommonTestAndPreview = (props) => {
2168
2605
  formatMessage,
2169
2606
  lastModified: formData?.lastModified,
2170
2607
  updatedByName: formData?.updatedByName,
2608
+ smsFallbackContent: isRcsSmsFallbackPreviewEnabled ? smsFallbackContent : null,
2609
+ smsFallbackResolvedText,
2610
+ activePreviewTab,
2611
+ onPreviewTabChange: setActivePreviewTab,
2171
2612
  };
2172
2613
  };
2173
2614
 
@@ -2207,7 +2648,12 @@ const CommonTestAndPreview = (props) => {
2207
2648
  }, [show, beeInstance, currentTab, channel]);
2208
2649
 
2209
2650
  /**
2210
- * Initial data load when slidebox opens
2651
+ * Initial data load when slidebox opens.
2652
+ * EXTRACT TAGS CALL SITES (on open/edit RCS):
2653
+ * 1. Here (non-email): actions.extractTagsRequested() at line ~2161 when show is true.
2654
+ * 2. RCS SMS fallback useEffect below: Api.extractTagsWithMetaData() for fallback message.
2655
+ * 3. handleExtractTags() (user clicks "Enter custom values for tags") – not from effects.
2656
+ * The "Process extracted tags" effect only processes API results and must not call extract again.
2211
2657
  */
2212
2658
  useEffect(() => {
2213
2659
  if (show) {
@@ -2292,7 +2738,61 @@ const CommonTestAndPreview = (props) => {
2292
2738
  actions.getTestGroupsRequested();
2293
2739
  }
2294
2740
  }
2295
- }, [show, beeInstance, currentTab, channel]);
2741
+ // getCurrentContent: RCS applies cardVarMapped → placeholder resolution; re-extract when it changes.
2742
+ }, [show, beeInstance, currentTab, channel, getCurrentContent]);
2743
+
2744
+ /**
2745
+ * RCS with SMS fallback: extract tags for fallback SMS content as well
2746
+ * (so we can show a separate "Fallback SMS tags" section in left panel).
2747
+ */
2748
+ useEffect(() => {
2749
+ let cancelled = false;
2750
+
2751
+ if (!show || channel !== CHANNELS.RCS) {
2752
+ return () => {
2753
+ cancelled = true;
2754
+ };
2755
+ }
2756
+
2757
+ if (!smsFallbackContent?.templateContent && !smsFallbackContent?.content) {
2758
+ setSmsFallbackExtractedTags([]);
2759
+ setSmsFallbackRequiredTags([]);
2760
+ setSmsFallbackOptionalTags([]);
2761
+ return () => {
2762
+ cancelled = true;
2763
+ };
2764
+ }
2765
+
2766
+ setIsExtractingSmsFallbackTags(true);
2767
+ (async () => {
2768
+ try {
2769
+ const fallbackBodyForExtractApi = getSmsFallbackTextForTagExtraction(smsFallbackContent);
2770
+ const payload = {
2771
+ messageTitle: '',
2772
+ messageBody: fallbackBodyForExtractApi || '',
2773
+ };
2774
+ 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
2775
+ let smsFallbackTagTree = response?.data ?? [];
2776
+ if (!Array.isArray(smsFallbackTagTree)) smsFallbackTagTree = [];
2777
+ if (!smsTemplateHasMustacheTags(fallbackBodyForExtractApi)) {
2778
+ smsFallbackTagTree = [];
2779
+ } else if (smsFallbackTagTree.length === 0) {
2780
+ smsFallbackTagTree = buildSyntheticSmsMustacheTags(fallbackBodyForExtractApi);
2781
+ }
2782
+ if (cancelled) return;
2783
+ setSmsFallbackExtractedTags(smsFallbackTagTree);
2784
+ } catch (e) {
2785
+ if (cancelled) return;
2786
+ setSmsFallbackExtractedTags([]);
2787
+ } finally {
2788
+ if (!cancelled) setIsExtractingSmsFallbackTags(false);
2789
+ }
2790
+ })();
2791
+
2792
+ return () => {
2793
+ cancelled = true;
2794
+ };
2795
+ }, [show, channel, smsFallbackContent]);
2296
2796
 
2297
2797
  /**
2298
2798
  * Email-specific: Handle content updates for both BEE and CKEditor
@@ -2344,15 +2844,22 @@ const CommonTestAndPreview = (props) => {
2344
2844
  setSelectedCustomer(null);
2345
2845
  setRequiredTags([]);
2346
2846
  setOptionalTags([]);
2847
+ setSmsFallbackExtractedTags([]);
2848
+ setSmsFallbackRequiredTags([]);
2849
+ setSmsFallbackOptionalTags([]);
2850
+ setIsExtractingSmsFallbackTags(false);
2347
2851
  setCustomValues({});
2348
2852
  setShowJSON(false);
2349
2853
  setTagsExtracted(false);
2350
2854
  setPreviewDevice(DESKTOP);
2855
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2856
+ setSmsFallbackPreviewText(undefined);
2351
2857
  setSelectedTestEntities([]);
2352
2858
  actions.clearPrefilledValues();
2353
2859
  } else {
2354
2860
  // Reset device to initialDevice when opening (Android for mobile channels, Desktop for others)
2355
2861
  setPreviewDevice(initialDevice);
2862
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2356
2863
  }
2357
2864
  }, [show, initialDevice]);
2358
2865
 
@@ -2361,79 +2868,10 @@ const CommonTestAndPreview = (props) => {
2361
2868
  */
2362
2869
  useEffect(() => {
2363
2870
  if (previewData) {
2364
- setPreviewDataHtml(previewData);
2871
+ setPreviewDataHtml(toPlainPreviewData(previewData));
2365
2872
  }
2366
2873
  }, [previewData]);
2367
2874
 
2368
- /**
2369
- * Process extracted tags and categorize them
2370
- */
2371
- useEffect(() => {
2372
- // Categorize tags into required and optional
2373
- const required = [];
2374
- const optional = [];
2375
- let hasPersonalizationTags = false;
2376
-
2377
- if (extractedTags?.length > 0) {
2378
- const processTag = (tag, parentPath = '') => {
2379
- const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
2380
-
2381
- // Skip unsubscribe tag for input fields
2382
- if (tag?.name === UNSUBSCRIBE_TAG_NAME) {
2383
- return;
2384
- }
2385
-
2386
- hasPersonalizationTags = true;
2387
-
2388
- if (tag?.metaData?.userDriven === false) {
2389
- required.push({
2390
- ...tag,
2391
- fullPath: currentPath,
2392
- });
2393
- } else if (tag?.metaData?.userDriven === true) {
2394
- optional.push({
2395
- ...tag,
2396
- fullPath: currentPath,
2397
- });
2398
- }
2399
-
2400
- if (tag?.children?.length > 0) {
2401
- tag.children.forEach((child) => processTag(child, currentPath));
2402
- }
2403
- };
2404
-
2405
- extractedTags.forEach((tag) => processTag(tag));
2406
-
2407
- if (hasPersonalizationTags) {
2408
- setRequiredTags(required);
2409
- setOptionalTags(optional);
2410
- setTagsExtracted(true); // Mark tags as extracted and processed
2411
-
2412
- // Initialize custom values for required tags
2413
- const initialValues = {};
2414
- required.forEach((tag) => {
2415
- initialValues[tag?.fullPath] = '';
2416
- });
2417
- optional.forEach((tag) => {
2418
- initialValues[tag?.fullPath] = '';
2419
- });
2420
- setCustomValues(initialValues);
2421
- } else {
2422
- // Reset all tag-related state if no personalization tags
2423
- setRequiredTags([]);
2424
- setOptionalTags([]);
2425
- setCustomValues({});
2426
- setTagsExtracted(false);
2427
- }
2428
- } else {
2429
- // Reset all tag-related state if no tags
2430
- setRequiredTags([]);
2431
- setOptionalTags([]);
2432
- setCustomValues({});
2433
- setTagsExtracted(false);
2434
- }
2435
- }, [extractedTags]);
2436
-
2437
2875
  /**
2438
2876
  * Handle customer selection and fetch prefilled values
2439
2877
  */
@@ -2441,17 +2879,15 @@ const CommonTestAndPreview = (props) => {
2441
2879
  if (selectedCustomer && config.enableCustomerSearch !== false) {
2442
2880
  setTagsExtracted(true); // Auto-open custom values editor
2443
2881
 
2444
- // Get all available tags
2445
- const allTags = [...requiredTags, ...optionalTags];
2446
2882
  const requiredTagObj = {};
2447
- requiredTags.forEach((tag) => {
2883
+ allRequiredTags.forEach((tag) => {
2448
2884
  requiredTagObj[tag?.fullPath] = '';
2449
2885
  });
2450
2886
  if (allTags.length > 0) {
2451
2887
  const payload = preparePreviewPayload(
2452
- channel,
2888
+ activeChannelForActions,
2453
2889
  formData || {},
2454
- getCurrentContent,
2890
+ activeContentForActions,
2455
2891
  {
2456
2892
  ...requiredTagObj,
2457
2893
  },
@@ -2460,7 +2896,7 @@ const CommonTestAndPreview = (props) => {
2460
2896
  actions.getPrefilledValuesRequested(payload);
2461
2897
  }
2462
2898
  }
2463
- }, [selectedCustomer]);
2899
+ }, [selectedCustomer, allTags.length, activeChannelForActions, activePreviewTab]);
2464
2900
 
2465
2901
  /**
2466
2902
  * Update custom values with prefilled values from API
@@ -2471,24 +2907,29 @@ const CommonTestAndPreview = (props) => {
2471
2907
  if (prefilledValues && selectedCustomer) {
2472
2908
  // Always replace all values with prefilled values
2473
2909
  const updatedValues = {};
2474
- [...requiredTags, ...optionalTags].forEach((tag) => {
2910
+ allTags.forEach((tag) => {
2475
2911
  updatedValues[tag?.fullPath] = prefilledValues[tag?.fullPath] || '';
2476
2912
  });
2477
2913
 
2478
2914
  setCustomValues(updatedValues);
2479
2915
 
2480
2916
  // Update preview with prefilled values (this is a valid preview call trigger)
2917
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped.
2918
+ // SMS fallback preview is kept in sync by syncSmsFallbackPreview via smsFallbackPreviewText.
2919
+ const previewChannelForPrefill = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
2920
+ const previewContentForPrefill = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2481
2921
  const payload = preparePreviewPayload(
2482
- channel,
2922
+ previewChannelForPrefill,
2483
2923
  formData || {},
2484
- getCurrentContent,
2924
+ previewContentForPrefill,
2485
2925
  updatedValues,
2486
2926
  selectedCustomer
2487
2927
  );
2488
2928
  actions.updatePreviewRequested(payload);
2489
2929
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2930
+ void syncSmsFallbackPreview(updatedValues, selectedCustomer);
2490
2931
  }
2491
- }, [JSON.stringify(prefilledValues), selectedCustomer]);
2932
+ }, [JSON.stringify(prefilledValues), selectedCustomer, activeChannelForActions, activePreviewTab]);
2492
2933
 
2493
2934
  /**
2494
2935
  * Map channel constants to display names (lowercase for message)
@@ -2582,11 +3023,7 @@ const CommonTestAndPreview = (props) => {
2582
3023
  setTagsExtracted(true); // Auto-open custom values editor
2583
3024
 
2584
3025
  // Clear any existing values while waiting for prefilled values
2585
- const emptyValues = {};
2586
- [...requiredTags, ...optionalTags].forEach((tag) => {
2587
- emptyValues[tag?.fullPath] = '';
2588
- });
2589
- setCustomValues(emptyValues);
3026
+ setCustomValues(buildEmptyValues());
2590
3027
  };
2591
3028
 
2592
3029
  /**
@@ -2600,11 +3037,7 @@ const CommonTestAndPreview = (props) => {
2600
3037
  actions.clearPreviewErrors();
2601
3038
 
2602
3039
  // Initialize empty values for all tags
2603
- const emptyValues = {};
2604
- [...requiredTags, ...optionalTags].forEach((tag) => {
2605
- emptyValues[tag?.fullPath] = '';
2606
- });
2607
- setCustomValues(emptyValues);
3040
+ setCustomValues(buildEmptyValues());
2608
3041
 
2609
3042
  // Don't make preview call when clearing selection - just reset to raw content
2610
3043
  // Preview will be shown using raw formData/content
@@ -2640,17 +3073,20 @@ const CommonTestAndPreview = (props) => {
2640
3073
  */
2641
3074
  const handleDiscardCustomValues = () => {
2642
3075
  // Initialize empty values for all tags
2643
- const emptyValues = {};
2644
- [...requiredTags, ...optionalTags].forEach((tag) => {
2645
- emptyValues[tag?.fullPath] = '';
2646
- });
3076
+ const emptyValues = buildEmptyValues();
2647
3077
  setCustomValues(emptyValues);
2648
3078
 
3079
+ // Reset SMS fallback preview so it shows raw template (with {{tags}} visible) after discard
3080
+ setSmsFallbackPreviewText(undefined);
3081
+
2649
3082
  // Update preview with empty values (this is a valid preview call trigger)
3083
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped.
3084
+ const previewChannelForDiscard = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
3085
+ const previewContentForDiscard = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2650
3086
  const payload = preparePreviewPayload(
2651
- channel,
3087
+ previewChannelForDiscard,
2652
3088
  formData || {},
2653
- getCurrentContent,
3089
+ previewContentForDiscard,
2654
3090
  emptyValues,
2655
3091
  selectedCustomer
2656
3092
  );
@@ -2664,14 +3100,24 @@ const CommonTestAndPreview = (props) => {
2664
3100
  */
2665
3101
  const handleUpdatePreview = async () => {
2666
3102
  try {
3103
+ // Capture customValues at click time so the carousel preview only updates here,
3104
+ // not on every subsequent keystroke.
3105
+ previewCustomValuesRef.current = customValues;
3106
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped,
3107
+ // even when the user triggers update from the SMS fallback tab.
3108
+ // SMS fallback preview is kept in sync by syncSmsFallbackPreview via smsFallbackPreviewText.
3109
+ const previewChannel = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
3110
+ const previewContent = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2667
3111
  const payload = preparePreviewPayload(
2668
- channel,
3112
+ previewChannel,
2669
3113
  formData || {},
2670
- getCurrentContent,
3114
+ previewContent,
2671
3115
  customValues,
2672
3116
  selectedCustomer
2673
3117
  );
2674
3118
  await actions.updatePreviewRequested(payload);
3119
+
3120
+ await syncSmsFallbackPreview(customValues, selectedCustomer);
2675
3121
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2676
3122
  } catch (error) {
2677
3123
  CapNotification.error({
@@ -2681,25 +3127,115 @@ const CommonTestAndPreview = (props) => {
2681
3127
  };
2682
3128
 
2683
3129
  /**
2684
- * Handle extract tags
3130
+ * Categorize extracted tags into required/optional.
2685
3131
  */
2686
- const handleExtractTags = () => {
2687
- // Get content based on channel
2688
- let contentToExtract = getCurrentContent;
3132
+ const categorizeTags = (tagsTree = []) => {
3133
+ const required = [];
3134
+ const optional = [];
3135
+ let hasPersonalizationTags = false;
3136
+ const processTag = (tag, parentPath = '') => {
3137
+ const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
3138
+
3139
+ // Skip unsubscribe tag for input fields
3140
+ if (tag?.name === UNSUBSCRIBE_TAG_NAME) return;
3141
+
3142
+ hasPersonalizationTags = true;
3143
+ const userDriven = tag?.metaData?.userDriven;
3144
+ if (userDriven === true) {
3145
+ optional.push({ ...tag, fullPath: currentPath });
3146
+ } else {
3147
+ // false or missing (SMS/DLT extract often omits metaData) → required for test values
3148
+ required.push({ ...tag, fullPath: currentPath });
3149
+ }
3150
+
3151
+ if (tag?.children?.length > 0) {
3152
+ tag.children.forEach((child) => processTag(child, currentPath));
3153
+ }
3154
+ };
3155
+
3156
+ (tagsTree || []).forEach((tag) => processTag(tag));
3157
+ return { required, optional, hasPersonalizationTags };
3158
+ };
2689
3159
 
3160
+ /**
3161
+ * Apply tag extraction when content comes from RCS + SMS fallback (no API call).
3162
+ */
3163
+ const applyRcsSmsFallbackTagExtraction = () => {
3164
+ const rcsPrimaryCategorized = categorizeTags(extractedTags ?? []);
3165
+ const fallbackSmsResolvedForTags = smsFallbackTextForTagExtraction ?? '';
3166
+ let fallbackSmsTagTree = smsFallbackExtractedTags?.length > 0
3167
+ ? smsFallbackExtractedTags
3168
+ : buildSyntheticSmsMustacheTags(fallbackSmsResolvedForTags);
3169
+ if (!smsTemplateHasMustacheTags(fallbackSmsResolvedForTags)) {
3170
+ fallbackSmsTagTree = [];
3171
+ }
3172
+ const fallbackSmsCategorized = categorizeTags(fallbackSmsTagTree);
3173
+ setRequiredTags(rcsPrimaryCategorized.required);
3174
+ setOptionalTags(rcsPrimaryCategorized.optional);
3175
+ setSmsFallbackRequiredTags(fallbackSmsCategorized.required);
3176
+ setSmsFallbackOptionalTags(fallbackSmsCategorized.optional);
3177
+ setTagsExtracted(
3178
+ rcsPrimaryCategorized.hasPersonalizationTags || fallbackSmsCategorized.hasPersonalizationTags,
3179
+ );
3180
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, rcsPrimaryCategorized, fallbackSmsCategorized));
3181
+ };
3182
+
3183
+ /**
3184
+ * When extract-tags API returns, map Redux `extractedTags` into required/optional so
3185
+ * CustomValuesEditor shows personalization fields (effect was previously commented out).
3186
+ * RCS + SMS fallback: merge primary + fallback tag trees when fallback template exists.
3187
+ */
3188
+ useEffect(() => {
3189
+ if (!show) return;
3190
+ const hasFallbackSmsTemplate = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
3191
+ if (channel === CHANNELS.RCS && hasFallbackSmsTemplate) {
3192
+ applyRcsSmsFallbackTagExtraction();
3193
+ return;
3194
+ }
3195
+ const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
3196
+ let smsTagSource = channel === CHANNELS.SMS && (!extractedTags || extractedTags.length === 0)
3197
+ ? buildSyntheticSmsMustacheTags(smsEditorBody)
3198
+ : (extractedTags ?? []);
3199
+ if (channel === CHANNELS.SMS && !smsTemplateHasMustacheTags(smsEditorBody)) {
3200
+ smsTagSource = [];
3201
+ }
3202
+ const { required, optional, hasPersonalizationTags } = categorizeTags(smsTagSource);
3203
+ setRequiredTags(required);
3204
+ setOptionalTags(optional);
3205
+ setTagsExtracted(hasPersonalizationTags);
3206
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, { required, optional }, { required: [], optional: [] }));
3207
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- applyRcsSmsFallbackTagExtraction closes over latest extractedTags/smsFallbackExtractedTags
3208
+ }, [show, extractedTags, channel, smsFallbackContent, smsFallbackExtractedTags, getCurrentContent, smsFallbackTextForTagExtraction]);
3209
+
3210
+ /**
3211
+ * Get content to run tag extraction on (channel-specific).
3212
+ */
3213
+ const getContentForTagExtraction = () => {
3214
+ let contentToExtract = activeContentForActions;
2690
3215
  if (channel === CHANNELS.EMAIL && formData) {
2691
3216
  const currentTabData = formData[currentTab - 1];
2692
3217
  const activeTab = currentTabData?.activeTab;
2693
3218
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
2694
3219
  contentToExtract = templateContent || contentToExtract;
2695
3220
  }
3221
+ return contentToExtract;
3222
+ };
3223
+
3224
+ /**
3225
+ * Handle extract tags
3226
+ */
3227
+ const handleExtractTags = () => {
3228
+ if (channel === CHANNELS.RCS) {
3229
+ applyRcsSmsFallbackTagExtraction();
3230
+ return;
3231
+ }
2696
3232
 
2697
- // Check for personalization tags (excluding unsubscribe)
3233
+ const contentToExtract = getContentForTagExtraction();
2698
3234
  const tags = contentToExtract.match(/{{[^}]+}}/g) || [];
2699
3235
  const hasPersonalizationTags = tags.some((tag) => !tag.includes(UNSUBSCRIBE_TAG_NAME));
3236
+ const onlyUnsubscribe = !hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME);
2700
3237
 
2701
- if (!hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME)) {
2702
- // If only unsubscribe tag is present, show noTagsExtracted message
3238
+ if (onlyUnsubscribe) {
2703
3239
  setTagsExtracted(false);
2704
3240
  setRequiredTags([]);
2705
3241
  setOptionalTags([]);
@@ -2707,10 +3243,9 @@ const CommonTestAndPreview = (props) => {
2707
3243
  return;
2708
3244
  }
2709
3245
 
2710
- // Extract tags
2711
3246
  setTagsExtracted(true);
2712
3247
  const { templateSubject, templateContent } = prepareTagExtractionPayload(
2713
- channel,
3248
+ activeChannelForActions,
2714
3249
  formData || {},
2715
3250
  contentToExtract
2716
3251
  );
@@ -2772,7 +3307,7 @@ const CommonTestAndPreview = (props) => {
2772
3307
  if (existingTestCustomer) {
2773
3308
  const entityId = existingTestCustomer.userId ?? existingTestCustomer.customerId;
2774
3309
  if (entityId != null) {
2775
- const id = 'customer:' + normalizeTestEntityId(entityId);
3310
+ const id = normalizeTestEntityId(entityId);
2776
3311
  setSelectedTestEntities((prev) => (
2777
3312
  prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
2778
3313
  ));
@@ -2809,7 +3344,7 @@ const CommonTestAndPreview = (props) => {
2809
3344
  (c) => String(c?.customerId) === customerIdFromLookup || String(c?.userId) === customerIdFromLookup
2810
3345
  );
2811
3346
  if (alreadyInTestListByCustomerId) {
2812
- const id = 'customer:' + normalizeTestEntityId(customerIdFromLookup);
3347
+ const id = normalizeTestEntityId(customerIdFromLookup);
2813
3348
  setSelectedTestEntities((prev) => (
2814
3349
  prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
2815
3350
  ));
@@ -2846,26 +3381,21 @@ const CommonTestAndPreview = (props) => {
2846
3381
  const handleSendTestMessage = () => {
2847
3382
  const allUserIds = [];
2848
3383
  selectedTestEntities.forEach((entityId) => {
2849
- if (String(entityId).startsWith('group:')) {
2850
- const rawId = String(entityId).slice('group:'.length);
2851
- const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, rawId));
2852
- if (group) {
2853
- allUserIds.push(...group.userIds);
2854
- }
3384
+ const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, entityId));
3385
+ if (group) {
3386
+ allUserIds.push(...group.userIds);
2855
3387
  } else {
2856
- const rawId = String(entityId).startsWith('customer:')
2857
- ? String(entityId).slice('customer:'.length)
2858
- : String(entityId);
2859
- allUserIds.push(Number(rawId));
3388
+ allUserIds.push(entityId);
2860
3389
  }
2861
3390
  });
2862
3391
  const uniqueUserIds = [...new Set(allUserIds)];
2863
3392
 
2864
- const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP].includes(channel)
3393
+ const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS].includes(channel)
2865
3394
  ? testPreviewDeliverySettings[channel]
2866
3395
  : null;
2867
3396
 
2868
- // Create initial payload based on channel
3397
+ // createMessageMeta must match the creative channel and full creative (RCS + SMS fallback in one meta).
3398
+ // Do not use activeChannelForActions / activeContentForActions — those follow the RCS vs Fallback SMS *preview* tab.
2869
3399
  const initialPayload = prepareTestMessagePayload(
2870
3400
  channel,
2871
3401
  formData || content || {},
@@ -2873,7 +3403,8 @@ const CommonTestAndPreview = (props) => {
2873
3403
  customValues,
2874
3404
  uniqueUserIds,
2875
3405
  previewData,
2876
- deliveryOverride
3406
+ deliveryOverride,
3407
+ {},
2877
3408
  );
2878
3409
 
2879
3410
  actions.createMessageMetaRequested(
@@ -2906,11 +3437,10 @@ const CommonTestAndPreview = (props) => {
2906
3437
  // ============================================
2907
3438
  // RENDER HELPER FUNCTIONS
2908
3439
  // ============================================
2909
-
2910
3440
  const renderLeftPanelContent = () => (
2911
3441
  <LeftPanelContent
2912
- isExtractingTags={isExtractingTags}
2913
- extractedTags={extractedTags}
3442
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
3443
+ extractedTags={leftPanelExtractedTags}
2914
3444
  selectedCustomer={selectedCustomer}
2915
3445
  handleCustomerSelect={handleCustomerSelect}
2916
3446
  handleSearchCustomer={handleSearchCustomer}
@@ -2928,15 +3458,29 @@ const CommonTestAndPreview = (props) => {
2928
3458
 
2929
3459
  const renderCustomValuesEditor = () => (
2930
3460
  <CustomValuesEditor
2931
- isExtractingTags={isExtractingTags}
3461
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
2932
3462
  isUpdatePreviewDisabled={isUpdatePreviewDisabled}
2933
3463
  showJSON={showJSON}
2934
3464
  setShowJSON={setShowJSON}
2935
3465
  customValues={customValues}
2936
3466
  handleJSONTextChange={handleJSONTextChange}
2937
- extractedTags={extractedTags}
2938
- requiredTags={requiredTags}
2939
- optionalTags={optionalTags}
3467
+ sections={[
3468
+ {
3469
+ key: channel,
3470
+ title:
3471
+ channel === CHANNELS.RCS
3472
+ ? messages.rcsTagsSectionTitle
3473
+ : messages[`${channel}TagsSectionTitle`],
3474
+ requiredTags,
3475
+ optionalTags,
3476
+ },
3477
+ {
3478
+ key: PREVIEW_TAB_SMS_FALLBACK,
3479
+ title: isRcsSmsFallbackPreviewEnabled ? messages.smsFallbackTagsSectionTitle : null,
3480
+ requiredTags: smsFallbackRequiredTags,
3481
+ optionalTags: smsFallbackOptionalTags,
3482
+ },
3483
+ ]}
2940
3484
  handleCustomValueChange={handleCustomValueChange}
2941
3485
  handleDiscardCustomValues={handleDiscardCustomValues}
2942
3486
  handleUpdatePreview={handleUpdatePreview}
@@ -2952,18 +3496,13 @@ const CommonTestAndPreview = (props) => {
2952
3496
  }));
2953
3497
  };
2954
3498
 
2955
- /** Trim pasted emails (trailing CR/LF). SMS: strip non-digits so pasted formatted numbers match isValidMobile / API. */
3499
+ /** Trim pasted emails (trailing CR/LF). Allow any input for SMS; valid-only check is in renderAddTestCustomerButton. */
2956
3500
  const handleTestCustomersSearch = useCallback((value) => {
2957
3501
  if (value == null || value === '') {
2958
3502
  setSearchValue('');
2959
3503
  return;
2960
3504
  }
2961
- const raw = String(value).trim();
2962
- if (channel === CHANNELS.SMS) {
2963
- setSearchValue(formatPhoneNumber(raw));
2964
- } else {
2965
- setSearchValue(raw);
2966
- }
3505
+ setSearchValue(String(value).trim());
2967
3506
  }, [channel]);
2968
3507
 
2969
3508
  const renderSendTestMessage = () => (
@@ -2981,12 +3520,13 @@ const CommonTestAndPreview = (props) => {
2981
3520
  renderAddTestCustomerButton={renderAddTestCustomerButton}
2982
3521
  formatMessage={formatMessage}
2983
3522
  deliverySettings={testPreviewDeliverySettings[channel]}
2984
- senderDetailsOptions={senderDetailsByChannel[channel]}
3523
+ senderDetailsByChannel={senderDetailsByChannel}
2985
3524
  wecrmAccounts={wecrmAccounts}
2986
3525
  onSaveDeliverySettings={handleSaveDeliverySettings}
2987
3526
  isLoadingSenderDetails={isLoadingSenderDetails}
2988
3527
  smsTraiDltEnabled={smsTraiDltEnabled}
2989
3528
  registeredSenderIds={registeredSenderIds}
3529
+ isChannelSmsFallbackPreviewEnabled={isRcsSmsFallbackPreviewEnabled}
2990
3530
  searchValue={searchValue}
2991
3531
  setSearchValue={handleTestCustomersSearch}
2992
3532
  />
@@ -3000,14 +3540,13 @@ const CommonTestAndPreview = (props) => {
3000
3540
 
3001
3541
  const renderAddTestCustomerButton = () => {
3002
3542
  const raw = (searchValue || '').trim();
3003
- const value = channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw;
3004
3543
  const showAddButton =
3005
3544
  [CHANNELS.EMAIL, CHANNELS.SMS].includes(channel) &&
3006
- (channel === CHANNELS.EMAIL ? isValidEmail(value) : isValidMobile(value));
3545
+ (channel === CHANNELS.EMAIL ? isValidEmail(raw) : isValidMobile(formatPhoneNumber(raw)));
3007
3546
  if (!showAddButton) return null;
3008
3547
  return (
3009
3548
  <AddTestCustomerButton
3010
- searchValue={value}
3549
+ searchValue={channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw}
3011
3550
  handleAddTestCustomer={handleAddTestCustomer}
3012
3551
  />
3013
3552
  );