@capillarytech/creatives-library 8.0.329 → 8.0.330-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 (89) hide show
  1. package/constants/unified.js +4 -0
  2. package/package.json +1 -1
  3. package/utils/commonUtils.js +19 -1
  4. package/utils/templateVarUtils.js +35 -6
  5. package/utils/tests/tagValidations.test.js +20 -0
  6. package/utils/tests/templateVarUtils.test.js +44 -0
  7. package/v2Components/CapActionButton/constants.js +7 -0
  8. package/v2Components/CapActionButton/index.js +167 -109
  9. package/v2Components/CapActionButton/index.scss +157 -6
  10. package/v2Components/CapActionButton/messages.js +19 -3
  11. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  12. package/v2Components/CapTagList/index.js +28 -23
  13. package/v2Components/CapTagList/style.scss +29 -0
  14. package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +63 -0
  15. package/v2Components/CapTagListWithInput/index.js +4 -0
  16. package/v2Components/CapWhatsappCTA/index.js +2 -0
  17. package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +1 -0
  18. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +160 -15
  19. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
  20. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +323 -77
  21. package/v2Components/CommonTestAndPreview/index.js +49 -57
  22. package/v2Components/CommonTestAndPreview/messages.js +8 -0
  23. package/v2Components/CommonTestAndPreview/reducer.js +3 -1
  24. package/v2Components/CommonTestAndPreview/sagas.js +2 -1
  25. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  26. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  27. package/v2Components/FormBuilder/index.js +1 -0
  28. package/v2Components/HtmlEditor/HTMLEditor.js +6 -1
  29. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  30. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +927 -2
  31. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +3 -0
  32. package/v2Components/SmsFallback/smsFallbackUtils.js +14 -3
  33. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +16 -0
  34. package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
  35. package/v2Components/TemplatePreview/constants.js +2 -0
  36. package/v2Components/TemplatePreview/index.js +143 -28
  37. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  38. package/v2Components/TestAndPreviewSlidebox/index.js +5 -0
  39. package/v2Components/mockdata.js +1 -0
  40. package/v2Containers/BeeEditor/index.js +19 -1
  41. package/v2Containers/CreativesContainer/SlideBoxContent.js +28 -1
  42. package/v2Containers/CreativesContainer/index.js +9 -3
  43. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +5 -0
  44. package/v2Containers/Email/index.js +78 -39
  45. package/v2Containers/Email/reducer.js +2 -2
  46. package/v2Containers/Email/sagas.js +3 -1
  47. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -2
  48. package/v2Containers/Email/tests/sagas.test.js +230 -0
  49. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +6 -1
  50. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +3 -0
  51. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -2
  52. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +16 -1
  53. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +3 -1
  54. package/v2Containers/EmailWrapper/index.js +4 -0
  55. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +1 -0
  56. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +9 -0
  57. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +1 -0
  58. package/v2Containers/MobilePush/Create/index.js +2 -0
  59. package/v2Containers/MobilePush/Edit/index.js +2 -0
  60. package/v2Containers/MobilepushWrapper/index.js +3 -1
  61. package/v2Containers/Rcs/constants.js +85 -7
  62. package/v2Containers/Rcs/index.js +1592 -156
  63. package/v2Containers/Rcs/index.js.rej +1336 -0
  64. package/v2Containers/Rcs/index.scss +191 -0
  65. package/v2Containers/Rcs/index.scss.rej +74 -0
  66. package/v2Containers/Rcs/messages.js +28 -2
  67. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +20 -0
  68. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +69178 -117691
  69. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
  70. package/v2Containers/Rcs/tests/index.test.js +132 -94
  71. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +67 -0
  72. package/v2Containers/Rcs/tests/utils.test.js +276 -38
  73. package/v2Containers/Rcs/utils.js +130 -7
  74. package/v2Containers/Sms/Edit/index.js +2 -0
  75. package/v2Containers/SmsTrai/Edit/index.js +27 -0
  76. package/v2Containers/SmsTrai/Edit/messages.js +5 -0
  77. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  78. package/v2Containers/SmsWrapper/index.js +2 -0
  79. package/v2Containers/TagList/index.js +73 -20
  80. package/v2Containers/TagList/messages.js +4 -0
  81. package/v2Containers/TagList/tests/TagList.test.js +124 -20
  82. package/v2Containers/TagList/tests/mockdata.js +17 -0
  83. package/v2Containers/Templates/_templates.scss +99 -0
  84. package/v2Containers/Templates/index.js +29 -14
  85. package/v2Containers/Viber/index.js +3 -0
  86. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +0 -2
  87. package/v2Containers/WebPush/Create/index.js +10 -2
  88. package/v2Containers/Whatsapp/index.js +5 -0
  89. package/v2Containers/Zalo/index.js +2 -0
@@ -21,20 +21,42 @@ import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
21
21
  import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
22
22
  import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
23
23
  import CapImage from '@capillarytech/cap-ui-library/CapImage';
24
+ import CapCard from '@capillarytech/cap-ui-library/CapCard';
24
25
  import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
25
26
  import CapSelect from '@capillarytech/cap-ui-library/CapSelect';
27
+ import CapCustomCard from '@capillarytech/cap-ui-library/CapCustomCard';
28
+ import CapDropdown from '@capillarytech/cap-ui-library/CapDropdown';
29
+ import CapMenu from '@capillarytech/cap-ui-library/CapMenu';
26
30
  import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
31
+ import CapTooltipWithInfo from '@capillarytech/cap-ui-library/CapTooltipWithInfo';
27
32
  import CapError from '@capillarytech/cap-ui-library/CapError';
33
+ import CapCheckbox from '@capillarytech/cap-ui-library/CapCheckbox';
28
34
  import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
35
+ import CapLink from '@capillarytech/cap-ui-library/CapLink';
36
+ import CapTab from '@capillarytech/cap-ui-library/CapTab';
37
+
38
+ import {
39
+ CAP_G01,
40
+ CAP_SPACE_04,
41
+ CAP_SPACE_16,
42
+ CAP_SPACE_24,
43
+ CAP_SPACE_28,
44
+ CAP_SPACE_32,
45
+ CAP_WHITE,
46
+ CAP_SECONDARY,
47
+ } from '@capillarytech/cap-ui-library/styled/variables';
29
48
 
30
49
  import CapVideoUpload from '../../v2Components/CapVideoUpload';
31
50
  import * as globalActions from '../Cap/actions';
32
51
  import CapActionButton from '../../v2Components/CapActionButton';
52
+ import TemplatePreview from '../../v2Components/TemplatePreview';
33
53
  import { makeSelectRcs, makeSelectAccount } from './selectors';
54
+ import { DATE_DISPLAY_FORMAT, TIME_DISPLAY_FORMAT } from '../App/constants';
34
55
  import {
35
56
  isLoadingMetaEntities,
36
57
  makeSelectMetaEntities,
37
58
  setInjectedTags,
59
+ selectCurrentOrgDetails,
38
60
  } from '../Cap/selectors';
39
61
  import * as RcsActions from './actions';
40
62
  import { isAiContentBotDisabled } from '../../utils/common';
@@ -44,10 +66,12 @@ import {
44
66
  normalizeLibraryLoadedTitleDesc,
45
67
  mergeRcsSmsFallBackContentFromDetails,
46
68
  mergeRcsSmsFallbackVarMapLayers,
69
+ extractRegisteredSenderIdsFromSmsFallbackRecord,
47
70
  pickFirstSmsFallbackTemplateString,
48
71
  syncCardVarMappedSemanticsFromSlots,
49
72
  hasMeaningfulSmsFallbackShape,
50
73
  getLibrarySmsFallbackApiBaselineFromTemplateData,
74
+ pickRcsCardVarMappedEntries,
51
75
  } from './rcsLibraryHydrationUtils';
