@capillarytech/creatives-library 9.0.29 → 9.0.31

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 (147) 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 +22 -1
  6. package/utils/rcsPayloadUtils.js +102 -0
  7. package/utils/templateVarUtils.js +198 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +295 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +21 -1
  11. package/v2Components/CapActionButton/index.js +261 -144
  12. package/v2Components/CapActionButton/index.scss +245 -14
  13. package/v2Components/CapActionButton/messages.js +32 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +74 -19
  15. package/v2Components/CapImageUpload/index.js +2 -2
  16. package/v2Components/CapTagList/index.js +9 -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 +58 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -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 +35 -2
  30. package/v2Components/CommonTestAndPreview/index.js +774 -231
  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 +412 -257
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
  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 +108 -15
  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/tests/utils.test.js +151 -0
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  49. package/v2Components/SmsFallback/constants.js +94 -0
  50. package/v2Components/SmsFallback/index.js +958 -0
  51. package/v2Components/SmsFallback/index.scss +266 -0
  52. package/v2Components/SmsFallback/messages.js +78 -0
  53. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  54. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  55. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  58. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  59. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
  60. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  61. package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
  62. package/v2Components/TemplatePreview/constants.js +2 -0
  63. package/v2Components/TemplatePreview/index.js +147 -32
  64. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  65. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  66. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  67. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  68. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  69. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  70. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  71. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  72. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  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 +27 -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 +77 -0
  81. package/v2Containers/CreativesContainer/index.js +322 -106
  82. package/v2Containers/CreativesContainer/index.scss +102 -1
  83. package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
  84. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  85. package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
  86. package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
  87. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
  88. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
  89. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
  90. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  92. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  93. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  94. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  95. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  96. package/v2Containers/Rcs/carouselUtils.js +224 -0
  97. package/v2Containers/Rcs/components/CarouselCard.js +317 -0
  98. package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
  99. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  100. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  101. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  102. package/v2Containers/Rcs/constants.js +132 -16
  103. package/v2Containers/Rcs/index.js +1822 -946
  104. package/v2Containers/Rcs/index.scss +443 -8
  105. package/v2Containers/Rcs/messages.js +45 -22
  106. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +494 -0
  107. package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
  108. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  109. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  110. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  111. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  112. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74440 -39387
  113. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  114. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  115. package/v2Containers/Rcs/tests/index.test.js +275 -40
  116. package/v2Containers/Rcs/tests/mockData.js +38 -0
  117. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +912 -0
  118. package/v2Containers/Rcs/tests/utils.test.js +682 -30
  119. package/v2Containers/Rcs/utils.js +514 -12
  120. package/v2Containers/Sms/Create/index.js +115 -48
  121. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  122. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  123. package/v2Containers/SmsTrai/Create/index.js +9 -4
  124. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  125. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  126. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  127. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  128. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  129. package/v2Containers/SmsWrapper/index.js +37 -8
  130. package/v2Containers/TagList/index.js +5 -0
  131. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  132. package/v2Containers/Templates/_templates.scss +196 -12
  133. package/v2Containers/Templates/actions.js +11 -0
  134. package/v2Containers/Templates/constants.js +2 -0
  135. package/v2Containers/Templates/index.js +131 -59
  136. package/v2Containers/Templates/sagas.js +57 -13
  137. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  138. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  139. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  140. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  141. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  142. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  143. package/v2Containers/TemplatesV2/index.js +88 -25
  144. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  145. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  146. package/v2Containers/Whatsapp/index.js +3 -20
  147. 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.
@@ -118,10 +161,10 @@ const normalizeTestEntityId = (id) => {
118
161
  return String(id);
119
162
  };
120
163
 
