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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/constants/unified.js +29 -0
  2. package/index.html +1 -0
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/cdnTransformation.js +75 -3
  6. package/utils/commonUtils.js +19 -1
  7. package/utils/rcsPayloadUtils.js +92 -0
  8. package/utils/templateVarUtils.js +201 -0
  9. package/utils/tests/cdnTransformation.test.js +127 -0
  10. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  11. package/utils/tests/templateVarUtils.test.js +204 -0
  12. package/v2Components/CapActionButton/constants.js +7 -0
  13. package/v2Components/CapActionButton/index.js +166 -108
  14. package/v2Components/CapActionButton/index.scss +157 -6
  15. package/v2Components/CapActionButton/messages.js +19 -3
  16. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  17. package/v2Components/CapImageUpload/index.js +2 -2
  18. package/v2Components/CapTagList/index.js +10 -0
  19. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +72 -49
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  24. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  25. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  26. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/PreviewHeader.js +16 -0
  28. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +157 -15
  29. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberPreviewContent.js +14 -132
  30. package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +169 -0
  31. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +400 -239
  32. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +202 -10
  33. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  34. package/v2Components/CommonTestAndPreview/constants.js +40 -2
  35. package/v2Components/CommonTestAndPreview/index.js +887 -453
  36. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  37. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  38. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  39. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  40. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  41. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  42. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  43. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  44. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  45. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/PreviewHeader.test.js +163 -0
  46. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  47. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/ViberPreviewContent.test.js +0 -364
  48. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WebPushPreviewContent.test.js +522 -0
  49. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +454 -1
  50. package/v2Components/CommonTestAndPreview/tests/constants.test.js +2 -1
  51. package/v2Components/CommonTestAndPreview/tests/index.test.js +327 -4
  52. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  53. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  54. package/v2Components/FormBuilder/index.js +167 -56
  55. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  56. package/v2Components/SmsFallback/constants.js +73 -0
  57. package/v2Components/SmsFallback/index.js +956 -0
  58. package/v2Components/SmsFallback/index.scss +265 -0
  59. package/v2Components/SmsFallback/messages.js +78 -0
  60. package/v2Components/SmsFallback/smsFallbackUtils.js +119 -0
  61. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  62. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  63. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  64. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +223 -0
  65. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  66. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  67. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  68. package/v2Components/TemplatePreview/_templatePreview.scss +37 -22
  69. package/v2Components/TemplatePreview/constants.js +2 -0
  70. package/v2Components/TemplatePreview/index.js +143 -31
  71. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  72. package/v2Components/TestAndPreviewSlidebox/index.js +15 -3
  73. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  74. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  75. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  76. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  77. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  78. package/v2Containers/App/constants.js +3 -0
  79. package/v2Containers/App/tests/constants.test.js +61 -0
  80. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +17 -0
  81. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  82. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  83. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  84. package/v2Containers/CreativesContainer/constants.js +9 -0
  85. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  86. package/v2Containers/CreativesContainer/index.js +382 -127
  87. package/v2Containers/CreativesContainer/index.scss +83 -1
  88. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  89. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +79 -34
  90. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  92. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  93. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  94. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  95. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  96. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  97. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  98. package/v2Containers/Rcs/constants.js +120 -11
  99. package/v2Containers/Rcs/index.js +2577 -812
  100. package/v2Containers/Rcs/index.scss +281 -8
  101. package/v2Containers/Rcs/messages.js +34 -3
  102. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  103. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98036 -70145
  104. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  105. package/v2Containers/Rcs/tests/index.test.js +152 -121
  106. package/v2Containers/Rcs/tests/mockData.js +38 -0
  107. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  108. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  109. package/v2Containers/Rcs/utils.js +478 -11
  110. package/v2Containers/Sms/Create/index.js +106 -40
  111. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  112. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  113. package/v2Containers/SmsTrai/Create/index.js +9 -4
  114. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  115. package/v2Containers/SmsTrai/Edit/index.js +640 -130
  116. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  117. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  118. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  119. package/v2Containers/SmsWrapper/index.js +37 -8
  120. package/v2Containers/TagList/index.js +6 -0
  121. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  122. package/v2Containers/Templates/_templates.scss +166 -86
  123. package/v2Containers/Templates/actions.js +11 -0
  124. package/v2Containers/Templates/constants.js +2 -0
  125. package/v2Containers/Templates/index.js +203 -145
  126. package/v2Containers/Templates/sagas.js +62 -13
  127. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  128. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1062 -1017
  129. package/v2Containers/Templates/tests/sagas.test.js +222 -22
  130. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  131. package/v2Containers/Templates/tests/webpush.test.js +375 -0
  132. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  133. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  134. package/v2Containers/TemplatesV2/index.js +86 -23
  135. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  136. package/v2Containers/Viber/constants.js +0 -19
  137. package/v2Containers/Viber/index.js +47 -714
  138. package/v2Containers/Viber/index.scss +0 -148
  139. package/v2Containers/Viber/messages.js +0 -116
  140. package/v2Containers/Viber/tests/index.test.js +0 -80
  141. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  142. package/v2Containers/WebPush/Create/index.js +91 -8
  143. package/v2Containers/WebPush/Create/index.scss +7 -0
  144. package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +348 -0
  145. package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +325 -0
  146. package/v2Containers/Whatsapp/index.js +3 -20
  147. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -31,7 +31,8 @@ import CustomValuesEditor from './CustomValuesEditor';
31
31
  import SendTestMessage from './SendTestMessage';
32
32
  import PreviewSection from './PreviewSection';
33
33
 
34
- // Import constants
34
+ import * as Api from '../../services/api';
35
+ import { extractTemplateVariables } from '../../utils/templateVarUtils';
35
36
  import AddTestCustomerButton from './AddTestCustomer';
36
37
  import ExistingCustomerModal from './ExistingCustomerModal';
37
38
  // Import constants
@@ -81,11 +82,24 @@ import {
81
82
  IMAGE,
82
83
  VIDEO,
83
84
  URL,
84
- CHANNELS_USING_ANDROID_PREVIEW_DEVICE
85
+ PREVIEW_TAB_RCS,
86
+ PREVIEW_TAB_SMS_FALLBACK,
87
+ CHANNELS_USING_ANDROID_PREVIEW_DEVICE,
88
+ DAYS,
89
+ RCS_TEST_META_CONTENT_TYPE_RICHCARD,
90
+ RCS_TEST_META_CARD_TYPE_STANDALONE,
91
+ RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
92
+ RCS_TEST_META_CARD_WIDTH_SMALL,
93
+ SMS_MUSTACHE_TAG_PATTERN,
85
94
  } from './constants';
86
-
87
- // Import utilities
88
95
  import { getCdnUrl } from '../../utils/cdnTransformation';
96
+ import {
97
+ normalizePreviewApiPayload,
98
+ extractPreviewFromLiquidResponse,
99
+ getSmsFallbackTextForTagExtraction,
100
+ } from './previewApiUtils';
101
+ import { pickFirstSmsFallbackTemplateString } from '../../v2Containers/Rcs/rcsLibraryHydrationUtils';
102
+
89
103
  import { isValidEmail, isValidMobile, formatPhoneNumber } from '../../utils/commonUtils';
90
104
  import { getMembersLookup } from '../../services/api';
91
105
 
@@ -108,6 +122,85 @@ const filterUsableGsmSendersForDomain = (domain, gsmSenders, { skipDomainNameEch
108
122
  });
109
123
  };
110
124
 
125
+ /** Preview payload from Redux may be an Immutable Map — normalize for React state. */
126
+ const toPlainPreviewData = (data) => {
127
+ if (data == null) return null;
128
+ const plain = typeof data.toJS === 'function' ? data.toJS() : data;
129
+ return normalizePreviewApiPayload(plain);
130
+ };
131
+
132
+ /**
133
+ * Merge existing customValues with tag keys from categorized groups.
134
+ * Each group is { required, optional } (arrays of tag objects with fullPath).
135
+ * Preserves existing values; adds '' for any tag key not yet present.
136
+ * Reusable for RCS+fallback and any flow that needs to ensure customValues has keys for tags.
137
+ */
138
+ const mergeCustomValuesWithTagKeys = (prev, ...categorizedGroups) => {
139
+ const next = { ...(prev || {}) };
140
+ categorizedGroups.forEach((group) => {
141
+ [...(group.required || []), ...(group.optional || [])].forEach((tag) => {
142
+ const key = tag?.fullPath;
143
+ if (key && next[key] === undefined) next[key] = '';
144
+ });
145
+ });
146
+ return next;
147
+ };
148
+
149
+ /** True when `body` contains `{{name}}` mustache tokens (user-fillable personalization tags).
150
+ * DLT `{#name#}` slots are pre-bound template variables and are intentionally excluded. */
151
+ const smsTemplateHasMustacheTags = (body) =>
152
+ typeof body === 'string' && SMS_MUSTACHE_TAG_PATTERN.test(body);
153
+
154
+ /**
155
+ * Build tag rows from `{{…}}` mustache tokens only — DLT `{#…#}` slots are excluded because
156
+ * they are pre-bound template variables, not user-fillable personalization tags.
157
+ * Passing a mustache-only captureRegex to extractTemplateVariables skips the DLT branch.
158
+ * A non-global regex is used so ensureGlobalRegexForExecLoop creates a fresh instance on each call.
159
+ */
160
+ const buildSyntheticSmsMustacheTags = (body = '') => {
161
+ if (!body || typeof body !== 'string') return [];
162
+ return extractTemplateVariables(body, /\{\{([^}]+)\}\}/).map((name) => ({
163
+ name,
164
+ metaData: { userDriven: false },
165
+ children: [],
166
+ }));
167
+ };
168
+
169
+ /** RCS createMessageMeta: media shape (mediaUrl, thumbnailUrl, height string). */
170
+ const normalizeRcsTestCardMedia = (media) => {
171
+ if (!media || typeof media !== 'object') return undefined;
172
+ const mediaUrl =
173
+ media.mediaUrl != null && String(media.mediaUrl).trim() !== ''
174
+ ? String(media.mediaUrl)
175
+ : media.url != null && String(media.url).trim() !== ''
176
+ ? String(media.url)
177
+ : '';
178
+ const thumbnailUrl = media.thumbnailUrl != null ? String(media.thumbnailUrl) : '';
179
+ const height = media.height != null ? String(media.height) : undefined;
180
+ const out = { mediaUrl, thumbnailUrl };
181
+ if (height) out.height = height;
182
+ return out;
183
+ };
184
+
185
+ /** RCS createMessageMeta: suggestion shape (index, type, text, phoneNumber, url, postback). */
186
+ const mapRcsSuggestionForTestMeta = (suggestionRow, index) => ({
187
+ index,
188
+ type: suggestionRow?.type ?? '',
189
+ text: suggestionRow?.text != null ? String(suggestionRow.text) : '',
190
+ phoneNumber:
191
+ suggestionRow?.phoneNumber != null
192
+ ? String(suggestionRow.phoneNumber)
193
+ : suggestionRow?.phone_number != null
194
+ ? String(suggestionRow.phone_number)
195
+ : '',
196
+ url: suggestionRow?.url !== undefined ? suggestionRow.url : null,
197
+ postback:
198
+ suggestionRow?.postback != null
199
+ ? String(suggestionRow.postback)
200
+ : suggestionRow?.text != null
201
+ ? String(suggestionRow.text)
202
+ : '',
203
+ });
111
204
 
112
205
  /**
113
206
  * CapTreeSelect and group resolution use strict equality; API data may mix numeric and string ids.
@@ -133,7 +226,7 @@ const testEntityIdsEqual = (a, b) => {
133
226
  */