52
76
  import {
53
77
  RCS,
@@ -65,11 +89,14 @@ import {
65
89
  RCS_IMG_SIZE,
66
90
  RCS_DLT_MODE,
67
91
  CTA,
92
+ AI_CONTENT_BOT_DISABLED,
68
93
  RCS_STATUSES,
69
94
  TITLE_TEXT,
70
95
  MESSAGE_TEXT,
71
96
  ALLOWED_EXTENSIONS_VIDEO_REGEX,
72
97
  RCS_VIDEO_SIZE,
98
+ TEMPLATE_HEADER_MAX_LENGTH,
99
+ TEMPLATE_MESSAGE_MAX_LENGTH,
73
100
  RCS_THUMBNAIL_MIN_SIZE,
74
101
  RCS_THUMBNAIL_MAX_SIZE,
75
102
  contentType,
@@ -77,16 +104,30 @@ import {
77
104
  rcsVarTestRegex,
78
105
  RCS_IMAGE_DIMENSIONS,
79
106
  RCS_TEXT_MESSAGE_MAX_LENGTH,
107
+ RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP,
80
108
  RCS_RICH_CARD_MAX_LENGTH,
81
109
  RCS_VIDEO_THUMBNAIL_DIMENSIONS,
110
+ RCS_CAROUSEL_IMAGE_DIMENSIONS,
111
+ RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS,
112
+ RCS_CAROUSEL_IMG_SIZE,
113
+ RCS_CAROUSEL_VIDEO_SIZE,
82
114
  MAX_BUTTONS,
115
+ INITIAL_SUGGESTIONS,
83
116
  INITIAL_SUGGESTIONS_DATA_STOP,
84
117
  RCS_BUTTON_TYPES,
118
+ titletype,
119
+ descType,
85
120
  STANDALONE,
86
121
  VERTICAL,
87
122
  SMALL,
88
123
  MEDIUM,
89
124
  RICHCARD,
125
+ HOST_INFOBIP,
126
+ HOST_ICS,
127
+ CAROUSEL_HEIGHT_OPTIONS,
128
+ CAROUSEL_WIDTH_OPTIONS,
129
+ STOP,
130
+ RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS,
90
131
  RCS_NUMERIC_VAR_NAME_REGEX,
91
132
  RCS_NUMERIC_VAR_TOKEN_REGEX,
92
133
  RCS_TAG_AREA_FIELD_TITLE,
@@ -118,10 +159,13 @@ import {
118
159
  getTemplateStatusType,
119
160
  normalizeCardVarMapped,
120
161
  coalesceCardVarMappedToTemplate,
162
+ getRcsSemanticVarNamesSpanningTitleAndDesc,
121
163
  resolveCardVarMappedSlotValue,
122
164
  sanitizeCardVarMappedValue,
123
165
  } from './utils';
124
166
 
167
+
168
+ const { Group: CapCheckboxGroup } = CapCheckbox;
125
169
  export const Rcs = (props) => {
126
170
  const {
127
171
  intl,
@@ -146,6 +190,7 @@ export const Rcs = (props) => {
146
190
  selectedOfferDetails,
147
191
  eventContextTags,
148
192
  accountData = {},
193
+ currentOrgDetails,
149
194
  // TestAndPreviewSlidebox props
150
195
  showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
151
196
  handleTestAndPreview: propsHandleTestAndPreview,
@@ -153,6 +198,24 @@ export const Rcs = (props) => {
153
198
  } = props || {};
154
199
  const { formatMessage } = intl;
155
200
  const { TextArea } = CapInput;
201
+ const { CapCustomCardList } = CapCustomCard;
202
+
203
+ // Defensive: React cannot render plain objects as children (crashes with
204
+ // "Objects are not valid as a React child"). Some campaigns (!isFullMode) flows
205
+ // can accidentally set an error state to an object (e.g. `{}`).
206
+ const normalizeErrorMessage = (err) => {
207
+ if (!err) return '';
208
+ if (React.isValidElement(err)) return err;
209
+ if (typeof err === 'string') return err;
210
+ if (typeof err === 'number') return String(err);
211
+ if (typeof err === 'object' && typeof err.message === 'string') return err.message;
212
+ try {
213
+ return JSON.stringify(err);
214
+ } catch (e) {
215
+ return String(err);
216
+ }
217
+ };
218
+
156
219
  const [isEditFlow, setEditFlow] = useState(false);
157
220
  const isEditLike = isEditFlow || !isFullMode;
158
221
  const [tags, updateTags] = useState([]);
@@ -176,16 +239,28 @@ export const Rcs = (props) => {
176
239
  const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS_DATA_STOP);
177
240
  const buttonType = RCS_BUTTON_TYPES.NONE;
178
241
  const [suggestionError, setSuggestionError] = useState(true);
242
+ const [isTestAndPreviewMode, setIsTestAndPreviewMode] = useState(false);
179
243
  const [templateType, setTemplateType] = useState('text_message');
180
244
  const [titleVarMappedData, setTitleVarMappedData] = useState({});
181
245
  const [descVarMappedData, setDescVarMappedData] = useState({});
182
246
  const [titleTextAreaId, setTitleTextAreaId] = useState();
183
247
  const [descTextAreaId, setDescTextAreaId] = useState();
184
248
  const [assetList, setAssetList] = useState({});
249
+ const [assetListImage, setAssetListImage] = useState('');
185
250
  const [rcsImageSrc, updateRcsImageSrc] = useState('');
186
251
  const [rcsVideoSrc, setRcsVideoSrc] = useState({});
187
252
  const [rcsThumbnailSrc, setRcsThumbnailSrc] = useState('');
188
253
  const [selectedDimension, setSelectedDimension] = useState(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
254
+ // Carousel (UI-only) state
255
+ const [selectedCarousel, setSelectedCarousel] = useState('');
256
+ const [selectedCarouselHeight, setSelectedCarouselHeight] = useState(MEDIUM);
257
+ const [selectedCarouselWidth, setSelectedCarouselWidth] = useState(SMALL);
258
+ const [carouselData, setCarouselData] = useState([]);
259
+ const [carouselErrors, setCarouselErrors] = useState([]); // [{ title: string|false, description: string|false }]
260
+ const [activeCarouselIndex, setActiveCarouselIndex] = useState('0');
261
+ const [carouselResetNonce, setCarouselResetNonce] = useState(0);
262
+ const [carouselFocusedVarId, setCarouselFocusedVarId] = useState('');
263
+ const [imageError, setImageError] = useState(null);
189
264
  const [templateTitleError, setTemplateTitleError] = useState(false);
190
265
  const [cardVarMapped, setCardVarMapped] = useState({});
191
266
  /** Bump when hydrated cardVarMapped payload changes so VarSegment TextAreas remount with fresh valueMap (controlled-input sync). */
@@ -236,11 +311,21 @@ export const Rcs = (props) => {
236
311
  // Handle close Test and Preview slidebox
237
312
  const handleCloseTestAndPreview = useCallback(() => {
238
313
  setShowTestAndPreviewSlidebox(false);
314
+ setIsTestAndPreviewMode(false);
239
315
  if (propsHandleCloseTestAndPreview) {
240
316
  propsHandleCloseTestAndPreview();
241
317
  }
242
318
  }, [propsHandleCloseTestAndPreview]);
243
319
 
320
+ // Helper to get RCS orientation from selectedDimension
321
+ const getRcsOrientation = () => {
322
+ const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
323
+ if (isMediaTypeImage) {
324
+ return RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
325
+ }
326
+ // For video
327
+ return RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
328
+ };
244
329
  /** Merge editor slot map into `smsFallbackData` (like `cardVarMapped` for title/desc). */
245
330
  const handleSmsFallbackEditorStateChange = useCallback((patch) => {
246
331
  setSmsFallbackData((prev) => {
@@ -261,6 +346,12 @@ export const Rcs = (props) => {
261
346
  const [accessToken, setAccessToken] = useState('');
262
347
  const [hostName, setHostName] = useState('');
263
348
  const [accountName, setAccountName] = useState('');
349
+ const isHostInfoBip = hostName === HOST_INFOBIP;
350
+ const isHostIcs = hostName === HOST_ICS;
351
+
352
+ useEffect(() => {
353
+ setSuggestions(isHostIcs ? INITIAL_SUGGESTIONS_DATA_STOP : []);
354
+ }, [isHostIcs]);
264
355
  useEffect(() => {
265
356
  const accountObj = accountData.selectedRcsAccount || {};
266
357
  if (!isEmpty(accountObj)) {
@@ -288,6 +379,668 @@ export const Rcs = (props) => {
288
379
  const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
289
380
  const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
290
381
  const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
382
+ const isCarouselType = templateType === contentType.carousel;
383
+
384
+ const MAX_RCS_CAROUSEL_ALLOWED = 10;
385
+ // Uploads for RCS are stored in redux under dynamic keys `uploadedAssetData${index}`.
386
+ // Carousel needs per-card indices; otherwise all cards "restore" the last uploaded asset
387
+ // and show the same media/thumbnail.
388
+ const RCS_CAROUSEL_ASSET_INDEX_BASE = 10; // keep away from standalone indices 0 (media) and 1 (thumbnail)
389
+ const getCarouselImageAssetIndex = (cardIndex) =>
390
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3);
391
+ const getCarouselVideoAssetIndex = (cardIndex) =>
392
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 1;
393
+ const getCarouselThumbnailAssetIndex = (cardIndex) =>
394
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 2;
395
+ const isThumbnailAssetIndex = (index) => {
396
+ if (index === 1) return true; // standalone thumbnail
397
+ if (index >= RCS_CAROUSEL_ASSET_INDEX_BASE) {
398
+ return ((index - RCS_CAROUSEL_ASSET_INDEX_BASE) % 3) === 2; // carousel thumbnail slot
399
+ }
400
+ return false;
401
+ };
402
+
403
+ // Carousel dimension key: `${HEIGHT}_${WIDTH}` (e.g., SHORT_SMALL)
404
+ const getCarouselDimensionKey = () => `${selectedCarouselHeight}_${selectedCarouselWidth}`;
405
+
406
+ const clearCarouselCardMedia = (cardIndex, { clearImage = true, clearVideo = true, clearThumb = true } = {}) => {
407
+ setCarouselData((prev = []) => {
408
+ const updated = cloneDeep(prev);
409
+ if (!updated?.[cardIndex]) return updated;
410
+ if (clearImage) updated[cardIndex].imageSrc = '';
411
+ if (clearVideo) updated[cardIndex].videoAsset = {};
412
+ if (clearThumb) updated[cardIndex].thumbnailSrc = '';
413
+ return updated;
414
+ });
415
+
416
+ if (clearImage) actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIndex));
417
+ if (clearVideo) actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIndex));
418
+ if (clearThumb) actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIndex));
419
+ };
420
+
421
+ const resetCarouselMediaForAllCards = () => {
422
+ setCarouselData((prev = []) => {
423
+ const updated = cloneDeep(prev);
424
+ updated.forEach((card) => {
425
+ if (!card) return;
426
+ card.imageSrc = '';
427
+ card.videoAsset = {};
428
+ card.thumbnailSrc = '';
429
+ });
430
+ return updated;
431
+ });
432
+ (carouselData || []).forEach((_, idx) => {
433
+ actions.clearRcsMediaAsset(getCarouselImageAssetIndex(idx));
434
+ actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(idx));
435
+ actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(idx));
436
+ });
437
+ // Force tab panes to remount after global reset (some tab implementations cache inactive panes)
438
+ setCarouselResetNonce((n) => n + 1);
439
+ };
440
+
441
+ const RCS_CAROUSEL_INITIAL_CARD = {
442
+ title: '',
443
+ description: '',
444
+ mediaType: RCS_MEDIA_TYPES.IMAGE, // per-card
445
+ imageSrc: '',
446
+ videoAsset: {}, // CapVideoUpload object shape
447
+ thumbnailSrc: '',
448
+ suggestions: [],
449
+ };
450
+
451
+ const RCS_CAROUSEL_INITIAL_FIRST_CARD = {
452
+ ...RCS_CAROUSEL_INITIAL_CARD,
453
+ suggestions: cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS),
454
+ };
455
+
456
+ const ensureFirstCardDefaultPhoneSuggestions = (cards) => {
457
+ const next = cloneDeep(cards || []);
458
+ if (next.length === 0) return next;
459
+ const s = next[0].suggestions;
460
+ if (!Array.isArray(s) || s.length === 0) {
461
+ next[0] = {
462
+ ...next[0],
463
+ suggestions: cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS),
464
+ };
465
+ }
466
+ return next;
467
+ };
468
+
469
+ // Always use functional updates: image upload completes in CapImageUpload's useEffect and calls
470
+ // updateImageSrc → this handler. If we cloned carouselData from render, we'd overwrite title/body/
471
+ // suggestions typed since the last commit (stale closure).
472
+ const handleCarouselValueChange = (carouselIndex, fields) => {
473
+ setCarouselData((prev = []) => {
474
+ const updated = cloneDeep(prev);
475
+ if (!updated[carouselIndex]) return prev;
476
+ fields.forEach(({ fieldName, value }) => {
477
+ updated[carouselIndex][fieldName] = value;
478
+ });
479
+ return updated;
480
+ });
481
+ };
482
+
483
+ const updateCarouselErrors = (carouselIndex, patch) => {
484
+ setCarouselErrors((prev = []) => {
485
+ const next = Array.isArray(prev) ? [...prev] : [];
486
+ next[carouselIndex] = { ...(next[carouselIndex] || {}), ...patch };
487
+ return next;
488
+ });
489
+ };
490
+
491
+ const deleteCarouselCard = (index) => {
492
+ let nextIdx = 0;
493
+ flushSync(() => {
494
+ setCarouselData((prev = []) => {
495
+ const updated = cloneDeep(prev);
496
+ if (index < 0 || index >= updated.length) return updated;
497
+ updated.splice(index, 1);
498
+ nextIdx = Math.max(0, Math.min(index - 1, updated.length - 1));
499
+ return ensureFirstCardDefaultPhoneSuggestions(updated);
500
+ });
501
+ });
502
+ setCarouselErrors((prev = []) => {
503
+ const next = Array.isArray(prev) ? [...prev] : [];
504
+ next.splice(index, 1);
505
+ return next;
506
+ });
507
+ setActiveCarouselIndex(`${nextIdx}`);
508
+ };
509
+
510
+ const carouselButtonTextHasForbiddenChars = (value) => {
511
+ if (!value) return false;
512
+ if (value.includes('[') || value.includes(']')) return true;
513
+ const withoutValidVariables = value.replace(/\{\{[^}]*\}\}/g, '');
514
+ if (withoutValidVariables.includes('{') || withoutValidVariables.includes('}')) return true;
515
+ return false;
516
+ };
517
+
518
+ const isCompleteSavedCarouselSuggestion = (s) => {
519
+ if (!s || !s.isSaved) return false;
520
+ const text = (s.text || '').trim();
521
+ if (!text || !isValidText(text) || carouselButtonTextHasForbiddenChars(text)) return false;
522
+ if (s.type === RCS_BUTTON_TYPES.PHONE_NUMBER) {
523
+ return String(s.phoneNumber || '').length >= 5;
524
+ }
525
+ if (s.type === RCS_BUTTON_TYPES.CTA) {
526
+ const url = String(s.url || '').trim();
527
+ if (!url || url.length > URL_MAX_LENGTH) return false;
528
+ const subtype = s.urlType || RCS_CTA_URL_TYPE.STATIC;
529
+ if (subtype === RCS_CTA_URL_TYPE.DYNAMIC) {
530
+ return true;
531
+ }
532
+ if (!isUrl(url)) return false;
533
+ const varMatches = url.match(invalidVarRegex);
534
+ return !(varMatches && varMatches.length > 0);
535
+ }
536
+ if (s.type === RCS_BUTTON_TYPES.QUICK_REPLY) {
537
+ return true;
538
+ }
539
+ return false;
540
+ };
541
+
542
+ const isCarouselCardValid = (card, cardIndex) => {
543
+ if (!card) return false;
544
+ if (!card.title || !card.title.trim()) return false;
545
+ if ((card.title || '').length > TEMPLATE_TITLE_MAX_LENGTH) return false;
546
+ if (!card.description || !card.description.trim()) return false;
547
+ if ((card.description || '').length > RCS_RICH_CARD_MAX_LENGTH) return false;
548
+ let mediaOk = false;
549
+ if (card.mediaType === RCS_MEDIA_TYPES.IMAGE) {
550
+ mediaOk = !!(card.imageSrc && String(card.imageSrc).trim());
551
+ } else if (card.mediaType === RCS_MEDIA_TYPES.VIDEO) {
552
+ const hasVideo = !!(card.videoAsset && card.videoAsset.videoSrc && String(card.videoAsset.videoSrc).trim());
553
+ const hasThumb = !!(card.thumbnailSrc && String(card.thumbnailSrc).trim());
554
+ mediaOk = hasVideo && hasThumb;
555
+ } else {
556
+ return false;
557
+ }
558
+ if (!mediaOk) return false;
559
+ if (cardIndex === 0) {
560
+ const sugg = Array.isArray(card.suggestions) ? card.suggestions : [];
561
+ if (!sugg.some(isCompleteSavedCarouselSuggestion)) return false;
562
+ }
563
+ return true;
564
+ };
565
+
566
+ const checkDisableAddCarouselButton = () => {
567
+ const idx = parseInt(activeCarouselIndex, 10);
568
+ const activeCard = carouselData?.[idx];
569
+ return !isCarouselCardValid(activeCard, idx);
570
+ };
571
+
572
+ const addCarouselCard = () => {
573
+ let newIndex = 0;
574
+ flushSync(() => {
575
+ setCarouselData((prev = []) => {
576
+ const updated = cloneDeep(prev);
577
+ updated.push(cloneDeep(RCS_CAROUSEL_INITIAL_CARD));
578
+ newIndex = updated.length - 1;
579
+ return updated;
580
+ });
581
+ });
582
+ setCarouselErrors((prev = []) => ([...(Array.isArray(prev) ? prev : []), {}]));
583
+ setActiveCarouselIndex(`${newIndex}`);
584
+ };
585
+
586
+ // Initialize carousel data when switching to carousel type
587
+ useEffect(() => {
588
+ if (!isCarouselType) return;
589
+ if (!carouselData || carouselData.length === 0) {
590
+ setCarouselData([cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)]);
591
+ setCarouselErrors([{}]);
592
+ setActiveCarouselIndex('0');
593
+ }
594
+ }, [isCarouselType]);
595
+
596
+ // keep derived carousel key in sync
597
+ useEffect(() => {
598
+ if (!isCarouselType) return;
599
+ if (!selectedCarouselHeight || !selectedCarouselWidth) return;
600
+ // Required format: HEIGHT_WIDTH
601
+ setSelectedCarousel(`${selectedCarouselHeight}_${selectedCarouselWidth}`);
602
+ }, [isCarouselType, selectedCarouselHeight, selectedCarouselWidth]);
603
+
604
+ const renderCarouselDimensionSelection = () => {
605
+ if (!isCarouselType) return null;
606
+ return (
607
+ <CapRow className="rcs-carousel-dimension-section">
608
+ <CapRow gutter={16} className="rcs-carousel-dimension-row">
609
+ <CapColumn span={12}>
610
+ <CapHeading type="h4" className="rcs-carousel-dimension-label">Card height</CapHeading>
611
+ <CapSelect
612
+ id="rcs-carousel-height-select"
613
+ value={selectedCarouselHeight}
614
+ onChange={(val) => {
615
+ // Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
616
+ resetCarouselMediaForAllCards();
617
+ setSelectedCarouselHeight(val);
618
+ }}
619
+ options={CAROUSEL_HEIGHT_OPTIONS}
620
+ disabled={isEditFlow || !isFullMode}
621
+ />
622
+ </CapColumn>
623
+ <CapColumn span={12}>
624
+ <CapHeading type="h4" className="rcs-carousel-dimension-label">Card width</CapHeading>
625
+ <CapSelect
626
+ id="rcs-carousel-width-select"
627
+ value={selectedCarouselWidth}
628
+ onChange={(val) => {
629
+ // Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
630
+ resetCarouselMediaForAllCards();
631
+ setSelectedCarouselWidth(val);
632
+ }}
633
+ options={CAROUSEL_WIDTH_OPTIONS}
634
+ disabled={isEditFlow || !isFullMode}
635
+ />
636
+ </CapColumn>
637
+ </CapRow>
638
+ {!!selectedCarousel && (
639
+ <CapLabel type="label3" className="rcs-carousel-selected-dimension">
640
+ Selected: {selectedCarousel}
641
+ </CapLabel>
642
+ )}
643
+ </CapRow>
644
+ );
645
+ };
646
+
647
+ // Reuse rich-card buttons UI per carousel card
648
+ const renderButtonComponentForCarouselCard = (cardIndex) => {
649
+ const card = carouselData?.[cardIndex] || {};
650
+ const suggestionsForCard = card.suggestions || [];
651
+ return (
652
+ <>
653
+ <CapHeader
654
+ className="rcs-button-cta"
655
+ title={(
656
+ <CapRow type="flex">
657
+ <CapHeading type="h4">
658
+ {formatMessage(messages.btnLabel)}
659
+ </CapHeading>
660
+ </CapRow>
661
+ )}
662
+ description={(
663
+ <CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
664
+ )}
665
+ />
666
+ <CapActionButton
667
+ buttonType={RCS_BUTTON_TYPES.NONE}
668
+ updateButtonChange={(data, btnIndex) => {
669
+ // Match existing behavior: allow CapActionButton to manage save gating.
670
+ const updated = cloneDeep(suggestionsForCard);
671
+ if (btnIndex === MAX_BUTTONS) {
672
+ handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: data }]);
673
+ return;
674
+ }
675
+ updated[btnIndex] = data;
676
+ handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: updated }]);
677
+ }}
678
+ deleteButtonHandler={(btnIndex) => {
679
+ if (cardIndex === 0 && btnIndex === 0) {
680
+ return;
681
+ }
682
+ const savedCount = (suggestionsForCard || []).filter((x) => x && x.isSaved).length;
683
+ const target = (suggestionsForCard || []).find((s) => s && s.index === btnIndex);
684
+ if (cardIndex === 0 && target?.isSaved && savedCount <= 1) {
685
+ return;
686
+ }
687
+ const updated = cloneDeep(suggestionsForCard)
688
+ .filter((i) => i.index !== btnIndex)
689
+ .map((i, idx) => ({ ...i, index: idx }));
690
+ handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: updated }]);
691
+ }}
692
+ suggestions={suggestionsForCard}
693
+ isEditFlow={isEditFlow}
694
+ isFullMode={isFullMode}
695
+ maxButtons={MAX_BUTTONS}
696
+ host={hostName}
697
+ minSavedSuggestions={cardIndex === 0 ? 1 : 0}
698
+ hideDeleteSuggestionIndexes={cardIndex === 0 ? [0] : []}
699
+ />
700
+ </>
701
+ );
702
+ };
703
+
704
+ const renderCarouselCardMedia = (cardIndex) => {
705
+ const card = carouselData?.[cardIndex] || {};
706
+ const dimKey = getCarouselDimensionKey();
707
+
708
+ if (card.mediaType === RCS_MEDIA_TYPES.VIDEO) {
709
+ return (
710
+ <>
711
+ <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Video</CapHeading>
712
+ <CapVideoUpload
713
+ index={getCarouselVideoAssetIndex(cardIndex)}
714
+ allowedExtensionsRegex={ALLOWED_EXTENSIONS_VIDEO_REGEX}
715
+ videoSize={RCS_CAROUSEL_VIDEO_SIZE}
716
+ isFullMode={isFullMode}
717
+ uploadAsset={uploadRcsVideo}
718
+ uploadedAssetList={card.videoAsset || {}}
719
+ onVideoUploadUpdateAssestList={(_, val) => {
720
+ handleCarouselValueChange(cardIndex, [{ fieldName: 'videoAsset', value: val }]);
721
+ }}
722
+ videoData={rcsData}
723
+ className="cap-custom-video-upload"
724
+ formClassName={"rcs-video-upload"}
725
+ channel={RCS}
726
+ errorMessage={formatMessage(messages.videoErrorMessage)}
727
+ showVideoNameAndDuration={false}
728
+ showReUploadButton={!isEditFlow && isFullMode}
729
+ channelSpecificStyle={!isFullMode}
730
+ />
731
+
732
+ <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
733
+ <CapImageUpload
734
+ allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
735
+ imgWidth={RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS?.[dimKey]?.width}
736
+ imgHeight={RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS?.[dimKey]?.height}
737
+ imgSize={RCS_THUMBNAIL_MAX_SIZE}
738
+ uploadAsset={uploadRcsImage}
739
+ isFullMode={isFullMode}
740
+ imageSrc={card.thumbnailSrc}
741
+ updateImageSrc={(val) => handleCarouselValueChange(cardIndex, [{ fieldName: 'thumbnailSrc', value: val }])}
742
+ updateOnReUpload={() => handleCarouselValueChange(cardIndex, [{ fieldName: 'thumbnailSrc', value: '' }])}
743
+ minImgSize={RCS_THUMBNAIL_MIN_SIZE}
744
+ index={getCarouselThumbnailAssetIndex(cardIndex)}
745
+ className="cap-custom-image-upload"
746
+ key={`rcs-carousel-thumb-${cardIndex}-${dimKey}`}
747
+ imageData={rcsData}
748
+ channel={RCS}
749
+ channelSpecificStyle={!isFullMode}
750
+ skipDimensionValidation={true}
751
+ showReUploadButton={!isEditFlow && isFullMode}
752
+ disabled={isEditFlow || !isFullMode}
753
+ />
754
+ </>
755
+ );
756
+ }
757
+
758
+ // Default: IMAGE
759
+ return (
760
+ <>
761
+ <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Image</CapHeading>
762
+ <CapImageUpload
763
+ allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
764
+ imgWidth={RCS_CAROUSEL_IMAGE_DIMENSIONS?.[dimKey]?.width}
765
+ imgHeight={RCS_CAROUSEL_IMAGE_DIMENSIONS?.[dimKey]?.height}
766
+ imgSize={RCS_CAROUSEL_IMG_SIZE}
767
+ uploadAsset={uploadRcsImage}
768
+ isFullMode={isFullMode}
769
+ imageSrc={card.imageSrc}
770
+ updateImageSrc={(val) => handleCarouselValueChange(cardIndex, [{ fieldName: 'imageSrc', value: val }])}
771
+ updateOnReUpload={() => handleCarouselValueChange(cardIndex, [{ fieldName: 'imageSrc', value: '' }])}
772
+ index={getCarouselImageAssetIndex(cardIndex)}
773
+ className="cap-custom-image-upload"
774
+ key={`rcs-carousel-image-${cardIndex}-${dimKey}`}
775
+ imageData={rcsData}
776
+ channel={RCS}
777
+ channelSpecificStyle={!isFullMode}
778
+ skipDimensionValidation={true}
779
+ showReUploadButton={!isEditFlow && isFullMode}
780
+ disabled={isEditFlow || !isFullMode}
781
+ />
782
+ </>
783
+ );
784
+ };
785
+
786
+ const renderCarouselCardButtons = (cardIndex) => {
787
+ return renderButtonComponentForCarouselCard(cardIndex);
788
+ };
789
+
790
+ const getCarouselTabPanes = () => {
791
+ return (carouselData || []).map((card, index) => {
792
+ return {
793
+ key: index,
794
+ tab: index + 1,
795
+ content: (
796
+ <CapCard
797
+ title={`Card ${index + 1}`}
798
+ extra={
799
+ !isEditFlow &&
800
+ (carouselData.length === 1 ? (
801
+ <CapTooltip title={formatMessage(messages.rcsCarouselMinCardDeleteTooltip)}>
802
+ <span className="button-disabled-tooltip-wrapper rcs-carousel-delete-tooltip-wrap">
803
+ <CapButton
804
+ className="rcs-carousel-card-delete"
805
+ type="flat"
806
+ onClick={() => deleteCarouselCard(index)}
807
+ disabled
808
+ aria-label={formatMessage(messages.rcsCarouselMinCardDeleteTooltip)}
809
+ >
810
+ <CapIcon type="delete" />
811
+ </CapButton>
812
+ </span>
813
+ </CapTooltip>
814
+ ) : (
815
+ <CapButton
816
+ className="rcs-carousel-card-delete"
817
+ type="flat"
818
+ onClick={() => deleteCarouselCard(index)}
819
+ aria-label={formatMessage(globalMessages.delete)}
820
+ >
821
+ <CapIcon type="delete" />
822
+ </CapButton>
823
+ ))
824
+ }
825
+ className="rcs-carousel-card"
826
+ >
827
+ {/* Media selection should be at top of card */}
828
+ <CapRow className="rcs-carousel-media-selection">
829
+ <CapColumn className="rcs-carousel-media-selection-heading">
830
+ <CapHeading type="h4">{formatMessage(messages.mediaTypeLabel)}</CapHeading>
831
+ </CapColumn>
832
+ <CapColumn>
833
+ <CapRadioGroup
834
+ id={`rcs-carousel-media-radio-${index}`}
835
+ options={mediaRadioOptions}
836
+ value={card.mediaType}
837
+ onChange={({ target: { value } }) => {
838
+ // Reset media fields when switching type
839
+ if (value === RCS_MEDIA_TYPES.IMAGE) {
840
+ // Switching to IMAGE: clear video + thumbnail uploads so they don't auto-restore.
841
+ clearCarouselCardMedia(index, { clearImage: false, clearVideo: true, clearThumb: true });
842
+ handleCarouselValueChange(index, [
843
+ { fieldName: 'mediaType', value },
844
+ { fieldName: 'videoAsset', value: {} },
845
+ { fieldName: 'thumbnailSrc', value: '' },
846
+ ]);
847
+ } else {
848
+ // Switching to VIDEO: clear image upload so it doesn't auto-restore.
849
+ clearCarouselCardMedia(index, { clearImage: true, clearVideo: false, clearThumb: false });
850
+ handleCarouselValueChange(index, [
851
+ { fieldName: 'mediaType', value },
852
+ { fieldName: 'imageSrc', value: '' },
853
+ ]);
854
+ }
855
+ }}
856
+ disabled={isEditFlow || !isFullMode}
857
+ className="rcs-radio"
858
+ />
859
+ </CapColumn>
860
+ </CapRow>
861
+
862
+ <CapRow className="rcs-carousel-media-upload">
863
+ {renderCarouselCardMedia(index)}
864
+ </CapRow>
865
+
866
+ {/* Title after media */}
867
+ <CapRow className="rcs-carousel-card-row">
868
+ <CapHeader
869
+ className="rcs-template-title-label"
870
+ title={<CapHeading type="h4">Card title</CapHeading>}
871
+ suffix={
872
+ (isEditFlow || !isFullMode) ? (
873
+ <TagList
874
+ label={formatMessage(globalMessages.addLabels)}
875
+ onTagSelect={onCarouselTagSelect}
876
+ location={location}
877
+ tags={getRcsTags()}
878
+ onContextChange={handleOnTagsContextChange}
879
+ injectedTags={injectedTags || {}}
880
+ selectedOfferDetails={selectedOfferDetails}
881
+ />
882
+ ) : (
883
+ <CapButton
884
+ data-testid={`rcs-carousel-title-add-var-${index}`}
885
+ type="flat"
886
+ isAddBtn
887
+ onClick={() => appendVarToCarouselField(index, 'title')}
888
+ disabled={!!carouselErrors?.[index]?.title}
889
+ >
890
+ {formatMessage(messages.addVar)}
891
+ </CapButton>
892
+ )
893
+ }
894
+ />
895
+ <CapRow className="rcs_text_area_wrapper">
896
+ {(isEditFlow || !isFullMode) ? (
897
+ renderCarouselEditMessage(card.title || '')
898
+ ) : (
899
+ <CapInput
900
+ value={card.title || ''}
901
+ placeholder={formatMessage(messages.templateTitlePlaceholder)}
902
+ onChange={({ target: { value } }) => {
903
+ let error = false;
904
+ if (value?.length > TEMPLATE_TITLE_MAX_LENGTH) {
905
+ error = formatMessage(messages.templateHeaderLengthError);
906
+ } else {
907
+ error = variableErrorHandling(value);
908
+ }
909
+ updateCarouselErrors(index, { title: error });
910
+ handleCarouselValueChange(index, [{ fieldName: 'title', value }]);
911
+ }}
912
+ disabled={isEditFlow || !isFullMode}
913
+ errorMessage={carouselErrors?.[index]?.title}
914
+ />
915
+ )}
916
+ </CapRow>
917
+ </CapRow>
918
+ {!isEditFlow && (
919
+ <CapRow className="rcs-carousel-character-count-row">
920
+ {renderCarouselCharacterCount(
921
+ getCarouselTitleCharacterCount(index),
922
+ getTitleMaxLength(),
923
+ )}
924
+ </CapRow>
925
+ )}
926
+
927
+ {/* Description after title */}
928
+ <CapRow className="rcs-carousel-card-row">
929
+ <CapHeader
930
+ title={<CapHeading type="h4">Card body text</CapHeading>}
931
+ suffix={
932
+ (isEditFlow || !isFullMode) ? (
933
+ <TagList
934
+ label={formatMessage(globalMessages.addLabels)}
935
+ onTagSelect={onCarouselTagSelect}
936
+ location={location}
937
+ tags={getRcsTags()}
938
+ onContextChange={handleOnTagsContextChange}
939
+ injectedTags={injectedTags || {}}
940
+ selectedOfferDetails={selectedOfferDetails}
941
+ />
942
+ ) : (
943
+ <CapButton
944
+ data-testid={`rcs-carousel-desc-add-var-${index}`}
945
+ type="flat"
946
+ isAddBtn
947
+ onClick={() => appendVarToCarouselField(index, 'description')}
948
+ disabled={!!carouselErrors?.[index]?.description}
949
+ >
950
+ {formatMessage(messages.addVar)}
951
+ </CapButton>
952
+ )
953
+ }
954
+ />
955
+ <CapRow className="rcs_text_area_wrapper">
956
+ {(isEditFlow || !isFullMode) ? (
957
+ renderCarouselEditMessage(card.description || '')
958
+ ) : (
959
+ <TextArea
960
+ autosize={{ minRows: 3, maxRows: 5 }}
961
+ value={card.description || ''}
962
+ placeholder={formatMessage(messages.templateDescPlaceholder)}
963
+ onChange={({ target: { value } }) => {
964
+ let error = false;
965
+ if (value?.length > RCS_RICH_CARD_MAX_LENGTH) {
966
+ error = formatMessage(messages.templateMessageLengthError);
967
+ } else {
968
+ error = variableErrorHandling(value);
969
+ }
970
+ updateCarouselErrors(index, { description: error });
971
+ handleCarouselValueChange(index, [{ fieldName: 'description', value }]);
972
+ }}
973
+ disabled={isEditFlow || !isFullMode}
974
+ errorMessage={
975
+ carouselErrors?.[index]?.description && (
976
+ <CapError className="rcs-template-message-error">
977
+ {carouselErrors[index].description}
978
+ </CapError>
979
+ )
980
+ }
981
+ />
982
+ )}
983
+ </CapRow>
984
+ </CapRow>
985
+ {!isEditFlow && (
986
+ <CapRow className="rcs-carousel-character-count-row">
987
+ {renderCarouselCharacterCount(
988
+ getCarouselDescriptionCharacterCount(index),
989
+ getDescriptionMaxLength(),
990
+ )}
991
+ </CapRow>
992
+ )}
993
+
994
+ <CapDivider className="rcs-carousel-card-divider" />
995
+ {renderCarouselCardButtons(index)}
996
+ </CapCard>
997
+ ),
998
+ };
999
+ });
1000
+ };
1001
+
1002
+ const renderCarouselSection = () => {
1003
+ if (!isCarouselType) return null;
1004
+
1005
+ const operations = (
1006
+ <>
1007
+ <CapDivider type="vertical" />
1008
+ <CapButton
1009
+ onClick={addCarouselCard}
1010
+ type="flat"
1011
+ className="add-carousel-content-button"
1012
+ disabled={
1013
+ isEditFlow ||
1014
+ !isFullMode ||
1015
+ MAX_RCS_CAROUSEL_ALLOWED === (carouselData?.length || 0) ||
1016
+ checkDisableAddCarouselButton()
1017
+ }
1018
+ >
1019
+ <CapIcon type="plus" />
1020
+ </CapButton>
1021
+ </>
1022
+ );
1023
+
1024
+ return (
1025
+ <CapRow className="rcs-carousel-section">
1026
+ {renderCarouselDimensionSelection()}
1027
+ <CapRow className="rcs-carousel-tab">
1028
+ <CapTab
1029
+ key={`rcs-carousel-tab-${carouselResetNonce}`}
1030
+ defaultActiveKey="0"
1031
+ activeKey={activeCarouselIndex}
1032
+ tabBarExtraContent={operations}
1033
+ onChange={(key) => {
1034
+ setActiveCarouselIndex(`${key}`);
1035
+ // reset focused var when switching cards (as per requirement)
1036
+ setCarouselFocusedVarId('');
1037
+ }}
1038
+ panes={getCarouselTabPanes()}
1039
+ />
1040
+ </CapRow>
1041
+ </CapRow>
1042
+ );
1043
+ };
291
1044
 