121
- const testEntityIdsEqual = (a, b) => {
122
- if (a == null && b == null) return true;
123
- if (a == null || b == null) return false;
124
- return String(a) === String(b);
164
+ const testEntityIdsEqual = (firstId, secondId) => {
165
+ if (firstId == null && secondId == null) return true;
166
+ if (firstId == null || secondId == null) return false;
167
+ return String(firstId) === String(secondId);
125
168
  };
126
169
 
127
170
  /**
@@ -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((card) => `${card.title || ''} ${card.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,163 @@ 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
+ // Title/description/suggestion text carry {{tag}} placeholders; resolve them against the
1075
+ // user's test values here, same as every other channel (see resolveTagsInText usage below
1076
+ // for SMS/EMAIL/WHATSAPP) — otherwise the test send goes out with literal tags unresolved.
1077
+ const cardContentForTestMetaApi = rcsCardPayloadList.map((singleRcsCardPayload) => {
1078
+ const normalizedCardMediaForTestApi = singleRcsCardPayload?.media
1079
+ ? normalizeRcsTestCardMedia(singleRcsCardPayload.media)
1080
+ : undefined;
1081
+ const suggestionsFromCard = Array.isArray(singleRcsCardPayload?.suggestions)
1082
+ ? singleRcsCardPayload.suggestions
1083
+ : [];
1084
+ const suggestionsFormattedForTestMeta = suggestionsFromCard.map((suggestionItem, index) => mapRcsSuggestionForTestMeta(
1085
+ { ...suggestionItem, text: resolveTagsInText(suggestionItem?.text, customValuesObj) },
1086
+ index,
1087
+ ));
1088
+ return {
1089
+ title: resolveTagsInText(singleRcsCardPayload?.title ?? '', customValuesObj),
1090
+ description: resolveTagsInText(singleRcsCardPayload?.description ?? '', customValuesObj),
1091
+ mediaType: singleRcsCardPayload?.mediaType ?? MEDIA_TYPE_TEXT,
1092
+ ...(normalizedCardMediaForTestApi && { media: normalizedCardMediaForTestApi }),
1093
+ ...(suggestionsFormattedForTestMeta.length > 0 && {
1094
+ suggestions: suggestionsFormattedForTestMeta,
1095
+ }),
1096
+ };
1097
+ });
1098
+ // Use the component-level smsFallbackContent prop (has rcsSmsFallbackVarMapped) so DLT
1099
+ // {#var#} slots are converted to {{tagName}} mustache tags before sending to createMessageMeta.
1100
+ const smsFallbackTaggedTemplateBody =
1101
+ getSmsFallbackTextForTagExtraction(smsFallbackContent)
1102
+ || pickFirstSmsFallbackTemplateString(smsFallbackFromCreativeForm)
1103
+ || '';
1104
+ const smsSenderFromDelivery = deliverySettingsOverride?.cdmaSenderId?.includes('|')
1105
+ ? deliverySettingsOverride.cdmaSenderId.split('|')[1]
1106
+ : deliverySettingsOverride?.cdmaSenderId;
1107
+ const deliveryFallbackSmsId =
1108
+ typeof smsSenderFromDelivery === 'string' ? smsSenderFromDelivery.trim() : '';
1109
+ const creativeFallbackSmsId =
1110
+ smsFallbackFromCreativeForm?.senderId != null
1111
+ ? String(smsFallbackFromCreativeForm.senderId).trim()
1112
+ : '';
1113
+ const fallbackSmsSenderIdForChannel = deliveryFallbackSmsId || creativeFallbackSmsId || '';
1114
+
1115
+ // For DLT orgs: include templateConfigs inside smsFallBackContent so createMessageMeta
1116
+ // receives the DLT template ID, registered sender IDs, and body alongside the message.
1117
+ // Priority: smsFallBackContent.templateConfigs from the RCS payload (clean API shape,
1118
+ // built by createPayload) → fallback to root-level formData.templateConfigs (strip the
1119
+ // UI-only traiDltEnabled flag before sending to the API).
1120
+ const smsFallbackTemplateConfigsForApi = (() => {
1121
+ if (
1122
+ smsFallbackFromCreativeForm?.templateConfigs
1123
+ && typeof smsFallbackFromCreativeForm.templateConfigs === 'object'
1124
+ ) {
1125
+ return smsFallbackFromCreativeForm.templateConfigs;
1126
+ }
1127
+ const rootTc = creativeFormData?.templateConfigs;
1128
+ if (rootTc && typeof rootTc === 'object') {
1129
+ // eslint-disable-next-line no-unused-vars
1130
+ const { traiDltEnabled: _uiFlag, ...tcForApi } = rootTc;
1131
+ return Object.keys(tcForApi).length > 0 ? tcForApi : null;
1132
+ }
1133
+ return null;
1134
+ })();
1135
+
1136
+ const smsFallBackContent =
1137
+ smsFallbackTaggedTemplateBody.trim() !== ''
1138
+ ? {
1139
+ message: smsFallbackTaggedTemplateBody,
1140
+ ...(smsFallbackTemplateConfigsForApi && {
1141
+ templateConfigs: smsFallbackTemplateConfigsForApi,
1142
+ }),
1143
+ }
1144
+ : undefined;
1145
+
1146
+ // accountId: WeCRM account id (not sourceAccountIdentifier) for createMessageMeta
1147
+ const accountIdForMeta =
1148
+ rcsContentFromForm?.accountId != null && String(rcsContentFromForm.accountId).trim() !== ''
1149
+ ? String(rcsContentFromForm.accountId)
1150
+ : undefined;
1151
+
1152
+ const rcsRichCardContent = {
1153
+ contentType: RCS_TEST_META_CONTENT_TYPE_RICHCARD,
1154
+ cardType: rcsContentFromForm?.cardType ?? RCS_TEST_META_CARD_TYPE_STANDALONE,
1155
+ cardSettings: rcsContentFromForm?.cardSettings ?? {
1156
+ cardOrientation: RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
1157
+ cardWidth: RCS_TEST_META_CARD_WIDTH_SMALL,
1158
+ },
1159
+ ...(cardContentForTestMetaApi.length > 0 && { cardContent: cardContentForTestMetaApi }),
1160
+ };
1161
+
1162
+ const rcsMessageContent = {
1163
+ channel: CHANNELS.RCS,
1164
+ ...(accountIdForMeta && { accountId: accountIdForMeta }),
1165
+ rcsRichCardContent,
1166
+ ...(smsFallBackContent && { smsFallBackContent }),
1167
+ };
1168
+ const rcsComposite = deliverySettingsOverride?.gsmSenderId ?? '';
1169
+ const [rcsDomainId, rcsSenderId] = rcsComposite.includes('|') ? rcsComposite.split('|') : ['', rcsComposite];
1170
+ const rcsDeliverySettings = {
1171
+ channelSettings: {
1172
+ channel: CHANNELS.RCS,
1173
+ rcsSender: (rcsSenderId || deliverySettingsOverride?.rcsSender) ?? '',
1174
+ domainId:
1175
+ rcsDomainId !== '' && rcsDomainId !== undefined && !Number.isNaN(Number(rcsDomainId))
1176
+ ? Number(rcsDomainId)
1177
+ : (deliverySettingsOverride?.domainId ?? 0),
1178
+ fallbackSmsSenderId: fallbackSmsSenderIdForChannel,
1179
+ },
1180
+ additionalSettings: {
1181
+ useTinyUrl: false,
1182
+ encryptUrl: false,
1183
+ linkTrackingEnabled: false,
1184
+ bypassControlUser: false,
1185
+ userSubscriptionDisabled: false,
1186
+ },
1187
+ };
1188
+ const { clientName: baseClientName = CLIENT_NAME_CREATIVES, ...restBase } = basePayload;
1189
+ return {
1190
+ ...restBase,
1191
+ rcsMessageContent,
1192
+ rcsDeliverySettings,
1193
+ executionParams: {},
1194
+ clientName: baseClientName,
1195
+ };
1196
+ };
1197
+
1198
+ const prepareTestMessagePayload = (
1199
+ channelType,
1200
+ formDataObj,
1201
+ contentStr,
1202
+ customValuesObj,
1203
+ recipientDetails,
1204
+ previewDataObj,
1205
+ deliverySettingsOverride,
1206
+ rcsExtra = {},
1207
+ ) => {
770
1208
  // Base payload structure common to all channels
771
1209
  const basePayload = {
772
1210
  ouId: -1,
@@ -847,12 +1285,12 @@ const CommonTestAndPreview = (props) => {
847
1285
  },
848
1286
  smsDeliverySettings: {
849
1287
  channelSettings: {
1288
+ channel: CHANNELS.SMS,
850
1289
  gsmSenderId: deliverySettingsOverride?.gsmSenderId ?? '',
851
1290
  domainId: deliverySettingsOverride?.domainId ?? null,
852
1291
  domainGatewayMapId: deliverySettingsOverride?.domainGatewayMapId ?? '',
853
1292
  targetNdnc: false,
854
1293
  cdmaSenderId: deliverySettingsOverride?.cdmaSenderId ?? '',
855
- channel: CHANNELS.SMS,
856
1294
  },
857
1295
  additionalSettings: {
858
1296
  useTinyUrl: false,
@@ -996,7 +1434,7 @@ const CommonTestAndPreview = (props) => {
996
1434
  return {
997
1435
  ...basePayload,
998
1436
  whatsappMessageContent: {
999
- messageBody: templateEditorValue || '',
1437
+ messageBody: resolvedMessageBody || templateEditorValue || '',
1000
1438
  accountId: formDataObj?.accountId || '',
1001
1439
  sourceAccountIdentifier: sourceAccountIdentifier || formDataObj?.sourceAccountIdentifier || '',
1002
1440
  accountName: formDataObj?.accountName || '',
@@ -1021,16 +1459,7 @@ const CommonTestAndPreview = (props) => {
1021
1459
  }
1022
1460
 
1023
1461
  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
- };
1462
+ return buildRcsTestMessagePayload(formDataObj, contentStr, customValuesObj, deliverySettingsOverride, basePayload, rcsExtra);
1034
1463
 
1035
1464
  case CHANNELS.INAPP: {
1036
1465
  // InApp payload structure similar to MobilePush
@@ -1172,7 +1601,7 @@ const CommonTestAndPreview = (props) => {
1172
1601
  },
1173
1602
  channelSettings: {
1174
1603
  channel: CHANNELS.INAPP,
1175
- domainGatewayMapId: formDataObj?.domainGatewayMapId || additionalProps?.domainGatewayMapId || 1,
1604
+ domainGatewayMapId: formDataObj?.domainGatewayMapId || additionalProps?.domainGatewayMapId || '',
1176
1605
  },
1177
1606
  },
1178
1607
  };
@@ -1557,7 +1986,7 @@ const CommonTestAndPreview = (props) => {
1557
1986
  },
1558
1987
  channelSettings: {
1559
1988
  channel: API_CHANNEL_PUSH,
1560
- domainGatewayMapId: formDataObj?.domainGatewayMapId || additionalProps?.domainGatewayMapId || 1,
1989
+ domainGatewayMapId: formDataObj?.domainGatewayMapId || additionalProps?.domainGatewayMapId || '',
1561
1990
  },
1562
1991
  },
1563
1992
  };
@@ -1869,6 +2298,46 @@ const CommonTestAndPreview = (props) => {
1869
2298
  *
1870
2299
  * IMPORTANT: Use raw content/formData initially, only use previewDataHtml if preview call was made
1871
2300
  */
2301
+ // Memoized RCS carousel content: only recomputes when the API response or raw template changes,
2302
+ // NOT on every customValues keystroke. Uses previewCustomValuesRef captured at Update Preview time.
2303
+ const rcsCarouselContentObj = useMemo(() => {
2304
+ if (channel !== CHANNELS.RCS) return null;
2305
+ let resolvedContent = null;
2306
+ if (hasPreviewCallBeenMade && previewDataHtml?.resolvedBody) {
2307
+ resolvedContent = previewDataHtml.resolvedBody;
2308
+ if (typeof resolvedContent === 'string') {
2309
+ try { resolvedContent = JSON.parse(resolvedContent); } catch (e) { resolvedContent = null; }
2310
+ }
2311
+ }
2312
+ if (resolvedContent && typeof resolvedContent === 'object') return resolvedContent;
2313
+
2314
+ let rawRcs = content;
2315
+ if (typeof rawRcs === 'string') {
2316
+ try { rawRcs = JSON.parse(rawRcs); } catch (e) { rawRcs = content; }
2317
+ }
2318
+ const snapCustomValues = previewCustomValuesRef.current;
2319
+ const hasEnteredValues = snapCustomValues && Object.keys(snapCustomValues).some((key) => snapCustomValues[key]);
2320
+ if (hasPreviewCallBeenMade && hasEnteredValues && Array.isArray(rawRcs?.carouselData)) {
2321
+ const formDataCardContent =
2322
+ formData?.versions?.base?.content?.RCS?.rcsContent?.cardContent
2323
+ ?? formData?.content?.RCS?.rcsContent?.cardContent
2324
+ ?? [];
2325
+ return {
2326
+ ...rawRcs,
2327
+ carouselData: rawRcs.carouselData.map((card, idx) => {
2328
+ const formCard = formDataCardContent[idx] || {};
2329
+ return {
2330
+ ...card,
2331
+ title: resolveTagsInText(formCard.title || card.title || '', snapCustomValues),
2332
+ bodyText: resolveTagsInText(formCard.description || card.bodyText || '', snapCustomValues),
2333
+ };
2334
+ }),
2335
+ };
2336
+ }
2337
+ return rawRcs || {};
2338
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2339
+ }, [channel, content, hasPreviewCallBeenMade, previewDataHtml, formData]);
2340
+
1872
2341
  const prepareUnifiedPreviewProps = () => {
1873
2342
  // Prepare content based on channel
1874
2343
  let contentObj = {};
@@ -1930,42 +2399,14 @@ const CommonTestAndPreview = (props) => {
1930
2399
  contentObj = parsedContent || {};
1931
2400
  }
1932
2401
  } 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
2402
+ // rcsCarouselContentObj is a useMemo that handles both the API-response path and the
2403
+ // client-side fallback. It is stable across keystrokes (only recomputes when
2404
+ // hasPreviewCallBeenMade / previewDataHtml / content / formData change).
1953
2405
  let parsedRcsContent = content;
1954
2406
  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 || {};
2407
+ try { parsedRcsContent = JSON.parse(content); } catch (e) { parsedRcsContent = content; }
1968
2408
  }