134
227
  const CommonTestAndPreview = (props) => {
135
228
  const {
136
- intl: { formatMessage },
229
+ intl: { formatMessage, locale: userLocale = 'en' },
137
230
  show,
138
231
  onClose,
139
232
  channel, // The channel: 'EMAIL', 'SMS', 'RCS', etc.
@@ -169,13 +262,23 @@ const CommonTestAndPreview = (props) => {
169
262
  ...additionalProps
170
263
  } = props;
171
264
 
265
+ const smsFallbackContent = additionalProps?.smsFallbackContent;
266
+ const smsFallbackTextForTagExtraction = useMemo(
267
+ () => getSmsFallbackTextForTagExtraction(smsFallbackContent),
268
+ [smsFallbackContent],
269
+ );
172
270
  // ============================================
173
271
  // STATE MANAGEMENT
174
272
  // ============================================
175
273
  const [selectedCustomer, setSelectedCustomer] = useState(null);
176
274
  const [requiredTags, setRequiredTags] = useState([]);
177
275
  const [optionalTags, setOptionalTags] = useState([]);
276
+ const [smsFallbackExtractedTags, setSmsFallbackExtractedTags] = useState([]);
277
+ const [smsFallbackRequiredTags, setSmsFallbackRequiredTags] = useState([]);
278
+ const [smsFallbackOptionalTags, setSmsFallbackOptionalTags] = useState([]);
279
+ const [isExtractingSmsFallbackTags, setIsExtractingSmsFallbackTags] = useState(false);
178
280
  const [customValues, setCustomValues] = useState({});
281
+ const previewCustomValuesRef = useRef({});
179
282
  const [showJSON, setShowJSON] = useState(false);
180
283
  const [tagsExtracted, setTagsExtracted] = useState(false);
181
284
 
@@ -186,10 +289,10 @@ const CommonTestAndPreview = (props) => {
186
289
  const [customerData, setCustomerData] = useState({ name: '', email: '', mobile: '', customerId: '' });
187
290
 
188
291
  const [previewDevice, setPreviewDevice] = useState(initialDevice);
292
+ const [activePreviewTab, setActivePreviewTab] = useState(PREVIEW_TAB_RCS);
293
+ const [smsFallbackPreviewText, setSmsFallbackPreviewText] = useState(undefined);
189
294
  // Track if a preview call has been made (to know when to use previewDataHtml vs raw content)
190
295
  const [hasPreviewCallBeenMade, setHasPreviewCallBeenMade] = useState(false);
191
- // Viber: tag values applied to preview only after explicit Update preview / Discard (not while typing)
192
- const [viberPreviewTagValues, setViberPreviewTagValues] = useState(null);
193
296
  const [previewDataHtml, setPreviewDataHtml] = useState(() => {
194
297
  // Initialize preview data based on channel
195
298
  if (channel === CHANNELS.EMAIL && formData) {
@@ -220,15 +323,22 @@ const CommonTestAndPreview = (props) => {
220
323
  [CHANNELS.WHATSAPP]: {
221
324
  domainId: null, senderMobNum: '', sourceAccountIdentifier: '',
222
325
  },
326
+ [CHANNELS.RCS]: {
327
+ domainId: null,
328
+ domainGatewayMapId: null,
329
+ gsmSenderId: '',
330
+ smsFallbackDomainId: null,
331
+ cdmaSenderId: '', // gsmSenderId = RCS sender (domainId|senderId), cdmaSenderId = SMS fallback
332
+ },
223
333
  });
224
334
 
225
- const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP];
335
+ const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS];
226
336
  const formDataForSendTest = formData ?? (content && typeof content === 'object' && !Array.isArray(content) ? content : formData);
227
337
  const smsTemplateConfigs = formDataForSendTest?.templateConfigs || {};
228
338
  const smsTraiDltEnabled = !!smsTemplateConfigs?.traiDltEnabled;
229
339
  const registeredSenderIds = smsTemplateConfigs?.registeredSenderIds || [];
230
340
 
231
- // Fetch sender details and WeCRM accounts when Test & Preview opens for SMS/Email/WhatsApp
341
+ // Fetch sender details and WeCRM accounts when Test & Preview opens (SMS, Email, WhatsApp, RCS — same process)
232
342
  useEffect(() => {
233
343
  if (!show || !channel) {
234
344
  return;
@@ -237,39 +347,99 @@ const CommonTestAndPreview = (props) => {
237
347
  if (actions.getSenderDetailsRequested) {
238
348
  actions.getSenderDetailsRequested({ channel, orgUnitId: orgUnitId ?? -1 });
239
349
  }
350
+ // SMS domains/senders are needed for RCS delivery UI (fallback row + slidebox) whenever RCS is open — not only when fallback body exists.
351
+ if (channel === CHANNELS.RCS && actions.getSenderDetailsRequested) {
352
+ actions.getSenderDetailsRequested({ channel: CHANNELS.SMS, orgUnitId: orgUnitId ?? -1 });
353
+ }
240
354
  if (channel === CHANNELS.WHATSAPP && actions.getWeCrmAccountsRequested) {
241
355
  actions.getWeCrmAccountsRequested({ sourceName: CHANNELS.WHATSAPP });
242
356
  }
243
357
  }
244
358
  }, [show, channel, orgUnitId, actions]);
245
359
 
246
- useEffect(() => {
247
- if (!show) {
248
- setCustomerModal([false, '']);
249
- setSearchValue('');
250
- setCustomerData({ name: '', email: '', mobile: '', customerId: '' });
251
- }
252
- }, [show]);
253
-
254
360
  const findDefault = (arr) => (arr && arr.find((x) => x.default)) || (arr && arr[0]) || {};
255
361
 
256
362
  // Auto-set default delivery setting when sender details load (campaigns-style: first domain + default/first sender)
257
363
  useEffect(() => {
258
364
  if (!channel || !channelsWithDeliverySettings.includes(channel)) return;
365
+
366
+ if (channel === CHANNELS.RCS) {
367
+ const rcsDomainRows = senderDetailsByChannel?.[CHANNELS.RCS] || [];
368
+ const smsFallbackDomainRows = senderDetailsByChannel?.[CHANNELS.SMS] || [];
369
+ if (!rcsDomainRows.length) return;
370
+
371
+ const currentRcsDeliverySettings = testPreviewDeliverySettings?.[CHANNELS.RCS] || {};
372
+ const isRcsGsmSenderUnset = !currentRcsDeliverySettings?.gsmSenderId;
373
+ const isSmsFallbackSenderUnset = !currentRcsDeliverySettings?.cdmaSenderId;
374
+ const isRcsDeliveryFullyUnset = isRcsGsmSenderUnset && isSmsFallbackSenderUnset;
375
+ const shouldOnlyFillSmsFallbackSender =
376
+ !isRcsGsmSenderUnset && isSmsFallbackSenderUnset && smsFallbackDomainRows.length > 0;
377
+
378
+ if (!isRcsDeliveryFullyUnset && !shouldOnlyFillSmsFallbackSender) return;
379
+
380
+ const firstRcsDomain = rcsDomainRows[0];
381
+ const firstSmsFallbackDomain = smsFallbackDomainRows[0];
382
+ const usableRcsGsmSenders = filterUsableGsmSendersForDomain(
383
+ firstRcsDomain,
384
+ firstRcsDomain?.gsmSenders,
385
+ { skipDomainNameEchoFilter: true },
386
+ );
387
+ const usableSmsFallbackGsmSenders = firstSmsFallbackDomain
388
+ ? filterUsableGsmSendersForDomain(firstSmsFallbackDomain, firstSmsFallbackDomain?.gsmSenders)
389
+ : [];
390
+ const defaultRcsGsmSender = usableRcsGsmSenders[0];
391
+ const defaultSmsFallbackGsmSender = usableSmsFallbackGsmSenders[0];
392
+ const rcsSenderCompositeValue =
393
+ firstRcsDomain?.domainId != null && defaultRcsGsmSender?.value != null
394
+ ? `${firstRcsDomain.domainId}|${defaultRcsGsmSender.value}`
395
+ : (defaultRcsGsmSender?.value || '');
396
+ const smsFallbackSenderCompositeValue =
397
+ firstSmsFallbackDomain?.domainId != null && defaultSmsFallbackGsmSender?.value != null
398
+ ? `${firstSmsFallbackDomain.domainId}|${defaultSmsFallbackGsmSender.value}`
399
+ : (defaultSmsFallbackGsmSender?.value || '');
400
+
401
+ setTestPreviewDeliverySettings((prev) => {
402
+ const previousRcsSettings = prev?.[CHANNELS.RCS] || {};
403
+ if (shouldOnlyFillSmsFallbackSender) {
404
+ if (!smsFallbackSenderCompositeValue) return prev;
405
+ return {
406
+ ...prev,
407
+ [CHANNELS.RCS]: {
408
+ ...previousRcsSettings,
409
+ smsFallbackDomainId: firstSmsFallbackDomain?.domainId ?? null,
410
+ cdmaSenderId: smsFallbackSenderCompositeValue,
411
+ },
412
+ };
413
+ }
414
+ return {
415
+ ...prev,
416
+ [CHANNELS.RCS]: {
417
+ domainId: firstRcsDomain?.domainId ?? null,
418
+ domainGatewayMapId: firstRcsDomain?.dgmId ?? null,
419
+ gsmSenderId: rcsSenderCompositeValue,
420
+ smsFallbackDomainId: firstSmsFallbackDomain?.domainId ?? null,
421
+ cdmaSenderId: smsFallbackSenderCompositeValue,
422
+ },
423
+ };
424
+ });
425
+ return;
426
+ }
427
+
259
428
  const domains = senderDetailsByChannel[channel];
260
429
  if (!domains || domains.length === 0) return;
261
430
  const {
262
- domainId = '', gsmSenderId = '', senderEmail = '', senderMobNum = '',
431
+ domainId = '', gsmSenderId = '', cdmaSenderId = '', senderEmail = '', senderMobNum = '',
263
432
  } = testPreviewDeliverySettings[channel] || {};
264
- const isEmptySelection = !domainId && !gsmSenderId && !senderEmail && !senderMobNum;
433
+ const isEmptySelection = !domainId && !gsmSenderId && !cdmaSenderId && !senderEmail && !senderMobNum;
265
434
  if (!isEmptySelection) return;
266
435
 
267
436
  const whatsappAccountFromForm = channel === CHANNELS.WHATSAPP ? formData?.accountName : undefined;
268
437
  const matchedWhatsappAccount = whatsappAccountFromForm
269
438
  ? (wecrmAccounts || []).find((account) => account?.name === whatsappAccountFromForm)
270
439
  : null;
271
- const smsDomains = channel === CHANNELS.SMS && smsTraiDltEnabled
272
- ? domains.filter((domain) => (domain.gsmSenders || []).some((gsm) => registeredSenderIds.includes(gsm.value)))
440
+ const smsDomains = channel === CHANNELS.SMS && smsTraiDltEnabled && registeredSenderIds?.length
441
+ ? domains.filter((domain) => (domain?.gsmSenders || []).some((gsm) =>
442
+ registeredSenderIds?.includes(gsm?.value)))
273
443
  : domains;
274
444
  const [defaultDomain] = domains;
275
445
  const [firstSmsDomain] = smsDomains;
@@ -284,8 +454,8 @@ const CommonTestAndPreview = (props) => {
284
454
  const next = { ...prev };
285
455
  if (channel === CHANNELS.SMS) {
286
456
  const smsGsmSenders = smsTraiDltEnabled
287
- ? (firstDomain.gsmSenders || []).filter((gsm) => registeredSenderIds.includes(gsm.value))
288
- : firstDomain.gsmSenders;
457
+ ? (firstDomain?.gsmSenders || []).filter((gsm) => registeredSenderIds?.includes(gsm?.value))
458
+ : firstDomain?.gsmSenders;
289
459
  next[channel] = {
290
460
  domainId: firstDomain.domainId,
291
461
  domainGatewayMapId: firstDomain.dgmId,
@@ -318,10 +488,29 @@ const CommonTestAndPreview = (props) => {
318
488
  // MEMOIZED VALUES
319
489
  // ============================================
320
490
 
491
+ const allTags = useMemo(
492
+ () => [...requiredTags, ...optionalTags, ...smsFallbackRequiredTags, ...smsFallbackOptionalTags],
493
+ [requiredTags, optionalTags, smsFallbackRequiredTags, smsFallbackOptionalTags]
494
+ );
495
+
496
+ const allRequiredTags = useMemo(
497
+ () => [...requiredTags, ...smsFallbackRequiredTags],
498
+ [requiredTags, smsFallbackRequiredTags]
499
+ );
500
+
501
+ const buildEmptyValues = useCallback(
502
+ () => allTags.reduce((acc, tag) => {
503
+ const key = tag?.fullPath;
504
+ if (key) acc[key] = '';
505
+ return acc;
506
+ }, {}),
507
+ [allTags]
508
+ );
509
+
321
510
  // Check if update preview button should be disabled
322
511
  const isUpdatePreviewDisabled = useMemo(() => (
323
- requiredTags.some((tag) => !customValues[tag.fullPath])
324
- ), [requiredTags, customValues]);
512
+ allRequiredTags.some((tag) => !customValues[tag.fullPath])
513
+ ), [allRequiredTags, customValues]);
325
514
 
326
515
  // Get current content based on channel and editor type
327
516
  const getCurrentContent = useMemo(() => {
@@ -365,6 +554,13 @@ const CommonTestAndPreview = (props) => {
365
554
  return currentTabData.base['sms-editor'];
366
555
  }
367
556
  }
557
+ // DLT / Test & Preview shape: { templateConfigs: { template, templateId, ... } }
558
+ if (formData.templateConfigs?.template) {
559
+ const smsDltTemplateValue = formData.templateConfigs.template;
560
+ if (typeof smsDltTemplateValue === 'string') return smsDltTemplateValue;
561
+ if (Array.isArray(smsDltTemplateValue)) return smsDltTemplateValue.join('');
562
+ return '';
563
+ }
368
564
  }
369
565
 
370
566
  // SMS channel fallback - if formData is not available, use content directly
@@ -410,7 +606,70 @@ const CommonTestAndPreview = (props) => {
410
606
  return content || '';
411
607
  }, [channel, formData, currentTab, beeContent, content, beeInstance]);
412
608
 
413
- // Build test entities tree data
609
+ const leftPanelExtractedTags = useMemo(() => {
610
+ if (channel === CHANNELS.SMS) {
611
+ const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
612
+ if (!smsTemplateHasMustacheTags(smsEditorBody)) return [];
613
+ const extractTagsFromApi = extractedTags ?? [];
614
+ if (extractTagsFromApi.length > 0) return extractTagsFromApi;
615
+ return buildSyntheticSmsMustacheTags(smsEditorBody);
616
+ }
617
+ const hasFallbackSmsBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
618
+ if (channel === CHANNELS.RCS && hasFallbackSmsBody) {
619
+ const rcsPrimaryTags = extractedTags ?? [];
620
+ const fallbackSmsTextForTags = smsFallbackTextForTagExtraction ?? '';
621
+ const fallbackSmsTagRows = smsTemplateHasMustacheTags(fallbackSmsTextForTags)
622
+ ? (smsFallbackExtractedTags?.length > 0
623
+ ? smsFallbackExtractedTags
624
+ : buildSyntheticSmsMustacheTags(fallbackSmsTextForTags))
625
+ : [];
626
+ const mergedRcsAndFallbackTags = [...rcsPrimaryTags, ...fallbackSmsTagRows];
627
+ if (mergedRcsAndFallbackTags.length > 0) return mergedRcsAndFallbackTags;
628
+ return buildSyntheticSmsMustacheTags(fallbackSmsTextForTags);
629
+ }
630
+ return extractedTags ?? [];
631
+ }, [
632
+ channel,
633
+ extractedTags,
634
+ getCurrentContent,
635
+ smsFallbackContent,
636
+ smsFallbackExtractedTags,
637
+ smsFallbackTextForTagExtraction,
638
+ ]);
639
+
640
+ const isRcsSmsFallbackPreviewEnabled =
641
+ channel === CHANNELS.RCS
642
+ && !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
643
+ // Only treat as SMS when user is on the Fallback SMS tab — not whenever fallback exists (RCS tab needs RCS preview API).
644
+ const isSmsFallbackTabActive = isRcsSmsFallbackPreviewEnabled && activePreviewTab === PREVIEW_TAB_SMS_FALLBACK;
645
+ const activeChannelForActions = isSmsFallbackTabActive ? CHANNELS.SMS : channel;
646
+ // VarSegment slot values live in rcsSmsFallbackVarMapped; raw templateContent alone is stale for /preview Body.
647
+ const resolvedSmsFallbackBodyForPreviewTab =
648
+ smsFallbackTextForTagExtraction
649
+ || smsFallbackContent?.templateContent
650
+ || smsFallbackContent?.content
651
+ || '';
652
+ const activeContentForActions = isSmsFallbackTabActive
653
+ ? resolvedSmsFallbackBodyForPreviewTab
654
+ : getCurrentContent;
655
+
656
+ /**
657
+ * SMS fallback pane must show /preview API result when user updated preview on that tab (plain text).
658
+ * Skip when resolvedBody is RCS-shaped (e.g. user last previewed on RCS tab).
659
+ */
660
+ // smsFallbackPreviewText is the single source of truth for the resolved SMS fallback preview.
661
+ // It is set only by syncSmsFallbackPreview (called from handleUpdatePreview and the
662
+ // prefilled-values effect) and reset to undefined on discard / slidebox close.
663
+ // Using previewDataHtml as a fallback is unsafe because that state is shared with the primary
664
+ // RCS preview and can contain stale SMS or RCS content.
665
+ const smsFallbackResolvedText = useMemo(() => {
666
+ const hasFallbackBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
667
+ if (channel !== CHANNELS.RCS || !hasFallbackBody) return undefined;
668
+ if (smsFallbackPreviewText != null) return smsFallbackPreviewText;
669
+ return undefined;
670
+ }, [channel, smsFallbackContent, smsFallbackPreviewText]);
671
+
672
+ // Build test entities tree data from testCustomers prop
414
673
  // Build test entities tree data from testCustomers prop (includes customers added via addTestCustomer action)
415
674
  const testEntitiesTreeData = useMemo(() => {
416
675
  const groupsNode = {
@@ -419,7 +678,7 @@ const CommonTestAndPreview = (props) => {
419
678
  selectable: false,
420
679
  children: testGroups?.map((group) => ({
421
680
  title: group?.groupName,
422
- value: 'group:' + normalizeTestEntityId(group?.groupId),
681
+ value: normalizeTestEntityId(group?.groupId),
423
682
  })),
424
683
  };
425
684
 
@@ -429,7 +688,7 @@ const CommonTestAndPreview = (props) => {
429
688
  selectable: false,
430
689
  children: testCustomers?.map((customer) => ({
431
690
  title: customer?.name?.trim() || customer?.email?.trim() || customer?.mobile?.trim() || customer?.userId || customer?.customerId,
432
- value: 'customer:' + normalizeTestEntityId(customer?.userId ?? customer?.customerId),
691
+ value: normalizeTestEntityId(customer?.userId ?? customer?.customerId),
433
692
  })) || [],
434
693
  };
435
694
 
@@ -457,144 +716,6 @@ const CommonTestAndPreview = (props) => {
457
716
  return resolvedText;
458
717
  };
459
718
 
460
- const getViberMergedFormData = useCallback((formDataOverride) => {
461
- const formDataObj = formDataOverride && typeof formDataOverride === 'object'
462
- ? formDataOverride
463
- : (formData && typeof formData === 'object' ? formData : {});
464
- let contentObj = {};
465
- if (content && typeof content === 'object') {
466
- contentObj = content;
467
- } else if (typeof content === 'string' && content.trim()) {
468
- try {
469
- contentObj = JSON.parse(content);
470
- } catch (e) {
471
- contentObj = {};
472
- }
473
- }
474
- const previewCards = formDataObj.viberPreviewContent?.cards
475
- ?? formDataObj.cards
476
- ?? contentObj.viberPreviewContent?.cards
477
- ?? contentObj.cards;
478
- const mergedPreview = {
479
- ...(contentObj.viberPreviewContent || {}),
480
- ...(formDataObj.viberPreviewContent || {}),
481
- ...(Array.isArray(previewCards) && previewCards.length ? { cards: previewCards } : {}),
482
- };
483
- return {
484
- ...contentObj,
485
- ...formDataObj,
486
- ...(Array.isArray(previewCards) && previewCards.length ? { cards: previewCards } : {}),
487
- ...(Object.keys(mergedPreview).length ? { viberPreviewContent: mergedPreview } : {}),
488
- };
489
- }, [formData, content]);
490
-
491
- const getViberCarouselCardsFromFormData = (formDataObj) => {
492
- const previewCards = formDataObj?.viberPreviewContent?.cards;
493
- if (Array.isArray(previewCards) && previewCards.length) {
494
- return previewCards;
495
- }
496
- const rootCards = formDataObj?.cards;
497
- if (Array.isArray(rootCards) && rootCards.length) {
498
- return rootCards;
499
- }
500
- return [];
501
- };
502
-
503
- const getViberTagExtractionContent = (formDataObj, contentStr = '') => {
504
- const messageText = formDataObj?.messageContent
505
- || formDataObj?.viberPreviewContent?.messageContent
506
- || contentStr
507
- || '';
508
- const cardFieldTexts = getViberCarouselCardsFromFormData(formDataObj).flatMap((card) => {
509
- const fields = [card?.text || ''];
510
- (card?.buttons || []).forEach((button) => {
511
- fields.push(button?.title || '', button?.action || '');
512
- });
513
- return fields;
514
- });
515
- return [messageText, ...cardFieldTexts]
516
- .filter((value) => typeof value === 'string' && value.trim())
517
- .join(' ');
518
- };
519
-
520
- const resolveViberCarouselCards = (cards, tagValues) => {
521
- if (!Array.isArray(cards) || !tagValues || !Object.keys(tagValues).length) {
522
- return cards;
523
- }
524
- return cards.map((card) => ({
525
- ...card,
526
- text: resolveTagsInText(card?.text || '', tagValues),
527
- buttons: (card?.buttons || []).map((button) => ({
528
- ...button,
529
- title: resolveTagsInText(button?.title || '', tagValues),
530
- action: resolveTagsInText(button?.action || '', tagValues),
531
- })),
532
- }));
533
- };
534
-
535
- const applyViberCustomValuesToContent = (contentObj, tagValues) => {
536
- if (!contentObj || typeof contentObj !== 'object' || !tagValues || !Object.keys(tagValues).length) {
537
- return contentObj;
538
- }
539
- const result = { ...contentObj };
540
- if (typeof result.messageContent === 'string') {
541
- result.messageContent = resolveTagsInText(result.messageContent, tagValues);
542
- }
543
- if (Array.isArray(result.cards)) {
544
- result.cards = resolveViberCarouselCards(result.cards, tagValues);
545
- }
546
- if (result.viberPreviewContent && typeof result.viberPreviewContent === 'object') {
547
- const preview = result.viberPreviewContent;
548
- result.viberPreviewContent = {
549
- ...preview,
550
- messageContent: resolveTagsInText(preview.messageContent || '', tagValues),
551
- cards: resolveViberCarouselCards(preview.cards, tagValues),
552
- };
553
- }
554
- return result;
555
- };
556
-
557
- const mergeViberPreviewWithResolved = (base, resolved) => {
558
- if (!resolved || typeof resolved !== 'object') {
559
- return base && typeof base === 'object' ? base : {};
560
- }
561
- const baseObj = base && typeof base === 'object' ? base : {};
562
- const basePreview = baseObj.viberPreviewContent || {};
563
- const resolvedPreview = resolved.viberPreviewContent || {};
564
- const baseCards = basePreview.cards || baseObj.cards || [];
565
- const resolvedCards = resolvedPreview.cards || resolved.cards || [];
566
- const mergedCards = baseCards.length
567
- ? baseCards.map((card, index) => {
568
- const resolvedCard = resolvedCards[index];
569
- if (!resolvedCard) {
570
- return card;
571
- }
572
- return {
573
- ...card,
574
- ...(resolvedCard.text != null ? { text: resolvedCard.text } : {}),
575
- ...(resolvedCard.mediaUrl != null ? { mediaUrl: resolvedCard.mediaUrl } : {}),
576
- buttons: (card.buttons || []).map((button, buttonIndex) => ({
577
- ...button,
578
- ...(resolvedCard.buttons?.[buttonIndex] || {}),
579
- })),
580
- };
581
- })
582
- : resolvedCards;
583
- const resolvedMessage = resolved.messageContent ?? resolvedPreview.messageContent;
584
-
585
- return {
586
- ...baseObj,
587
- ...resolved,
588
- viberPreviewContent: {
589
- ...basePreview,
590
- ...resolvedPreview,
591
- type: resolvedPreview.type || basePreview.type || baseObj.type,
592
- cards: mergedCards.length ? mergedCards : (resolvedPreview.cards || basePreview.cards),
593
- ...(resolvedMessage != null ? { messageContent: resolvedMessage } : {}),
594
- },
595
- };
596
- };
597
-
598
719
  /**
599
720
  * Common handler for saving test customers (both new and existing)
600
721
  */
@@ -647,11 +768,10 @@ const CommonTestAndPreview = (props) => {
647
768
  email: customerData?.email || '',
648
769
  mobile: customerData?.mobile || '',
649
770
  });
650
- const prefixedAddedId = 'customer:' + normalizedAddedId;
651
771
  setSelectedTestEntities((prev) => (
652
- prev.some((id) => id === prefixedAddedId)
772
+ prev.some((id) => testEntityIdsEqual(id, normalizedAddedId))
653
773
  ? prev
654
- : [...prev, prefixedAddedId]
774
+ : [...prev, normalizedAddedId]
655
775
  ));
656
776
  }
657
777
  handleCloseCustomerModal();
@@ -715,13 +835,33 @@ const CommonTestAndPreview = (props) => {
715
835
  whatsappContent: formDataObj?.whatsappContent,
716
836
  };
717
837
 
718
- case CHANNELS.RCS:
838
+ case CHANNELS.RCS: {
839
+ // For carousel, replace {{N}} tokens in carouselData with resolved semantic vars from
840
+ // formData.cardContent so the preview API can resolve {{member.firstName}} etc.
841
+ const formDataCardContent =
842
+ formDataObj?.versions?.base?.content?.RCS?.rcsContent?.cardContent
843
+ ?? formDataObj?.content?.RCS?.rcsContent?.cardContent;
844
+ let parsedRcs = {};
845
+ try { parsedRcs = JSON.parse(contentStr); } catch (e) { parsedRcs = typeof contentStr === 'object' ? contentStr : {}; }
846
+ if (Array.isArray(parsedRcs.carouselData) && Array.isArray(formDataCardContent) && formDataCardContent.length > 0) {
847
+ const resolvedCarouselData = parsedRcs.carouselData.map((card, idx) => {
848
+ const formCard = formDataCardContent[idx] || {};
849
+ return {
850
+ ...card,
851
+ title: formCard.title || card.title,
852
+ bodyText: formCard.description || card.bodyText,
853
+ };
854
+ });
855
+ return {
856
+ ...basePayload,
857
+ messageBody: JSON.stringify({ ...parsedRcs, carouselData: resolvedCarouselData }),
858
+ };
859
+ }
719
860
  return {
720
861
  ...basePayload,
721
862
  messageBody: contentStr,
722
- // messageTitle: resolveTagsInText(formDataObj?.rcsTitle || '', customValuesObj),
723
- // rcsDesc: formDataObj?.rcsDesc,
724
863
  };
864
+ }
725
865
 
726
866
  case CHANNELS.INAPP:
727
867
  return {
@@ -750,11 +890,48 @@ const CommonTestAndPreview = (props) => {
750
890
  messageBody: contentStr,
751
891
  };
752
892
 
893
+ case CHANNELS.WEBPUSH:
894
+ return {
895
+ ...basePayload,
896
+ messageBody: contentStr,
897
+ };
898
+
753
899
  default:
754
900
  return basePayload;
755
901
  }
756
902
  };
757
903
 
904
+ /**
905
+ * When RCS has SMS fallback, refresh fallback preview text via the same Liquid /preview API
906
+ * (separate call with SMS channel + fallback template body). Used after primary preview updates.
907
+ */
908
+ const syncSmsFallbackPreview = async (customValuesForResolve, selectedCustomerObj) => {
909
+ const fallbackBodyForLiquidPreview =
910
+ getSmsFallbackTextForTagExtraction(smsFallbackContent)
911
+ || smsFallbackContent?.templateContent
912
+ || smsFallbackContent?.content
913
+ || '';
914
+ if (channel !== CHANNELS.RCS || !String(fallbackBodyForLiquidPreview).trim()) return;
915
+ try {
916
+ const smsFallbackPayload = preparePreviewPayload(
917
+ CHANNELS.SMS,
918
+ formData || {},
919
+ fallbackBodyForLiquidPreview,
920
+ customValuesForResolve,
921
+ selectedCustomerObj
922
+ );
923
+ const fallbackResponse = await Api.updateEmailPreview(smsFallbackPayload);
924
+ const fallbackPreview = extractPreviewFromLiquidResponse(fallbackResponse);
925
+ setSmsFallbackPreviewText(
926
+ typeof fallbackPreview?.resolvedBody === 'string'
927
+ ? fallbackPreview.resolvedBody
928
+ : undefined
929
+ );
930
+ } catch (e) {
931
+ /* keep existing smsFallbackPreviewText on failure */
932
+ }
933
+ };
934
+
758
935
  /**
759
936
  * Prepare payload for tag extraction based on channel
760
937
  */
@@ -778,11 +955,32 @@ const CommonTestAndPreview = (props) => {
778
955
  templateContent: contentStr,
779
956
  };
780
957
 
781
- case CHANNELS.RCS:
958
+ case CHANNELS.RCS: {
959
+ // Carousel templates don't have rcsTitle/rcsDesc — their content lives in cardContent[].
960
+ // Use the resolved card strings from formDataObj (resolved by resolveTemplateWithMap in
961
+ // testPreviewFormData) so the extraction API receives actual Capillary tag expressions
962
+ // instead of numeric {{1}}/{{2}} placeholders.
963
+ const rcsCardContentArr =
964
+ formDataObj?.versions?.base?.content?.RCS?.rcsContent?.cardContent
965
+ ?? formDataObj?.content?.RCS?.rcsContent?.cardContent;
966
+ if (Array.isArray(rcsCardContentArr) && rcsCardContentArr.length > 0) {
967
+ const carouselTagText = rcsCardContentArr
968
+ .map((c) => `${c.title || ''} ${c.description || ''}`)
969
+ .join(' ')
970
+ .trim();
971
+ if (carouselTagText) {
972
+ return {
973
+ templateSubject: formDataObj?.rcsTitle || '',
974
+ templateContent: carouselTagText,
975
+ };
976
+ }
977
+ }
978
+ // Standalone (non-carousel) card: use rcsTitle/rcsDesc directly.
782
979
  return {
783
980
  templateSubject: formDataObj?.rcsTitle || '',
784
981
  templateContent: formDataObj?.rcsDesc || contentStr,
785
982
  };
983
+ }
786
984
 
787
985
  case CHANNELS.INAPP:
788
986
  return {
@@ -796,13 +994,17 @@ const CommonTestAndPreview = (props) => {
796
994
  templateContent: formDataObj?.bodyText || contentStr,
797
995
  };
798
996
 
799
- case CHANNELS.VIBER: {
800
- const viberFormData = getViberMergedFormData(formDataObj);
997
+ case CHANNELS.VIBER:
801
998
  return {
802
- templateSubject: viberFormData?.messageTitle || '',
803
- templateContent: getViberTagExtractionContent(viberFormData, contentStr),
999
+ templateSubject: formDataObj?.messageTitle || '',
1000
+ templateContent: contentStr,
1001
+ };
1002
+
1003
+ case CHANNELS.WEBPUSH:
1004
+ return {
1005
+ templateSubject: formDataObj?.content?.title || '',
1006
+ templateContent: contentStr,
804
1007
  };
805
- }
806
1008
 
807
1009
  case CHANNELS.ZALO: {
808
1010
  // For Zalo, extract content from templateListParams array
@@ -851,7 +1053,7 @@ const CommonTestAndPreview = (props) => {
851
1053
  } = carousel || {};
852
1054
  const buttonData = buttons.map((button, index) => {
853
1055
  const {
854
- type, text, phone_number, urlType, url,
1056
+ type, text, phone_number: phoneNumber, urlType, url,
855
1057
  } = button || {};
856
1058
  const buttonObj = {
857
1059
  type,
@@ -859,7 +1061,7 @@ const CommonTestAndPreview = (props) => {
859
1061
  index,
860
1062
  };
861
1063
  if (type === PHONE_NUMBER) {
862
- buttonObj.phoneNumber = phone_number;
1064
+ buttonObj.phoneNumber = phoneNumber;
863
1065
  }
864
1066
  if (type === URL) {
865
1067
  buttonObj.url = url;
@@ -888,7 +1090,159 @@ const CommonTestAndPreview = (props) => {
888
1090
  };
889
1091
  });
890
1092
 
891
- const prepareTestMessagePayload = (channelType, formDataObj, contentStr, customValuesObj, recipientDetails, previewDataObj, deliverySettingsOverride) => {
1093
+ /**
1094
+ * Build createMessageMeta payload for RCS (test message).
1095
+ * rcsMessageContent: { channel, accountId?, rcsRichCardContent: { contentType, cardType, cardSettings, cardContent }, smsFallBackContent? }
1096
+ * Then rcsDeliverySettings, executionParams, clientName last.
1097
+ */
1098
+ const buildRcsTestMessagePayload = (
1099
+ creativeFormData,
1100
+ _unusedEditorContentString,
1101
+ _customValuesObj,
1102
+ deliverySettingsOverride,
1103
+ basePayload,
1104
+ _rcsTestMetaExtras = {},
1105
+ ) => {
1106
+ const rcsSectionFromForm =
1107
+ creativeFormData?.versions?.base?.content?.RCS ?? creativeFormData?.content?.RCS ?? {};
1108
+ const rcsContentFromForm = rcsSectionFromForm?.rcsContent || {};
1109
+ const smsFallbackFromCreativeForm = rcsSectionFromForm?.smsFallBackContent || {};
1110
+ let rcsCardPayloadList = [];
1111
+ if (Array.isArray(rcsContentFromForm?.cardContent)) {
1112
+ rcsCardPayloadList = rcsContentFromForm.cardContent;
1113
+ } else if (rcsContentFromForm?.cardContent) {
1114
+ rcsCardPayloadList = [rcsContentFromForm.cardContent];
1115
+ }
1116
+ // Raw title/description with template tags; SMS fallback uses tagged template fields (pickFirst…).
1117
+ const cardContentForTestMetaApi = rcsCardPayloadList.map((singleRcsCardPayload) => {
1118
+ const normalizedCardMediaForTestApi = singleRcsCardPayload?.media
1119
+ ? normalizeRcsTestCardMedia(singleRcsCardPayload.media)
1120
+ : undefined;
1121
+ const suggestionsFromCard = Array.isArray(singleRcsCardPayload?.suggestions)
1122
+ ? singleRcsCardPayload.suggestions
1123
+ : [];
1124
+ const suggestionsFormattedForTestMeta = suggestionsFromCard.map((suggestionItem, index) =>
1125
+ mapRcsSuggestionForTestMeta(suggestionItem, index));
1126
+ return {
1127
+ title: singleRcsCardPayload?.title ?? '',
1128
+ description: singleRcsCardPayload?.description ?? '',
1129
+ mediaType: singleRcsCardPayload?.mediaType ?? MEDIA_TYPE_TEXT,
1130
+ ...(normalizedCardMediaForTestApi && { media: normalizedCardMediaForTestApi }),
1131
+ ...(suggestionsFormattedForTestMeta.length > 0 && {
1132
+ suggestions: suggestionsFormattedForTestMeta,
1133
+ }),
1134
+ };
1135
+ });
1136
+ // Use the component-level smsFallbackContent prop (has rcsSmsFallbackVarMapped) so DLT
1137
+ // {#var#} slots are converted to {{tagName}} mustache tags before sending to createMessageMeta.
1138
+ const smsFallbackTaggedTemplateBody =
1139
+ getSmsFallbackTextForTagExtraction(smsFallbackContent)
1140
+ || pickFirstSmsFallbackTemplateString(smsFallbackFromCreativeForm)
1141
+ || '';
1142
+ const smsSenderFromDelivery = deliverySettingsOverride?.cdmaSenderId?.includes('|')
1143
+ ? deliverySettingsOverride.cdmaSenderId.split('|')[1]
1144
+ : deliverySettingsOverride?.cdmaSenderId;
1145
+ const deliveryFallbackSmsId =
1146
+ typeof smsSenderFromDelivery === 'string' ? smsSenderFromDelivery.trim() : '';
1147
+ const creativeFallbackSmsId =
1148
+ smsFallbackFromCreativeForm?.senderId != null
1149
+ ? String(smsFallbackFromCreativeForm.senderId).trim()
1150
+ : '';
1151
+ const fallbackSmsSenderIdForChannel = deliveryFallbackSmsId || creativeFallbackSmsId || '';
1152
+
1153
+ // For DLT orgs: include templateConfigs inside smsFallBackContent so createMessageMeta
1154
+ // receives the DLT template ID, registered sender IDs, and body alongside the message.
1155
+ // Priority: smsFallBackContent.templateConfigs from the RCS payload (clean API shape,
1156
+ // built by createPayload) → fallback to root-level formData.templateConfigs (strip the
1157
+ // UI-only traiDltEnabled flag before sending to the API).
1158
+ const smsFallbackTemplateConfigsForApi = (() => {
1159
+ if (
1160
+ smsFallbackFromCreativeForm?.templateConfigs
1161
+ && typeof smsFallbackFromCreativeForm.templateConfigs === 'object'
1162
+ ) {
1163
+ return smsFallbackFromCreativeForm.templateConfigs;
1164
+ }
1165
+ const rootTc = creativeFormData?.templateConfigs;
1166
+ if (rootTc && typeof rootTc === 'object') {
1167
+ // eslint-disable-next-line no-unused-vars
1168
+ const { traiDltEnabled: _uiFlag, ...tcForApi } = rootTc;
1169
+ return Object.keys(tcForApi).length > 0 ? tcForApi : null;
1170
+ }
1171
+ return null;
1172
+ })();
1173
+
1174
+ const smsFallBackContent =
1175
+ smsFallbackTaggedTemplateBody.trim() !== ''
1176
+ ? {
1177
+ message: smsFallbackTaggedTemplateBody,
1178
+ ...(smsFallbackTemplateConfigsForApi && {
1179
+ templateConfigs: smsFallbackTemplateConfigsForApi,
1180
+ }),
1181
+ }
1182
+ : undefined;
1183
+
1184
+ // accountId: WeCRM account id (not sourceAccountIdentifier) for createMessageMeta
1185
+ const accountIdForMeta =
1186
+ rcsContentFromForm?.accountId != null && String(rcsContentFromForm.accountId).trim() !== ''
1187
+ ? String(rcsContentFromForm.accountId)
1188
+ : undefined;
1189
+
1190
+ const rcsRichCardContent = {
1191
+ contentType: RCS_TEST_META_CONTENT_TYPE_RICHCARD,
1192
+ cardType: rcsContentFromForm?.cardType ?? RCS_TEST_META_CARD_TYPE_STANDALONE,
1193
+ cardSettings: rcsContentFromForm?.cardSettings ?? {
1194
+ cardOrientation: RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
1195
+ cardWidth: RCS_TEST_META_CARD_WIDTH_SMALL,
1196
+ },
1197
+ ...(cardContentForTestMetaApi.length > 0 && { cardContent: cardContentForTestMetaApi }),
1198
+ };
1199
+
1200
+ const rcsMessageContent = {
1201
+ channel: CHANNELS.RCS,
1202
+ ...(accountIdForMeta && { accountId: accountIdForMeta }),
1203
+ rcsRichCardContent,
1204
+ ...(smsFallBackContent && { smsFallBackContent }),
1205
+ };
1206
+ const rcsComposite = deliverySettingsOverride?.gsmSenderId ?? '';
1207
+ const [rcsDomainId, rcsSenderId] = rcsComposite.includes('|') ? rcsComposite.split('|') : ['', rcsComposite];
1208
+ const rcsDeliverySettings = {
1209
+ channelSettings: {
1210
+ channel: CHANNELS.RCS,
1211
+ rcsSender: (rcsSenderId || deliverySettingsOverride?.rcsSender) ?? '',
1212
+ domainId:
1213
+ rcsDomainId !== '' && rcsDomainId !== undefined && !Number.isNaN(Number(rcsDomainId))
1214
+ ? Number(rcsDomainId)
1215
+ : (deliverySettingsOverride?.domainId ?? 0),
1216
+ fallbackSmsSenderId: fallbackSmsSenderIdForChannel,
1217
+ },
1218
+ additionalSettings: {
1219
+ useTinyUrl: false,
1220
+ encryptUrl: false,
1221
+ linkTrackingEnabled: false,
1222
+ bypassControlUser: false,
1223
+ userSubscriptionDisabled: false,
1224
+ },
1225
+ };
1226
+ const { clientName: baseClientName = CLIENT_NAME_CREATIVES, ...restBase } = basePayload;
1227
+ return {
1228
+ ...restBase,
1229
+ rcsMessageContent,
1230
+ rcsDeliverySettings,
1231
+ executionParams: {},
1232
+ clientName: baseClientName,
1233
+ };
1234
+ };
1235
+
1236
+ const prepareTestMessagePayload = (
1237
+ channelType,
1238
+ formDataObj,
1239
+ contentStr,
1240
+ customValuesObj,
1241
+ recipientDetails,
1242
+ previewDataObj,
1243
+ deliverySettingsOverride,
1244
+ rcsExtra = {},
1245
+ ) => {
892
1246
  // Base payload structure common to all channels
893
1247
  const basePayload = {
894
1248
  ouId: -1,
@@ -969,12 +1323,12 @@ const CommonTestAndPreview = (props) => {
969
1323
  },
970
1324
  smsDeliverySettings: {
971
1325
  channelSettings: {
1326
+ channel: CHANNELS.SMS,
972
1327
  gsmSenderId: deliverySettingsOverride?.gsmSenderId ?? '',
973
1328
  domainId: deliverySettingsOverride?.domainId ?? null,
974
1329
  domainGatewayMapId: deliverySettingsOverride?.domainGatewayMapId ?? '',
975
1330
  targetNdnc: false,
976
1331
  cdmaSenderId: deliverySettingsOverride?.cdmaSenderId ?? '',
977
- channel: CHANNELS.SMS,
978
1332
  },
979
1333
  additionalSettings: {
980
1334
  useTinyUrl: false,
@@ -1118,7 +1472,7 @@ const CommonTestAndPreview = (props) => {
1118
1472
  return {
1119
1473
  ...basePayload,
1120
1474
  whatsappMessageContent: {
1121
- messageBody: templateEditorValue || '',
1475
+ messageBody: resolvedMessageBody || templateEditorValue || '',
1122
1476
  accountId: formDataObj?.accountId || '',
1123
1477
  sourceAccountIdentifier: sourceAccountIdentifier || formDataObj?.sourceAccountIdentifier || '',
1124
1478
  accountName: formDataObj?.accountName || '',
@@ -1143,16 +1497,7 @@ const CommonTestAndPreview = (props) => {
1143
1497
  }
1144
1498
 
1145
1499
  case CHANNELS.RCS:
1146
- return {
1147
- ...basePayload,
1148
- rcsMessageContent: {
1149
- channel: CHANNELS.RCS,
1150
- messageBody: contentStr,
1151
- rcsType: additionalProps?.rcsType,
1152
- rcsImageSrc: formDataObj?.rcsImageSrc,
1153
- rcsSuggestions: formDataObj?.rcsSuggestions,
1154
- },
1155
- };
1500
+ return buildRcsTestMessagePayload(formDataObj, contentStr, customValuesObj, deliverySettingsOverride, basePayload, rcsExtra);
1156
1501
 
1157
1502
  case CHANNELS.INAPP: {
1158
1503
  // InApp payload structure similar to MobilePush
@@ -1691,16 +2036,11 @@ const CommonTestAndPreview = (props) => {
1691
2036
  // 1. formDataObj from getTemplateContent (contains both viberPreviewContent and payload fields)
1692
2037
  // 2. formDataObj[0] (array format - legacy)
1693
2038
  // 3. contentStr (direct string - legacy)
1694
- // Merge content prop so listing / preview-only flows include carousel card tags
1695
- const viberFormData = getViberMergedFormData(formDataObj);
1696
- formDataObj = viberFormData;
1697
2039
 
1698
2040
  let messageText = '';
1699
2041
  let imageData = null;
1700
2042
  let videoData = null;
1701
2043
  let buttonData = null;
1702
- let cardsData = [];
1703
- let messageType = '';
1704
2044
  let accountId = null;
1705
2045
  let accountDetails = null;
1706
2046
  let scenarioKey = '';
@@ -1715,8 +2055,6 @@ const CommonTestAndPreview = (props) => {
1715
2055
  imageData = formDataObj.image || null;
1716
2056
  videoData = formDataObj.video || null;
1717
2057
  buttonData = formDataObj.button || null;
1718
- cardsData = formDataObj.cards || [];
1719
- messageType = formDataObj.type || '';
1720
2058
  accountId = formDataObj.accountId || null;
1721
2059
  accountDetails = formDataObj.accountDetails || null;
1722
2060
  scenarioKey = formDataObj.scenarioKey || VIBER_API_SCENARIO_KEY;
@@ -1743,8 +2081,6 @@ const CommonTestAndPreview = (props) => {
1743
2081
  url: formDataObj?.buttonURL || '',
1744
2082
  };
1745
2083
  }
1746
- cardsData = viberPreview.cards || formDataObj?.cards || [];
1747
- messageType = viberPreview.type || formDataObj?.type || '';
1748
2084
  // Extract account info from parent formDataObj if available
1749
2085
  accountId = formDataObj.accountId || null;
1750
2086
  accountDetails = formDataObj.accountDetails || null;
@@ -1787,10 +2123,6 @@ const CommonTestAndPreview = (props) => {
1787
2123
  text: messageText,
1788
2124
  };
1789
2125
 
1790
- if (messageType === CHANNELS.VIBER) {
1791
- messageType = '';
1792
- }
1793
-
1794
2126
  // Add image if present
1795
2127
  if (imageData && imageData.url) {
1796
2128
  viberContent.image = {
@@ -1819,34 +2151,9 @@ const CommonTestAndPreview = (props) => {
1819
2151
  }
1820
2152
  }
1821
2153
 
1822
- if (messageType === MEDIA_TYPE_CAROUSEL || (Array.isArray(cardsData) && cardsData.length)) {
1823
- viberContent.type = MEDIA_TYPE_CAROUSEL;
1824
- viberContent.cards = (cardsData || []).map((card) => ({
1825
- text: card?.text || '',
1826
- mediaUrl: card?.mediaUrl || '',
1827
- buttons: (card?.buttons || []).map((button) => ({
1828
- title: button?.title || '',
1829
- action: button?.action || '',
1830
- })),
1831
- }));
1832
- delete viberContent.button;
1833
- }
1834
-
1835
- // Resolve tags in text/cards if custom values are provided
1836
- if (customValuesObj && Object.keys(customValuesObj).length > 0) {
1837
- if (viberContent.text) {
1838
- viberContent.text = resolveTagsInText(viberContent.text, customValuesObj);
1839
- }
1840
- if (Array.isArray(viberContent.cards)) {
1841
- viberContent.cards = resolveViberCarouselCards(viberContent.cards, customValuesObj);
1842
- }
1843
- if (viberContent.button) {
1844
- viberContent.button = {
1845
- ...viberContent.button,
1846
- text: resolveTagsInText(viberContent.button.text || '', customValuesObj),
1847
- url: resolveTagsInText(viberContent.button.url || '', customValuesObj),
1848
- };
1849
- }
2154
+ // Resolve tags in text if custom values are provided
2155
+ if (customValuesObj && Object.keys(customValuesObj).length > 0 && viberContent.text) {
2156
+ viberContent.text = resolveTagsInText(viberContent.text, customValuesObj);
1850
2157
  }
1851
2158
 
1852
2159
  // Build messageBody JSON string
@@ -1981,6 +2288,42 @@ const CommonTestAndPreview = (props) => {
1981
2288
  };
1982
2289
  }
1983
2290
 
2291
+ case CHANNELS.WEBPUSH: {
2292
+ const webpushData = (typeof formDataObj === 'object' && formDataObj !== null)
2293
+ ? formDataObj
2294
+ : {};
2295
+ const innerContent = webpushData?.content || {};
2296
+
2297
+ const resolvedTitle = resolveTagsInText(innerContent?.title || '', customValuesObj);
2298
+ const resolvedMessage = resolveTagsInText(innerContent?.message || '', customValuesObj);
2299
+
2300
+ return {
2301
+ ...basePayload,
2302
+ webPushMessageContent: {
2303
+ channel: CHANNELS.WEBPUSH,
2304
+ accountId: webpushData?.accountId || null,
2305
+ content: {
2306
+ title: resolvedTitle,
2307
+ message: resolvedMessage,
2308
+ ...(innerContent?.iconImageUrl && { iconImageUrl: innerContent.iconImageUrl }),
2309
+ ...(innerContent?.cta && { cta: innerContent.cta }),
2310
+ ...(innerContent?.expandableDetails && { expandableDetails: innerContent.expandableDetails }),
2311
+ },
2312
+ messageSubject: webpushData?.messageSubject || resolvedTitle || '',
2313
+ },
2314
+ webPushDeliverySettings: {
2315
+ channelSettings: {
2316
+ channel: CHANNELS.WEBPUSH,
2317
+ notificationTtl: {
2318
+ duration: 7,
2319
+ timeUnit: DAYS,
2320
+ },
2321
+ },
2322
+ additionalSettings: {},
2323
+ },
2324
+ };
2325
+ }
2326
+
1984
2327
  default:
1985
2328
  return basePayload;
1986
2329
  }
@@ -1993,6 +2336,46 @@ const CommonTestAndPreview = (props) => {
1993
2336
  *
1994
2337
  * IMPORTANT: Use raw content/formData initially, only use previewDataHtml if preview call was made
1995
2338
  */
2339
+ // Memoized RCS carousel content: only recomputes when the API response or raw template changes,
2340
+ // NOT on every customValues keystroke. Uses previewCustomValuesRef captured at Update Preview time.
2341
+ const rcsCarouselContentObj = useMemo(() => {
2342
+ if (channel !== CHANNELS.RCS) return null;
2343
+ let resolvedContent = null;
2344
+ if (hasPreviewCallBeenMade && previewDataHtml?.resolvedBody) {
2345
+ resolvedContent = previewDataHtml.resolvedBody;
2346
+ if (typeof resolvedContent === 'string') {
2347
+ try { resolvedContent = JSON.parse(resolvedContent); } catch (e) { resolvedContent = null; }
2348
+ }
2349
+ }
2350
+ if (resolvedContent && typeof resolvedContent === 'object') return resolvedContent;
2351
+
2352
+ let rawRcs = content;
2353
+ if (typeof rawRcs === 'string') {
2354
+ try { rawRcs = JSON.parse(rawRcs); } catch (e) { rawRcs = content; }
2355
+ }
2356
+ const snapCustomValues = previewCustomValuesRef.current;
2357
+ const hasEnteredValues = snapCustomValues && Object.keys(snapCustomValues).some((k) => snapCustomValues[k]);
2358
+ if (hasPreviewCallBeenMade && hasEnteredValues && Array.isArray(rawRcs?.carouselData)) {
2359
+ const formDataCardContent =
2360
+ formData?.versions?.base?.content?.RCS?.rcsContent?.cardContent
2361
+ ?? formData?.content?.RCS?.rcsContent?.cardContent
2362
+ ?? [];
2363
+ return {
2364
+ ...rawRcs,
2365
+ carouselData: rawRcs.carouselData.map((card, idx) => {
2366
+ const formCard = formDataCardContent[idx] || {};
2367
+ return {
2368
+ ...card,
2369
+ title: resolveTagsInText(formCard.title || card.title || '', snapCustomValues),
2370
+ bodyText: resolveTagsInText(formCard.description || card.bodyText || '', snapCustomValues),
2371
+ };
2372
+ }),
2373
+ };
2374
+ }
2375
+ return rawRcs || {};
2376
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2377
+ }, [channel, content, hasPreviewCallBeenMade, previewDataHtml, formData]);
2378
+
1996
2379
  const prepareUnifiedPreviewProps = () => {
1997
2380
  // Prepare content based on channel
1998
2381
  let contentObj = {};
@@ -2012,7 +2395,7 @@ const CommonTestAndPreview = (props) => {
2012
2395
  contentObj = hasPreviewCallBeenMade && previewDataHtml?.resolvedBody
2013
2396
  ? previewDataHtml.resolvedBody
2014
2397
  : getCurrentContent || '';
2015
- } else if (channel === CHANNELS.WHATSAPP) {
2398
+ } else if ([CHANNELS.WHATSAPP, CHANNELS.WEBPUSH].includes(channel)) {
2016
2399
  // For WhatsApp, content is an object with templateMsg, media, CTA, etc.
2017
2400
  // Content comes from WhatsApp component state, passed via content prop
2018
2401
  let resolvedContent = null;
@@ -2054,42 +2437,14 @@ const CommonTestAndPreview = (props) => {
2054
2437
  contentObj = parsedContent || {};
2055
2438
  }
2056
2439
  } else if (channel === CHANNELS.RCS) {
2057
- // For RCS, content is an object with rcsTitle, rcsDesc, rcsImageSrc, suggestions, etc.
2058
- // Content comes from RCS component state, passed via content prop
2059
- let resolvedContent = null;
2060
-
2061
- // Only use previewDataHtml if preview call was made
2062
- if (hasPreviewCallBeenMade && previewDataHtml?.resolvedBody) {
2063
- resolvedContent = previewDataHtml.resolvedBody;
2064
-
2065
- // Handle case where resolvedBody might be a JSON string
2066
- if (typeof resolvedContent === 'string') {
2067
- try {
2068
- resolvedContent = JSON.parse(resolvedContent);
2069
- } catch (e) {
2070
- // If parsing fails, treat as null so we fall back to content
2071
- resolvedContent = null;
2072
- }
2073
- }
2074
- }
2075
-
2076
- // Parse content if it's a string
2440
+ // rcsCarouselContentObj is a useMemo that handles both the API-response path and the
2441
+ // client-side fallback. It is stable across keystrokes (only recomputes when
2442
+ // hasPreviewCallBeenMade / previewDataHtml / content / formData change).
2077
2443
  let parsedRcsContent = content;
2078
2444
  if (typeof content === 'string') {
2079
- try {
2080
- parsedRcsContent = JSON.parse(content);
2081
- } catch (e) {
2082
- parsedRcsContent = content;
2083
- }
2084
- }
2085
-
2086
- // Use resolvedContent if available (from preview call), otherwise use raw content
2087
- if (resolvedContent && typeof resolvedContent === 'object') {
2088
- contentObj = { ...resolvedContent };
2089
- } else {
2090
- // Use raw content if no preview call was made or resolvedContent is not available
2091
- contentObj = parsedRcsContent || {};
2445
+ try { parsedRcsContent = JSON.parse(content); } catch (e) { parsedRcsContent = content; }
2092
2446
  }
2447
+ contentObj = rcsCarouselContentObj || parsedRcsContent || {};
2093
2448
  } else if (channel === CHANNELS.INAPP) {
2094
2449
  // For InApp, content is an object with androidContent and iosContent (similar to MobilePush)
2095
2450
  // Content comes from InApp component state, passed via content prop
@@ -2190,39 +2545,33 @@ const CommonTestAndPreview = (props) => {
2190
2545
  // Only use previewDataHtml if preview call was made
2191
2546
  if (hasPreviewCallBeenMade && previewDataHtml?.resolvedBody) {
2192
2547
  resolvedContent = previewDataHtml.resolvedBody;
2193
- if (typeof resolvedContent === 'string') {
2194
- try {
2195
- resolvedContent = JSON.parse(resolvedContent);
2196
- } catch (e) {
2197
- resolvedContent = null;
2198
- }
2199
- }
2200
2548
  }
2201
2549
 
2202
- // Parse content if it's a string, then merge with formData for carousel + tags in all entry flows
2203
- let parsedViberContent = getViberMergedFormData();
2204
- if (!parsedViberContent || typeof parsedViberContent !== 'object') {
2205
- parsedViberContent = {};
2550
+ // Parse content if it's a string
2551
+ let parsedViberContent = content;
2552
+ if (typeof content === 'string') {
2553
+ try {
2554
+ parsedViberContent = JSON.parse(content);
2555
+ } catch (e) {
2556
+ parsedViberContent = {};
2557
+ }
2206
2558
  }
2207
- // Merge template preview state with API resolved body so carousel type/cards survive slim responses
2559
+ // Use resolvedContent if available (from preview call), otherwise use raw content
2208
2560
  if (resolvedContent && typeof resolvedContent === 'object') {
2209
- const base = parsedViberContent && typeof parsedViberContent === 'object' ? parsedViberContent : {};
2210
- contentObj = mergeViberPreviewWithResolved(base, resolvedContent);
2211
- if (base.accountName && !contentObj.accountName) {
2212
- contentObj.accountName = base.accountName;
2561
+ contentObj = { ...resolvedContent };
2562
+ // Merge in accountName and brandName from raw content if not in resolvedContent
2563
+ if (parsedViberContent?.accountName && !contentObj.accountName) {
2564
+ contentObj.accountName = parsedViberContent.accountName;
2213
2565
  }
2214
- if (base.brandName && !contentObj.brandName) {
2215
- contentObj.brandName = base.brandName;
2566
+ if (parsedViberContent?.brandName && !contentObj.brandName) {
2567
+ contentObj.brandName = parsedViberContent.brandName;
2216
2568
  }
2217
2569
  } else {
2218
- contentObj = parsedViberContent && typeof parsedViberContent === 'object' ? parsedViberContent : {};
2219
- }
2220
- if (
2221
- hasPreviewCallBeenMade
2222
- && viberPreviewTagValues
2223
- && Object.keys(viberPreviewTagValues).length > 0
2224
- ) {
2225
- contentObj = applyViberCustomValuesToContent(contentObj, viberPreviewTagValues);
2570
+ // Use raw content if no preview call was made or resolvedContent is not available
2571
+ contentObj = {...parsedViberContent, messageContent: resolvedContent};
2572
+ if (resolvedContent) {
2573
+ contentObj.viberPreviewContent = {...parsedViberContent?.viberPreviewContent, messageContent: resolvedContent};
2574
+ }
2226
2575
  }
2227
2576
  } else if (channel === CHANNELS.ZALO) {
2228
2577
  // For Zalo, content is an object with templatePreviewUrl, templateStatus, etc.
@@ -2298,6 +2647,10 @@ const CommonTestAndPreview = (props) => {
2298
2647
  formatMessage,
2299
2648
  lastModified: formData?.lastModified,
2300
2649
  updatedByName: formData?.updatedByName,
2650
+ smsFallbackContent: isRcsSmsFallbackPreviewEnabled ? smsFallbackContent : null,
2651
+ smsFallbackResolvedText,
2652
+ activePreviewTab,
2653
+ onPreviewTabChange: setActivePreviewTab,
2301
2654
  };
2302
2655
  };
2303
2656
 
@@ -2337,7 +2690,12 @@ const CommonTestAndPreview = (props) => {
2337
2690
  }, [show, beeInstance, currentTab, channel]);
2338
2691
 
2339
2692
  /**
2340
- * Initial data load when slidebox opens
2693
+ * Initial data load when slidebox opens.
2694
+ * EXTRACT TAGS CALL SITES (on open/edit RCS):
2695
+ * 1. Here (non-email): actions.extractTagsRequested() at line ~2161 when show is true.
2696
+ * 2. RCS SMS fallback useEffect below: Api.extractTagsWithMetaData() for fallback message.
2697
+ * 3. handleExtractTags() (user clicks "Enter custom values for tags") – not from effects.
2698
+ * The "Process extracted tags" effect only processes API results and must not call extract again.
2341
2699
  */
2342
2700
  useEffect(() => {
2343
2701
  if (show) {
@@ -2422,7 +2780,61 @@ const CommonTestAndPreview = (props) => {
2422
2780
  actions.getTestGroupsRequested();
2423
2781
  }
2424
2782
  }
2425
- }, [show, beeInstance, currentTab, channel]);
2783
+ // getCurrentContent: RCS applies cardVarMapped → placeholder resolution; re-extract when it changes.
2784
+ }, [show, beeInstance, currentTab, channel, getCurrentContent]);
2785
+
2786
+ /**
2787
+ * RCS with SMS fallback: extract tags for fallback SMS content as well
2788
+ * (so we can show a separate "Fallback SMS tags" section in left panel).
2789
+ */
2790
+ useEffect(() => {
2791
+ let cancelled = false;
2792
+
2793
+ if (!show || channel !== CHANNELS.RCS) {
2794
+ return () => {
2795
+ cancelled = true;
2796
+ };
2797
+ }
2798
+
2799
+ if (!smsFallbackContent?.templateContent && !smsFallbackContent?.content) {
2800
+ setSmsFallbackExtractedTags([]);
2801
+ setSmsFallbackRequiredTags([]);
2802
+ setSmsFallbackOptionalTags([]);
2803
+ return () => {
2804
+ cancelled = true;
2805
+ };
2806
+ }
2807
+
2808
+ setIsExtractingSmsFallbackTags(true);
2809
+ (async () => {
2810
+ try {
2811
+ const fallbackBodyForExtractApi = getSmsFallbackTextForTagExtraction(smsFallbackContent);
2812
+ const payload = {
2813
+ messageTitle: '',
2814
+ messageBody: fallbackBodyForExtractApi || '',
2815
+ };
2816
+ const response = await Api.extractTagsWithMetaData(payload); //not using saga action here because we dont store fallbacksms related data in store but only in useState since this is only used in RCS SMS fallback
2817
+ let smsFallbackTagTree = response?.data ?? [];
2818
+ if (!Array.isArray(smsFallbackTagTree)) smsFallbackTagTree = [];
2819
+ if (!smsTemplateHasMustacheTags(fallbackBodyForExtractApi)) {
2820
+ smsFallbackTagTree = [];
2821
+ } else if (smsFallbackTagTree.length === 0) {
2822
+ smsFallbackTagTree = buildSyntheticSmsMustacheTags(fallbackBodyForExtractApi);
2823
+ }
2824
+ if (cancelled) return;
2825
+ setSmsFallbackExtractedTags(smsFallbackTagTree);
2826
+ } catch (e) {
2827
+ if (cancelled) return;
2828
+ setSmsFallbackExtractedTags([]);
2829
+ } finally {
2830
+ if (!cancelled) setIsExtractingSmsFallbackTags(false);
2831
+ }
2832
+ })();
2833
+
2834
+ return () => {
2835
+ cancelled = true;
2836
+ };
2837
+ }, [show, channel, smsFallbackContent]);
2426
2838
 
2427
2839
  /**
2428
2840
  * Email-specific: Handle content updates for both BEE and CKEditor
@@ -2474,17 +2886,22 @@ const CommonTestAndPreview = (props) => {
2474
2886
  setSelectedCustomer(null);
2475
2887
  setRequiredTags([]);
2476
2888
  setOptionalTags([]);
2889
+ setSmsFallbackExtractedTags([]);
2890
+ setSmsFallbackRequiredTags([]);
2891
+ setSmsFallbackOptionalTags([]);
2892
+ setIsExtractingSmsFallbackTags(false);
2477
2893
  setCustomValues({});
2478
2894
  setShowJSON(false);
2479
2895
  setTagsExtracted(false);
2480
2896
  setPreviewDevice(DESKTOP);
2481
- setHasPreviewCallBeenMade(false);
2482
- setViberPreviewTagValues(null);
2897
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2898
+ setSmsFallbackPreviewText(undefined);
2483
2899
  setSelectedTestEntities([]);
2484
2900
  actions.clearPrefilledValues();
2485
2901
  } else {
2486
2902
  // Reset device to initialDevice when opening (Android for mobile channels, Desktop for others)
2487
2903
  setPreviewDevice(initialDevice);
2904
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2488
2905
  }
2489
2906
  }, [show, initialDevice]);
2490
2907
 
@@ -2493,79 +2910,10 @@ const CommonTestAndPreview = (props) => {
2493
2910
  */
2494
2911
  useEffect(() => {
2495
2912
  if (previewData) {
2496
- setPreviewDataHtml(previewData);
2913
+ setPreviewDataHtml(toPlainPreviewData(previewData));
2497
2914
  }
2498
2915
  }, [previewData]);
2499
2916
 
2500
- /**
2501
- * Process extracted tags and categorize them
2502
- */
2503
- useEffect(() => {
2504
- // Categorize tags into required and optional
2505
- const required = [];
2506
- const optional = [];
2507
- let hasPersonalizationTags = false;
2508
-
2509
- if (extractedTags?.length > 0) {
2510
- const processTag = (tag, parentPath = '') => {
2511
- const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
2512
-
2513
- // Skip unsubscribe tag for input fields
2514
- if (tag?.name === UNSUBSCRIBE_TAG_NAME) {
2515
- return;
2516
- }
2517
-
2518
- hasPersonalizationTags = true;
2519
-
2520
- if (tag?.metaData?.userDriven === false) {
2521
- required.push({
2522
- ...tag,
2523
- fullPath: currentPath,
2524
- });
2525
- } else if (tag?.metaData?.userDriven === true) {
2526
- optional.push({
2527
- ...tag,
2528
- fullPath: currentPath,
2529
- });
2530
- }
2531
-
2532
- if (tag?.children?.length > 0) {
2533
- tag.children.forEach((child) => processTag(child, currentPath));
2534
- }
2535
- };
2536
-
2537
- extractedTags.forEach((tag) => processTag(tag));
2538
-
2539
- if (hasPersonalizationTags) {
2540
- setRequiredTags(required);
2541
- setOptionalTags(optional);
2542
- setTagsExtracted(true); // Mark tags as extracted and processed
2543
-
2544
- // Initialize custom values for required tags
2545
- const initialValues = {};
2546
- required.forEach((tag) => {
2547
- initialValues[tag?.fullPath] = '';
2548
- });
2549
- optional.forEach((tag) => {
2550
- initialValues[tag?.fullPath] = '';
2551
- });
2552
- setCustomValues(initialValues);
2553
- } else {
2554
- // Reset all tag-related state if no personalization tags
2555
- setRequiredTags([]);
2556
- setOptionalTags([]);
2557
- setCustomValues({});
2558
- setTagsExtracted(false);
2559
- }
2560
- } else {
2561
- // Reset all tag-related state if no tags
2562
- setRequiredTags([]);
2563
- setOptionalTags([]);
2564
- setCustomValues({});
2565
- setTagsExtracted(false);
2566
- }
2567
- }, [extractedTags]);
2568
-
2569
2917
  /**
2570
2918
  * Handle customer selection and fetch prefilled values
2571
2919
  */
@@ -2573,17 +2921,15 @@ const CommonTestAndPreview = (props) => {
2573
2921
  if (selectedCustomer && config.enableCustomerSearch !== false) {
2574
2922
  setTagsExtracted(true); // Auto-open custom values editor
2575
2923
 
2576
- // Get all available tags
2577
- const allTags = [...requiredTags, ...optionalTags];
2578
2924
  const requiredTagObj = {};
2579
- requiredTags.forEach((tag) => {
2925
+ allRequiredTags.forEach((tag) => {
2580
2926
  requiredTagObj[tag?.fullPath] = '';
2581
2927
  });
2582
2928
  if (allTags.length > 0) {
2583
2929
  const payload = preparePreviewPayload(
2584
- channel,
2930
+ activeChannelForActions,
2585
2931
  formData || {},
2586
- getCurrentContent,
2932
+ activeContentForActions,
2587
2933
  {
2588
2934
  ...requiredTagObj,
2589
2935
  },
@@ -2592,7 +2938,7 @@ const CommonTestAndPreview = (props) => {
2592
2938
  actions.getPrefilledValuesRequested(payload);
2593
2939
  }
2594
2940
  }
2595
- }, [selectedCustomer]);
2941
+ }, [selectedCustomer, allTags.length, activeChannelForActions, activePreviewTab]);
2596
2942
 
2597
2943
  /**
2598
2944
  * Update custom values with prefilled values from API
@@ -2603,29 +2949,29 @@ const CommonTestAndPreview = (props) => {
2603
2949
  if (prefilledValues && selectedCustomer) {
2604
2950
  // Always replace all values with prefilled values
2605
2951
  const updatedValues = {};
2606
- [...requiredTags, ...optionalTags].forEach((tag) => {
2952
+ allTags.forEach((tag) => {
2607
2953
  updatedValues[tag?.fullPath] = prefilledValues[tag?.fullPath] || '';
2608
2954
  });
2609
2955
 
2610
2956
  setCustomValues(updatedValues);
2611
2957
 
2612
- // Viber: do not auto-resolve tags in preview; user must click Update preview
2613
- if (channel === CHANNELS.VIBER) {
2614
- return;
2615
- }
2616
-
2617
2958
  // Update preview with prefilled values (this is a valid preview call trigger)
2959
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped.
2960
+ // SMS fallback preview is kept in sync by syncSmsFallbackPreview via smsFallbackPreviewText.
2961
+ const previewChannelForPrefill = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
2962
+ const previewContentForPrefill = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2618
2963
  const payload = preparePreviewPayload(
2619
- channel,
2964
+ previewChannelForPrefill,
2620
2965
  formData || {},
2621
- getCurrentContent,
2966
+ previewContentForPrefill,
2622
2967
  updatedValues,
2623
2968
  selectedCustomer
2624
2969
  );
2625
2970
  actions.updatePreviewRequested(payload);
2626
2971
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2972
+ void syncSmsFallbackPreview(updatedValues, selectedCustomer);
2627
2973
  }
2628
- }, [JSON.stringify(prefilledValues), selectedCustomer]);
2974
+ }, [JSON.stringify(prefilledValues), selectedCustomer, activeChannelForActions, activePreviewTab]);
2629
2975
 
2630
2976
  /**
2631
2977
  * Map channel constants to display names (lowercase for message)
@@ -2691,7 +3037,6 @@ const CommonTestAndPreview = (props) => {
2691
3037
  setPreviewDevice(DESKTOP);
2692
3038
  setPreviewDataHtml('');
2693
3039
  setHasPreviewCallBeenMade(false); // Reset preview call flag
2694
- setViberPreviewTagValues(null);
2695
3040
  setSelectedTestEntities([]);
2696
3041
  setBeeContent('');
2697
3042
  previousBeeContentRef.current = '';
@@ -2720,11 +3065,7 @@ const CommonTestAndPreview = (props) => {
2720
3065
  setTagsExtracted(true); // Auto-open custom values editor
2721
3066
 
2722
3067
  // Clear any existing values while waiting for prefilled values
2723
- const emptyValues = {};
2724
- [...requiredTags, ...optionalTags].forEach((tag) => {
2725
- emptyValues[tag?.fullPath] = '';
2726
- });
2727
- setCustomValues(emptyValues);
3068
+ setCustomValues(buildEmptyValues());
2728
3069
  };
2729
3070
 
2730
3071
  /**
@@ -2733,17 +3074,12 @@ const CommonTestAndPreview = (props) => {
2733
3074
  const handleClearSelection = () => {
2734
3075
  setSelectedCustomer(null);
2735
3076
  setHasPreviewCallBeenMade(false); // Reset flag when customer is cleared
2736
- setViberPreviewTagValues(null);
2737
3077
 
2738
3078
  // Clear all preview errors when customer is cleared
2739
3079
  actions.clearPreviewErrors();
2740
3080
 
2741
3081
  // Initialize empty values for all tags
2742
- const emptyValues = {};
2743
- [...requiredTags, ...optionalTags].forEach((tag) => {
2744
- emptyValues[tag?.fullPath] = '';
2745
- });
2746
- setCustomValues(emptyValues);
3082
+ setCustomValues(buildEmptyValues());
2747
3083
 
2748
3084
  // Don't make preview call when clearing selection - just reset to raw content
2749
3085
  // Preview will be shown using raw formData/content
@@ -2779,23 +3115,23 @@ const CommonTestAndPreview = (props) => {
2779
3115
  */
2780
3116
  const handleDiscardCustomValues = () => {
2781
3117
  // Initialize empty values for all tags
2782
- const emptyValues = {};
2783
- [...requiredTags, ...optionalTags].forEach((tag) => {
2784
- emptyValues[tag?.fullPath] = '';
2785
- });
3118
+ const emptyValues = buildEmptyValues();
2786
3119
  setCustomValues(emptyValues);
2787
3120
 
3121
+ // Reset SMS fallback preview so it shows raw template (with {{tags}} visible) after discard
3122
+ setSmsFallbackPreviewText(undefined);
3123
+
2788
3124
  // Update preview with empty values (this is a valid preview call trigger)
3125
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped.
3126
+ const previewChannelForDiscard = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
3127
+ const previewContentForDiscard = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2789
3128
  const payload = preparePreviewPayload(
2790
- channel,
3129
+ previewChannelForDiscard,
2791
3130
  formData || {},
2792
- getCurrentContent,
3131
+ previewContentForDiscard,
2793
3132
  emptyValues,
2794
3133
  selectedCustomer
2795
3134
  );
2796
- if (channel === CHANNELS.VIBER) {
2797
- setViberPreviewTagValues({ ...emptyValues });
2798
- }
2799
3135
  actions.updatePreviewRequested(payload);
2800
3136
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2801
3137
  };
@@ -2806,17 +3142,24 @@ const CommonTestAndPreview = (props) => {
2806
3142
  */
2807
3143
  const handleUpdatePreview = async () => {
2808
3144
  try {
3145
+ // Capture customValues at click time so the carousel preview only updates here,
3146
+ // not on every subsequent keystroke.
3147
+ previewCustomValuesRef.current = customValues;
3148
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped,
3149
+ // even when the user triggers update from the SMS fallback tab.
3150
+ // SMS fallback preview is kept in sync by syncSmsFallbackPreview via smsFallbackPreviewText.
3151
+ const previewChannel = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
3152
+ const previewContent = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2809
3153
  const payload = preparePreviewPayload(
2810
- channel,
3154
+ previewChannel,
2811
3155
  formData || {},
2812
- getCurrentContent,
3156
+ previewContent,
2813
3157
  customValues,
2814
3158
  selectedCustomer
2815
3159
  );
2816
- if (channel === CHANNELS.VIBER) {
2817
- setViberPreviewTagValues({ ...customValues });
2818
- }
2819
3160
  await actions.updatePreviewRequested(payload);
3161
+
3162
+ await syncSmsFallbackPreview(customValues, selectedCustomer);
2820
3163
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2821
3164
  } catch (error) {
2822
3165
  CapNotification.error({
@@ -2826,27 +3169,115 @@ const CommonTestAndPreview = (props) => {
2826
3169
  };
2827
3170
 
2828
3171
  /**
2829
- * Handle extract tags
3172
+ * Categorize extracted tags into required/optional.
2830
3173
  */
2831
- const handleExtractTags = () => {
2832
- // Get content based on channel
2833
- let contentToExtract = getCurrentContent;
3174
+ const categorizeTags = (tagsTree = []) => {
3175
+ const required = [];
3176
+ const optional = [];
3177
+ let hasPersonalizationTags = false;
3178
+ const processTag = (tag, parentPath = '') => {
3179
+ const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
3180
+
3181
+ // Skip unsubscribe tag for input fields
3182
+ if (tag?.name === UNSUBSCRIBE_TAG_NAME) return;
3183
+
3184
+ hasPersonalizationTags = true;
3185
+ const userDriven = tag?.metaData?.userDriven;
3186
+ if (userDriven === true) {
3187
+ optional.push({ ...tag, fullPath: currentPath });
3188
+ } else {
3189
+ // false or missing (SMS/DLT extract often omits metaData) → required for test values
3190
+ required.push({ ...tag, fullPath: currentPath });
3191
+ }
3192
+
3193
+ if (tag?.children?.length > 0) {
3194
+ tag.children.forEach((child) => processTag(child, currentPath));
3195
+ }
3196
+ };
2834
3197
 
3198
+ (tagsTree || []).forEach((tag) => processTag(tag));
3199
+ return { required, optional, hasPersonalizationTags };
3200
+ };
3201
+
3202
+ /**
3203
+ * Apply tag extraction when content comes from RCS + SMS fallback (no API call).
3204
+ */
3205
+ const applyRcsSmsFallbackTagExtraction = () => {
3206
+ const rcsPrimaryCategorized = categorizeTags(extractedTags ?? []);
3207
+ const fallbackSmsResolvedForTags = smsFallbackTextForTagExtraction ?? '';
3208
+ let fallbackSmsTagTree = smsFallbackExtractedTags?.length > 0
3209
+ ? smsFallbackExtractedTags
3210
+ : buildSyntheticSmsMustacheTags(fallbackSmsResolvedForTags);
3211
+ if (!smsTemplateHasMustacheTags(fallbackSmsResolvedForTags)) {
3212
+ fallbackSmsTagTree = [];
3213
+ }
3214
+ const fallbackSmsCategorized = categorizeTags(fallbackSmsTagTree);
3215
+ setRequiredTags(rcsPrimaryCategorized.required);
3216
+ setOptionalTags(rcsPrimaryCategorized.optional);
3217
+ setSmsFallbackRequiredTags(fallbackSmsCategorized.required);
3218
+ setSmsFallbackOptionalTags(fallbackSmsCategorized.optional);
3219
+ setTagsExtracted(
3220
+ rcsPrimaryCategorized.hasPersonalizationTags || fallbackSmsCategorized.hasPersonalizationTags,
3221
+ );
3222
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, rcsPrimaryCategorized, fallbackSmsCategorized));
3223
+ };
3224
+
3225
+ /**
3226
+ * When extract-tags API returns, map Redux `extractedTags` into required/optional so
3227
+ * CustomValuesEditor shows personalization fields (effect was previously commented out).
3228
+ * RCS + SMS fallback: merge primary + fallback tag trees when fallback template exists.
3229
+ */
3230
+ useEffect(() => {
3231
+ if (!show) return;
3232
+ const hasFallbackSmsTemplate = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
3233
+ if (channel === CHANNELS.RCS && hasFallbackSmsTemplate) {
3234
+ applyRcsSmsFallbackTagExtraction();
3235
+ return;
3236
+ }
3237
+ const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
3238
+ let smsTagSource = channel === CHANNELS.SMS && (!extractedTags || extractedTags.length === 0)
3239
+ ? buildSyntheticSmsMustacheTags(smsEditorBody)
3240
+ : (extractedTags ?? []);
3241
+ if (channel === CHANNELS.SMS && !smsTemplateHasMustacheTags(smsEditorBody)) {
3242
+ smsTagSource = [];
3243
+ }
3244
+ const { required, optional, hasPersonalizationTags } = categorizeTags(smsTagSource);
3245
+ setRequiredTags(required);
3246
+ setOptionalTags(optional);
3247
+ setTagsExtracted(hasPersonalizationTags);
3248
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, { required, optional }, { required: [], optional: [] }));
3249
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- applyRcsSmsFallbackTagExtraction closes over latest extractedTags/smsFallbackExtractedTags
3250
+ }, [show, extractedTags, channel, smsFallbackContent, smsFallbackExtractedTags, getCurrentContent, smsFallbackTextForTagExtraction]);
3251
+
3252
+ /**
3253
+ * Get content to run tag extraction on (channel-specific).
3254
+ */
3255
+ const getContentForTagExtraction = () => {
3256
+ let contentToExtract = activeContentForActions;
2835
3257
  if (channel === CHANNELS.EMAIL && formData) {
2836
3258
  const currentTabData = formData[currentTab - 1];
2837
3259
  const activeTab = currentTabData?.activeTab;
2838
3260
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
2839
3261
  contentToExtract = templateContent || contentToExtract;
2840
- } else if (channel === CHANNELS.VIBER) {
2841
- contentToExtract = getViberTagExtractionContent(getViberMergedFormData(), contentToExtract);
2842
3262
  }
3263
+ return contentToExtract;
3264
+ };
2843
3265
 
2844
- // Check for personalization tags (excluding unsubscribe)
3266
+ /**
3267
+ * Handle extract tags
3268
+ */
3269
+ const handleExtractTags = () => {
3270
+ if (channel === CHANNELS.RCS) {
3271
+ applyRcsSmsFallbackTagExtraction();
3272
+ return;
3273
+ }
3274
+
3275
+ const contentToExtract = getContentForTagExtraction();
2845
3276
  const tags = contentToExtract.match(/{{[^}]+}}/g) || [];
2846
3277
  const hasPersonalizationTags = tags.some((tag) => !tag.includes(UNSUBSCRIBE_TAG_NAME));
3278
+ const onlyUnsubscribe = !hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME);
2847
3279
 
2848
- if (!hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME)) {
2849
- // If only unsubscribe tag is present, show noTagsExtracted message
3280
+ if (onlyUnsubscribe) {
2850
3281
  setTagsExtracted(false);
2851
3282
  setRequiredTags([]);
2852
3283
  setOptionalTags([]);
@@ -2854,10 +3285,9 @@ const CommonTestAndPreview = (props) => {
2854
3285
  return;
2855
3286
  }
2856
3287
 
2857
- // Extract tags
2858
3288
  setTagsExtracted(true);
2859
3289
  const { templateSubject, templateContent } = prepareTagExtractionPayload(
2860
- channel,
3290
+ activeChannelForActions,
2861
3291
  formData || {},
2862
3292
  contentToExtract
2863
3293
  );
@@ -2919,7 +3349,7 @@ const CommonTestAndPreview = (props) => {
2919
3349
  if (existingTestCustomer) {
2920
3350
  const entityId = existingTestCustomer.userId ?? existingTestCustomer.customerId;
2921
3351
  if (entityId != null) {
2922
- const id = 'customer:' + normalizeTestEntityId(entityId);
3352
+ const id = normalizeTestEntityId(entityId);
2923
3353
  setSelectedTestEntities((prev) => (
2924
3354
  prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
2925
3355
  ));
@@ -2956,7 +3386,7 @@ const CommonTestAndPreview = (props) => {
2956
3386
  (c) => String(c?.customerId) === customerIdFromLookup || String(c?.userId) === customerIdFromLookup
2957
3387
  );
2958
3388
  if (alreadyInTestListByCustomerId) {
2959
- const id = 'customer:' + normalizeTestEntityId(customerIdFromLookup);
3389
+ const id = normalizeTestEntityId(customerIdFromLookup);
2960
3390
  setSelectedTestEntities((prev) => (
2961
3391
  prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
2962
3392
  ));
@@ -2993,26 +3423,21 @@ const CommonTestAndPreview = (props) => {
2993
3423
  const handleSendTestMessage = () => {
2994
3424
  const allUserIds = [];
2995
3425
  selectedTestEntities.forEach((entityId) => {
2996
- if (String(entityId).startsWith('group:')) {
2997
- const rawId = String(entityId).slice('group:'.length);
2998
- const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, rawId));
2999
- if (group) {
3000
- allUserIds.push(...group.userIds);
3001
- }
3426
+ const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, entityId));
3427
+ if (group) {
3428
+ allUserIds.push(...group.userIds);
3002
3429
  } else {
3003
- const rawId = String(entityId).startsWith('customer:')
3004
- ? String(entityId).slice('customer:'.length)
3005
- : String(entityId);
3006
- allUserIds.push(Number(rawId));
3430
+ allUserIds.push(entityId);
3007
3431
  }
3008
3432
  });
3009
3433
  const uniqueUserIds = [...new Set(allUserIds)];
3010
3434
 
3011
- const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP].includes(channel)
3435
+ const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS].includes(channel)
3012
3436
  ? testPreviewDeliverySettings[channel]
3013
3437
  : null;
3014
3438
 
3015
- // Create initial payload based on channel
3439
+ // createMessageMeta must match the creative channel and full creative (RCS + SMS fallback in one meta).
3440
+ // Do not use activeChannelForActions / activeContentForActions — those follow the RCS vs Fallback SMS *preview* tab.
3016
3441
  const initialPayload = prepareTestMessagePayload(
3017
3442
  channel,
3018
3443
  formData || content || {},
@@ -3020,7 +3445,8 @@ const CommonTestAndPreview = (props) => {
3020
3445
  customValues,
3021
3446
  uniqueUserIds,
3022
3447
  previewData,
3023
- deliveryOverride
3448
+ deliveryOverride,
3449
+ {},
3024
3450
  );
3025
3451
 
3026
3452
  actions.createMessageMetaRequested(
@@ -3053,11 +3479,10 @@ const CommonTestAndPreview = (props) => {
3053
3479
  // ============================================
3054
3480
  // RENDER HELPER FUNCTIONS
3055
3481
  // ============================================
3056
-
3057
3482
  const renderLeftPanelContent = () => (
3058
3483
  <LeftPanelContent
3059
- isExtractingTags={isExtractingTags}
3060
- extractedTags={extractedTags}
3484
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
3485
+ extractedTags={leftPanelExtractedTags}
3061
3486
  selectedCustomer={selectedCustomer}
3062
3487
  handleCustomerSelect={handleCustomerSelect}
3063
3488
  handleSearchCustomer={handleSearchCustomer}
@@ -3075,15 +3500,29 @@ const CommonTestAndPreview = (props) => {
3075
3500
 
3076
3501
  const renderCustomValuesEditor = () => (
3077
3502
  <CustomValuesEditor
3078
- isExtractingTags={isExtractingTags}
3503
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
3079
3504
  isUpdatePreviewDisabled={isUpdatePreviewDisabled}
3080
3505
  showJSON={showJSON}
3081
3506
  setShowJSON={setShowJSON}
3082
3507
  customValues={customValues}
3083
3508
  handleJSONTextChange={handleJSONTextChange}
3084
- extractedTags={extractedTags}
3085
- requiredTags={requiredTags}
3086
- optionalTags={optionalTags}
3509
+ sections={[
3510
+ {
3511
+ key: channel,
3512
+ title:
3513
+ channel === CHANNELS.RCS
3514
+ ? messages.rcsTagsSectionTitle
3515
+ : messages[`${channel}TagsSectionTitle`],
3516
+ requiredTags,
3517
+ optionalTags,
3518
+ },
3519
+ {
3520
+ key: PREVIEW_TAB_SMS_FALLBACK,
3521
+ title: isRcsSmsFallbackPreviewEnabled ? messages.smsFallbackTagsSectionTitle : null,
3522
+ requiredTags: smsFallbackRequiredTags,
3523
+ optionalTags: smsFallbackOptionalTags,
3524
+ },
3525
+ ]}
3087
3526
  handleCustomValueChange={handleCustomValueChange}
3088
3527
  handleDiscardCustomValues={handleDiscardCustomValues}
3089
3528
  handleUpdatePreview={handleUpdatePreview}
@@ -3099,18 +3538,13 @@ const CommonTestAndPreview = (props) => {
3099
3538
  }));
3100
3539
  };
3101
3540
 
3102
- /** Trim pasted emails (trailing CR/LF). SMS: strip non-digits so pasted formatted numbers match isValidMobile / API. */
3541
+ /** Trim pasted emails (trailing CR/LF). Allow any input for SMS; valid-only check is in renderAddTestCustomerButton. */
3103
3542
  const handleTestCustomersSearch = useCallback((value) => {
3104
3543
  if (value == null || value === '') {
3105
3544
  setSearchValue('');
3106
3545
  return;
3107
3546
  }
3108
- const raw = String(value).trim();
3109
- if (channel === CHANNELS.SMS) {
3110
- setSearchValue(formatPhoneNumber(raw));
3111
- } else {
3112
- setSearchValue(raw);
3113
- }
3547
+ setSearchValue(String(value).trim());
3114
3548
  }, [channel]);
3115
3549
 
3116
3550
  const renderSendTestMessage = () => (
@@ -3128,12 +3562,13 @@ const CommonTestAndPreview = (props) => {
3128
3562
  renderAddTestCustomerButton={renderAddTestCustomerButton}
3129
3563
  formatMessage={formatMessage}
3130
3564
  deliverySettings={testPreviewDeliverySettings[channel]}
3131
- senderDetailsOptions={senderDetailsByChannel[channel]}
3565
+ senderDetailsByChannel={senderDetailsByChannel}
3132
3566
  wecrmAccounts={wecrmAccounts}
3133
3567
  onSaveDeliverySettings={handleSaveDeliverySettings}
3134
3568
  isLoadingSenderDetails={isLoadingSenderDetails}
3135
3569
  smsTraiDltEnabled={smsTraiDltEnabled}
3136
3570
  registeredSenderIds={registeredSenderIds}
3571
+ isChannelSmsFallbackPreviewEnabled={isRcsSmsFallbackPreviewEnabled}
3137
3572
  searchValue={searchValue}
3138
3573
  setSearchValue={handleTestCustomersSearch}
3139
3574
  />
@@ -3147,14 +3582,13 @@ const CommonTestAndPreview = (props) => {
3147
3582
 
3148
3583
  const renderAddTestCustomerButton = () => {
3149
3584
  const raw = (searchValue || '').trim();
3150
- const value = channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw;
3151
3585
  const showAddButton =
3152
3586
  [CHANNELS.EMAIL, CHANNELS.SMS].includes(channel) &&
3153
- (channel === CHANNELS.EMAIL ? isValidEmail(value) : isValidMobile(value));
3587
+ (channel === CHANNELS.EMAIL ? isValidEmail(raw) : isValidMobile(formatPhoneNumber(raw)));
3154
3588
  if (!showAddButton) return null;
3155
3589
  return (
3156
3590
  <AddTestCustomerButton
3157
- searchValue={value}
3591
+ searchValue={channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw}
3158
3592
  handleAddTestCustomer={handleAddTestCustomer}
3159
3593
  />
3160
3594
  );