292
1045
  const mediaRadioOptions = [
293
1046
  {
@@ -296,11 +1049,16 @@ export const Rcs = (props) => {
296
1049
  },
297
1050
  {
298
1051
  value: RCS_MEDIA_TYPES.VIDEO,
299
- label: formatMessage(messages.mediaVideo),
1052
+ label: formatMessage(
1053
+ templateType === contentType.carousel
1054
+ ? messages.carouselMediaVideoOption
1055
+ : messages.mediaVideo,
1056
+ ),
300
1057
  },
301
1058
  ];
302
1059
  const aiContentBotDisabled = isAiContentBotDisabled();
303
1060
 
1061
+
304
1062
  const updateButtonChange = (data, index) => {
305
1063
  if (data && data.text) {
306
1064
  const forbiddenError = forbiddenCharactersValidation(data.text);
@@ -366,10 +1124,16 @@ export const Rcs = (props) => {
366
1124
  tagModule: getDefaultTags,
367
1125
  isFullMode,
368
1126
  }) || {};
369
- const errorMsg =
370
- (validationResponse?.isBraceError &&
371
- formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
372
- false;
1127
+ const unsupportedTagsLengthCheck =
1128
+ validationResponse?.unsupportedTags?.length > 0;
1129
+ const errorMsg =
1130
+ (unsupportedTagsLengthCheck &&
1131
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
1132
+ unsupportedTags: validationResponse.unsupportedTags,
1133
+ })) ||
1134
+ (validationResponse.isBraceError &&
1135
+ formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
1136
+ false;
373
1137
  if (type === TITLE_TEXT) setTemplateTitleError(errorMsg);
374
1138
  if (type === MESSAGE_TEXT) setTemplateDescError(errorMsg);
375
1139
  };
@@ -382,10 +1146,60 @@ export const Rcs = (props) => {
382
1146
  validateResolvedTagsForType(MESSAGE_TEXT);
383
1147
  }, [cardVarMapped, templateDesc, tags, injectedTags, loadingTags]);