2409
+ contentObj = rcsCarouselContentObj || parsedRcsContent || {};
1969
2410
  } else if (channel === CHANNELS.INAPP) {
1970
2411
  // For InApp, content is an object with androidContent and iosContent (similar to MobilePush)
1971
2412
  // Content comes from InApp component state, passed via content prop
@@ -2168,6 +2609,10 @@ const CommonTestAndPreview = (props) => {
2168
2609
  formatMessage,
2169
2610
  lastModified: formData?.lastModified,
2170
2611
  updatedByName: formData?.updatedByName,
2612
+ smsFallbackContent: isRcsSmsFallbackPreviewEnabled ? smsFallbackContent : null,
2613
+ smsFallbackResolvedText,
2614
+ activePreviewTab,
2615
+ onPreviewTabChange: setActivePreviewTab,
2171
2616
  };
2172
2617
  };
2173
2618
 
@@ -2207,7 +2652,12 @@ const CommonTestAndPreview = (props) => {
2207
2652
  }, [show, beeInstance, currentTab, channel]);
2208
2653
 
2209
2654
  /**
2210
- * Initial data load when slidebox opens
2655
+ * Initial data load when slidebox opens.
2656
+ * EXTRACT TAGS CALL SITES (on open/edit RCS):
2657
+ * 1. Here (non-email): actions.extractTagsRequested() at line ~2161 when show is true.
2658
+ * 2. RCS SMS fallback useEffect below: Api.extractTagsWithMetaData() for fallback message.
2659
+ * 3. handleExtractTags() (user clicks "Enter custom values for tags") – not from effects.
2660
+ * The "Process extracted tags" effect only processes API results and must not call extract again.
2211
2661
  */