384
1148
 
1149
+ useEffect(() => {
1150
+ if (isFullMode || !isCarouselType) return;
1151
+ if (loadingTags || !tags || tags.length === 0) return;
1152
+ (carouselData || []).forEach((card, idx) => {
1153
+ ['title', 'description'].forEach((field) => {
1154
+ const templateStr = card?.[field] || '';
1155
+ if (!templateStr) return;
1156
+ const resolved = resolveTemplateWithMap(templateStr);
1157
+ if (!resolved) {
1158
+ updateCarouselErrors(idx, { [field]: false });
1159
+ return;
1160
+ }
1161
+ let contentForValidation = resolved;
1162
+ const placeholderTokens = templateStr.match(rcsVarRegex) || [];
1163
+ placeholderTokens.forEach((t) => {
1164
+ const escaped = t.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
1165
+ contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
1166
+ });
1167
+ if (!contentForValidation.trim()) {
1168
+ updateCarouselErrors(idx, { [field]: false });
1169
+ return;
1170
+ }
1171
+ const validationResponse = validateTags({
1172
+ content: contentForValidation,
1173
+ tagsParam: tags,
1174
+ injectedTagsParams: injectedTags,
1175
+ location,
1176
+ tagModule: getDefaultTags,
1177
+ eventContextTags,
1178
+ isFullMode,
1179
+ }) || {};
1180
+ const errorMsg =
1181
+ (validationResponse?.unsupportedTags?.length > 0 &&
1182
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
1183
+ unsupportedTags: validationResponse.unsupportedTags,
1184
+ })) ||
1185
+ (validationResponse.isBraceError &&
1186
+ formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
1187
+ false;
1188
+ updateCarouselErrors(idx, { [field]: errorMsg });
1189
+ });
1190
+ });
1191
+ }, [cardVarMapped, carouselData, tags, injectedTags, loadingTags, isCarouselType]);
1192
+
385
1193
  const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
386
1194
 
387
1195
  const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
388
1196
 
1197
+ /** Same `{{tag}}` in both title and description must not share one semantic map entry. */
1198
+ const rcsSpanningSemanticVarNames = useMemo(
1199
+ () => getRcsSemanticVarNamesSpanningTitleAndDesc(templateTitle, templateDesc, rcsVarRegex),
1200
+ [templateTitle, templateDesc],
1201
+ );
1202
+
389
1203
  /** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
390
1204
  const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
391
1205
  const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
@@ -404,6 +1218,10 @@ export const Rcs = (props) => {
404
1218
  return null;
405
1219
  };
406
1220
 
1221
+ /**
1222
+ * Master-branch resolve: uses numeric slot keys + semantic spanning detection for correct
1223
+ * multi-field variable resolution.
1224
+ */
407
1225
  const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
408
1226
  if (!str) return '';
409
1227
  const arr = splitTemplateVarStringRcs(str);
@@ -418,6 +1236,7 @@ export const Rcs = (props) => {
418
1236
  key,
419
1237
  globalSlot,
420
1238
  isEditLike,
1239
+ rcsSpanningSemanticVarNames.has(key),
421
1240
  );
422
1241
  if (isNil(v) || String(v)?.trim?.() === '') return elem;
423
1242
  return String(v);
@@ -426,12 +1245,45 @@ export const Rcs = (props) => {
426
1245
  }).join('');
427
1246
  };
428
1247
 
1248
+ const buildCarouselCardsForPreview = (cards = []) => (cards || []).map((card = {}) => {
1249
+ const videoThumb = card.thumbnailSrc || card.videoAsset?.videoThumbnail || '';
1250
+ const videoSrc = card.videoAsset?.videoSrc || '';
1251
+ const resolvedTitle = !isFullMode ? resolveTemplateWithMap(card.title || '') : (card.title || '');
1252
+ const resolvedDesc = !isFullMode ? resolveTemplateWithMap(card.description || '') : (card.description || '');
1253
+ return {
1254
+ mediaType: (card.mediaType || '').toLowerCase(),
1255
+ imageSrc: card.imageSrc || '',
1256
+ videoSrc,
1257
+ videoPreviewImg: videoThumb,
1258
+ title: resolvedTitle,
1259
+ bodyText: resolvedDesc,
1260
+ suggestions: card.suggestions || [],
1261
+ };
1262
+ });
1263
+
429
1264
  /**
430
1265
  * Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
431
1266
  * (VarSegmentMessageEditor: isEditFlow || !isFullMode). Full-mode create without edit uses plain
432
1267
  * TextArea only — no mapping. Full-mode + edit uses slots; !isFullMode alone is not enough.
433
1268
  */
434
1269
  const getTemplateContent = useCallback(() => {
1270
+ if (templateType === contentType.carousel) {
1271
+ const carouselDimKey = getCarouselDimensionKey();
1272
+ const carouselImgDims =
1273
+ RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
1274
+ const carouselVidDims =
1275
+ RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
1276
+ || RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
1277
+ return {
1278
+ carouselData: buildCarouselCardsForPreview(carouselData),
1279
+ carouselPreviewDimensions: {
1280
+ imageWidth: carouselImgDims.width,
1281
+ imageHeight: carouselImgDims.height,
1282
+ videoThumbWidth: carouselVidDims.width,
1283
+ videoThumbHeight: carouselVidDims.height,
1284
+ },
1285
+ };
1286
+ }
435
1287
  const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
436
1288
  const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
437
1289
  const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
@@ -484,21 +1336,42 @@ export const Rcs = (props) => {
484
1336
  isFullMode,
485
1337
  isEditFlow,
486
1338
  cardVarMapped,
1339
+ rcsSpanningSemanticVarNames,
1340
+ carouselData,
1341
+ selectedCarouselHeight,
1342
+ selectedCarouselWidth,
487
1343
  ]);
488
1344
 
489
1345
  const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
490
1346
 
491
1347
  const paramObj = params || {};
492
1348
  useEffect(() => {
493
- const { id } = paramObj;
494
- if (id && isFullMode) {
495
- setSpin(true);
496
- actions.getTemplateDetails(id, setSpin);
497
- }
498
- return () => {
499
- actions.clearEditResponse();
500
- };
501
- }, [paramObj.id, isFullMode]);
1349
+ const { id } = paramObj;
1350
+ if (id && isFullMode) {
1351
+ setSpin(true);
1352
+ actions.getTemplateDetails(id, setSpin);
1353
+ } else if (!id && isFullMode) {
1354
+ // Create New: clear standalone media and ALL possible carousel card Redux slots.
1355
+ // Redux persists across mounts so we must clear unconditionally (carouselData may be [] on fresh mount).
1356
+ updateRcsImageSrc('');
1357
+ setRcsVideoSrc({});
1358
+ setRcsThumbnailSrc('');
1359
+ setAssetList({});
1360
+ setEditFlow(false);
1361
+ actions.clearRcsMediaAsset(0);
1362
+ actions.clearRcsMediaAsset(1);
1363
+ for (let cardIdx = 0; cardIdx < MAX_RCS_CAROUSEL_ALLOWED; cardIdx += 1) {
1364
+ actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIdx));
1365
+ actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIdx));
1366
+ actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIdx));
1367
+ }
1368
+ setCarouselData([]);
1369
+ setCarouselErrors([]);
1370
+ }
1371
+ return () => {
1372
+ actions.clearEditResponse();
1373
+ };
1374
+ }, [paramObj.id]);
502
1375
 
503
1376
  useEffect(() => {
504
1377
  if (!(isEditFlow || !isFullMode)) return;
@@ -514,7 +1387,7 @@ export const Rcs = (props) => {
514
1387
  const nextVarMap = {};
515
1388
  let varOrdinal = 0;
516
1389
  arr.forEach((elem, idx) => {
517
- // RCS placeholders are alphanumeric/underscore (e.g. {{user_name}}), not numeric-only
1390
+ // Mustache tokens: {{1}}, {{user_name}}, {{tag.FORMAT_1}}, etc.
518
1391
  if (rcsVarTestRegex.test(elem)) {
519
1392
  const id = `${elem}_${idx}`;
520
1393
  const varName = getVarNameFromToken(elem);
@@ -525,6 +1398,7 @@ export const Rcs = (props) => {
525
1398
  varName,
526
1399
  globalSlot,
527
1400
  isEditLike,
1401
+ rcsSpanningSemanticVarNames.has(varName),
528
1402
  );
529
1403
  nextVarMap[id] = mappedValue;
530
1404
  }
@@ -534,14 +1408,13 @@ export const Rcs = (props) => {
534
1408
 
535
1409
  initField(templateTitle, setTitleVarMappedData, 0);
536
1410
  initField(templateDesc, setDescVarMappedData, titleTokenCount);
537
- }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode]);
1411
+ }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames]);
538
1412
 
539
1413
  useEffect(() => {
540
1414
  if (!isEditFlow && isFullMode) {
541
1415
  setRcsVideoSrc({});
542
1416
  updateRcsImageSrc('');
543
- setUpdateRcsImageSrc('');
544
- updateRcsThumbnailSrc('');
1417
+ setRcsThumbnailSrc('');
545
1418
  setAssetList({});
546
1419
  }
547
1420
  }, [templateMediaType]);
@@ -584,6 +1457,7 @@ export const Rcs = (props) => {
584
1457
  if (mediaType) {
585
1458
  setTemplateMediaType(mediaType);
586
1459
  }
1460
+ const tempOrientation = cardSettings.cardOrientation;
587
1461
  const tempAlignment = cardSettings.mediaAlignment;
588
1462
  const tempHeight = mediaData.height;
589
1463
 
@@ -633,6 +1507,12 @@ export const Rcs = (props) => {
633
1507
  details,
634
1508
  'versions.base.content.RCS.rcsContent.cardContent[0]',
635
1509
  );
1510
+ const rcsContent = get(details, 'versions.base.content.RCS.rcsContent', {});
1511
+ const cardType = (rcsContent?.cardType || '').toString().toLowerCase();
1512
+
1513
+ setEditFlow(true);
1514
+ setTemplateName(details?.name || details?.creativeName || '');
1515
+
636
1516
  const cardFromTop = get(details, 'rcsContent.cardContent[0]');
637
1517
  const card0 = { ...(cardFromTop || {}), ...(cardFromVersions || {}) };
638
1518
  const cardVarMappedFromCardContent =
@@ -702,16 +1582,83 @@ export const Rcs = (props) => {
702
1582
  }
703
1583
  return hydratedCardVarMappedResult;
704
1584
  });