2212
2662
  useEffect(() => {
2213
2663
  if (show) {
@@ -2292,7 +2742,61 @@ const CommonTestAndPreview = (props) => {
2292
2742
  actions.getTestGroupsRequested();
2293
2743
  }
2294
2744
  }
2295
- }, [show, beeInstance, currentTab, channel]);
2745
+ // getCurrentContent: RCS applies cardVarMapped → placeholder resolution; re-extract when it changes.
2746
+ }, [show, beeInstance, currentTab, channel, getCurrentContent]);
2747
+
2748
+ /**
2749
+ * RCS with SMS fallback: extract tags for fallback SMS content as well
2750
+ * (so we can show a separate "Fallback SMS tags" section in left panel).
2751
+ */
2752
+ useEffect(() => {
2753
+ let cancelled = false;
2754
+
2755
+ if (!show || channel !== CHANNELS.RCS) {
2756
+ return () => {
2757
+ cancelled = true;
2758
+ };
2759
+ }
2760
+
2761
+ if (!smsFallbackContent?.templateContent && !smsFallbackContent?.content) {
2762
+ setSmsFallbackExtractedTags([]);
2763
+ setSmsFallbackRequiredTags([]);
2764
+ setSmsFallbackOptionalTags([]);
2765
+ return () => {
2766
+ cancelled = true;
2767
+ };
2768
+ }
2769
+
2770
+ setIsExtractingSmsFallbackTags(true);
2771
+ (async () => {
2772
+ try {
2773
+ const fallbackBodyForExtractApi = getSmsFallbackTextForTagExtraction(smsFallbackContent);
2774
+ const payload = {
2775
+ messageTitle: '',
2776
+ messageBody: fallbackBodyForExtractApi || '',
2777
+ };
2778
+ 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
2779
+ let smsFallbackTagTree = response?.data ?? [];
2780
+ if (!Array.isArray(smsFallbackTagTree)) smsFallbackTagTree = [];
2781
+ if (!smsTemplateHasMustacheTags(fallbackBodyForExtractApi)) {
2782
+ smsFallbackTagTree = [];
2783
+ } else if (smsFallbackTagTree.length === 0) {
2784
+ smsFallbackTagTree = buildSyntheticSmsMustacheTags(fallbackBodyForExtractApi);
2785
+ }
2786
+ if (cancelled) return;
2787
+ setSmsFallbackExtractedTags(smsFallbackTagTree);
2788
+ } catch (e) {
2789
+ if (cancelled) return;
2790
+ setSmsFallbackExtractedTags([]);
2791
+ } finally {
2792
+ if (!cancelled) setIsExtractingSmsFallbackTags(false);
2793
+ }
2794
+ })();
2795
+
2796
+ return () => {
2797
+ cancelled = true;
2798
+ };
2799
+ }, [show, channel, smsFallbackContent]);
2296
2800
 
2297
2801
  /**
2298
2802
  * Email-specific: Handle content updates for both BEE and CKEditor
@@ -2344,15 +2848,22 @@ const CommonTestAndPreview = (props) => {
2344
2848
  setSelectedCustomer(null);
2345
2849
  setRequiredTags([]);
2346
2850
  setOptionalTags([]);
2851
+ setSmsFallbackExtractedTags([]);
2852
+ setSmsFallbackRequiredTags([]);
2853
+ setSmsFallbackOptionalTags([]);
2854
+ setIsExtractingSmsFallbackTags(false);
2347
2855
  setCustomValues({});
2348
2856
  setShowJSON(false);
2349
2857
  setTagsExtracted(false);
2350
2858
  setPreviewDevice(DESKTOP);
2859
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2860
+ setSmsFallbackPreviewText(undefined);
2351
2861
  setSelectedTestEntities([]);
2352
2862
  actions.clearPrefilledValues();
2353
2863
  } else {
2354
2864
  // Reset device to initialDevice when opening (Android for mobile channels, Desktop for others)
2355
2865
  setPreviewDevice(initialDevice);
2866
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2356
2867
  }
2357
2868
  }, [show, initialDevice]);
2358
2869
 
@@ -2361,79 +2872,10 @@ const CommonTestAndPreview = (props) => {
2361
2872
  */