1585
+
1586
+ if (cardType === contentType.carousel) {
1587
+
1588
+ setTemplateType(contentType.carousel);
1589
+ setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
1590
+ const cardSettings = rcsContent?.cardSettings || {};
1591
+ const cardWidth = cardSettings?.cardWidth || SMALL;
1592
+ setSelectedCarouselWidth(cardWidth);
1593
+
1594
+ const cards = Array.isArray(rcsContent?.cardContent) ? rcsContent.cardContent : [];
1595
+ const firstHeight = cards?.[0]?.media?.height || MEDIUM;
1596
+ setSelectedCarouselHeight(firstHeight);
1597
+ setSelectedCarousel(`${firstHeight}_${cardWidth}`);
1598
+ setActiveCarouselIndex('0');
1599
+
1600
+ const hydratedCards = cards.map((c = {}, idx) => {
1601
+ const mediaType = c.mediaType;
1602
+ const media = c.media || {};
1603
+ const mediaUrl = media.mediaUrl || '';
1604
+ const thumbUrl = media.thumbnailUrl || '';
1605
+ const rawSuggestions = Array.isArray(c.suggestions) ? c.suggestions : [];
1606
+ const suggestions = idx === 0 && rawSuggestions.length === 0
1607
+ ? cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS)
1608
+ : rawSuggestions;
1609
+ return {
1610
+ title: c.title || '',
1611
+ description: c.description || '',
1612
+ mediaType,
1613
+ imageSrc: mediaType === RCS_MEDIA_TYPES.IMAGE ? mediaUrl : '',
1614
+ videoAsset: mediaType === RCS_MEDIA_TYPES.VIDEO ? {
1615
+ videoSrc: mediaUrl,
1616
+ previewUrl: thumbUrl,
1617
+ videoThumbnail: thumbUrl,
1618
+ videoName: c?.media?.videoName || '',
1619
+ } : {},
1620
+ thumbnailSrc: mediaType === RCS_MEDIA_TYPES.VIDEO ? thumbUrl : '',
1621
+ suggestions,
1622
+ };
1623
+ });
1624
+ setCarouselData(
1625
+ hydratedCards.length > 0
1626
+ ? ensureFirstCardDefaultPhoneSuggestions(hydratedCards)
1627
+ : [cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)],
1628
+ );
1629
+ setCarouselErrors(new Array(hydratedCards.length > 0 ? hydratedCards.length : 1).fill({}));
1630
+
1631
+ // Status bar uses first card's Status, keep existing behavior.
1632
+ if (isHostInfoBip) {
1633
+ setTemplateStatus('');
1634
+ } else {
1635
+ const firstCard = cards?.[0] || {};
1636
+ const cardForCarouselStatus = {
1637
+ ...firstCard,
1638
+ Status:
1639
+ firstCard.Status
1640
+ ?? firstCard.status
1641
+ ?? firstCard.approvalStatus
1642
+ ?? get(details, 'templateStatus')
1643
+ ?? get(details, 'approvalStatus')
1644
+ ?? get(details, 'creativeStatus')
1645
+ ?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
1646
+ ?? '',
1647
+ };
1648
+ templateStatusHelper(cardForCarouselStatus);
1649
+ }
1650
+ return;
1651
+ }
1652
+
705
1653
  const mediaType =
706
1654
  card0.mediaType
707
1655
  || get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
708
- if (mediaType === RCS_MEDIA_TYPES.NONE) {
1656
+ if (cardType !== contentType.carousel && mediaType === RCS_MEDIA_TYPES.NONE) {
709
1657
  setTemplateType(contentType.text_message);
710
- } else {
1658
+ } else if (cardType !== contentType.carousel && mediaType !== RCS_MEDIA_TYPES.NONE) {
711
1659
  setTemplateType(contentType.rich_card);
712
1660
  }
713
- setEditFlow(true);
714
- setTemplateName(details?.name || details?.creativeName || '');
1661
+
715
1662
  const loadedTitle = loadedTitleForMap;
716
1663
  const loadedDesc = loadedDescForMap;
717
1664
  const { normalizedTitle, normalizedDesc } = normalizeLibraryLoadedTitleDesc({
@@ -740,7 +1687,11 @@ export const Rcs = (props) => {
740
1687
  ?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
741
1688
  ?? '',
742
1689
  };
743
- templateStatusHelper(cardForStatus);
1690
+ if (isHostInfoBip) {
1691
+ setTemplateStatus('');
1692
+ } else {
1693
+ templateStatusHelper(cardForStatus);
1694
+ }
744
1695
  const mediaData =
745
1696
  card0.media != null && card0.media !== ''
746
1697
  ? card0.media
@@ -780,12 +1731,17 @@ export const Rcs = (props) => {
780
1731
  typeof smsFallbackContent.unicodeValidity === 'boolean'
781
1732
  ? smsFallbackContent.unicodeValidity
782
1733
  : (typeof base['unicode-validity'] === 'boolean' ? base['unicode-validity'] : true);
1734
+ const registeredSenderIdsFromApi =
1735
+ extractRegisteredSenderIdsFromSmsFallbackRecord(smsFallbackContent);
783
1736
  const nextSmsState = {
784
1737
  templateName: smsFallbackContent.smsTemplateName || '',
785
1738
  content: fallbackMessage,
786
1739
  templateContent: fallbackMessage,
787
1740
  unicodeValidity: unicodeFromApi,
788
1741
  ...(hasVarMapped && { rcsSmsFallbackVarMapped: varMappedFromPayload }),
1742
+ ...(Array.isArray(registeredSenderIdsFromApi) && registeredSenderIdsFromApi.length > 0
1743
+ ? { registeredSenderIds: registeredSenderIdsFromApi }
1744
+ : {}),
789
1745
  };
790
1746
  const hydrationKey = JSON.stringify({
791
1747
  creativeKey: details._id || details.name || details.creativeName || '',
@@ -793,6 +1749,10 @@ export const Rcs = (props) => {
793
1749
  content: nextSmsState.content,
794
1750
  unicodeValidity: nextSmsState.unicodeValidity,
795
1751
  varMapped: nextSmsState.rcsSmsFallbackVarMapped || {},
1752
+ senderIds:
1753
+ Array.isArray(registeredSenderIdsFromApi)
1754
+ ? registeredSenderIdsFromApi.join('\u001f')
1755
+ : '',
796
1756
  });
797
1757
  if (
798
1758
  isFullMode
@@ -806,7 +1766,7 @@ export const Rcs = (props) => {
806
1766
  setSmsFallbackData(null);
807
1767
  }
808
1768
  }
809
- }, [rcsHydrationDetails, isFullMode]);
1769
+ }, [rcsHydrationDetails, isFullMode, isHostInfoBip]);
810
1770
 
811
1771
  useEffect(() => {
812
1772
  if (templateType === contentType.text_message) {
@@ -884,6 +1844,7 @@ export const Rcs = (props) => {
884
1844
  query.context = getDefaultTags;
885
1845
  }
886
1846
  fetchTagSchemaIfNewQuery(query);
1847
+ globalActions.fetchSchemaForEntity(query);
887
1848
  };
888
1849
 
889
1850
  const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
@@ -892,58 +1853,87 @@ export const Rcs = (props) => {
892
1853
  return templateStr.replace(re, `{{${tagName}}}`);
893
1854
  };
894
1855
 
895
- const onTagSelect = (data, areaId, field) => {
896
- if (!areaId) return;
897
- const sep = areaId.lastIndexOf('_');
898
- if (sep === -1) return;
899
- const slotSuffix = areaId.slice(sep + 1);
900
- if (slotSuffix === '' || isNaN(Number(slotSuffix))) return;
901
- const token = areaId.slice(0, sep);
902
- const variableName = getVarNameFromToken(token);
903
- if (!variableName) return;
904
- const isNumericSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(variableName));
905
- const fieldStr = field === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
906
- const fieldType = field === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
907
- const globalSlotForArea = getGlobalSlotIndexForRcsFieldId(areaId, fieldStr, fieldType);
908
-
909
- setCardVarMapped((prev) => {
910
- const base = (prev?.[variableName] ?? '').toString();
911
- const nextVal = `${base}{{${data}}}`;
912
- const next = { ...(prev || {}) };
913
- if (isNumericSlot) {
914
- delete next[variableName];
915
- next[data] = nextVal;
1856
+ const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
1857
+ if (!varSegmentCompositeDomId) return;
1858
+ const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
1859
+ if (underscoreIndexInCompositeId === -1) return;
1860
+ const segmentIndexSuffix = varSegmentCompositeDomId.slice(underscoreIndexInCompositeId + 1);
1861
+ if (segmentIndexSuffix === '' || isNaN(Number(segmentIndexSuffix))) return;
1862
+ const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
1863
+ const semanticOrNumericVarName = getVarNameFromToken(mustacheTokenFromCompositeId);
1864
+ if (!semanticOrNumericVarName) return;
1865
+ const isNumericPlaceholderSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(semanticOrNumericVarName));
1866
+ const templateStringForField =
1867
+ tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
1868
+ const titleOrMessageFieldType =
1869
+ tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
1870
+ const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
1871
+ varSegmentCompositeDomId,
1872
+ templateStringForField,
1873
+ titleOrMessageFieldType,
1874
+ );
1875
+ const cardVarMappedNumericSlotKey =
1876
+ globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined
1877
+ ? String(globalVarSlotIndexZeroBased + 1)
1878
+ : null;
1879
+
1880
+ setCardVarMapped((previousCardVarMapped) => {
1881
+ const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
1882
+ if (isNumericPlaceholderSlot) {
1883
+ const existingValueBeforeAppend = (
1884
+ previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
1885
+ ).toString();
1886
+ const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
1887
+ delete updatedCardVarMapped[semanticOrNumericVarName];
1888
+ updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
916
1889
  } else {
917
- // Use the global slot index key only writing by semantic name (variableName)
918
- // would contaminate any other field that shares the same var name (e.g. {{gt}}
919
- // in both title and description), causing the label value to bleed across fields.
920
- if (globalSlotForArea !== null && globalSlotForArea !== undefined) {
921
- next[String(globalSlotForArea + 1)] = nextVal;
922
- // Same reasoning as handleRcsVarChange: delete the semantic key so
923
- // resolveCardVarMappedSlotValue uses only the numeric slot and the
924
- // value cannot bleed across fields that share the same var name.
925
- delete next[variableName];
1890
+ // Same semantic token (e.g. {{adv}}) in title and body must not share one map key for
1891
+ // "existing value" that appends the new tag onto the other field. Match handleRcsVarChange:
1892
+ // read/write the global numeric slot only and drop the shared semantic key.
1893
+ const existingValueBeforeAppend = cardVarMappedNumericSlotKey
1894
+ ? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
1895
+ : String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
1896
+ const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
1897
+ delete updatedCardVarMapped[semanticOrNumericVarName];
1898
+ if (cardVarMappedNumericSlotKey) {
1899
+ updatedCardVarMapped[cardVarMappedNumericSlotKey] = mappedValueAfterAppendingTag;
926
1900
  } else {
927
- next[variableName] = nextVal;
1901
+ updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
928
1902
  }
929
1903
  }
930
- return next;
1904
+ return updatedCardVarMapped;
931
1905
  });
932
1906
 
933
- if (isNumericSlot && (field === RCS_TAG_AREA_FIELD_TITLE || field === RCS_TAG_AREA_FIELD_DESC)) {
934
- if (field === RCS_TAG_AREA_FIELD_TITLE) {
935
- setTemplateTitle((prev) => {
936
- const nextStr = replaceNumericPlaceholderWithTagInTemplate(prev || '', variableName, data);
937
- if (nextStr === prev) return prev;
938
- setTemplateTitleError(variableErrorHandling(nextStr));
939
- return nextStr;
1907
+ if (
1908
+ isNumericPlaceholderSlot
1909
+ && (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC)
1910
+ ) {
1911
+ if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
1912
+ setTemplateTitle((previousTitle) => {
1913
+ const titleAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
1914
+ previousTitle || '',
1915
+ semanticOrNumericVarName,
1916
+ selectedTagNameFromPicker,
1917
+ );
1918
+ if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
1919
+ setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
1920
+ // Remount segment editor: tag insert replaces {{n}} with e.g. {{tag.FORMAT_1}} — slot ids change; avoids stale UI vs manual typing in full-mode TextArea
1921
+ setRcsVarSegmentEditorRemountKey((k) => k + 1);
1922
+ return titleAfterReplacingNumericPlaceholder;
940
1923
  });
941
1924
  } else {
942
- setTemplateDesc((prev) => {
943
- const nextStr = replaceNumericPlaceholderWithTagInTemplate(prev || '', variableName, data);
944
- if (nextStr === prev) return prev;
945
- setTemplateDescError(variableErrorHandling(nextStr));
946
- return nextStr;
1925
+ setTemplateDesc((previousDescription) => {
1926
+ const descriptionAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
1927
+ previousDescription || '',
1928
+ semanticOrNumericVarName,
1929
+ selectedTagNameFromPicker,
1930
+ );
1931
+ if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
1932
+ return previousDescription;
1933
+ }
1934
+ setTemplateDescError(variableErrorHandling(descriptionAfterReplacingNumericPlaceholder));
1935
+ setRcsVarSegmentEditorRemountKey((k) => k + 1);
1936
+ return descriptionAfterReplacingNumericPlaceholder;
947
1937
  });
948
1938
  }
949
1939
  }
@@ -953,6 +1943,31 @@ export const Rcs = (props) => {
953
1943
 
954
1944
  const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
955
1945
 
1946
+ const onCarouselTagSelect = (data) => {
1947
+ if (!carouselFocusedVarId) return;
1948
+ const sep = carouselFocusedVarId.lastIndexOf('_');
1949
+ if (sep === -1) return;
1950
+ const token = carouselFocusedVarId.slice(0, sep);
1951
+ const variableName = getVarNameFromToken(token);
1952
+ if (!variableName) return;
1953
+ setCardVarMapped((prev) => {
1954
+ const base = (prev?.[variableName] ?? '').toString();
1955
+ const nextVal = `${base}{{${data}}}`;
1956
+ return {
1957
+ ...(prev || {}),
1958
+ [variableName]: nextVal,
1959
+ };
1960
+ });
1961
+ };
1962
+
1963
+ const onTagSelectFallback = (data) => {
1964
+ const tempMsg = `${fallbackMessage}{{${data}}}`;
1965
+ const error = fallbackMessageErrorHandler(tempMsg);
1966
+ setFallbackMessage(tempMsg);
1967
+ setFallbackMessageError(error);
1968
+ };
1969
+
1970
+
956
1971
  //removing optout tag for rcs
957
1972
  const getRcsTags = () => {
958
1973
  const tempTags = cloneDeep(tags);
@@ -988,15 +2003,14 @@ export const Rcs = (props) => {
988
2003
  value: contentType.rich_card,
989
2004
  label: formatMessage(messages.richCard),
990
2005
  },
991
- {
992
- value: contentType.carousel,
993
- label: (
994
- <CapTooltip title={formatMessage(messages.disabledCarouselTooltip)}>
995
- {formatMessage(messages.carousel)}
996
- </CapTooltip>
997
- ),
998
- disabled: true,
999
- },
2006
+ ...(!isHostInfoBip
2007
+ ? [
2008
+ {
2009
+ value: contentType.carousel,
2010
+ label: formatMessage(messages.carousel),
2011
+ },
2012
+ ]
2013
+ : []),
1000
2014
  ];
1001
2015
 
1002
2016
  const onTemplateNameChange = ({ target: { value } }) => {
@@ -1007,6 +2021,10 @@ export const Rcs = (props) => {
1007
2021
 
1008
2022
  const onTemplateTypeChange = ({ target: { value } }) => {
1009
2023
  setTemplateType(value);
2024
+ // Carousel has per-card media; keep template-level media type neutral.
2025
+ if (value === contentType.carousel) {
2026
+ setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
2027
+ }
1010
2028
  };
1011
2029
 
1012
2030
 
@@ -1027,8 +2045,39 @@ export const Rcs = (props) => {
1027
2045
  const onTemplateMediaTypeChange = ({ target: { value } }) => {
1028
2046
  setTemplateMediaType(value);
1029
2047
  };
1030
-
1031
-
2048
+ const renderedRCSEditMessage = (descArray, type) => {
2049
+ const renderArray = [];
2050
+ if (descArray?.length) {
2051
+ descArray.forEach((elem, index) => {
2052
+ if (rcsVarTestRegex.test(elem)) {
2053
+ // Variable input
2054
+ renderArray.push(
2055
+ <TextArea
2056
+ id={`${elem}_${index}`}
2057
+ key={`${elem}_${index}`}
2058
+ placeholder={`enter the value for ${elem}`}
2059
+ autosize={{ minRows: 1, maxRows: 3 }}
2060
+ onChange={e => textAreaValueChange(e, type)}
2061
+ value={textAreaValue(index, type)}
2062
+ onFocus={(e) => setTextAreaId(e, type)}
2063
+ />
2064
+ );
2065
+ } else if (elem) {
2066
+ // Static text
2067
+ renderArray.push(
2068
+ <TextArea
2069
+ key={`static_${index}`}
2070
+ value={elem}
2071
+ autosize={{ minRows: 1, maxRows: 3 }}
2072
+ disabled
2073
+ className="rcs-edit-template-message-static-textarea"
2074
+ />
2075
+ );
2076
+ }
2077
+ });
2078
+ }
2079
+ return renderArray;
2080
+ };
1032
2081
  const onTemplateTitleChange = ({ target: { value } }) => {
1033
2082
  let errorMessage = false;
1034
2083
  if (templateType === contentType.rich_card && !value.trim()) {
@@ -1044,7 +2093,7 @@ export const Rcs = (props) => {
1044
2093
 
1045
2094
  const onTemplateDescChange = ({ target: { value } }) => {
1046
2095
  let errorMessage = false;
1047
- if(templateType === contentType.text_message && value?.length > RCS_TEXT_MESSAGE_MAX_LENGTH){
2096
+ if(templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)){
1048
2097
  errorMessage = formatMessage(messages.templateMessageLengthError);
1049
2098
  } else if(templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH){
1050
2099
  errorMessage = formatMessage(messages.templateMessageLengthError);
@@ -1057,6 +2106,62 @@ export const Rcs = (props) => {
1057
2106
  setTemplateDescError(error);
1058
2107
  };
1059
2108
 
2109
+
2110
+ const templateDescErrorHandler = (value) => {
2111
+ let errorMessage = false;
2112
+ const { unsupportedTags, isBraceError } = validateTags({
2113
+ content: value,
2114
+ tagsParam: tags,
2115
+ injectedTagsParams: injectedTags,
2116
+ location,
2117
+ tagModule: getDefaultTags,
2118
+ }) || {};
2119
+
2120
+ const maxLength = templateType === contentType.text_message
2121
+ ? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)
2122
+ : RCS_RICH_CARD_MAX_LENGTH;
2123
+
2124
+ if (value === '' && isMediaTypeText) {
2125
+ errorMessage = formatMessage(messages.emptyTemplateDescErrorMessage);
2126
+ } else if (value?.length > maxLength) {
2127
+ errorMessage = formatMessage(messages.templateMessageLengthError);
2128
+ }
2129
+
2130
+ if (isBraceError) {
2131
+ errorMessage = formatMessage(globalMessages.unbalanacedCurlyBraces);
2132
+ }
2133
+ return errorMessage;
2134
+ };
2135
+
2136
+
2137
+ const onFallbackMessageChange = ({ target: { value } }) => {
2138
+ const error = fallbackMessageErrorHandler(value);
2139
+ setFallbackMessage(value);
2140
+ setFallbackMessageError(error);
2141
+ };
2142
+
2143
+ const fallbackMessageErrorHandler = (value) => {
2144
+ let errorMessage = false;
2145
+ const { unsupportedTags } = validateTags({
2146
+ content: value,
2147
+ tagsParam: tags,
2148
+ injectedTagsParams: injectedTags,
2149
+ location,
2150
+ tagModule: getDefaultTags,
2151
+ }) || {};
2152
+ if (value?.length > FALLBACK_MESSAGE_MAX_LENGTH) {
2153
+ errorMessage = formatMessage(messages.fallbackMsgLenError);
2154
+ } else if (unsupportedTags?.length > 0) {
2155
+ errorMessage = formatMessage(
2156
+ globalMessages.unsupportedTagsValidationError,
2157
+ {
2158
+ unsupportedTags,
2159
+ },
2160
+ );
2161
+ }
2162
+ return errorMessage;
2163
+ };
2164
+
1060
2165
  // Check for forbidden characters: square brackets [] and single curly braces {}
1061
2166
  const forbiddenCharactersValidation = (value) => {
1062
2167
  if (!value) return false;
@@ -1101,7 +2206,8 @@ export const Rcs = (props) => {
1101
2206
  if(!isFullMode){
1102
2207
  return false;
1103
2208
  }
1104
- if (!/^\w+$/.test(paramName)) {
2209
+ // Allow Liquid-style param names: letters, digits, underscore, dots (e.g. dynamic_expiry_date_after_3_days.FORMAT_1)
2210
+ if (!/^[\w.]+$/.test(paramName)) {
1105
2211
  return formatMessage(messages.unknownCharactersError);
1106
2212
  }
1107
2213
  }
@@ -1166,6 +2272,117 @@ const onTitleAddVar = () => {
1166
2272
  setTemplateTitleError(error);
1167
2273
  };
1168
2274
 
2275
+ // Carousel: global variables across the whole carousel (all cards, title+body)
2276
+ const getNextCarouselVarToken = () => {
2277
+ const nums = [];
2278
+ (carouselData || []).forEach((c = {}) => {
2279
+ const s1 = (c.title || '').match(/\{\{(\d+)\}\}/g) || [];
2280
+ const s2 = (c.description || '').match(/\{\{(\d+)\}\}/g) || [];
2281
+ [...s1, ...s2].forEach((tok) => {
2282
+ const n = parseInt((tok.match(/\d+/) || [])[0], 10);
2283
+ if (!Number.isNaN(n)) nums.push(n);
2284
+ });
2285
+ });
2286
+ const existing = new Set(nums);
2287
+ let nextNumber = 1;
2288
+ while (existing.has(nextNumber)) nextNumber++;
2289
+ if (nextNumber > 19) return '';
2290
+ return `{{${nextNumber}}}`;
2291
+ };
2292
+
2293
+ const appendVarToCarouselField = (cardIndex, fieldName) => {
2294
+ const token = getNextCarouselVarToken();
2295
+ if (!token) return;
2296
+ setCarouselData((prev = []) => {
2297
+ const updated = cloneDeep(prev);
2298
+ if (!updated[cardIndex]) return prev;
2299
+ const current = (updated[cardIndex][fieldName] || '').toString();
2300
+ updated[cardIndex][fieldName] = `${current}${token}`;
2301
+ return updated;
2302
+ });
2303
+ };
2304
+
2305
+ const textAreaValue = (idValue, type) => {
2306
+ if (idValue >= 0) {
2307
+ const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
2308
+ const templateArr = splitTemplateVarString(templateStr);
2309
+ const token = templateArr?.[idValue] || "";
2310
+ if (token && rcsVarTestRegex.test(token)) {
2311
+ const varName = getVarNameFromToken(token);
2312
+ return (cardVarMapped?.[varName] ?? '').toString();
2313
+ }
2314
+ return "";
2315
+ }
2316
+ return "";
2317
+ };
2318
+
2319
+ // Carousel: render variable-value editor for a given template string (title/description).
2320
+ // This matches rich-card/text edit behavior: static pieces are read-only, variable tokens are editable.
2321
+ const renderCarouselEditMessage = (templateStr) => {
2322
+ const renderArray = [];
2323
+ const templateArr = splitTemplateVarString(templateStr);
2324
+ if (templateArr?.length) {
2325
+ templateArr.forEach((elem, index) => {
2326
+ if (rcsVarTestRegex.test(elem)) {
2327
+ const varName = getVarNameFromToken(elem);
2328
+ renderArray.push(
2329
+ <div key={`${elem}_${index}`} className="var-segment-message-editor__var-slot">
2330
+ <TextArea
2331
+ id={`${elem}_${index}`}
2332
+ placeholder={`enter the value for ${elem}`}
2333
+ autosize={{ minRows: 1, maxRows: 3 }}
2334
+ onChange={(e) => textAreaValueChange(e, TITLE_TEXT)}
2335
+ value={varName ? ((cardVarMapped?.[varName] ?? '').toString()) : ''}
2336
+ onFocus={(e) => {
2337
+ const id = e?.target?.id || e?.currentTarget?.id || '';
2338
+ setCarouselFocusedVarId(id);
2339
+ }}
2340
+ />
2341
+ </div>
2342
+ );
2343
+ } else if (elem) {
2344
+ renderArray.push(
2345
+ <CapHeading
2346
+ key={`static_${index}`}
2347
+ type="h4"
2348
+ className="rcs-edit-template-message-split"
2349
+ >
2350
+ {elem}
2351
+ </CapHeading>
2352
+ );
2353
+ }
2354
+ });
2355
+ }
2356
+ return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
2357
+ };
2358
+
2359
+ const textAreaValueChange = (e, type) => {
2360
+ const value = e?.target?.value ?? '';
2361
+ const id = e?.target?.id || e?.currentTarget?.id || '';
2362
+ if (!id) return;
2363
+ const sep = id.lastIndexOf('_');
2364
+ if (sep === -1) return;
2365
+ const isInvalidValue = value?.trim() === "";
2366
+ const token = id.slice(0, sep);
2367
+ const variableName = getVarNameFromToken(token);
2368
+
2369
+ if (variableName) {
2370
+ setCardVarMapped((prev) => ({
2371
+ ...prev,
2372
+ [variableName]: isInvalidValue ? "" : value,
2373
+ }));
2374
+ }
2375
+ };
2376
+
2377
+ const setTextAreaId = (e, type) => {
2378
+ // VarSegmentMessageEditor calls onFocus(id) with a plain string; DOM events
2379
+ // have an `.target.id` shape. Support both.
2380
+ const id = typeof e === 'string' ? e : (e?.target?.id || e?.currentTarget?.id || '');
2381
+ if (!id) return;
2382
+ if (type === TITLE_TEXT) setTitleTextAreaId(id);
2383
+ else setDescTextAreaId(id);
2384
+ };
2385
+
1169
2386
  const renderButtonComponent = () => {
1170
2387
  return (
1171
2388
  <>
@@ -1191,7 +2408,8 @@ const onTitleAddVar = () => {
1191
2408
  isEditFlow={isEditFlow}
1192
2409
  isFullMode={isFullMode}
1193
2410
  maxButtons={MAX_BUTTONS}
1194
- />
2411
+ host={hostName}
2412
+ />
1195
2413
  </>
1196
2414
  );
1197
2415
  };
@@ -1214,6 +2432,7 @@ const onTitleAddVar = () => {
1214
2432
  varName,
1215
2433
  globalSlot,
1216
2434
  isEditLike,
2435
+ rcsSpanningSemanticVarNames.has(varName),
1217
2436
  );
1218
2437
  }
1219
2438
  });
@@ -1222,42 +2441,37 @@ const onTitleAddVar = () => {
1222
2441
 
1223
2442
  const titleVarSegmentValueMapById = useMemo(
1224
2443
  () => getRcsValueMap(templateTitle, TITLE_TEXT),
1225
- [templateTitle, cardVarMapped, isEditFlow, isFullMode],
2444
+ [templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
1226
2445
  );
1227
2446
  const descriptionVarSegmentValueMapById = useMemo(
1228
2447
  () => getRcsValueMap(templateDesc, MESSAGE_TEXT),
1229
- [templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode],
2448
+ [templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
1230
2449
  );
1231
2450
 
1232
- const handleRcsVarChange = (id, value, type) => {
1233
- const sep = id.lastIndexOf('_');
1234
- if (sep === -1) return;
1235
- const token = id.slice(0, sep);
1236
- const variableName = getVarNameFromToken(token);
2451
+ const handleRcsVarChange = (varSegmentCompositeDomId, value, type) => {
2452
+ const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
2453
+ if (underscoreIndexInCompositeId === -1) return;
2454
+ const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
2455
+ const variableName = getVarNameFromToken(mustacheTokenFromCompositeId);
1237
2456
  if (variableName === undefined || variableName === null || variableName === '') return;
1238
2457
  const isInvalidValue = value?.trim() === '';
1239
2458
  const coercedSlotValue = isInvalidValue ? '' : value;
1240
- const fieldStr = type === TITLE_TEXT ? templateTitle : templateDesc;
1241
- const globalSlot = getGlobalSlotIndexForRcsFieldId(id, fieldStr, type);
1242
- setCardVarMapped((previousVarMap) => {
1243
- const nextVarMap = { ...previousVarMap };
1244
- if (globalSlot !== null && globalSlot !== undefined) {
1245
- // Write by global slot index only — title and description can share the
1246
- // same var name (e.g. {{gt}}), and writing by semantic name would cause
1247
- // the description slot to resolve the title's value and vice-versa.
1248
- const numericKey = String(globalSlot + 1);
1249
- nextVarMap[numericKey] = coercedSlotValue;
1250
- // Remove any stale semantic key so resolveCardVarMappedSlotValue never
1251
- // falls back to it. Guard: when variableName is already the numeric slot
1252
- // key (e.g. {{1}} at slot 0 both equal "1"), skip the delete or it
1253
- // would erase the value we just wrote.
1254
- if (variableName !== numericKey) {
1255
- delete nextVarMap[variableName];
1256
- }
1257
- } else {
1258
- nextVarMap[variableName] = coercedSlotValue;
2459
+ const templateStringForField = type === TITLE_TEXT ? templateTitle : templateDesc;
2460
+ const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
2461
+ varSegmentCompositeDomId,
2462
+ templateStringForField,
2463
+ type,
2464
+ );
2465
+ setCardVarMapped((previousCardVarMapped) => {
2466
+ const updatedCardVarMapped = { ...previousCardVarMapped };
2467
+ // Remove stale semantic key: keeping it causes every other slot sharing the same
2468
+ // variable name (e.g. {{adv}} in both title and description) to read the same value
2469
+ // via the semantic-key fallback in resolveCardVarMappedSlotValue.
2470
+ delete updatedCardVarMapped[variableName];
2471
+ if (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined) {
2472
+ updatedCardVarMapped[String(globalVarSlotIndexZeroBased + 1)] = coercedSlotValue;
1259
2473
  }
1260
- return nextVarMap;
2474
+ return updatedCardVarMapped;
1261
2475
  });
1262
2476
  };
1263
2477
 
@@ -1278,6 +2492,7 @@ const onTitleAddVar = () => {
1278
2492
  }
1279
2493
  suffix={
1280
2494
  <>
2495
+
1281
2496
  {(isEditFlow || !isFullMode) ? (
1282
2497
  <TagList
1283
2498
  label={formatMessage(globalMessages.addLabels)}
@@ -1294,15 +2509,16 @@ const onTitleAddVar = () => {
1294
2509
  type="flat"
1295
2510
  isAddBtn
1296
2511
  onClick={onTitleAddVar}
1297
- disabled={!!templateTitleError}
2512
+ disabled={templateTitleError}
1298
2513
  >
1299
2514
  {formatMessage(messages.addVar)}
1300
2515
  </CapButton>
1301
- )}
2516
+ )}
1302
2517
  </>
1303
- }
2518
+ }
1304
2519
  />
1305
- {(isEditFlow || !isFullMode) ? (
2520
+
2521
+ {(isEditFlow || !isFullMode) ? (
1306
2522
  <VarSegmentMessageEditor
1307
2523
  key={`rcs-title-vars-${rcsVarSegmentEditorRemountKey}`}
1308
2524
  templateString={templateTitle}
@@ -1336,7 +2552,7 @@ const onTitleAddVar = () => {
1336
2552
  )}
1337
2553
  {!isEditFlow && isFullMode && renderTitleCharacterCount()}
1338
2554
  </>
1339
- )}
2555
+ )}
1340
2556
 
1341
2557
  {/* Template Message */}
1342
2558
  <CapRow id="rcs-template-message-label">
@@ -1374,9 +2590,10 @@ const onTitleAddVar = () => {
1374
2590
  />
1375
2591
  </CapRow>
1376
2592
  <CapRow className="rcs-create-template-message-input">
1377
- <div className="rcs_text_area_wrapper">
1378
- {(isEditFlow || !isFullMode)
1379
- ? (
2593
+ {/* Edit/library: segmented inputs (split on {{…}}). Full-mode create: single TextArea below — manual entry there never hits segment split. TagList replaces {{n}} in template string here. */}
2594
+ <CapRow className="rcs_text_area_wrapper">
2595
+ {(isEditFlow || !isFullMode)?
2596
+ (
1380
2597
  <VarSegmentMessageEditor
1381
2598
  key={`rcs-desc-vars-${rcsVarSegmentEditorRemountKey}`}
1382
2599
  templateString={templateDesc}
@@ -1425,7 +2642,7 @@ const onTitleAddVar = () => {
1425
2642
  </>
1426
2643
  )
1427
2644
  }
1428
- </div>
2645
+ </CapRow>
1429
2646
  {(isEditFlow || !isFullMode) && templateDescError && (
1430
2647
  <CapError className="rcs-template-message-error">
1431
2648
  {templateDescError}
@@ -1447,7 +2664,8 @@ const onTitleAddVar = () => {
1447
2664
  />
1448
2665
  )}
1449
2666
  </CapRow>
1450
- {renderButtonComponent()}
2667
+ {((!isEditFlow && isFullMode) || (isEditFlow && (suggestions?.length ?? 0) > 0)) &&
2668
+ renderButtonComponent()}
1451
2669
  </>
1452
2670
 
1453
2671
  );
@@ -1467,7 +2685,7 @@ const onTitleAddVar = () => {
1467
2685
  // Get max length for description based on template type
1468
2686
  const getDescriptionMaxLength = () => {
1469
2687
  return templateType === contentType.text_message
1470
- ? RCS_TEXT_MESSAGE_MAX_LENGTH // 160 for text message
2688
+ ? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH) // 160 for text message
1471
2689
  : RCS_RICH_CARD_MAX_LENGTH; // 2000 for rich card description
1472
2690
  };
1473
2691
 
@@ -1493,6 +2711,63 @@ const onTitleAddVar = () => {
1493
2711
  );
1494
2712
  };
1495
2713
 
2714
+ const rcsDltCardDeleteHandler = () => {
2715
+ closeDltContainerHandler();
2716
+ setDltEditData({});
2717
+ setFallbackMessage('');
2718
+ setFallbackMessageError(false);
2719
+ setShowDltCard(false);
2720
+ };
2721
+
2722
+ const dltFallbackListingPreviewhandler = (data) => {
2723
+ const {
2724
+ 'updated-sms-editor': updatedSmsEditor = [],
2725
+ 'sms-editor': smsEditor = '',
2726
+ } = data.versions.base || {};
2727
+ setFallbackPreviewmode(true);
2728
+ setDltPreviewData(
2729
+ updatedSmsEditor === '' ? smsEditor : updatedSmsEditor.join(''),
2730
+ );
2731
+ };
2732
+
2733
+ const getDltContentCardList = (content, channel) => {
2734
+ const extra = [
2735
+ <CapIcon
2736
+ type="edit"
2737
+ style={{ marginRight: '8px' }}
2738
+ onClick={() => rcsDltEditSelectHandler(dltEditData)}
2739
+ />,
2740
+ <CapDropdown
2741
+ overlay={(
2742
+ <CapMenu>
2743
+ <>
2744
+ <CapMenu.Item
2745
+ className="ant-dropdown-menu-item"
2746
+ onClick={() => setFallbackPreviewmode(true)}
2747
+ >
2748
+ {formatMessage(globalMessages.preview)}
2749
+ </CapMenu.Item>
2750
+ <CapMenu.Item
2751
+ className="ant-dropdown-menu-item"
2752
+ onClick={rcsDltCardDeleteHandler}
2753
+ >
2754
+ {formatMessage(globalMessages.delete)}
2755
+ </CapMenu.Item>
2756
+ </>
2757
+ </CapMenu>
2758
+ )}
2759
+ >
2760
+ <CapIcon type="more" />
2761
+ </CapDropdown>,
2762
+ ];
2763
+ return {
2764
+ title: channel,
2765
+ content,
2766
+ cardType: channel,
2767
+ extra,
2768
+ };
2769
+ };
2770
+
1496
2771
  // Render character count for description/message
1497
2772
  const renderDescriptionCharacterCount = (className = "rcs-character-count") => {
1498
2773
  const currentLength = getDescriptionCharacterCount();
@@ -1508,11 +2783,31 @@ const onTitleAddVar = () => {
1508
2783
  );
1509
2784
  };
1510
2785
 
2786
+ // Carousel: per-card character counts (same limits as rich card)
2787
+ const getCarouselTitleCharacterCount = (cardIndex) => {
2788
+ const t = carouselData?.[cardIndex]?.title || '';
2789
+ return t ? t.length : 0;
2790
+ };
2791
+
2792
+ const getCarouselDescriptionCharacterCount = (cardIndex) => {
2793
+ const d = carouselData?.[cardIndex]?.description || '';
2794
+ return d ? d.length : 0;
2795
+ };
2796
+
2797
+ const renderCarouselCharacterCount = (currentLength, maxLength, className = "rcs-character-count") => (
2798
+ <CapLabel type="label1" className={className}>
2799
+ {formatMessage(messages.templateMessageLength, {
2800
+ currentLength,
2801
+ maxLength,
2802
+ })}
2803
+ </CapLabel>
2804
+ );
2805
+
1511
2806
  // Check if any RCS variables contain tags (similar to Zalo hasTag logic)
1512
2807
  const hasTag = () => {
1513
2808
  // Check cardVarMapped values for tags
1514
2809
  if (cardVarMapped && Object.keys(cardVarMapped).length > 0) {
1515
- const hasTagInMapped = Object.values(cardVarMapped).some((value) =>
2810
+ const hasTagInMapped = Object.values(cardVarMapped).some(value =>
1516
2811
  isTagIncluded(value)
1517
2812
  );
1518
2813
  if (hasTagInMapped) return true;
@@ -1530,6 +2825,14 @@ const onTitleAddVar = () => {
1530
2825
  return false;
1531
2826
  };
1532
2827
 
2828
+ //adding creative dlt fallback sms handlers
2829
+ const addDltMsgHandler = () => {
2830
+ setShowDltContainer(true);
2831
+ setDltMode(RCS_DLT_MODE.TEMPLATES);
2832
+ setDltEditData({});
2833
+ setFallbackMessage('');
2834
+ };
2835
+
1533
2836
  const closeDltContainerHandler = () => {
1534
2837
  setShowDltContainer(false);
1535
2838
  setDltMode('');
@@ -1589,6 +2892,7 @@ const onTitleAddVar = () => {
1589
2892
  isFullMode={isFullMode}
1590
2893
  isDltFromRcs
1591
2894
  onSelectTemplate={rcsDltEditSelectHandler}
2895
+ handlePeviewTemplate={dltFallbackListingPreviewhandler}
1592
2896
  />
1593
2897
  );
1594
2898
  } else if (dltMode === RCS_DLT_MODE.EDIT) {
@@ -1635,7 +2939,8 @@ const onTitleAddVar = () => {
1635
2939
  );
1636
2940
 
1637
2941
  const uploadRcsImage = useCallback((file, type, fileParams, index) => {
1638
- const isRcsThumbnail = index === 1;
2942
+ setImageError(null);
2943
+ const isRcsThumbnail = isThumbnailAssetIndex(index);
1639
2944
  actions.uploadRcsAsset(file, type, {
1640
2945
  isRcsThumbnail,
1641
2946
  ...fileParams,
@@ -1646,6 +2951,7 @@ const onTitleAddVar = () => {
1646
2951
 
1647
2952
  const setUpdateRcsImageSrc = useCallback(
1648
2953
  (val) => {
2954
+ setAssetListImage(val);
1649
2955
  updateRcsImageSrc(val);
1650
2956
  actions.clearRcsMediaAsset(0);
1651
2957
  },
@@ -1672,8 +2978,6 @@ const onTitleAddVar = () => {
1672
2978
  const updateOnRcsImageReUpload = useCallback(() => {
1673
2979
  setUpdateRcsImageSrc('');
1674
2980
  }, []);
1675
-
1676
-
1677
2981
  const uploadRcsVideo = (file, type, fileParams) => {
1678
2982
  actions.uploadRcsAsset(file, type, {
1679
2983
  ...fileParams,
@@ -1705,13 +3009,13 @@ const onTitleAddVar = () => {
1705
3009
  updateRcsThumbnailSrc('');
1706
3010
  };
1707
3011
 
1708
- const renderThumbnailComponent = () => {
3012
+ const renderThumbnailComponent = () => {
1709
3013
  const currentDimension = selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
1710
3014
  return !isEditFlow && (
1711
3015
  <>
1712
3016
  <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
1713
3017
  <CapImageUpload
1714
- className="cap-custom-image-upload rcs-image-upload--top-spacing"
3018
+ style={{ paddingTop: '20px' }}
1715
3019
  allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
1716
3020
  imgWidth={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].width}
1717
3021
  imgHeight={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].height}
@@ -1723,11 +3027,13 @@ const onTitleAddVar = () => {
1723
3027
  updateOnReUpload={updateOnRcsThumbnailReUpload}
1724
3028
  minImgSize={RCS_THUMBNAIL_MIN_SIZE}
1725
3029
  index={1}
3030
+ className="cap-custom-image-upload"
1726
3031
  key={`rcs-uploaded-image-${currentDimension}`}
1727
3032
  imageData={thumbnailData}
1728
3033
  channel={RCS}
1729
3034
  channelSpecificStyle={!isFullMode}
1730
3035
  skipDimensionValidation={true}
3036
+ showReUploadButton={!isEditFlow && isFullMode}
1731
3037
  />
1732
3038
  </>
1733
3039
  )
@@ -1767,6 +3073,7 @@ const onTitleAddVar = () => {
1767
3073
  </div>
1768
3074
  ) : (
1769
3075
  <CapImageUpload
3076
+ style={{ paddingTop: '20px' }}
1770
3077
  allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
1771
3078
  imgWidth={RCS_IMAGE_DIMENSIONS[selectedDimension].width}
1772
3079
  imgHeight={RCS_IMAGE_DIMENSIONS[selectedDimension].height}
@@ -1788,7 +3095,7 @@ const onTitleAddVar = () => {
1788
3095
 
1789
3096
  </>
1790
3097
  );
1791
- }
3098
+ }
1792
3099
 
1793
3100
  const renderVideoComponent = () => {
1794
3101
  const currentDimension =selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
@@ -1865,6 +3172,36 @@ const onTitleAddVar = () => {
1865
3172
  };
1866
3173
 
1867
3174
  const getRcsPreview = () => {
3175
+
3176
+ if (templateType === contentType.carousel) {
3177
+ const cardsForPreview = buildCarouselCardsForPreview(carouselData);
3178
+ const carouselDimKey = getCarouselDimensionKey();
3179
+ const carouselImgDims =
3180
+ RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
3181
+ const carouselVidDims =
3182
+ RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
3183
+ || RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
3184
+ // Debug log for embedded/library mode preview payload (carousel)
3185
+ // eslint-disable-next-line no-console
3186
+ return (
3187
+ <UnifiedPreview
3188
+ channel={RCS}
3189
+ content={{
3190
+ carouselData: cardsForPreview,
3191
+ carouselPreviewDimensions: {
3192
+ imageWidth: carouselImgDims.width,
3193
+ imageHeight: carouselImgDims.height,
3194
+ videoThumbWidth: carouselVidDims.width,
3195
+ videoThumbHeight: carouselVidDims.height,
3196
+ },
3197
+ }}
3198
+ device={ANDROID}
3199
+ showDeviceToggle={false}
3200
+ showHeader={false}
3201
+ formatMessage={formatMessage}
3202
+ />
3203
+ );
3204
+ }
1868
3205
 
1869
3206
  const dimensionObj = RCS_IMAGE_DIMENSIONS[selectedDimension];
1870
3207
  const isSlotMappingMode = isEditFlow || !isFullMode;
@@ -1990,23 +3327,25 @@ const onTitleAddVar = () => {
1990
3327
  ...(templateTitle?.match(rcsVarRegex) ?? []),
1991
3328
  ...(templateDesc?.match(rcsVarRegex) ?? []),
1992
3329
  ];
3330
+ const cardVarMappedForRcsCardOnly = pickRcsCardVarMappedEntries(
3331
+ cardVarMapped,
3332
+ );
3333
+ // Persist numeric slot keys only ("1","2",…) — avoids duplicating the same value under
3334
+ // semantic names (e.g. both "1" and dynamic_expiry_date_after_3_days.FORMAT_1). Hydration
3335
+ // and coalesceCardVarMappedToTemplate still resolve from numeric keys + template tokens.
1993
3336
  const persistedSlotVarMap = {};
1994
- const seenSemanticVarNames = new Set();
1995
3337
  templateVarTokens.forEach((token, slotIndexZeroBased) => {
1996
3338
  const varName = getVarNameFromToken(token);
1997
3339
  if (!varName) return;
1998
3340
  const resolvedRawValue = resolveCardVarMappedSlotValue(
1999
- cardVarMapped,
3341
+ cardVarMappedForRcsCardOnly,
2000
3342
  varName,
2001
3343
  slotIndexZeroBased,
2002
3344
  isSlotMappingMode,
3345
+ rcsSpanningSemanticVarNames.has(varName),
2003
3346
  );
2004
3347
  const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
2005
3348
  persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
2006
- if (!seenSemanticVarNames.has(varName)) {
2007
- seenSemanticVarNames.add(varName);
2008
- persistedSlotVarMap[varName] = sanitizedSlotValue;
2009
- }
2010
3349
  });
2011
3350
  return { cardVarMapped: persistedSlotVarMap };
2012
3351
  })()),
@@ -2023,6 +3362,53 @@ const onTitleAddVar = () => {
2023
3362
  || smsFallbackForPayload.message
2024
3363
  || smsFallbackForPayload.smsContent
2025
3364
  || '';
3365
+ /**
3366
+ * Campaigns `getTraiSenderIds` / Iris read `smsFallBackContent.templateConfigs.registeredSenderIds`.
3367
+ * Library `smsFallbackForPayload` omits ids — use merged state (`smsFallbackMerged`) like test preview.
3368
+ */
3369
+ const m = smsFallbackMerged || {};
3370
+ const tcSibling = m.templateConfigs && typeof m.templateConfigs === 'object'
3371
+ ? m.templateConfigs
3372
+ : {};
3373
+ const smsFallbackTemplateId =
3374
+ (m.smsTemplateId != null && String(m.smsTemplateId).trim() !== ''
3375
+ ? String(m.smsTemplateId)
3376
+ : '')
3377
+ || (tcSibling.templateId != null && String(tcSibling.templateId).trim() !== ''
3378
+ ? String(tcSibling.templateId)
3379
+ : '');
3380
+ const smsFallbackTemplateStr =
3381
+ pickFirstSmsFallbackTemplateString(m)
3382
+ || (typeof m.templateContent === 'string' ? m.templateContent : '')
3383
+ || (typeof tcSibling.template === 'string' ? tcSibling.template : '')
3384
+ || '';
3385
+ const smsFallbackTemplateName =
3386
+ m.templateName
3387
+ || m.smsTemplateName
3388
+ || tcSibling.templateName
3389
+ || tcSibling.name
3390
+ || '';
3391
+ const registeredSenderIdsForPayload = Array.isArray(m.registeredSenderIds)
3392
+ ? m.registeredSenderIds
3393
+ : Array.isArray(tcSibling.registeredSenderIds)
3394
+ ? tcSibling.registeredSenderIds
3395
+ : Array.isArray(tcSibling.header)
3396
+ ? tcSibling.header
3397
+ : null;
3398
+ const hasRegisteredSenderIds = Array.isArray(registeredSenderIdsForPayload);
3399
+ const smsFallbackTemplateConfigs =
3400
+ smsFallbackTemplateId || hasRegisteredSenderIds
3401
+ ? {
3402
+ ...(smsFallbackTemplateId && { templateId: smsFallbackTemplateId }),
3403
+ ...(smsFallbackTemplateStr && { template: smsFallbackTemplateStr }),
3404
+ ...(smsFallbackTemplateName && {
3405
+ templateName: smsFallbackTemplateName,
3406
+ }),
3407
+ ...(hasRegisteredSenderIds && {
3408
+ registeredSenderIds: registeredSenderIdsForPayload,
3409
+ }),
3410
+ }
3411
+ : null;
2026
3412
  return {
2027
3413
  smsFallBackContent: {
2028
3414
  smsTemplateName: smsFallbackForPayload.templateName || '',
@@ -2036,6 +3422,9 @@ const onTitleAddVar = () => {
2036
3422
  && Object.keys(smsFallbackForPayload.rcsSmsFallbackVarMapped).length > 0 && {
2037
3423
  [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackForPayload.rcsSmsFallbackVarMapped,
2038
3424
  }),
3425
+ ...(smsFallbackTemplateConfigs && {
3426
+ templateConfigs: smsFallbackTemplateConfigs,
3427
+ }),
2039
3428
  },
2040
3429
  };
2041
3430
  })()),
@@ -2058,13 +3447,36 @@ const onTitleAddVar = () => {
2058
3447
  (wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
2059
3448
  ? String(wecrmAccountId)
2060
3449
  : accountId;
2061
- const rcsForTest = {
3450
+ const isSlotMappingModeForPreview = isEditFlow || !isFullMode;
3451
+ let rcsForTest = {
2062
3452
  ...rcs,
2063
3453
  rcsContent: {
2064
3454
  ...rcs.rcsContent,
2065
3455
  ...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
2066
3456
  },
2067
3457
  };
3458
+ /** Approval payload keeps numeric-only `cardVarMapped`; preview APIs still need semantic keys. */
3459
+ if (isSlotMappingModeForPreview) {
3460
+ const cardContent = rcsForTest.rcsContent?.cardContent;
3461
+ if (Array.isArray(cardContent) && cardContent[0]) {
3462
+ const fullCardVarMapped = coalesceCardVarMappedToTemplate(
3463
+ pickRcsCardVarMappedEntries(cardVarMapped),
3464
+ templateTitle,
3465
+ templateDesc,
3466
+ rcsVarRegex,
3467
+ );
3468
+ rcsForTest = {
3469
+ ...rcsForTest,
3470
+ rcsContent: {
3471
+ ...rcsForTest.rcsContent,
3472
+ cardContent: [
3473
+ { ...cardContent[0], cardVarMapped: fullCardVarMapped },
3474
+ ...cardContent.slice(1),
3475
+ ],
3476
+ },
3477
+ };
3478
+ }
3479
+ }
2068
3480
  const out = {
2069
3481
  versions: {
2070
3482
  base: {
@@ -2112,7 +3524,7 @@ const onTitleAddVar = () => {
2112
3524
  * with `smsFallbackData`. Done/slot validation must use the same merge — otherwise local state can
2113
3525
  * miss `templateContent` / var map while the parent payload still has them (DLT campaigns).
2114
3526
  */
2115
- const librarySmsFallbackMergedForValidation = useMemo(() => {
3527
+ const librarySmsFallbackMergedForValidation = useMemo(() => {
2116
3528
  if (isFullMode) {
2117
3529
  return smsFallbackData;
2118
3530
  }
@@ -2126,6 +3538,8 @@ const onTitleAddVar = () => {
2126
3538
  };
2127
3539
  }, [isFullMode, templateData, smsFallbackData]);
2128
3540
 
3541
+
3542
+
2129
3543
  const actionCallback = ({ errorMessage, resp }, isEdit) => {
2130
3544
  // eslint-disable-next-line no-undef
2131
3545
  const error = errorMessage?.message || errorMessage;
@@ -2235,7 +3649,13 @@ const onTitleAddVar = () => {
2235
3649
  return true;
2236
3650
  }
2237
3651
  return orderedVarNames.some((name, globalIdx) => {
2238
- const v = resolveCardVarMappedSlotValue(cardVarMapped, name, globalIdx, true);
3652
+ const v = resolveCardVarMappedSlotValue(
3653
+ cardVarMapped,
3654
+ name,
3655
+ globalIdx,
3656
+ true,
3657
+ rcsSpanningSemanticVarNames.has(name),
3658
+ );
2239
3659
  const s = v == null ? '' : String(v);
2240
3660
  return s.trim() === '';
2241
3661
  });
@@ -2266,7 +3686,7 @@ const onTitleAddVar = () => {
2266
3686
  return true;
2267
3687
  }
2268
3688
 
2269
- if (isMediaTypeVideo && (rcsVideoSrc.videoSrc === '' || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
3689
+ if (isMediaTypeVideo && (!rcsVideoSrc.videoSrc || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
2270
3690
  return true;
2271
3691
  }
2272
3692
  if (buttonType.includes(CTA)) {
@@ -2291,7 +3711,7 @@ const onTitleAddVar = () => {
2291
3711
  };
2292
3712
 
2293
3713
  const isEditDisableDone = () => {
2294
- if (templateStatus !== RCS_STATUSES.approved) {
3714
+ if (!isHostInfoBip && templateStatus !== RCS_STATUSES.approved) {
2295
3715
  return true;
2296
3716
  }
2297
3717
 
@@ -2385,7 +3805,6 @@ const onTitleAddVar = () => {
2385
3805
  // Slideboxes are rendered outside the page-level spinner to avoid
2386
3806
  // stacking/blur issues during initial loads.
2387
3807
  if (showDltContainer) return null;
2388
-
2389
3808
  return (
2390
3809
  <>
2391
3810
  {templateStatus !== '' && (
@@ -2395,7 +3814,7 @@ const onTitleAddVar = () => {
2395
3814
  {formatMessage(messages.templateStatusLabel)}
2396
3815
  </CapLabel>
2397
3816
 
2398
- {templateStatus && (
3817
+ {!isHostInfoBip && templateStatus && (
2399
3818
  <CapAlert
2400
3819
  message={getTemplateStatusMessage()}
2401
3820
  type={getTemplateStatusType(templateStatus)}
@@ -2434,6 +3853,7 @@ const onTitleAddVar = () => {
2434
3853
  )
2435
3854
  )}
2436
3855
  {renderLabel('templateTypeLabel')}
3856
+
2437
3857
  <CapRadioGroup
2438
3858
  id="select-rcs-template-type"
2439
3859
  options={TEMPLATE_TYPE_OPTIONS}
@@ -2442,23 +3862,29 @@ const onTitleAddVar = () => {
2442
3862
  disabled={(isEditFlow || !isFullMode)}
2443
3863
  />
2444
3864
 
2445
- {/* Show media only for rich_card or carousel */}
2446
- {(templateType === contentType.rich_card || templateType === contentType.carousel) && (
3865
+ {templateType === contentType.carousel ? (
3866
+ renderCarouselSection()
3867
+ ) : (
2447
3868
  <>
2448
- {renderLabel('mediaLabel')}
2449
- <CapRadioGroup
2450
- options={mediaRadioOptions || []}
2451
- value={templateMediaType}
2452
- onChange={onTemplateMediaTypeChange}
2453
- disabled={(isEditFlow || !isFullMode)}
2454
- className="rcs-radio"
2455
- />
2456
- <div className="rcs-container-image">
2457
- {getMediaBasedComponent()}
2458
- </div>
3869
+ {/* Show media only for rich_card */}
3870
+ {templateType === contentType.rich_card && (
3871
+ <>
3872
+ {renderLabel('mediaLabel')}
3873
+ <CapRadioGroup
3874
+ options={mediaRadioOptions || []}
3875
+ value={templateMediaType}
3876
+ onChange={onTemplateMediaTypeChange}
3877
+ disabled={(isEditFlow || !isFullMode)}
3878
+ className="rcs-radio"
3879
+ />
3880
+ <div className="rcs-container-image">
3881
+ {getMediaBasedComponent()}
3882
+ </div>
3883
+ </>
3884
+ )}
3885
+ {renderTextComponent()}
2459
3886
  </>
2460
3887
  )}
2461
- {renderTextComponent()}
2462
3888
  <CapDivider className="rcs-fallback-section-divider" />
2463
3889
  {renderFallBackSmsComponent()}
2464
3890
  <div className="rcs-scroll-div" />
@@ -2480,7 +3906,9 @@ const onTitleAddVar = () => {
2480
3906
  disabled={isDisableDone()}
2481
3907
  className="rcs-done-btn"
2482
3908
  >
2483
- <FormattedMessage {...messages.sendForApprovalButtonLabel} />
3909
+ <FormattedMessage
3910
+ {...(isHostInfoBip ? messages.doneButtonLabel : messages.sendForApprovalButtonLabel)}
3911
+ />
2484
3912
  </CapButton>
2485
3913
  </div>
2486
3914
  <CapTooltip
@@ -2554,13 +3982,19 @@ const onTitleAddVar = () => {
2554
3982
  content={testAndPreviewContent}
2555
3983
  currentChannel={RCS}
2556
3984
  orgUnitId={orgUnitId}
3985
+ rcsTestPreviewOptions={{ isLibraryMode: !isFullMode }}
2557
3986
  smsFallbackContent={
2558
3987
  smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
2559
3988
  ? {
2560
3989
  templateContent:
2561
3990
  smsFallbackData.templateContent || smsFallbackData.content || '',
2562
3991
  templateName: smsFallbackData.templateName || '',
2563
- [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackData?.rcsSmsFallbackVarMapped ?? {},
3992
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: !isFullMode
3993
+ ? mergeRcsSmsFallbackVarMapLayers(
3994
+ getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
3995
+ smsFallbackData,
3996
+ )
3997
+ : mergeRcsSmsFallbackVarMapLayers({}, smsFallbackData),
2564
3998
  }
2565
3999
  : null
2566
4000
  }
@@ -2570,12 +4004,14 @@ const onTitleAddVar = () => {
2570
4004
  );
2571
4005
  };
2572
4006
 
4007
+
2573
4008
  const mapStateToProps = createStructuredSelector({
2574
4009
  rcsData: makeSelectRcs(),
2575
4010
  accountData: makeSelectAccount(),
2576
4011
  metaEntities: makeSelectMetaEntities(),
2577
4012
  loadingTags: isLoadingMetaEntities(),
2578
4013
  injectedTags: setInjectedTags(),
4014
+ currentOrgDetails: selectCurrentOrgDetails(),
2579
4015
  });
2580
4016
 
2581
4017
  const mapDispatchToProps = (dispatch) => ({