2362
2873
  useEffect(() => {
2363
2874
  if (previewData) {
2364
- setPreviewDataHtml(previewData);
2875
+ setPreviewDataHtml(toPlainPreviewData(previewData));
2365
2876
  }
2366
2877
  }, [previewData]);
2367
2878
 
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
2879
  /**
2438
2880
  * Handle customer selection and fetch prefilled values
2439
2881
  */
@@ -2441,17 +2883,15 @@ const CommonTestAndPreview = (props) => {
2441
2883
  if (selectedCustomer && config.enableCustomerSearch !== false) {
2442
2884
  setTagsExtracted(true); // Auto-open custom values editor
2443
2885
 
2444
- // Get all available tags
2445
- const allTags = [...requiredTags, ...optionalTags];
2446
2886
  const requiredTagObj = {};
2447
- requiredTags.forEach((tag) => {
2887
+ allRequiredTags.forEach((tag) => {
2448
2888
  requiredTagObj[tag?.fullPath] = '';
2449
2889
  });
2450
2890
  if (allTags.length > 0) {
2451
2891
  const payload = preparePreviewPayload(
2452
- channel,
2892
+ activeChannelForActions,
2453
2893
  formData || {},
2454
- getCurrentContent,
2894
+ activeContentForActions,
2455
2895
  {
2456
2896
  ...requiredTagObj,
2457
2897
  },
@@ -2460,7 +2900,7 @@ const CommonTestAndPreview = (props) => {
2460
2900
  actions.getPrefilledValuesRequested(payload);
2461
2901
  }
2462
2902
  }
2463
- }, [selectedCustomer]);
2903
+ }, [selectedCustomer, allTags.length, activeChannelForActions, activePreviewTab]);
2464
2904
 
2465
2905
  /**
2466
2906
  * Update custom values with prefilled values from API
@@ -2471,24 +2911,29 @@ const CommonTestAndPreview = (props) => {
2471
2911
  if (prefilledValues && selectedCustomer) {
2472
2912
  // Always replace all values with prefilled values
2473
2913
  const updatedValues = {};
2474
- [...requiredTags, ...optionalTags].forEach((tag) => {
2914
+ allTags.forEach((tag) => {
2475
2915
  updatedValues[tag?.fullPath] = prefilledValues[tag?.fullPath] || '';
2476
2916
  });
2477
2917
 
2478
2918
  setCustomValues(updatedValues);
2479
2919
 
2480
2920
  // Update preview with prefilled values (this is a valid preview call trigger)
2921
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped.
2922
+ // SMS fallback preview is kept in sync by syncSmsFallbackPreview via smsFallbackPreviewText.
2923
+ const previewChannelForPrefill = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
2924
+ const previewContentForPrefill = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2481
2925
  const payload = preparePreviewPayload(
2482
- channel,
2926
+ previewChannelForPrefill,
2483
2927
  formData || {},
2484
- getCurrentContent,
2928
+ previewContentForPrefill,
2485
2929
  updatedValues,
2486
2930
  selectedCustomer
2487
2931
  );
2488
2932
  actions.updatePreviewRequested(payload);
2489
2933
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2934
+ void syncSmsFallbackPreview(updatedValues, selectedCustomer);
2490
2935
  }
2491
- }, [JSON.stringify(prefilledValues), selectedCustomer]);
2936
+ }, [JSON.stringify(prefilledValues), selectedCustomer, activeChannelForActions, activePreviewTab]);
2492
2937
 
2493
2938
  /**
2494
2939
  * Map channel constants to display names (lowercase for message)
@@ -2582,11 +3027,7 @@ const CommonTestAndPreview = (props) => {
2582
3027
  setTagsExtracted(true); // Auto-open custom values editor
2583
3028
 
2584
3029
  // 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);
3030
+ setCustomValues(buildEmptyValues());
2590
3031
  };
2591
3032
 
2592
3033
  /**
@@ -2600,11 +3041,7 @@ const CommonTestAndPreview = (props) => {
2600
3041
  actions.clearPreviewErrors();
2601
3042
 
2602
3043
  // Initialize empty values for all tags
2603
- const emptyValues = {};
2604
- [...requiredTags, ...optionalTags].forEach((tag) => {
2605
- emptyValues[tag?.fullPath] = '';
2606
- });
2607
- setCustomValues(emptyValues);
3044
+ setCustomValues(buildEmptyValues());
2608
3045
 
2609
3046
  // Don't make preview call when clearing selection - just reset to raw content
2610
3047
  // Preview will be shown using raw formData/content
@@ -2640,17 +3077,20 @@ const CommonTestAndPreview = (props) => {
2640
3077
  */
2641
3078
  const handleDiscardCustomValues = () => {
2642
3079
  // Initialize empty values for all tags
2643
- const emptyValues = {};
2644
- [...requiredTags, ...optionalTags].forEach((tag) => {
2645
- emptyValues[tag?.fullPath] = '';
2646
- });
3080
+ const emptyValues = buildEmptyValues();
2647
3081
  setCustomValues(emptyValues);
2648
3082
 
3083
+ // Reset SMS fallback preview so it shows raw template (with {{tags}} visible) after discard
3084
+ setSmsFallbackPreviewText(undefined);
3085
+
2649
3086
  // Update preview with empty values (this is a valid preview call trigger)
3087
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped.
3088
+ const previewChannelForDiscard = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
3089
+ const previewContentForDiscard = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2650
3090
  const payload = preparePreviewPayload(
2651
- channel,
3091
+ previewChannelForDiscard,
2652
3092
  formData || {},
2653
- getCurrentContent,
3093
+ previewContentForDiscard,
2654
3094
  emptyValues,
2655
3095
  selectedCustomer
2656
3096
  );
@@ -2664,14 +3104,24 @@ const CommonTestAndPreview = (props) => {
2664
3104
  */
2665
3105
  const handleUpdatePreview = async () => {
2666
3106
  try {
3107
+ // Capture customValues at click time so the carousel preview only updates here,
3108
+ // not on every subsequent keystroke.
3109
+ previewCustomValuesRef.current = customValues;
3110
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped,
3111
+ // even when the user triggers update from the SMS fallback tab.
3112
+ // SMS fallback preview is kept in sync by syncSmsFallbackPreview via smsFallbackPreviewText.
3113
+ const previewChannel = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
3114
+ const previewContent = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2667
3115
  const payload = preparePreviewPayload(
2668
- channel,
3116
+ previewChannel,
2669
3117
  formData || {},
2670
- getCurrentContent,
3118
+ previewContent,
2671
3119
  customValues,
2672
3120
  selectedCustomer
2673
3121
  );
2674
3122
  await actions.updatePreviewRequested(payload);
3123
+
3124
+ await syncSmsFallbackPreview(customValues, selectedCustomer);
2675
3125
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2676
3126
  } catch (error) {
2677
3127
  CapNotification.error({
@@ -2681,25 +3131,115 @@ const CommonTestAndPreview = (props) => {
2681
3131
  };
2682
3132
 
2683
3133
  /**
2684
- * Handle extract tags
3134
+ * Categorize extracted tags into required/optional.
2685
3135
  */
2686
- const handleExtractTags = () => {
2687
- // Get content based on channel
2688
- let contentToExtract = getCurrentContent;
3136
+ const categorizeTags = (tagsTree = []) => {
3137
+ const required = [];
3138
+ const optional = [];
3139
+ let hasPersonalizationTags = false;
3140
+ const processTag = (tag, parentPath = '') => {
3141
+ const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
3142
+
3143
+ // Skip unsubscribe tag for input fields
3144
+ if (tag?.name === UNSUBSCRIBE_TAG_NAME) return;
3145
+
3146
+ hasPersonalizationTags = true;
3147
+ const userDriven = tag?.metaData?.userDriven;
3148
+ if (userDriven === true) {
3149
+ optional.push({ ...tag, fullPath: currentPath });
3150
+ } else {
3151
+ // false or missing (SMS/DLT extract often omits metaData) → required for test values
3152
+ required.push({ ...tag, fullPath: currentPath });
3153
+ }
3154
+
3155
+ if (tag?.children?.length > 0) {
3156
+ tag.children.forEach((child) => processTag(child, currentPath));
3157
+ }
3158
+ };
3159
+
3160
+ (tagsTree || []).forEach((tag) => processTag(tag));
3161
+ return { required, optional, hasPersonalizationTags };
3162
+ };
2689
3163
 
3164
+ /**
3165
+ * Apply tag extraction when content comes from RCS + SMS fallback (no API call).
3166
+ */
3167
+ const applyRcsSmsFallbackTagExtraction = () => {
3168
+ const rcsPrimaryCategorized = categorizeTags(extractedTags ?? []);
3169
+ const fallbackSmsResolvedForTags = smsFallbackTextForTagExtraction ?? '';
3170
+ let fallbackSmsTagTree = smsFallbackExtractedTags?.length > 0
3171
+ ? smsFallbackExtractedTags
3172
+ : buildSyntheticSmsMustacheTags(fallbackSmsResolvedForTags);
3173
+ if (!smsTemplateHasMustacheTags(fallbackSmsResolvedForTags)) {
3174
+ fallbackSmsTagTree = [];
3175
+ }
3176
+ const fallbackSmsCategorized = categorizeTags(fallbackSmsTagTree);
3177
+ setRequiredTags(rcsPrimaryCategorized.required);
3178
+ setOptionalTags(rcsPrimaryCategorized.optional);
3179
+ setSmsFallbackRequiredTags(fallbackSmsCategorized.required);
3180
+ setSmsFallbackOptionalTags(fallbackSmsCategorized.optional);
3181
+ setTagsExtracted(
3182
+ rcsPrimaryCategorized.hasPersonalizationTags || fallbackSmsCategorized.hasPersonalizationTags,
3183
+ );
3184
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, rcsPrimaryCategorized, fallbackSmsCategorized));
3185
+ };
3186
+
3187
+ /**
3188
+ * When extract-tags API returns, map Redux `extractedTags` into required/optional so
3189
+ * CustomValuesEditor shows personalization fields (effect was previously commented out).
3190
+ * RCS + SMS fallback: merge primary + fallback tag trees when fallback template exists.
3191
+ */
3192
+ useEffect(() => {
3193
+ if (!show) return;
3194
+ const hasFallbackSmsTemplate = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
3195
+ if (channel === CHANNELS.RCS && hasFallbackSmsTemplate) {
3196
+ applyRcsSmsFallbackTagExtraction();
3197
+ return;
3198
+ }
3199
+ const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
3200
+ let smsTagSource = channel === CHANNELS.SMS && (!extractedTags || extractedTags.length === 0)
3201
+ ? buildSyntheticSmsMustacheTags(smsEditorBody)
3202
+ : (extractedTags ?? []);
3203
+ if (channel === CHANNELS.SMS && !smsTemplateHasMustacheTags(smsEditorBody)) {
3204
+ smsTagSource = [];
3205
+ }
3206
+ const { required, optional, hasPersonalizationTags } = categorizeTags(smsTagSource);
3207
+ setRequiredTags(required);
3208
+ setOptionalTags(optional);
3209
+ setTagsExtracted(hasPersonalizationTags);
3210
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, { required, optional }, { required: [], optional: [] }));
3211
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- applyRcsSmsFallbackTagExtraction closes over latest extractedTags/smsFallbackExtractedTags
3212
+ }, [show, extractedTags, channel, smsFallbackContent, smsFallbackExtractedTags, getCurrentContent, smsFallbackTextForTagExtraction]);
3213
+
3214
+ /**
3215
+ * Get content to run tag extraction on (channel-specific).
3216
+ */
3217
+ const getContentForTagExtraction = () => {
3218
+ let contentToExtract = activeContentForActions;
2690
3219
  if (channel === CHANNELS.EMAIL && formData) {
2691
3220
  const currentTabData = formData[currentTab - 1];
2692
3221
  const activeTab = currentTabData?.activeTab;
2693
3222
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
2694
3223
  contentToExtract = templateContent || contentToExtract;
2695
3224
  }
3225
+ return contentToExtract;
3226
+ };
3227
+
3228
+ /**
3229
+ * Handle extract tags
3230
+ */
3231
+ const handleExtractTags = () => {
3232
+ if (channel === CHANNELS.RCS) {
3233
+ applyRcsSmsFallbackTagExtraction();
3234
+ return;
3235
+ }
2696
3236
 
2697
- // Check for personalization tags (excluding unsubscribe)
3237
+ const contentToExtract = getContentForTagExtraction();
2698
3238
  const tags = contentToExtract.match(/{{[^}]+}}/g) || [];
2699
3239
  const hasPersonalizationTags = tags.some((tag) => !tag.includes(UNSUBSCRIBE_TAG_NAME));
3240
+ const onlyUnsubscribe = !hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME);
2700
3241
 
2701
- if (!hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME)) {
2702
- // If only unsubscribe tag is present, show noTagsExtracted message
3242
+ if (onlyUnsubscribe) {
2703
3243
  setTagsExtracted(false);
2704
3244
  setRequiredTags([]);
2705
3245
  setOptionalTags([]);
@@ -2707,10 +3247,9 @@ const CommonTestAndPreview = (props) => {
2707
3247
  return;
2708
3248
  }
2709
3249
 
2710
- // Extract tags
2711
3250
  setTagsExtracted(true);
2712
3251
  const { templateSubject, templateContent } = prepareTagExtractionPayload(
2713
- channel,
3252
+ activeChannelForActions,
2714
3253
  formData || {},
2715
3254
  contentToExtract
2716
3255
  );
@@ -2729,7 +3268,7 @@ const CommonTestAndPreview = (props) => {
2729
3268
  setSelectedTestEntities([normalizeTestEntityId(value)]);
2730
3269
  return;
2731
3270
  }
2732
- setSelectedTestEntities(value.map((v) => normalizeTestEntityId(v)));
3271
+ setSelectedTestEntities(value.map((entityId) => normalizeTestEntityId(entityId)));
2733
3272
  };
2734
3273
 
2735
3274
  /**
@@ -2742,7 +3281,7 @@ const CommonTestAndPreview = (props) => {
2742
3281
  const getIdentifierValue = (type) => {
2743
3282
  const fromIdentifiers = detail.identifiers?.find((i) => i.type === type)?.value;
2744
3283
  if (fromIdentifiers) return fromIdentifiers;
2745
- const fromCommChannels = detail.commChannels?.find((c) => c.type === type)?.value;
3284
+ const fromCommChannels = detail.commChannels?.find((commChannel) => commChannel.type === type)?.value;
2746
3285
  return fromCommChannels || (channel === CHANNELS.EMAIL && type === IDENTIFIER_TYPE_EMAIL ? identifierValue : channel === CHANNELS.SMS && type === IDENTIFIER_TYPE_MOBILE ? identifierValue : '');
2747
3286
  };
2748
3287
  return {
@@ -2772,7 +3311,7 @@ const CommonTestAndPreview = (props) => {
2772
3311
  if (existingTestCustomer) {
2773
3312
  const entityId = existingTestCustomer.userId ?? existingTestCustomer.customerId;
2774
3313
  if (entityId != null) {
2775
- const id = 'customer:' + normalizeTestEntityId(entityId);
3314
+ const id = normalizeTestEntityId(entityId);
2776
3315
  setSelectedTestEntities((prev) => (
2777
3316
  prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
2778
3317
  ));
@@ -2806,10 +3345,10 @@ const CommonTestAndPreview = (props) => {
2806
3345
  const mapped = mapCustomerDetailsToCustomerData(details[0], searchValueToCheck);
2807
3346
  const customerIdFromLookup = mapped.customerId;
2808
3347
  const alreadyInTestListByCustomerId = customerIdFromLookup && testCustomers?.some(
2809
- (c) => String(c?.customerId) === customerIdFromLookup || String(c?.userId) === customerIdFromLookup
3348
+ (testCustomer) => String(testCustomer?.customerId) === customerIdFromLookup || String(testCustomer?.userId) === customerIdFromLookup
2810
3349
  );
2811
3350
  if (alreadyInTestListByCustomerId) {
2812
- const id = 'customer:' + normalizeTestEntityId(customerIdFromLookup);
3351
+ const id = normalizeTestEntityId(customerIdFromLookup);
2813
3352
  setSelectedTestEntities((prev) => (
2814
3353
  prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
2815
3354
  ));
@@ -2846,26 +3385,21 @@ const CommonTestAndPreview = (props) => {
2846
3385
  const handleSendTestMessage = () => {
2847
3386
  const allUserIds = [];
2848
3387
  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
- }
3388
+ const group = testGroups.find((testGroup) => testEntityIdsEqual(testGroup.groupId, entityId));
3389
+ if (group) {
3390
+ allUserIds.push(...group.userIds);
2855
3391
  } else {
2856
- const rawId = String(entityId).startsWith('customer:')
2857
- ? String(entityId).slice('customer:'.length)
2858
- : String(entityId);
2859
- allUserIds.push(Number(rawId));
3392
+ allUserIds.push(entityId);
2860
3393
  }
2861
3394
  });
2862
3395
  const uniqueUserIds = [...new Set(allUserIds)];
2863
3396
 
2864
- const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP].includes(channel)
3397
+ const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS].includes(channel)
2865
3398
  ? testPreviewDeliverySettings[channel]
2866
3399
  : null;
2867
3400
 
2868
- // Create initial payload based on channel
3401
+ // createMessageMeta must match the creative channel and full creative (RCS + SMS fallback in one meta).
3402
+ // Do not use activeChannelForActions / activeContentForActions — those follow the RCS vs Fallback SMS *preview* tab.
2869
3403
  const initialPayload = prepareTestMessagePayload(
2870
3404
  channel,
2871
3405
  formData || content || {},
@@ -2873,7 +3407,8 @@ const CommonTestAndPreview = (props) => {
2873
3407
  customValues,
2874
3408
  uniqueUserIds,
2875
3409
  previewData,
2876
- deliveryOverride
3410
+ deliveryOverride,
3411
+ {},
2877
3412
  );
2878
3413
 
2879
3414
  actions.createMessageMetaRequested(
@@ -2906,11 +3441,10 @@ const CommonTestAndPreview = (props) => {
2906
3441
  // ============================================
2907
3442
  // RENDER HELPER FUNCTIONS
2908
3443
  // ============================================
2909
-
2910
3444
  const renderLeftPanelContent = () => (
2911
3445
  <LeftPanelContent
2912
- isExtractingTags={isExtractingTags}
2913
- extractedTags={extractedTags}
3446
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
3447
+ extractedTags={leftPanelExtractedTags}
2914
3448
  selectedCustomer={selectedCustomer}
2915
3449
  handleCustomerSelect={handleCustomerSelect}
2916
3450
  handleSearchCustomer={handleSearchCustomer}
@@ -2928,15 +3462,29 @@ const CommonTestAndPreview = (props) => {
2928
3462
 
2929
3463
  const renderCustomValuesEditor = () => (
2930
3464
  <CustomValuesEditor
2931
- isExtractingTags={isExtractingTags}
3465
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
2932
3466
  isUpdatePreviewDisabled={isUpdatePreviewDisabled}
2933
3467
  showJSON={showJSON}
2934
3468
  setShowJSON={setShowJSON}
2935
3469
  customValues={customValues}
2936
3470
  handleJSONTextChange={handleJSONTextChange}
2937
- extractedTags={extractedTags}
2938
- requiredTags={requiredTags}
2939
- optionalTags={optionalTags}
3471
+ sections={[
3472
+ {
3473
+ key: channel,
3474
+ title:
3475
+ channel === CHANNELS.RCS
3476
+ ? messages.rcsTagsSectionTitle
3477
+ : messages[`${channel}TagsSectionTitle`],
3478
+ requiredTags,
3479
+ optionalTags,
3480
+ },
3481
+ {
3482
+ key: PREVIEW_TAB_SMS_FALLBACK,
3483
+ title: isRcsSmsFallbackPreviewEnabled ? messages.smsFallbackTagsSectionTitle : null,
3484
+ requiredTags: smsFallbackRequiredTags,
3485
+ optionalTags: smsFallbackOptionalTags,
3486
+ },
3487
+ ]}
2940
3488
  handleCustomValueChange={handleCustomValueChange}
2941
3489
  handleDiscardCustomValues={handleDiscardCustomValues}
2942
3490
  handleUpdatePreview={handleUpdatePreview}
@@ -2952,18 +3500,13 @@ const CommonTestAndPreview = (props) => {
2952
3500
  }));
2953
3501
  };
2954
3502
 
2955
- /** Trim pasted emails (trailing CR/LF). SMS: strip non-digits so pasted formatted numbers match isValidMobile / API. */
3503
+ /** Trim pasted emails (trailing CR/LF). Allow any input for SMS; valid-only check is in renderAddTestCustomerButton. */
2956
3504
  const handleTestCustomersSearch = useCallback((value) => {
2957
3505
  if (value == null || value === '') {
2958
3506
  setSearchValue('');
2959
3507
  return;
2960
3508
  }
2961
- const raw = String(value).trim();
2962
- if (channel === CHANNELS.SMS) {
2963
- setSearchValue(formatPhoneNumber(raw));
2964
- } else {
2965
- setSearchValue(raw);
2966
- }
3509
+ setSearchValue(String(value).trim());
2967
3510
  }, [channel]);
2968
3511
 
2969
3512
  const renderSendTestMessage = () => (
@@ -2981,12 +3524,13 @@ const CommonTestAndPreview = (props) => {
2981
3524
  renderAddTestCustomerButton={renderAddTestCustomerButton}
2982
3525
  formatMessage={formatMessage}
2983
3526
  deliverySettings={testPreviewDeliverySettings[channel]}
2984
- senderDetailsOptions={senderDetailsByChannel[channel]}
3527
+ senderDetailsByChannel={senderDetailsByChannel}
2985
3528
  wecrmAccounts={wecrmAccounts}
2986
3529
  onSaveDeliverySettings={handleSaveDeliverySettings}
2987
3530
  isLoadingSenderDetails={isLoadingSenderDetails}
2988
3531
  smsTraiDltEnabled={smsTraiDltEnabled}
2989
3532
  registeredSenderIds={registeredSenderIds}
3533
+ isChannelSmsFallbackPreviewEnabled={isRcsSmsFallbackPreviewEnabled}
2990
3534
  searchValue={searchValue}
2991
3535
  setSearchValue={handleTestCustomersSearch}
2992
3536
  />
@@ -3000,14 +3544,13 @@ const CommonTestAndPreview = (props) => {
3000
3544
 
3001
3545
  const renderAddTestCustomerButton = () => {
3002
3546
  const raw = (searchValue || '').trim();
3003
- const value = channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw;
3004
3547
  const showAddButton =
3005
3548
  [CHANNELS.EMAIL, CHANNELS.SMS].includes(channel) &&
3006
- (channel === CHANNELS.EMAIL ? isValidEmail(value) : isValidMobile(value));
3549
+ (channel === CHANNELS.EMAIL ? isValidEmail(raw) : isValidMobile(formatPhoneNumber(raw)));
3007
3550
  if (!showAddButton) return null;
3008
3551
  return (
3009
3552
  <AddTestCustomerButton
3010
- searchValue={value}
3553
+ searchValue={channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw}
3011
3554
  handleAddTestCustomer={handleAddTestCustomer}
3012
3555
  />
3013
3556
  );