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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/constants/unified.js +29 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/commonUtils.js +19 -1
  6. package/utils/rcsPayloadUtils.js +92 -0
  7. package/utils/templateVarUtils.js +201 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +8 -1
  11. package/v2Components/CapActionButton/index.js +202 -144
  12. package/v2Components/CapActionButton/index.scss +232 -14
  13. package/v2Components/CapActionButton/messages.js +19 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +42 -18
  15. package/v2Components/CapImageUpload/index.js +2 -2
  16. package/v2Components/CapTagList/index.js +10 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +60 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +202 -22
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  28. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  29. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  30. package/v2Components/CommonTestAndPreview/index.js +761 -222
  31. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  32. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  33. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  34. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +284 -267
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  39. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  40. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  46. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  47. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  48. package/v2Components/SmsFallback/constants.js +94 -0
  49. package/v2Components/SmsFallback/index.js +958 -0
  50. package/v2Components/SmsFallback/index.scss +266 -0
  51. package/v2Components/SmsFallback/messages.js +78 -0
  52. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  53. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  54. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  58. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  59. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  60. package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
  61. package/v2Components/TemplatePreview/constants.js +2 -0
  62. package/v2Components/TemplatePreview/index.js +147 -32
  63. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  64. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  65. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  66. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  67. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  68. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  69. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  70. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  71. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  72. package/v2Containers/CommunicationFlow/index.js +3 -3
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  75. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +17 -0
  76. package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
  77. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  78. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  79. package/v2Containers/CreativesContainer/constants.js +11 -0
  80. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  81. package/v2Containers/CreativesContainer/index.js +323 -104
  82. package/v2Containers/CreativesContainer/index.scss +83 -1
  83. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  84. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +79 -34
  85. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  86. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  87. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +333 -90
  88. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  89. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  90. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  91. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  92. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  93. package/v2Containers/Rcs/constants.js +131 -11
  94. package/v2Containers/Rcs/index.js +2685 -832
  95. package/v2Containers/Rcs/index.scss +443 -8
  96. package/v2Containers/Rcs/messages.js +45 -6
  97. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +227 -0
  98. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74176 -39409
  99. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  100. package/v2Containers/Rcs/tests/index.test.js +151 -40
  101. package/v2Containers/Rcs/tests/mockData.js +38 -0
  102. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  103. package/v2Containers/Rcs/tests/utils.test.js +643 -30
  104. package/v2Containers/Rcs/utils.js +466 -12
  105. package/v2Containers/Sms/Create/index.js +115 -48
  106. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  107. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  108. package/v2Containers/SmsTrai/Create/index.js +9 -4
  109. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  110. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  111. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  112. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  113. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  114. package/v2Containers/SmsWrapper/index.js +37 -8
  115. package/v2Containers/TagList/index.js +6 -0
  116. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  117. package/v2Containers/Templates/_templates.scss +196 -12
  118. package/v2Containers/Templates/actions.js +11 -0
  119. package/v2Containers/Templates/constants.js +2 -0
  120. package/v2Containers/Templates/index.js +131 -59
  121. package/v2Containers/Templates/sagas.js +57 -13
  122. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  123. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  124. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  125. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  126. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  127. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  128. package/v2Containers/TemplatesV2/index.js +86 -23
  129. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  130. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  131. package/v2Containers/Whatsapp/index.js +3 -20
  132. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -1,13 +1,12 @@
1
1
  /* eslint-disable no-unused-expressions */
2
- import React, { useState, useEffect, useCallback } from 'react';
2
+ import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
3
3
  import { bindActionCreators } from 'redux';
4
4
  import { createStructuredSelector } from 'reselect';
5
- import { injectIntl, FormattedMessage } from 'react-intl';
5
+ import { FormattedMessage } from 'react-intl';
6
6
  import get from 'lodash/get';
7
7
  import isEmpty from 'lodash/isEmpty';
8
8
  import cloneDeep from 'lodash/cloneDeep';
9
9
  import isNil from 'lodash/isNil';
10
- import styled from 'styled-components';
11
10
  import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
12
11
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
13
12
  import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
@@ -35,6 +34,14 @@ import CapError from '@capillarytech/cap-ui-library/CapError';
35
34
  import CapCheckbox from '@capillarytech/cap-ui-library/CapCheckbox';
36
35
  import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
37
36
  import CapLink from '@capillarytech/cap-ui-library/CapLink';
37
+ import CapTab from '@capillarytech/cap-ui-library/CapTab';
38
+ import { flushSync } from 'react-dom';
39
+ import { isUrl, isValidText } from '../Line/Container/Wrapper/utils';
40
+ import {
41
+ invalidVarRegex,
42
+ RCS_CTA_URL_TYPE,
43
+ URL_MAX_LENGTH,
44
+ } from '../../v2Components/CapActionButton/constants';
38
45
 
39
46
  import {
40
47
  CAP_G01,
@@ -50,17 +57,30 @@ import {
50
57
  import CapVideoUpload from '../../v2Components/CapVideoUpload';
51
58
  import * as globalActions from '../Cap/actions';
52
59
  import CapActionButton from '../../v2Components/CapActionButton';
60
+ import TemplatePreview from '../../v2Components/TemplatePreview';
53
61
  import { makeSelectRcs, makeSelectAccount } from './selectors';
54
62
  import { DATE_DISPLAY_FORMAT, TIME_DISPLAY_FORMAT } from '../App/constants';
55
63
  import {
56
64
  isLoadingMetaEntities,
57
65
  makeSelectMetaEntities,
58
66
  setInjectedTags,
67
+ selectCurrentOrgDetails,
59
68
  } from '../Cap/selectors';
60
69
  import * as RcsActions from './actions';
61
70
  import { isAiContentBotDisabled } from '../../utils/common';
62
71
  import * as TemplatesActions from '../Templates/actions';
63
72
  import './index.scss';
73
+ import {
74
+ normalizeLibraryLoadedTitleDesc,
75
+ mergeRcsSmsFallBackContentFromDetails,
76
+ mergeRcsSmsFallbackVarMapLayers,
77
+ extractRegisteredSenderIdsFromSmsFallbackRecord,
78
+ pickFirstSmsFallbackTemplateString,
79
+ syncCardVarMappedSemanticsFromSlots,
80
+ hasMeaningfulSmsFallbackShape,
81
+ getLibrarySmsFallbackApiBaselineFromTemplateData,
82
+ pickRcsCardVarMappedEntries,
83
+ } from './rcsLibraryHydrationUtils';
64
84
  import {
65
85
  RCS,
66
86
  SMS,
@@ -77,6 +97,7 @@ import {
77
97
  RCS_IMG_SIZE,
78
98
  RCS_DLT_MODE,
79
99
  CTA,
100
+ AI_CONTENT_BOT_DISABLED,
80
101
  RCS_STATUSES,
81
102
  TITLE_TEXT,
82
103
  MESSAGE_TEXT,
@@ -91,9 +112,15 @@ import {
91
112
  rcsVarTestRegex,
92
113
  RCS_IMAGE_DIMENSIONS,
93
114
  RCS_TEXT_MESSAGE_MAX_LENGTH,
115
+ RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP,
94
116
  RCS_RICH_CARD_MAX_LENGTH,
95
117
  RCS_VIDEO_THUMBNAIL_DIMENSIONS,
118
+ RCS_CAROUSEL_IMAGE_DIMENSIONS,
119
+ RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS,
120
+ RCS_CAROUSEL_IMG_SIZE,
121
+ RCS_CAROUSEL_VIDEO_SIZE,
96
122
  MAX_BUTTONS,
123
+ INITIAL_SUGGESTIONS,
97
124
  INITIAL_SUGGESTIONS_DATA_STOP,
98
125
  RCS_BUTTON_TYPES,
99
126
  titletype,
@@ -103,25 +130,50 @@ import {
103
130
  SMALL,
104
131
  MEDIUM,
105
132
  RICHCARD,
133
+ HOST_INFOBIP,
134
+ HOST_ICS,
135
+ CAROUSEL_HEIGHT_OPTIONS,
136
+ CAROUSEL_WIDTH_OPTIONS,
137
+ STOP,
138
+ RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS,
139
+ RCS_NUMERIC_VAR_NAME_REGEX,
140
+ RCS_NUMERIC_VAR_TOKEN_REGEX,
141
+ RCS_STRIP_MUSTACHE_DELIMITERS_REGEX,
142
+ RCS_TAG_AREA_FIELD_TITLE,
143
+ RCS_TAG_AREA_FIELD_DESC,
144
+ REGEX_SPECIAL_CHARS_ESCAPE_PATTERN,
145
+ JS_TYPES,
106
146
  } from './constants';
107
147
  import globalMessages from '../Cap/messages';
108
148
  import messages from './messages';
109
149
  import creativesMessages from '../CreativesContainer/messages';
110
150
  import withCreatives from '../../hoc/withCreatives';
111
151
  import UnifiedPreview from '../../v2Components/CommonTestAndPreview/UnifiedPreview';
112
- import { ANDROID } from '../../v2Components/CommonTestAndPreview/constants';
152
+ import VarSegmentMessageEditor from '../../v2Components/VarSegmentMessageEditor';
153
+ import { ANDROID, RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
113
154
  import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
155
+ import { splitTemplateVarString } from '../../utils/templateVarUtils';
114
156
  import CapImageUpload from '../../v2Components/CapImageUpload';
115
- import addCreativesIcon from '../Assets/images/addCreativesIllustration.svg';
116
157
  import Templates from '../Templates';
117
158
  import SmsTraiEdit from '../SmsTrai/Edit';
159
+ import SmsFallback from '../../v2Components/SmsFallback';
160
+ import { CHANNELS_TO_HIDE_FOR_SMS_ONLY } from '../../v2Components/SmsFallback/constants';
118
161
  import TagList from '../TagList';
119
162
  import { validateTags } from '../../utils/tagValidations';
120
- import { getCdnUrl } from '../../utils/cdnTransformation';
163
+ import { isTraiDLTEnable } from '../../utils/common';
121
164
  import { isTagIncluded } from '../../utils/commonUtils';
122
165
  import injectReducer from '../../utils/injectReducer';
123
166
  import v2RcsReducer from './reducer';
124
- import { getTemplateStatusType } from './utils';
167
+ import {
168
+ areAllRcsSmsFallbackVarSlotsFilled,
169
+ buildRcsNumericMustachePlaceholderRegex,
170
+ getTemplateStatusType,
171
+ normalizeCardVarMapped,
172
+ coalesceCardVarMappedToTemplate,
173
+ getRcsSemanticVarNamesSpanningTitleAndDesc,
174
+ resolveCardVarMappedSlotValue,
175
+ sanitizeCardVarMappedValue,
176
+ } from './utils';
125
177
 
126
178
 
127
179
  const { Group: CapCheckboxGroup } = CapCheckbox;
@@ -138,28 +190,46 @@ export const Rcs = (props) => {
138
190
  templatesActions,
139
191
  globalActions,
140
192
  location,
141
- handleClose,
142
193
  getDefaultTags,
143
194
  supportedTags,
144
195
  metaEntities,
145
196
  injectedTags,
146
197
  loadingTags,
147
198
  getFormData,
148
- isDltEnabled,
149
199
  smsRegister,
200
+ orgUnitId,
150
201
  selectedOfferDetails,
151
202
  eventContextTags,
152
- waitEventContextTags,
153
203
  accountData = {},
204
+ currentOrgDetails,
154
205
  // TestAndPreviewSlidebox props
155
206
  showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
156
207
  handleTestAndPreview: propsHandleTestAndPreview,
157
208
  handleCloseTestAndPreview: propsHandleCloseTestAndPreview,
209
+ handleClose,
158
210
  } = props || {};
159
211
  const { formatMessage } = intl;
160
212
  const { TextArea } = CapInput;
161
213
  const { CapCustomCardList } = CapCustomCard;
214
+
215
+ // Defensive: React cannot render plain objects as children (crashes with
216
+ // "Objects are not valid as a React child"). Some campaigns (!isFullMode) flows
217
+ // can accidentally set an error state to an object (e.g. `{}`).
218
+ const normalizeErrorMessage = (err) => {
219
+ if (!err) return '';
220
+ if (React.isValidElement(err)) return err;
221
+ if (typeof err === JS_TYPES.STRING) return err;
222
+ if (typeof err === JS_TYPES.NUMBER) return String(err);
223
+ if (typeof err === JS_TYPES.OBJECT && typeof err.message === JS_TYPES.STRING) return err.message;
224
+ try {
225
+ return JSON.stringify(err);
226
+ } catch (e) {
227
+ return String(err);
228
+ }
229
+ };
230
+
162
231
  const [isEditFlow, setEditFlow] = useState(false);
232
+ const isEditLike = isEditFlow || !isFullMode;
163
233
  const [tags, updateTags] = useState([]);
164
234
  const [spin, setSpin] = useState(false);
165
235
  //template
@@ -168,33 +238,21 @@ export const Rcs = (props) => {
168
238
  const [templateMediaType, setTemplateMediaType] = useState(
169
239
  RCS_MEDIA_TYPES.NONE,
170
240
  );
171
- const [templateRejectionReason, setTemplateRejectionReason] = useState(null);
172
241
  const [templateTitle, setTemplateTitle] = useState('');
173
242
  const [templateDesc, setTemplateDesc] = useState('');
174
243
  const [templateDescError, setTemplateDescError] = useState(false);
175
244
  const [templateStatus, setTemplateStatus] = useState('');
176
- const [templateDate, setTemplateDate] = useState('');
177
- //fallback
178
- const [fallbackMessage, setFallbackMessage] = useState('');
179
- const [fallbackMessageError, setFallbackMessageError] = useState(false);
180
245
  //fallback dlt
181
246
  const [showDltContainer, setShowDltContainer] = useState(false);
182
247
  const [dltMode, setDltMode] = useState('');
183
248
  const [dltEditData, setDltEditData] = useState({});
184
- const [showDltCard, setShowDltCard] = useState(false);
185
- const [fallbackPreviewmode, setFallbackPreviewmode] = useState(false);
186
- const [dltPreviewData, setDltPreviewData] = useState('');
249
+ /** `undefined` = not hydrated yet; `null` = no fallback / user removed template; object = selected fallback */
250
+ const [smsFallbackData, setSmsFallbackData] = useState(undefined);
187
251
  const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS_DATA_STOP);
188
- const [buttonType, setButtonType] = useState(RCS_BUTTON_TYPES.NONE);
252
+ const buttonType = RCS_BUTTON_TYPES.NONE;
189
253
  const [suggestionError, setSuggestionError] = useState(true);
254
+ const [isTestAndPreviewMode, setIsTestAndPreviewMode] = useState(false);
190
255
  const [templateType, setTemplateType] = useState('text_message');
191
- const [templateHeader, setTemplateHeader] = useState('');
192
- const [templateMessage, setTemplateMessage] = useState('');
193
- const [templateHeaderError, setTemplateHeaderError] = useState('');
194
- const [templateMessageError, setTemplateMessageError] = useState('');
195
- const validVarRegex = /\{\{(\d+)\}\}/g;
196
- const [updatedTitleData, setUpdatedTitleData] = useState([]);
197
- const [updatedDescData, setUpdatedDescData] = useState([]);
198
256
  const [titleVarMappedData, setTitleVarMappedData] = useState({});
199
257
  const [descVarMappedData, setDescVarMappedData] = useState({});
200
258
  const [titleTextAreaId, setTitleTextAreaId] = useState();
@@ -205,75 +263,58 @@ export const Rcs = (props) => {
205
263
  const [rcsVideoSrc, setRcsVideoSrc] = useState({});
206
264
  const [rcsThumbnailSrc, setRcsThumbnailSrc] = useState('');
207
265
  const [selectedDimension, setSelectedDimension] = useState(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
208
- const [imageError, setImageError] = useState(null);
266
+ // Carousel (UI-only) state
267
+ const [selectedCarousel, setSelectedCarousel] = useState('');
268
+ const [selectedCarouselHeight, setSelectedCarouselHeight] = useState(MEDIUM);
269
+ const [selectedCarouselWidth, setSelectedCarouselWidth] = useState(SMALL);
270
+ const [carouselData, setCarouselData] = useState([]);
271
+ const [carouselErrors, setCarouselErrors] = useState([]); // [{ title: string|false, description: string|false }]
272
+ const [activeCarouselIndex, setActiveCarouselIndex] = useState('0');
273
+ const [carouselResetNonce, setCarouselResetNonce] = useState(0);
274
+ const [carouselFocusedVarId, setCarouselFocusedVarId] = useState('');
275
+ const [imageError, setImageError] = useState(null);
209
276
  const [templateTitleError, setTemplateTitleError] = useState(false);
210
277
  const [cardVarMapped, setCardVarMapped] = useState({});
278
+ /** Bump when hydrated cardVarMapped payload changes so VarSegment TextAreas remount with fresh valueMap (controlled-input sync). */
279
+ const [rcsVarSegmentEditorRemountKey, setRcsVarSegmentEditorRemountKey] = useState(0);
280
+ const lastHydratedRcsCardVarSignatureRef = useRef(null);
281
+
282
+ /**
283
+ * Hydrate only from template payload — not from full `rcsData`. Uploads/asset updates change `rcsData`
284
+ * without changing `templateDetails`; re-running hydration then cleared `smsFallbackData`, so the SMS
285
+ * fallback card / content stopped appearing until re-selected.
286
+ */
287
+ const rcsHydrationDetails = useMemo(
288
+ () => (isFullMode ? rcsData?.templateDetails : templateData),
289
+ [isFullMode, rcsData?.templateDetails, templateData],
290
+ );
211
291
 
212
- // TestAndPreviewSlidebox state
213
- const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
214
- const [isTestAndPreviewMode, setIsTestAndPreviewMode] = useState(false);
215
-
216
- const tempMsg = dltPreviewData === '' ? fallbackMessage : dltPreviewData;
217
-
218
- // Get template content for TestAndPreviewSlidebox
219
- // Reference: Based on getRcsPreview() function (lines 2087-2111) which prepares content for TemplatePreview
220
- // getRcsPreview ALWAYS uses templateTitle and templateDesc for ALL template types (text_message, rich_card, carousel)
221
- // renderTextComponent (lines 1317-1485) also uses templateTitle and templateDesc
222
- // Note: templateHeader and templateMessage are defined but NOT used in the component
223
- const getTemplateContent = useCallback(() => {
224
- const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
225
- const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
226
- const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
227
-
228
- // Build media preview object (same pattern as getRcsPreview)
229
- const mediaPreview = {};
230
- if (isMediaTypeImage && rcsImageSrc) {
231
- mediaPreview.rcsImageSrc = rcsImageSrc;
232
- }
233
- if (isMediaTypeVideo && !isMediaTypeText) {
234
- // For video, use thumbnailSrc as rcsVideoSrc (same as getRcsPreview line 2104)
235
- if (rcsThumbnailSrc) {
236
- mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
237
- } else if (rcsVideoSrc?.videoSrc) {
238
- mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
239
- }
240
- // Also include thumbnailSrc separately if available
241
- if (rcsThumbnailSrc) {
242
- mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
292
+ /** Skip duplicate /meta/TAG fetches: same query is triggered from (1) useEffect below, (2) title TagList mount, (3) description TagList mount — each calls getTagsforContext('Outbound'). */
293
+ const lastTagSchemaQueryKeyRef = useRef(null);
294
+ /**
295
+ * Library: parent often passes a new `templateData` object reference every render. Re-applying the same
296
+ * SMS fallback snapshot was resetting `smsFallbackData` and caused VarSegment inputs to flicker old/new.
297
+ */
298
+ const lastSmsFallbackHydrationKeyRef = useRef(null);
299
+
300
+ const fetchTagSchemaIfNewQuery = useCallback(
301
+ (query) => {
302
+ const key = JSON.stringify(query);
303
+ if (lastTagSchemaQueryKeyRef.current === key) {
304
+ return;
243
305
  }
244
- }
245
-
246
- // Build content object
247
- // Reference: getRcsPreview (line 2091-2092) uses templateTitle and templateDesc for ALL cases
248
- // templateTitle is used for rich_card/carousel title, empty for text_message
249
- // templateDesc is used for ALL types (text message body or rich card description)
250
- // For UnifiedPreview, we map templateTitle -> templateHeader and templateDesc -> templateMessage
251
- const contentObj = {
252
- // Map templateTitle to templateHeader and templateDesc to templateMessage
253
- templateHeader: templateTitle,
254
- templateMessage: templateDesc,
255
- ...mediaPreview,
256
- ...(suggestions.length > 0 && {
257
- suggestions: suggestions,
258
- }),
259
- };
306
+ lastTagSchemaQueryKeyRef.current = key;
307
+ globalActions.fetchSchemaForEntity(query);
308
+ },
309
+ [globalActions],
310
+ );
260
311
 
261
- return contentObj;
262
- }, [
263
- templateMediaType,
264
- templateTitle,
265
- templateDesc,
266
- rcsImageSrc,
267
- rcsVideoSrc,
268
- rcsThumbnailSrc,
269
- suggestions,
270
- selectedDimension,
271
- ]);
312
+ // TestAndPreviewSlidebox state
313
+ const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
272
314
 
273
315
  // Handle Test and Preview button click
274
316
  const handleTestAndPreview = useCallback(() => {
275
317
  setShowTestAndPreviewSlidebox(true);
276
- setIsTestAndPreviewMode(true);
277
318
  if (propsHandleTestAndPreview) {
278
319
  propsHandleTestAndPreview();
279
320
  }
@@ -297,31 +338,758 @@ export const Rcs = (props) => {
297
338
  // For video
298
339
  return RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
299
340
  };
341
+ /** Merge editor slot map into `smsFallbackData` (like `cardVarMapped` for title/desc). */
342
+ const handleSmsFallbackEditorStateChange = useCallback((patch) => {
343
+ setSmsFallbackData((prev) => {
344
+ if (!patch || typeof patch !== 'object') return prev;
345
+ // Bail out when no template has been selected yet — SmsTraiEdit fires
346
+ // onRcsFallbackEditorStateChange on mount (unicodeValidity, varMapped),
347
+ // which would create a non-null smsFallbackData from nothing and cause
348
+ // the card to appear as "Untitled creative" before any save.
349
+ if (!prev) return prev;
350
+ return { ...prev, ...patch };
351
+ });
352
+ }, []);
300
353
 
354
+ /** RCS template save / edit API: `rcsContent.accountId` must stay `sourceAccountIdentifier` (pairs with accessToken). */
301
355
  const [accountId, setAccountId] = useState('');
356
+ /** WeCRM list row `id` — only for CommonTestAndPreview → createMessageMeta payload, not for template save. */
357
+ const [wecrmAccountId, setWecrmAccountId] = useState('');
302
358
  const [accessToken, setAccessToken] = useState('');
303
359
  const [hostName, setHostName] = useState('');
304
360
  const [accountName, setAccountName] = useState('');
361
+ const isHostInfoBip = hostName === HOST_INFOBIP;
362
+ const isHostIcs = hostName === HOST_ICS;
363
+
364
+ useEffect(() => {
365
+ setSuggestions(isHostIcs ? INITIAL_SUGGESTIONS_DATA_STOP : []);
366
+ }, [isHostIcs]);
305
367
  const [rcsAccount, setRcsAccount] = useState('');
306
368
  useEffect(() => {
307
369
  const accountObj = accountData.selectedRcsAccount || {};
308
370
  if (!isEmpty(accountObj)) {
309
371
  const {
372
+ id: wecrmId,
310
373
  sourceAccountIdentifier = '',
311
374
  configs = {},
312
375
  } = accountObj;
313
-
314
376
  setAccountId(sourceAccountIdentifier);
377
+ setWecrmAccountId(
378
+ wecrmId != null && String(wecrmId).trim() !== '' ? String(wecrmId) : '',
379
+ );
315
380
  setAccessToken(configs.accessToken || '');
316
381
  setHostName(accountObj.hostName || '');
317
382
  setAccountName(accountObj.name || '');
318
383
  setRcsAccount(accountObj.id || '');
384
+ } else {
385
+ setAccountId('');
386
+ setWecrmAccountId('');
387
+ setAccessToken('');
388
+ setHostName('');
389
+ setAccountName('');
319
390
  }
320
391
  }, [accountData.selectedRcsAccount]);
321
392
 
322
393
  const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
323
394
  const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
324
395
  const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
396
+ const isCarouselType = templateType === contentType.carousel;
397
+
398
+ const MAX_RCS_CAROUSEL_ALLOWED = 10;
399
+ // Uploads for RCS are stored in redux under dynamic keys `uploadedAssetData${index}`.
400
+ // Carousel needs per-card indices; otherwise all cards "restore" the last uploaded asset
401
+ // and show the same media/thumbnail.
402
+ const RCS_CAROUSEL_ASSET_INDEX_BASE = 10; // keep away from standalone indices 0 (media) and 1 (thumbnail)
403
+ const getCarouselImageAssetIndex = (cardIndex) =>
404
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3);
405
+ const getCarouselVideoAssetIndex = (cardIndex) =>
406
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 1;
407
+ const getCarouselThumbnailAssetIndex = (cardIndex) =>
408
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 2;
409
+ const isThumbnailAssetIndex = (index) => {
410
+ if (index === 1) return true; // standalone thumbnail
411
+ if (index >= RCS_CAROUSEL_ASSET_INDEX_BASE) {
412
+ return ((index - RCS_CAROUSEL_ASSET_INDEX_BASE) % 3) === 2; // carousel thumbnail slot
413
+ }
414
+ return false;
415
+ };
416
+
417
+ // Carousel dimension key: `${HEIGHT}_${WIDTH}` (e.g., SHORT_SMALL)
418
+ const getCarouselDimensionKey = () => `${selectedCarouselHeight}_${selectedCarouselWidth}`;
419
+
420
+ const clearCarouselCardMedia = (cardIndex, { clearImage = true, clearVideo = true, clearThumb = true } = {}) => {
421
+ setCarouselData((prev = []) => {
422
+ const updated = cloneDeep(prev);
423
+ if (!updated?.[cardIndex]) return updated;
424
+ if (clearImage) updated[cardIndex].imageSrc = '';
425
+ if (clearVideo) updated[cardIndex].videoAsset = {};
426
+ if (clearThumb) updated[cardIndex].thumbnailSrc = '';
427
+ return updated;
428
+ });
429
+
430
+ if (clearImage) actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIndex));
431
+ if (clearVideo) actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIndex));
432
+ if (clearThumb) actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIndex));
433
+ };
434
+
435
+ const resetCarouselMediaForAllCards = () => {
436
+ setCarouselData((prev = []) => {
437
+ const updated = cloneDeep(prev);
438
+ updated.forEach((card) => {
439
+ if (!card) return;
440
+ card.imageSrc = '';
441
+ card.videoAsset = {};
442
+ card.thumbnailSrc = '';
443
+ });
444
+ return updated;
445
+ });
446
+ (carouselData || []).forEach((_, idx) => {
447
+ actions.clearRcsMediaAsset(getCarouselImageAssetIndex(idx));
448
+ actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(idx));
449
+ actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(idx));
450
+ });
451
+ // Force tab panes to remount after global reset (some tab implementations cache inactive panes)
452
+ setCarouselResetNonce((n) => n + 1);
453
+ };
454
+
455
+ const RCS_CAROUSEL_INITIAL_CARD = {
456
+ title: '',
457
+ description: '',
458
+ mediaType: RCS_MEDIA_TYPES.IMAGE, // per-card
459
+ imageSrc: '',
460
+ videoAsset: {}, // CapVideoUpload object shape
461
+ thumbnailSrc: '',
462
+ suggestions: [],
463
+ };
464
+
465
+ const RCS_CAROUSEL_INITIAL_FIRST_CARD = {
466
+ ...RCS_CAROUSEL_INITIAL_CARD,
467
+ suggestions: cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS),
468
+ };
469
+
470
+ const ensureFirstCardDefaultPhoneSuggestions = (cards) => {
471
+ const next = cloneDeep(cards || []);
472
+ if (next.length === 0) return next;
473
+ const s = next[0].suggestions;
474
+ if (!Array.isArray(s) || s.length === 0) {
475
+ next[0] = {
476
+ ...next[0],
477
+ suggestions: cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS),
478
+ };
479
+ }
480
+ return next;
481
+ };
482
+
483
+ // Always use functional updates: image upload completes in CapImageUpload's useEffect and calls
484
+ // updateImageSrc → this handler. If we cloned carouselData from render, we'd overwrite title/body/
485
+ // suggestions typed since the last commit (stale closure).
486
+ const handleCarouselValueChange = (carouselIndex, fields) => {
487
+ setCarouselData((prev = []) => {
488
+ const updated = cloneDeep(prev);
489
+ if (!updated[carouselIndex]) return prev;
490
+ fields.forEach(({ fieldName, value }) => {
491
+ updated[carouselIndex][fieldName] = value;
492
+ });
493
+ return updated;
494
+ });
495
+ };
496
+
497
+ const updateCarouselErrors = (carouselIndex, patch) => {
498
+ setCarouselErrors((prev = []) => {
499
+ const next = Array.isArray(prev) ? [...prev] : [];
500
+ next[carouselIndex] = { ...(next[carouselIndex] || {}), ...patch };
501
+ return next;
502
+ });
503
+ };
504
+
505
+ const deleteCarouselCard = (index) => {
506
+ let nextIdx = 0;
507
+ flushSync(() => {
508
+ setCarouselData((prev = []) => {
509
+ const updated = cloneDeep(prev);
510
+ if (index < 0 || index >= updated.length) return updated;
511
+ updated.splice(index, 1);
512
+ nextIdx = Math.max(0, Math.min(index - 1, updated.length - 1));
513
+ return ensureFirstCardDefaultPhoneSuggestions(updated);
514
+ });
515
+ });
516
+ setCarouselErrors((prev = []) => {
517
+ const next = Array.isArray(prev) ? [...prev] : [];
518
+ next.splice(index, 1);
519
+ return next;
520
+ });
521
+ setActiveCarouselIndex(`${nextIdx}`);
522
+ };
523
+
524
+ const carouselButtonTextHasForbiddenChars = (value) => {
525
+ if (!value) return false;
526
+ if (value.includes('[') || value.includes(']')) return true;
527
+ const withoutValidVariables = value.replace(/\{\{[^}]*\}\}/g, '');
528
+ if (withoutValidVariables.includes('{') || withoutValidVariables.includes('}')) return true;
529
+ return false;
530
+ };
531
+
532
+ const isCompleteSavedCarouselSuggestion = (s) => {
533
+ if (!s || !s.isSaved) return false;
534
+ const text = (s.text || '').trim();
535
+ if (!text || !isValidText(text) || carouselButtonTextHasForbiddenChars(text)) return false;
536
+ if (s.type === RCS_BUTTON_TYPES.PHONE_NUMBER) {
537
+ return String(s.phoneNumber || '').length >= 5;
538
+ }
539
+ if (s.type === RCS_BUTTON_TYPES.CTA) {
540
+ const url = String(s.url || '').trim();
541
+ if (!url || url.length > URL_MAX_LENGTH) return false;
542
+ const subtype = s.urlType || RCS_CTA_URL_TYPE.STATIC;
543
+ if (subtype === RCS_CTA_URL_TYPE.DYNAMIC) {
544
+ return true;
545
+ }
546
+ if (!isUrl(url)) return false;
547
+ const varMatches = url.match(invalidVarRegex);
548
+ return !(varMatches && varMatches.length > 0);
549
+ }
550
+ if (s.type === RCS_BUTTON_TYPES.QUICK_REPLY) {
551
+ return true;
552
+ }
553
+ return false;
554
+ };
555
+
556
+ const isCarouselCardValid = (card, cardIndex) => {
557
+ if (!card) return false;
558
+ if (!card.title || !card.title.trim()) return false;
559
+ if ((card.title || '').length > TEMPLATE_TITLE_MAX_LENGTH) return false;
560
+ if (!card.description || !card.description.trim()) return false;
561
+ if ((card.description || '').length > RCS_RICH_CARD_MAX_LENGTH) return false;
562
+ let mediaOk = false;
563
+ if (card.mediaType === RCS_MEDIA_TYPES.IMAGE) {
564
+ mediaOk = !!(card.imageSrc && String(card.imageSrc).trim());
565
+ } else if (card.mediaType === RCS_MEDIA_TYPES.VIDEO) {
566
+ const hasVideo = !!(card.videoAsset && card.videoAsset.videoSrc && String(card.videoAsset.videoSrc).trim());
567
+ const hasThumb = !!(card.thumbnailSrc && String(card.thumbnailSrc).trim());
568
+ mediaOk = hasVideo && hasThumb;
569
+ } else {
570
+ return false;
571
+ }
572
+ if (!mediaOk) return false;
573
+ if (cardIndex === 0) {
574
+ const sugg = Array.isArray(card.suggestions) ? card.suggestions : [];
575
+ if (!sugg.some(isCompleteSavedCarouselSuggestion)) return false;
576
+ }
577
+ return true;
578
+ };
579
+
580
+ const checkDisableAddCarouselButton = () => {
581
+ const idx = parseInt(activeCarouselIndex, 10);
582
+ const activeCard = carouselData?.[idx];
583
+ return !isCarouselCardValid(activeCard, idx);
584
+ };
585
+
586
+ const addCarouselCard = () => {
587
+ let newIndex = 0;
588
+ flushSync(() => {
589
+ setCarouselData((prev = []) => {
590
+ const updated = cloneDeep(prev);
591
+ updated.push(cloneDeep(RCS_CAROUSEL_INITIAL_CARD));
592
+ newIndex = updated.length - 1;
593
+ return updated;
594
+ });
595
+ });
596
+ setCarouselErrors((prev = []) => ([...(Array.isArray(prev) ? prev : []), {}]));
597
+ setActiveCarouselIndex(`${newIndex}`);
598
+ };
599
+
600
+ // Initialize carousel data when switching to carousel type
601
+ useEffect(() => {
602
+ if (!isCarouselType) return;
603
+ if (!carouselData || carouselData.length === 0) {
604
+ setCarouselData([cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)]);
605
+ setCarouselErrors([{}]);
606
+ setActiveCarouselIndex('0');
607
+ }
608
+ }, [isCarouselType]);
609
+
610
+ // keep derived carousel key in sync
611
+ useEffect(() => {
612
+ if (!isCarouselType) return;
613
+ if (!selectedCarouselHeight || !selectedCarouselWidth) return;
614
+ // Required format: HEIGHT_WIDTH
615
+ setSelectedCarousel(`${selectedCarouselHeight}_${selectedCarouselWidth}`);
616
+ }, [isCarouselType, selectedCarouselHeight, selectedCarouselWidth]);
617
+
618
+ const renderCarouselDimensionSelection = () => {
619
+ if (!isCarouselType) return null;
620
+ return (
621
+ <CapRow className="rcs-carousel-dimension-section">
622
+ <CapRow gutter={16} className="rcs-carousel-dimension-row">
623
+ <CapColumn span={12}>
624
+ <CapHeading type="h4" className="rcs-carousel-dimension-label">Card height</CapHeading>
625
+ {isEditFlow ? (
626
+ <CapHeading type="h5" className="rcs-creative-name-value">
627
+ {CAROUSEL_HEIGHT_OPTIONS.find((option) => option.value === selectedCarouselHeight)?.label || '-'}
628
+ </CapHeading>
629
+ ) : (
630
+ <CapSelect
631
+ id="rcs-carousel-height-select"
632
+ value={selectedCarouselHeight}
633
+ onChange={(val) => {
634
+ // Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
635
+ resetCarouselMediaForAllCards();
636
+ setSelectedCarouselHeight(val);
637
+ }}
638
+ options={CAROUSEL_HEIGHT_OPTIONS}
639
+ disabled={!isFullMode}
640
+ />
641
+ )}
642
+ </CapColumn>
643
+ <CapColumn span={12}>
644
+ <CapHeading type="h4" className="rcs-carousel-dimension-label">Card width</CapHeading>
645
+ {isEditFlow ? (
646
+ <CapHeading type="h5" className="rcs-creative-name-value">
647
+ {CAROUSEL_WIDTH_OPTIONS.find((option) => option.value === selectedCarouselWidth)?.label || '-'}
648
+ </CapHeading>
649
+ ) : (
650
+ <CapSelect
651
+ id="rcs-carousel-width-select"
652
+ value={selectedCarouselWidth}
653
+ onChange={(val) => {
654
+ // Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
655
+ resetCarouselMediaForAllCards();
656
+ setSelectedCarouselWidth(val);
657
+ }}
658
+ options={CAROUSEL_WIDTH_OPTIONS}
659
+ disabled={!isFullMode}
660
+ />
661
+ )}
662
+ </CapColumn>
663
+ </CapRow>
664
+ {!!selectedCarousel && (
665
+ <CapLabel type="label3" className="rcs-carousel-selected-dimension">
666
+ Selected: {selectedCarousel}
667
+ </CapLabel>
668
+ )}
669
+ </CapRow>
670
+ );
671
+ };
672
+
673
+ // Reuse rich-card buttons UI per carousel card
674
+ const renderButtonComponentForCarouselCard = (cardIndex) => {
675
+ const card = carouselData?.[cardIndex] || {};
676
+ const suggestionsForCard = card.suggestions || [];
677
+ return (
678
+ <>
679
+ <CapHeader
680
+ className="rcs-button-cta"
681
+ title={(
682
+ <CapRow type="flex">
683
+ <CapHeading type="h4">
684
+ {formatMessage(messages.btnLabel)}
685
+ </CapHeading>
686
+ </CapRow>
687
+ )}
688
+ description={!isEditFlow && (
689
+ <CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
690
+ )}
691
+ />
692
+ <CapActionButton
693
+ buttonType={RCS_BUTTON_TYPES.NONE}
694
+ updateButtonChange={(data, btnIndex) => {
695
+ // Match existing behavior: allow CapActionButton to manage save gating.
696
+ const updated = cloneDeep(suggestionsForCard);
697
+ if (btnIndex === MAX_BUTTONS) {
698
+ handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: data }]);
699
+ return;
700
+ }
701
+ updated[btnIndex] = data;
702
+ handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: updated }]);
703
+ }}
704
+ deleteButtonHandler={(btnIndex) => {
705
+ if (cardIndex === 0 && btnIndex === 0) {
706
+ return;
707
+ }
708
+ const savedCount = (suggestionsForCard || []).filter((x) => x && x.isSaved).length;
709
+ const target = (suggestionsForCard || []).find((s) => s && s.index === btnIndex);
710
+ if (cardIndex === 0 && target?.isSaved && savedCount <= 1) {
711
+ return;
712
+ }
713
+ const updated = cloneDeep(suggestionsForCard)
714
+ .filter((i) => i.index !== btnIndex)
715
+ .map((i, idx) => ({ ...i, index: idx }));
716
+ handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: updated }]);
717
+ }}
718
+ suggestions={suggestionsForCard}
719
+ isEditFlow={isEditFlow}
720
+ isFullMode={isFullMode}
721
+ maxButtons={MAX_BUTTONS}
722
+ host={hostName}
723
+ minSavedSuggestions={cardIndex === 0 ? 1 : 0}
724
+ hideDeleteSuggestionIndexes={cardIndex === 0 ? [0] : []}
725
+ />
726
+ </>
727
+ );
728
+ };
729
+
730
+ const renderCarouselCardMedia = (cardIndex) => {
731
+ const card = carouselData?.[cardIndex] || {};
732
+ const dimKey = getCarouselDimensionKey();
733
+
734
+ if (card.mediaType === RCS_MEDIA_TYPES.VIDEO) {
735
+ return (
736
+ <>
737
+ <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Video</CapHeading>
738
+ <CapVideoUpload
739
+ index={getCarouselVideoAssetIndex(cardIndex)}
740
+ allowedExtensionsRegex={ALLOWED_EXTENSIONS_VIDEO_REGEX}
741
+ videoSize={RCS_CAROUSEL_VIDEO_SIZE}
742
+ isFullMode={isFullMode}
743
+ uploadAsset={uploadRcsVideo}
744
+ uploadedAssetList={card.videoAsset || {}}
745
+ onVideoUploadUpdateAssestList={(_, val) => {
746
+ handleCarouselValueChange(cardIndex, [{ fieldName: 'videoAsset', value: val }]);
747
+ }}
748
+ videoData={rcsData}
749
+ className="cap-custom-video-upload"
750
+ formClassName={"rcs-video-upload"}
751
+ channel={RCS}
752
+ errorMessage={formatMessage(messages.videoErrorMessage)}
753
+ showVideoNameAndDuration={false}
754
+ showReUploadButton={!isEditFlow && isFullMode}
755
+ channelSpecificStyle={!isFullMode}
756
+ />
757
+
758
+ <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
759
+ <CapImageUpload
760
+ allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
761
+ imgWidth={RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS?.[dimKey]?.width}
762
+ imgHeight={RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS?.[dimKey]?.height}
763
+ imgSize={RCS_THUMBNAIL_MAX_SIZE}
764
+ uploadAsset={uploadRcsImage}
765
+ isFullMode={isFullMode}
766
+ imageSrc={card.thumbnailSrc}
767
+ updateImageSrc={(val) => handleCarouselValueChange(cardIndex, [{ fieldName: 'thumbnailSrc', value: val }])}
768
+ updateOnReUpload={() => handleCarouselValueChange(cardIndex, [{ fieldName: 'thumbnailSrc', value: '' }])}
769
+ minImgSize={RCS_THUMBNAIL_MIN_SIZE}
770
+ index={getCarouselThumbnailAssetIndex(cardIndex)}
771
+ className="cap-custom-image-upload"
772
+ key={`rcs-carousel-thumb-${cardIndex}-${dimKey}`}
773
+ imageData={rcsData}
774
+ channel={RCS}
775
+ channelSpecificStyle={!isFullMode}
776
+ skipDimensionValidation={true}
777
+ showReUploadButton={!isEditFlow && isFullMode}
778
+ disabled={isEditFlow || !isFullMode}
779
+ />
780
+ </>
781
+ );
782
+ }
783
+
784
+ // Default: IMAGE
785
+ return (
786
+ <>
787
+ <CapHeading type="h4" className="rcs-image-dimensions-label">{formatMessage(messages.mediaImage)}</CapHeading>
788
+ <CapImageUpload
789
+ allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
790
+ imgWidth={RCS_CAROUSEL_IMAGE_DIMENSIONS?.[dimKey]?.width}
791
+ imgHeight={RCS_CAROUSEL_IMAGE_DIMENSIONS?.[dimKey]?.height}
792
+ imgSize={RCS_CAROUSEL_IMG_SIZE}
793
+ uploadAsset={uploadRcsImage}
794
+ isFullMode={isFullMode}
795
+ imageSrc={card.imageSrc}
796
+ updateImageSrc={(val) => handleCarouselValueChange(cardIndex, [{ fieldName: 'imageSrc', value: val }])}
797
+ updateOnReUpload={() => handleCarouselValueChange(cardIndex, [{ fieldName: 'imageSrc', value: '' }])}
798
+ index={getCarouselImageAssetIndex(cardIndex)}
799
+ className="cap-custom-image-upload"
800
+ key={`rcs-carousel-image-${cardIndex}-${dimKey}`}
801
+ imageData={rcsData}
802
+ channel={RCS}
803
+ channelSpecificStyle={!isFullMode}
804
+ skipDimensionValidation={true}
805
+ showReUploadButton={!isEditFlow && isFullMode}
806
+ disabled={isEditFlow || !isFullMode}
807
+ />
808
+ </>
809
+ );
810
+ };
811
+
812
+ const renderCarouselCardButtons = (cardIndex) => {
813
+ return renderButtonComponentForCarouselCard(cardIndex);
814
+ };
815
+
816
+ const getCarouselTabPanes = () => {
817
+ return (carouselData || []).map((card, index) => {
818
+ return {
819
+ key: String(index),
820
+ tab: index + 1,
821
+ content: (
822
+ <CapCard
823
+ title={`Card ${index + 1}`}
824
+ extra={
825
+ !isEditFlow &&
826
+ (carouselData.length === 1 ? (
827
+ <CapTooltip title={formatMessage(messages.rcsCarouselMinCardDeleteTooltip)}>
828
+ <span className="button-disabled-tooltip-wrapper rcs-carousel-delete-tooltip-wrap">
829
+ <CapButton
830
+ className="rcs-carousel-card-delete"
831
+ type="flat"
832
+ onClick={() => deleteCarouselCard(index)}
833
+ disabled
834
+ aria-label={formatMessage(messages.rcsCarouselMinCardDeleteTooltip)}
835
+ >
836
+ <CapIcon type="delete" />
837
+ </CapButton>
838
+ </span>
839
+ </CapTooltip>
840
+ ) : (
841
+ <CapButton
842
+ className="rcs-carousel-card-delete"
843
+ type="flat"
844
+ onClick={() => deleteCarouselCard(index)}
845
+ aria-label={formatMessage(globalMessages.delete)}
846
+ >
847
+ <CapIcon type="delete" />
848
+ </CapButton>
849
+ ))
850
+ }
851
+ className="rcs-carousel-card"
852
+ >
853
+ {/* Media selection should be at top of card */}
854
+ {isEditFlow ? (
855
+ <div className="rcs-creative-name-readonly">
856
+ <CapHeading type="h4">{formatMessage(messages.mediaTypeLabel)}</CapHeading>
857
+ <CapHeading type="h5" className="rcs-creative-name-value">
858
+ {mediaRadioOptions.find((option) => option.value === card.mediaType)?.label || '-'}
859
+ </CapHeading>
860
+ </div>
861
+ ) : (
862
+ <CapRow className="rcs-carousel-media-selection">
863
+ <CapColumn className="rcs-carousel-media-selection-heading">
864
+ <CapHeading type="h4">{formatMessage(messages.mediaTypeLabel)}</CapHeading>
865
+ </CapColumn>
866
+ <CapColumn>
867
+ <CapRadioGroup
868
+ id={`rcs-carousel-media-radio-${index}`}
869
+ options={mediaRadioOptions}
870
+ value={card.mediaType}
871
+ onChange={({ target: { value } }) => {
872
+ // Reset media fields when switching type
873
+ if (value === RCS_MEDIA_TYPES.IMAGE) {
874
+ // Switching to IMAGE: clear video + thumbnail uploads so they don't auto-restore.
875
+ clearCarouselCardMedia(index, { clearImage: false, clearVideo: true, clearThumb: true });
876
+ handleCarouselValueChange(index, [
877
+ { fieldName: 'mediaType', value },
878
+ { fieldName: 'videoAsset', value: {} },
879
+ { fieldName: 'thumbnailSrc', value: '' },
880
+ ]);
881
+ } else {
882
+ // Switching to VIDEO: clear image upload so it doesn't auto-restore.
883
+ clearCarouselCardMedia(index, { clearImage: true, clearVideo: false, clearThumb: false });
884
+ handleCarouselValueChange(index, [
885
+ { fieldName: 'mediaType', value },
886
+ { fieldName: 'imageSrc', value: '' },
887
+ ]);
888
+ }
889
+ }}
890
+ disabled={!isFullMode}
891
+ className="rcs-radio"
892
+ />
893
+ </CapColumn>
894
+ </CapRow>
895
+ )}
896
+
897
+ <CapRow className={`rcs-carousel-media-upload ${isEditFlow ? 'rcs-carousel-media-upload--readonly-above' : ''}`}>
898
+ {renderCarouselCardMedia(index)}
899
+ </CapRow>
900
+
901
+ {/* Title after media */}
902
+ <CapRow className="rcs-carousel-card-row">
903
+ <CapHeader
904
+ className="rcs-template-title-label"
905
+ title={<CapHeading type="h4">Card title</CapHeading>}
906
+ suffix={
907
+ (isEditFlow || !isFullMode) ? (
908
+ <TagList
909
+ label={formatMessage(globalMessages.addLabels)}
910
+ onTagSelect={onCarouselTagSelect}
911
+ location={location}
912
+ tags={getRcsTags()}
913
+ onContextChange={handleOnTagsContextChange}
914
+ injectedTags={injectedTags || {}}
915
+ selectedOfferDetails={selectedOfferDetails}
916
+ />
917
+ ) : (
918
+ <CapButton
919
+ data-testid={`rcs-carousel-title-add-var-${index}`}
920
+ type="flat"
921
+ isAddBtn
922
+ onClick={() => appendVarToCarouselField(index, 'title')}
923
+ disabled={!!carouselErrors?.[index]?.title}
924
+ >
925
+ {formatMessage(messages.addVar)}
926
+ </CapButton>
927
+ )
928
+ }
929
+ />
930
+ <CapRow className="rcs_text_area_wrapper">
931
+ {(isEditFlow || !isFullMode) ? (
932
+ renderCarouselEditMessage(card.title || '')
933
+ ) : (
934
+ <CapInput
935
+ value={card.title || ''}
936
+ placeholder={formatMessage(messages.templateTitlePlaceholder)}
937
+ onChange={({ target: { value } }) => {
938
+ let error = false;
939
+ if (value?.length > TEMPLATE_TITLE_MAX_LENGTH) {
940
+ error = formatMessage(messages.templateHeaderLengthError);
941
+ } else {
942
+ error = variableErrorHandling(value);
943
+ }
944
+ updateCarouselErrors(index, { title: error });
945
+ handleCarouselValueChange(index, [{ fieldName: 'title', value }]);
946
+ }}
947
+ disabled={isEditFlow || !isFullMode}
948
+ errorMessage={carouselErrors?.[index]?.title}
949
+ />
950
+ )}
951
+ </CapRow>
952
+ </CapRow>
953
+ {!isEditFlow && (
954
+ <CapRow className="rcs-carousel-character-count-row">
955
+ {renderCarouselCharacterCount(
956
+ getCarouselTitleCharacterCount(index),
957
+ getTitleMaxLength(),
958
+ )}
959
+ </CapRow>
960
+ )}
961
+
962
+ {/* Description after title */}
963
+ <CapRow className="rcs-carousel-card-row">
964
+ <CapHeader
965
+ title={<CapHeading type="h4">Card body text</CapHeading>}
966
+ suffix={
967
+ (isEditFlow || !isFullMode) ? (
968
+ <TagList
969
+ label={formatMessage(globalMessages.addLabels)}
970
+ onTagSelect={onCarouselTagSelect}
971
+ location={location}
972
+ tags={getRcsTags()}
973
+ onContextChange={handleOnTagsContextChange}
974
+ injectedTags={injectedTags || {}}
975
+ selectedOfferDetails={selectedOfferDetails}
976
+ />
977
+ ) : (
978
+ <CapButton
979
+ data-testid={`rcs-carousel-desc-add-var-${index}`}
980
+ type="flat"
981
+ isAddBtn
982
+ onClick={() => appendVarToCarouselField(index, 'description')}
983
+ disabled={!!carouselErrors?.[index]?.description}
984
+ >
985
+ {formatMessage(messages.addVar)}
986
+ </CapButton>
987
+ )
988
+ }
989
+ />
990
+ <CapRow className="rcs_text_area_wrapper">
991
+ {(isEditFlow || !isFullMode) ? (
992
+ renderCarouselEditMessage(card.description || '')
993
+ ) : (
994
+ <TextArea
995
+ autosize={{ minRows: 3, maxRows: 5 }}
996
+ value={card.description || ''}
997
+ placeholder={formatMessage(messages.templateDescPlaceholder)}
998
+ onChange={({ target: { value } }) => {
999
+ let error = false;
1000
+ if (value?.length > RCS_RICH_CARD_MAX_LENGTH) {
1001
+ error = formatMessage(messages.templateMessageLengthError);
1002
+ } else {
1003
+ error = variableErrorHandling(value);
1004
+ }
1005
+ updateCarouselErrors(index, { description: error });
1006
+ handleCarouselValueChange(index, [{ fieldName: 'description', value }]);
1007
+ }}
1008
+ disabled={isEditFlow || !isFullMode}
1009
+ errorMessage={
1010
+ carouselErrors?.[index]?.description && (
1011
+ <CapError className="rcs-template-message-error">
1012
+ {carouselErrors[index].description}
1013
+ </CapError>
1014
+ )
1015
+ }
1016
+ />
1017
+ )}
1018
+ </CapRow>
1019
+ </CapRow>
1020
+ {!isEditFlow && (
1021
+ <CapRow className="rcs-carousel-character-count-row">
1022
+ {renderCarouselCharacterCount(
1023
+ getCarouselDescriptionCharacterCount(index),
1024
+ getDescriptionMaxLength(),
1025
+ )}
1026
+ </CapRow>
1027
+ )}
1028
+
1029
+ <CapDivider className="rcs-carousel-card-divider" />
1030
+ {renderCarouselCardButtons(index)}
1031
+ </CapCard>
1032
+ ),
1033
+ };
1034
+ });
1035
+ };
1036
+
1037
+ const renderCarouselSection = () => {
1038
+ if (!isCarouselType) return null;
1039
+
1040
+ const isMaxCarouselCardsReached = MAX_RCS_CAROUSEL_ALLOWED === (carouselData?.length || 0);
1041
+ const addCardTooltipTitle = isMaxCarouselCardsReached
1042
+ ? formatMessage(messages.rcsCarouselMaxCardTooltip, { max: MAX_RCS_CAROUSEL_ALLOWED })
1043
+ // "Set up the first card" only makes sense while creating a new template —
1044
+ // in edit flow the cards already exist, so don't show it there.
1045
+ : (!isEditFlow && checkDisableAddCarouselButton() ? formatMessage(messages.rcsCarouselAddCardDisabledTooltip) : '');
1046
+
1047
+ // No "+" in edit flow (cards can't be added there) — and with no "+", the
1048
+ // divider before it has nothing to separate, so skip both together instead
1049
+ // of rendering a dangling divider.
1050
+ const operations = isEditFlow ? null : (
1051
+ <>
1052
+ <CapDivider type="vertical" />
1053
+ <CapTooltip title={addCardTooltipTitle}>
1054
+ <span className="button-disabled-tooltip-wrapper">
1055
+ <CapButton
1056
+ onClick={addCarouselCard}
1057
+ type="flat"
1058
+ className="add-carousel-content-button"
1059
+ disabled={
1060
+ isEditFlow ||
1061
+ !isFullMode ||
1062
+ isMaxCarouselCardsReached ||
1063
+ checkDisableAddCarouselButton()
1064
+ }
1065
+ >
1066
+ <CapIcon type="plus" />
1067
+ </CapButton>
1068
+ </span>
1069
+ </CapTooltip>
1070
+ </>
1071
+ );
1072
+
1073
+ return (
1074
+ <CapRow className={`rcs-carousel-section ${isEditFlow ? 'rcs-carousel-section--readonly-above' : ''}`}>
1075
+ {renderCarouselDimensionSelection()}
1076
+ <CapRow className="rcs-carousel-tab">
1077
+ <CapTab
1078
+ key={`rcs-carousel-tab-${carouselResetNonce}`}
1079
+ defaultActiveKey="0"
1080
+ activeKey={activeCarouselIndex}
1081
+ tabBarExtraContent={operations}
1082
+ onChange={(key) => {
1083
+ setActiveCarouselIndex(`${key}`);
1084
+ // reset focused var when switching cards (as per requirement)
1085
+ setCarouselFocusedVarId('');
1086
+ }}
1087
+ panes={getCarouselTabPanes()}
1088
+ />
1089
+ </CapRow>
1090
+ </CapRow>
1091
+ );
1092
+ };
325
1093
 
326
1094
  const mediaRadioOptions = [
327
1095
  {
@@ -330,11 +1098,16 @@ export const Rcs = (props) => {
330
1098
  },
331
1099
  {
332
1100
  value: RCS_MEDIA_TYPES.VIDEO,
333
- label: formatMessage(messages.mediaVideo),
1101
+ label: formatMessage(
1102
+ templateType === contentType.carousel
1103
+ ? messages.carouselMediaVideoOption
1104
+ : messages.mediaVideo,
1105
+ ),
334
1106
  },
335
1107
  ];
336
1108
  const aiContentBotDisabled = isAiContentBotDisabled();
337
1109
 
1110
+
338
1111
  const updateButtonChange = (data, index) => {
339
1112
  if (data && data.text) {
340
1113
  const forbiddenError = forbiddenCharactersValidation(data.text);
@@ -371,7 +1144,9 @@ export const Rcs = (props) => {
371
1144
  if (isFullMode) return;
372
1145
  if (loadingTags || !tags || tags.length === 0) return;
373
1146
  const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
374
- const resolved = resolveTemplateWithMap(templateStr); // placeholders -> mapped value (or '')
1147
+ const slotOffset =
1148
+ type === TITLE_TEXT ? 0 : (templateTitle ? templateTitle.match(rcsVarRegex) || [] : []).length;
1149
+ const resolved = resolveTemplateWithMap(templateStr, slotOffset); // placeholders -> mapped value (or '')
375
1150
  if (!resolved) {
376
1151
  if (type === TITLE_TEXT) setTemplateTitleError(false);
377
1152
  if (type === MESSAGE_TEXT) setTemplateDescError(false);
@@ -381,7 +1156,7 @@ export const Rcs = (props) => {
381
1156
  let contentForValidation = resolved;
382
1157
  const placeholderTokens = templateStr.match(rcsVarRegex) || [];
383
1158
  placeholderTokens.forEach((t) => {
384
- const escaped = t.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
1159
+ const escaped = t.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
385
1160
  contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
386
1161
  });
387
1162
  if (!contentForValidation.trim()) {
@@ -398,10 +1173,16 @@ export const Rcs = (props) => {
398
1173
  tagModule: getDefaultTags,
399
1174
  isFullMode,
400
1175
  }) || {};
401
- const errorMsg =
402
- (validationResponse?.isBraceError &&
403
- formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
404
- false;
1176
+ const unsupportedTagsLengthCheck =
1177
+ validationResponse?.unsupportedTags?.length > 0;
1178
+ const errorMsg =
1179
+ (unsupportedTagsLengthCheck &&
1180
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
1181
+ unsupportedTags: validationResponse.unsupportedTags,
1182
+ })) ||
1183
+ (validationResponse.isBraceError &&
1184
+ formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
1185
+ false;
405
1186
  if (type === TITLE_TEXT) setTemplateTitleError(errorMsg);
406
1187
  if (type === MESSAGE_TEXT) setTemplateDescError(errorMsg);
407
1188
  };
@@ -414,15 +1195,98 @@ export const Rcs = (props) => {
414
1195
  validateResolvedTagsForType(MESSAGE_TEXT);
415
1196
  }, [cardVarMapped, templateDesc, tags, injectedTags, loadingTags]);
416
1197
 
1198
+ useEffect(() => {
1199
+ if (isFullMode || !isCarouselType) return;
1200
+ if (loadingTags || !tags || tags.length === 0) return;
1201
+ (carouselData || []).forEach((card, idx) => {
1202
+ ['title', 'description'].forEach((field) => {
1203
+ const templateStr = card?.[field] || '';
1204
+ if (!templateStr) return;
1205
+ const resolved = resolveTemplateWithMap(templateStr);
1206
+ if (!resolved) {
1207
+ updateCarouselErrors(idx, { [field]: false });
1208
+ return;
1209
+ }
1210
+ let contentForValidation = resolved;
1211
+ const placeholderTokens = templateStr.match(rcsVarRegex) || [];
1212
+ placeholderTokens.forEach((t) => {
1213
+ const escaped = t.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
1214
+ contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
1215
+ });
1216
+ if (!contentForValidation.trim()) {
1217
+ updateCarouselErrors(idx, { [field]: false });
1218
+ return;
1219
+ }
1220
+ const validationResponse = validateTags({
1221
+ content: contentForValidation,
1222
+ tagsParam: tags,
1223
+ injectedTagsParams: injectedTags,
1224
+ location,
1225
+ tagModule: getDefaultTags,
1226
+ eventContextTags,
1227
+ isFullMode,
1228
+ }) || {};
1229
+ const errorMsg =
1230
+ (validationResponse?.unsupportedTags?.length > 0 &&
1231
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
1232
+ unsupportedTags: validationResponse.unsupportedTags,
1233
+ })) ||
1234
+ (validationResponse.isBraceError &&
1235
+ formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
1236
+ false;
1237
+ updateCarouselErrors(idx, { [field]: errorMsg });
1238
+ });
1239
+ });
1240
+ }, [cardVarMapped, carouselData, tags, injectedTags, loadingTags, isCarouselType]);
1241
+
417
1242
  const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
418
1243
 
419
- const resolveTemplateWithMap = (str = '') => {
1244
+ const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
1245
+
1246
+ /** Same `{{tag}}` in both title and description must not share one semantic map entry. */
1247
+ const rcsSpanningSemanticVarNames = useMemo(
1248
+ () => getRcsSemanticVarNamesSpanningTitleAndDesc(templateTitle, templateDesc, rcsVarRegex),
1249
+ [templateTitle, templateDesc],
1250
+ );
1251
+
1252
+ /** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
1253
+ const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
1254
+ const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
1255
+ const offset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
1256
+ const templateSegments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
1257
+ let varOrdinal = 0;
1258
+ for (let segmentIndexInField = 0; segmentIndexInField < templateSegments.length; segmentIndexInField += 1) {
1259
+ const segmentToken = templateSegments[segmentIndexInField];
1260
+ if (rcsVarTestRegex.test(segmentToken)) {
1261
+ if (`${segmentToken}_${segmentIndexInField}` === varSegmentCompositeId) {
1262
+ return offset + varOrdinal;
1263
+ }
1264
+ varOrdinal += 1;
1265
+ }
1266
+ }
1267
+ return null;
1268
+ };
1269
+
1270
+ /**
1271
+ * Master-branch resolve: uses numeric slot keys + semantic spanning detection for correct
1272
+ * multi-field variable resolution.
1273
+ */
1274
+ const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
420
1275
  if (!str) return '';
421
- const arr = splitTemplateVarString(str);
1276
+ const arr = splitTemplateVarStringRcs(str);
1277
+ let varOrdinal = 0;
422
1278
  return arr.map((elem) => {
423
1279
  if (rcsVarTestRegex.test(elem)) {
424
1280
  const key = getVarNameFromToken(elem);
425
- const v = cardVarMapped?.[key];
1281
+ const globalSlot = slotOffset + varOrdinal;
1282
+ varOrdinal += 1;
1283
+ const v = resolveCardVarMappedSlotValue(
1284
+ cardVarMapped,
1285
+ key,
1286
+ globalSlot,
1287
+ isEditLike,
1288
+ rcsSpanningSemanticVarNames.has(key),
1289
+ );
426
1290
  if (isNil(v) || String(v)?.trim?.() === '') return elem;
427
1291
  return String(v);
428
1292
  }
@@ -430,39 +1294,145 @@ export const Rcs = (props) => {
430
1294
  }).join('');
431
1295
  };
432
1296
 
433
-
434
- useEffect(() => {
435
- if (isFullMode || isEditFlow) return;
436
- const tokens = [
437
- ...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
438
- ...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
439
- ];
440
- if (!tokens.length) return;
441
- setCardVarMapped((prev) => {
442
- const next = { ...(prev || {}) };
443
- let changed = false;
444
- tokens.forEach((t) => {
445
- const name = getVarNameFromToken(t);
446
- if (name && !Object.prototype.hasOwnProperty.call(next, name)) {
447
- next[name] = '';
448
- changed = true;
449
- }
450
- });
451
- return changed ? next : prev;
1297
+ const buildCarouselCardsForPreview = (cards = []) => {
1298
+ // Track cumulative variable slot index across all cards so resolveTemplateWithMap
1299
+ // uses the correct globalSlot → slotKey for each token (e.g. card 1's {{3}} needs
1300
+ // slotOffset=2 so slotKey becomes "3", not "1").
1301
+ let slotOffset = 0;
1302
+ return (cards || []).map((card = {}) => {
1303
+ const rawTitle = card.title || '';
1304
+ const rawDesc = card.description || '';
1305
+ const titleVarCount = (rawTitle.match(rcsVarRegex) || []).length;
1306
+ const descVarCount = (rawDesc.match(rcsVarRegex) || []).length;
1307
+ const resolvedTitle = !isFullMode
1308
+ ? resolveTemplateWithMap(rawTitle, slotOffset)
1309
+ : rawTitle;
1310
+ const resolvedDesc = !isFullMode
1311
+ ? resolveTemplateWithMap(rawDesc, slotOffset + titleVarCount)
1312
+ : rawDesc;
1313
+ if (!isFullMode) {
1314
+ slotOffset += titleVarCount + descVarCount;
1315
+ }
1316
+ const videoThumb = card.thumbnailSrc || card.videoAsset?.videoThumbnail || '';
1317
+ const videoSrc = card.videoAsset?.videoSrc || '';
1318
+ return {
1319
+ mediaType: (card.mediaType || '').toLowerCase(),
1320
+ imageSrc: card.imageSrc || '',
1321
+ videoSrc,
1322
+ videoPreviewImg: videoThumb,
1323
+ title: resolvedTitle,
1324
+ bodyText: resolvedDesc,
1325
+ suggestions: card.suggestions || [],
1326
+ };
452
1327
  });
453
- }, [isFullMode, templateTitle, templateDesc]);
1328
+ };
1329
+
1330
+ /**
1331
+ * Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
1332
+ * (VarSegmentMessageEditor: isEditFlow || !isFullMode). Full-mode create without edit uses plain
1333
+ * TextArea only — no mapping. Full-mode + edit uses slots; !isFullMode alone is not enough.
1334
+ */
1335
+ const getTemplateContent = useCallback(() => {
1336
+ if (templateType === contentType.carousel) {
1337
+ const carouselDimKey = getCarouselDimensionKey();
1338
+ const carouselImgDims =
1339
+ RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
1340
+ const carouselVidDims =
1341
+ RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
1342
+ || RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
1343
+ return {
1344
+ carouselData: buildCarouselCardsForPreview(carouselData),
1345
+ carouselPreviewDimensions: {
1346
+ imageWidth: carouselImgDims.width,
1347
+ imageHeight: carouselImgDims.height,
1348
+ videoThumbWidth: carouselVidDims.width,
1349
+ videoThumbHeight: carouselVidDims.height,
1350
+ },
1351
+ };
1352
+ }
1353
+ const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
1354
+ const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
1355
+ const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
454
1356
 
1357
+ const isSlotMappingMode = isEditFlow || !isFullMode;
1358
+ const titleVarCountForResolve = isMediaTypeText
1359
+ ? 0
1360
+ : ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
1361
+ const resolvedTitle = isMediaTypeText
1362
+ ? ''
1363
+ : (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
1364
+ const resolvedDesc = isSlotMappingMode
1365
+ ? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
1366
+ : templateDesc;
1367
+
1368
+ const mediaPreview = {};
1369
+ if (isMediaTypeImage && rcsImageSrc) {
1370
+ mediaPreview.rcsImageSrc = rcsImageSrc;
1371
+ }
1372
+ if (isMediaTypeVideo && !isMediaTypeText) {
1373
+ if (rcsThumbnailSrc) {
1374
+ mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
1375
+ } else if (rcsVideoSrc?.videoSrc) {
1376
+ mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
1377
+ }
1378
+ if (rcsThumbnailSrc) {
1379
+ mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
1380
+ }
1381
+ }
1382
+
1383
+ const contentObj = {
1384
+ templateHeader: resolvedTitle,
1385
+ templateMessage: resolvedDesc,
1386
+ ...mediaPreview,
1387
+ ...(suggestions.length > 0 && {
1388
+ suggestions: suggestions,
1389
+ }),
1390
+ };
1391
+
1392
+ return contentObj;
1393
+ }, [
1394
+ templateMediaType,
1395
+ templateTitle,
1396
+ templateDesc,
1397
+ rcsImageSrc,
1398
+ rcsVideoSrc,
1399
+ rcsThumbnailSrc,
1400
+ suggestions,
1401
+ selectedDimension,
1402
+ isFullMode,
1403
+ isEditFlow,
1404
+ cardVarMapped,
1405
+ rcsSpanningSemanticVarNames,
1406
+ carouselData,
1407
+ selectedCarouselHeight,
1408
+ selectedCarouselWidth,
1409
+ ]);
1410
+
1411
+ const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
455
1412
 
456
- const RcsLabel = styled.div`
457
- display: flex;
458
- margin-top: 20px;
459
- `;
460
1413
  const paramObj = params || {};
461
1414
  useEffect(() => {
462
1415
  const { id } = paramObj;
463
1416
  if (id && isFullMode) {
464
1417
  setSpin(true);
465
1418
  actions.getTemplateDetails(id, setSpin);
1419
+ } else if (!id && isFullMode) {
1420
+ // Create New: clear standalone media and ALL possible carousel card Redux slots.
1421
+ // Redux persists across mounts so we must clear unconditionally (carouselData may be [] on fresh mount).
1422
+ updateRcsImageSrc('');
1423
+ setRcsVideoSrc({});
1424
+ setRcsThumbnailSrc('');
1425
+ setAssetList({});
1426
+ setEditFlow(false);
1427
+ actions.clearRcsMediaAsset(0);
1428
+ actions.clearRcsMediaAsset(1);
1429
+ for (let cardIdx = 0; cardIdx < MAX_RCS_CAROUSEL_ALLOWED; cardIdx += 1) {
1430
+ actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIdx));
1431
+ actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIdx));
1432
+ actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIdx));
1433
+ }
1434
+ setCarouselData([]);
1435
+ setCarouselErrors([]);
466
1436
  }
467
1437
  return () => {
468
1438
  actions.clearEditResponse();
@@ -470,59 +1440,68 @@ export const Rcs = (props) => {
470
1440
  }, [paramObj.id]);
471
1441
 
472
1442
  useEffect(() => {
473
- if (!(isEditFlow || !isFullMode)) return;
1443
+ if (!(isEditFlow || !isFullMode)) return;
474
1444
 
475
- const initField = (targetString, currentVarMap, setVarMap, setUpdated) => {
476
- const arr = splitTemplateVarString(targetString);
477
- if (!arr?.length) {
478
- setVarMap({});
479
- setUpdated([]);
480
- return;
1445
+ const titleTokenCount = (templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length;
1446
+
1447
+ const initField = (targetString, setVarMap, slotOffset) => {
1448
+ const arr = splitTemplateVarStringRcs(targetString);
1449
+ if (!arr?.length) {
1450
+ setVarMap({});
1451
+ return;
1452
+ }
1453
+ const nextVarMap = {};
1454
+ let varOrdinal = 0;
1455
+ arr.forEach((elem, idx) => {
1456
+ // Mustache tokens: {{1}}, {{user_name}}, {{tag.FORMAT_1}}, etc.
1457
+ if (rcsVarTestRegex.test(elem)) {
1458
+ const id = `${elem}_${idx}`;
1459
+ const varName = getVarNameFromToken(elem);
1460
+ const globalSlot = slotOffset + varOrdinal;
1461
+ varOrdinal += 1;
1462
+ const mappedValue = resolveCardVarMappedSlotValue(
1463
+ cardVarMapped,
1464
+ varName,
1465
+ globalSlot,
1466
+ isEditLike,
1467
+ rcsSpanningSemanticVarNames.has(varName),
1468
+ );
1469
+ nextVarMap[id] = mappedValue;
481
1470
  }
482
- const nextVarMap = {};
483
- const nextUpdated = [...arr];
484
- arr.forEach((elem, idx) => {
485
- // RCS placeholders are alphanumeric/underscore (e.g. {{user_name}}), not numeric-only
486
- if (rcsVarTestRegex.test(elem)) {
487
- const id = `${elem}_${idx}`;
488
- const varName = getVarNameFromToken(elem);
489
- const mappedValue = (cardVarMapped?.[varName] ?? '').toString();
490
- nextVarMap[id] = mappedValue;
491
- if (mappedValue !== '') {
492
- nextUpdated[idx] = mappedValue;
493
- } else {
494
- nextUpdated[idx] = elem;
495
- }
496
- }
497
- });
498
- setVarMap(nextVarMap);
499
- setUpdated(nextUpdated);
500
- };
1471
+ });
1472
+ setVarMap(nextVarMap);
1473
+ };
501
1474
 
502
- initField(templateTitle, titleVarMappedData, setTitleVarMappedData, setUpdatedTitleData);
503
- initField(templateDesc, descVarMappedData, setDescVarMappedData, setUpdatedDescData);
504
- }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode]);
505
-
506
- useEffect(() => {
507
- if(!isEditFlow && isFullMode){
1475
+ initField(templateTitle, setTitleVarMappedData, 0);
1476
+ initField(templateDesc, setDescVarMappedData, titleTokenCount);
1477
+ }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames]);
1478
+
1479
+ useEffect(() => {
1480
+ if (!isEditFlow && isFullMode) {
508
1481
  setRcsVideoSrc({});
509
1482
  updateRcsImageSrc('');
510
- setUpdateRcsImageSrc('');
511
- updateRcsThumbnailSrc('');
1483
+ setRcsThumbnailSrc('');
512
1484
  setAssetList({});
513
- }
514
- }, [templateMediaType]);
1485
+ }
1486
+ }, [templateMediaType]);
515
1487
 
516
- const templateStatusHelper = (details) => {
517
- const status = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
518
- switch (status) {
1488
+ /** Status on first card — same merged card as title/description/cardVarMapped (library may only set rcsContent at root). */
1489
+ const templateStatusHelper = (cardContentFirst) => {
1490
+ const raw =
1491
+ cardContentFirst?.Status
1492
+ ?? cardContentFirst?.status
1493
+ ?? cardContentFirst?.approvalStatus
1494
+ ?? '';
1495
+ const status = typeof raw === 'string' ? raw.trim() : String(raw);
1496
+ const n = status.toLowerCase();
1497
+ switch (n) {
519
1498
  case RCS_STATUSES.approved:
520
1499
  setTemplateStatus(RCS_STATUSES.approved);
521
1500
  break;
522
1501
  case RCS_STATUSES.pending:
523
1502
  setTemplateStatus(RCS_STATUSES.pending);
524
1503
  break;
525
- case RCS_STATUSES.awaitingApproval:
1504
+ case RCS_STATUSES.awaitingApproval.toLowerCase():
526
1505
  setTemplateStatus(RCS_STATUSES.awaitingApproval);
527
1506
  break;
528
1507
  case RCS_STATUSES.unavailable:
@@ -531,6 +1510,9 @@ export const Rcs = (props) => {
531
1510
  case RCS_STATUSES.rejected:
532
1511
  setTemplateStatus(RCS_STATUSES.rejected);
533
1512
  break;
1513
+ case RCS_STATUSES.created:
1514
+ setTemplateStatus(RCS_STATUSES.created);
1515
+ break;
534
1516
  default:
535
1517
  setTemplateStatus(status);
536
1518
  break;
@@ -584,48 +1566,357 @@ export const Rcs = (props) => {
584
1566
  };
585
1567
 
586
1568
  useEffect(() => {
587
- const details = isFullMode ? rcsData?.templateDetails : templateData;
1569
+ const details = rcsHydrationDetails;
588
1570
  if (details && Object.keys(details).length > 0) {
589
- if (!isFullMode) {
590
- const tempCardVarMapped = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped', {});
591
- setCardVarMapped(tempCardVarMapped);
1571
+ // Library/campaign: match SMS fallback — read from versions… and from top-level rcsContent (getCreativesData / parent shape).
1572
+ const cardFromVersions = get(
1573
+ details,
1574
+ 'versions.base.content.RCS.rcsContent.cardContent[0]',
1575
+ );
1576
+ const rcsContent = get(details, 'versions.base.content.RCS.rcsContent', {});
1577
+ const cardType = (rcsContent?.cardType || '').toString();
1578
+
1579
+ setEditFlow(true);
1580
+ setTemplateName(details?.name || details?.creativeName || '');
1581
+
1582
+ const cardFromTop = get(details, 'rcsContent.cardContent[0]');
1583
+ const card0 = { ...(cardFromTop || {}), ...(cardFromVersions || {}) };
1584
+ const cardVarMappedFromCardContent =
1585
+ card0?.cardVarMapped != null && typeof card0.cardVarMapped === 'object'
1586
+ ? card0.cardVarMapped
1587
+ : {};
1588
+ const cardVarMappedFromRootMirror =
1589
+ details?.rcsCardVarMapped != null && typeof details.rcsCardVarMapped === 'object'
1590
+ ? details.rcsCardVarMapped
1591
+ : {};
1592
+ // Root mirror from getCreativesData / getFormData — campaigns often preserve flat fields when
1593
+ // nested versions.cardContent[0].cardVarMapped is dropped on reload.
1594
+ const mergedCardVarMappedFromPayload = {
1595
+ ...cardVarMappedFromRootMirror,
1596
+ ...cardVarMappedFromCardContent,
1597
+ };
1598
+ const loadedTitleForMap = card0?.title != null ? String(card0.title) : '';
1599
+ const loadedDescForMap = card0?.description != null ? String(card0.description) : '';
1600
+ const hydratedCardVarPayloadSignature = `${loadedTitleForMap}\u0000${loadedDescForMap}\u0000${JSON.stringify(
1601
+ Object.keys(mergedCardVarMappedFromPayload)
1602
+ .sort()
1603
+ .reduce((accumulator, mapKey) => {
1604
+ accumulator[mapKey] = mergedCardVarMappedFromPayload[mapKey];
1605
+ return accumulator;
1606
+ }, {}),
1607
+ )}`;
1608
+ if (lastHydratedRcsCardVarSignatureRef.current !== hydratedCardVarPayloadSignature) {
1609
+ lastHydratedRcsCardVarSignatureRef.current = hydratedCardVarPayloadSignature;
1610
+ setRcsVarSegmentEditorRemountKey((previousKey) => previousKey + 1);
592
1611
  }
593
- const mediaType = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
594
- if (mediaType === RCS_MEDIA_TYPES.NONE) {
1612
+ const tokenListForMap = [
1613
+ ...(loadedTitleForMap ? loadedTitleForMap.match(rcsVarRegex) ?? [] : []),
1614
+ ...(loadedDescForMap ? loadedDescForMap.match(rcsVarRegex) ?? [] : []),
1615
+ ];
1616
+ const orderedTagNamesForMap = tokenListForMap.map((token) => getVarNameFromToken(token)).filter(Boolean);
1617
+ // Full-mode library/API payloads need normalize for legacy slot shapes. Campaign round-trip from
1618
+ // getFormData already stores TagList values as {{TagName}}; normalize can strip or remap them.
1619
+ const cardVarMappedBeforeCoalesce = isFullMode
1620
+ ? normalizeCardVarMapped(mergedCardVarMappedFromPayload, orderedTagNamesForMap)
1621
+ : { ...mergedCardVarMappedFromPayload };
1622
+ const cardVarMappedAfterCoalesce = coalesceCardVarMappedToTemplate(
1623
+ cardVarMappedBeforeCoalesce,
1624
+ loadedTitleForMap,
1625
+ loadedDescForMap,
1626
+ rcsVarRegex,
1627
+ );
1628
+ const cardVarMappedAfterNumericSlotSync = !isFullMode
1629
+ ? syncCardVarMappedSemanticsFromSlots(
1630
+ cardVarMappedAfterCoalesce,
1631
+ loadedTitleForMap,
1632
+ loadedDescForMap,
1633
+ rcsVarRegex,
1634
+ )
1635
+ : cardVarMappedAfterCoalesce;
1636
+ const hydratedCardVarMappedResult = { ...cardVarMappedAfterNumericSlotSync };
1637
+ // Pre-populate variable/tag mappings while opening an existing template in edit flows
1638
+ setCardVarMapped((previousVarMapState) => {
1639
+ const previousVarMap = previousVarMapState ?? {};
1640
+ if (previousVarMap === hydratedCardVarMappedResult) return previousVarMapState;
1641
+ const previousVarMapKeys = Object.keys(previousVarMap);
1642
+ const nextVarMapKeys = Object.keys(hydratedCardVarMappedResult);
1643
+ if (previousVarMapKeys.length === nextVarMapKeys.length) {
1644
+ const allSlotValuesMatchPrevious = previousVarMapKeys.every(
1645
+ (key) => previousVarMap[key] === hydratedCardVarMappedResult[key],
1646
+ );
1647
+ if (allSlotValuesMatchPrevious) return previousVarMapState;
1648
+ }
1649
+ return hydratedCardVarMappedResult;
1650
+ });
1651
+
1652
+ if (cardType.toUpperCase() === contentType.carousel) {
1653
+
1654
+ setTemplateType(contentType.carousel);
1655
+ setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
1656
+ // Library/campaign payloads can store cardSettings/cardContent at the flat top-level
1657
+ // `rcsContent` instead of nested under versions.base.content.RCS (same fallback already
1658
+ // used for the rich_card media lookup below, line ~1793). Without it, cardWidth/height
1659
+ // silently fall back to SMALL/MEDIUM, mismatching the dimensions the thumbnail was
1660
+ // actually uploaded at and causing object-fit:cover to crop the preview image.
1661
+ const cardSettings = rcsContent?.cardSettings
1662
+ || get(details, 'rcsContent.cardSettings', {});
1663
+ const cardWidth = cardSettings?.cardWidth || SMALL;
1664
+ setSelectedCarouselWidth(cardWidth);
1665
+
1666
+ const cards = Array.isArray(rcsContent?.cardContent)
1667
+ ? rcsContent.cardContent
1668
+ : get(details, 'rcsContent.cardContent', []);
1669
+ const firstHeight = cards?.[0]?.media?.height || MEDIUM;
1670
+ setSelectedCarouselHeight(firstHeight);
1671
+ setSelectedCarousel(`${firstHeight}_${cardWidth}`);
1672
+ setActiveCarouselIndex('0');
1673
+
1674
+ const hydratedCards = cards.map((c = {}, idx) => {
1675
+ const mediaType = c.mediaType;
1676
+ const media = c.media || {};
1677
+ const mediaUrl = media.mediaUrl || '';
1678
+ const thumbUrl = media.thumbnailUrl || '';
1679
+ const rawSuggestions = Array.isArray(c.suggestions) ? c.suggestions : [];
1680
+ const suggestions = idx === 0 && rawSuggestions.length === 0
1681
+ ? cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS)
1682
+ : rawSuggestions;
1683
+ return {
1684
+ title: c.title || '',
1685
+ description: c.description || '',
1686
+ mediaType,
1687
+ imageSrc: mediaType === RCS_MEDIA_TYPES.IMAGE ? mediaUrl : '',
1688
+ videoAsset: mediaType === RCS_MEDIA_TYPES.VIDEO ? {
1689
+ videoSrc: mediaUrl,
1690
+ previewUrl: thumbUrl,
1691
+ videoThumbnail: thumbUrl,
1692
+ videoName: c?.media?.videoName || '',
1693
+ } : {},
1694
+ thumbnailSrc: mediaType === RCS_MEDIA_TYPES.VIDEO ? thumbUrl : '',
1695
+ suggestions,
1696
+ };
1697
+ });
1698
+ setCarouselData(
1699
+ hydratedCards.length > 0
1700
+ ? ensureFirstCardDefaultPhoneSuggestions(hydratedCards)
1701
+ : [cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)],
1702
+ );
1703
+ setCarouselErrors(new Array(hydratedCards.length > 0 ? hydratedCards.length : 1).fill({}));
1704
+
1705
+ // Status bar uses first card's Status, keep existing behavior.
1706
+ if (isHostInfoBip) {
1707
+ setTemplateStatus('');
1708
+ } else {
1709
+ const firstCard = cards?.[0] || {};
1710
+ const cardForCarouselStatus = {
1711
+ ...firstCard,
1712
+ Status:
1713
+ firstCard.Status
1714
+ ?? firstCard.status
1715
+ ?? firstCard.approvalStatus
1716
+ ?? get(details, 'templateStatus')
1717
+ ?? get(details, 'approvalStatus')
1718
+ ?? get(details, 'creativeStatus')
1719
+ ?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
1720
+ ?? '',
1721
+ };
1722
+ templateStatusHelper(cardForCarouselStatus);
1723
+ }
1724
+
1725
+ // Hydrate SMS fallback for carousel — same logic as for non-carousel types below.
1726
+ // Without this the early `return` skips fallback hydration entirely, so reopening
1727
+ // a carousel template never restores the saved SMS fallback data.
1728
+ const carouselSmsFallbackContent = mergeRcsSmsFallBackContentFromDetails(details);
1729
+ const carouselSmsBase = get(carouselSmsFallbackContent, 'versions.base', {});
1730
+ const carouselUpdatedEditor = carouselSmsBase['updated-sms-editor'] ?? carouselSmsBase['sms-editor'];
1731
+ const carouselSmsEditor = carouselSmsBase['sms-editor'];
1732
+ const carouselFromNested = Array.isArray(carouselUpdatedEditor)
1733
+ ? carouselUpdatedEditor.join('')
1734
+ : (typeof carouselUpdatedEditor === 'string' ? carouselUpdatedEditor : (carouselSmsEditor || ''));
1735
+ const carouselFallbackMessage = carouselSmsFallbackContent.smsContent
1736
+ || carouselSmsFallbackContent.smsTemplateContent
1737
+ || carouselSmsFallbackContent.message
1738
+ || carouselFromNested
1739
+ || '';
1740
+ const carouselVarMappedFromPayload = carouselSmsFallbackContent[RCS_SMS_FALLBACK_VAR_MAPPED_PROP] || {};
1741
+ const carouselHasVarMapped = Object.keys(carouselVarMappedFromPayload).length > 0;
1742
+ const carouselHasFallbackPayload =
1743
+ carouselSmsFallbackContent
1744
+ && Object.keys(carouselSmsFallbackContent).length > 0
1745
+ && (
1746
+ !!carouselSmsFallbackContent.smsTemplateName
1747
+ || !!carouselFallbackMessage
1748
+ || carouselHasVarMapped
1749
+ );
1750
+ if (carouselHasFallbackPayload) {
1751
+ const carouselUnicodeFromApi =
1752
+ typeof carouselSmsFallbackContent.unicodeValidity === 'boolean'
1753
+ ? carouselSmsFallbackContent.unicodeValidity
1754
+ : (typeof carouselSmsBase['unicode-validity'] === 'boolean' ? carouselSmsBase['unicode-validity'] : true);
1755
+ const carouselRegisteredSenderIds =
1756
+ extractRegisteredSenderIdsFromSmsFallbackRecord(carouselSmsFallbackContent);
1757
+ const carouselNextSmsState = {
1758
+ templateName: carouselSmsFallbackContent.smsTemplateName || '',
1759
+ content: carouselFallbackMessage,
1760
+ templateContent: carouselFallbackMessage,
1761
+ unicodeValidity: carouselUnicodeFromApi,
1762
+ ...(carouselHasVarMapped && { rcsSmsFallbackVarMapped: carouselVarMappedFromPayload }),
1763
+ ...(Array.isArray(carouselRegisteredSenderIds) && carouselRegisteredSenderIds.length > 0
1764
+ ? { registeredSenderIds: carouselRegisteredSenderIds }
1765
+ : {}),
1766
+ };
1767
+ const carouselHydrationKey = JSON.stringify({
1768
+ creativeKey: details._id || details.name || details.creativeName || '',
1769
+ templateName: carouselNextSmsState.templateName,
1770
+ content: carouselNextSmsState.content,
1771
+ unicodeValidity: carouselNextSmsState.unicodeValidity,
1772
+ varMapped: carouselNextSmsState.rcsSmsFallbackVarMapped || {},
1773
+ senderIds: Array.isArray(carouselRegisteredSenderIds)
1774
+ ? carouselRegisteredSenderIds.join('')
1775
+ : '',
1776
+ });
1777
+ if (isFullMode || lastSmsFallbackHydrationKeyRef.current !== carouselHydrationKey) {
1778
+ lastSmsFallbackHydrationKeyRef.current = carouselHydrationKey;
1779
+ setSmsFallbackData(carouselNextSmsState);
1780
+ }
1781
+ } else if (isFullMode || lastSmsFallbackHydrationKeyRef.current !== '__EMPTY__') {
1782
+ lastSmsFallbackHydrationKeyRef.current = '__EMPTY__';
1783
+ setSmsFallbackData(null);
1784
+ }
1785
+
1786
+ return;
1787
+ }
1788
+
1789
+ const mediaType =
1790
+ card0.mediaType
1791
+ || get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
1792
+ if (cardType.toUpperCase() !== contentType.carousel && mediaType === RCS_MEDIA_TYPES.NONE) {
595
1793
  setTemplateType(contentType.text_message);
596
- } else {
1794
+ } else if (cardType.toUpperCase() !== contentType.carousel && mediaType !== RCS_MEDIA_TYPES.NONE) {
597
1795
  setTemplateType(contentType.rich_card);
598
1796
  }
599
- setEditFlow(true);
600
- setTemplateName(details.name || '');
601
- const loadedTitle = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].title', '');
602
- const loadedDesc = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].description', '');
603
- const loadedMap = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped', {});
604
- const normalizedTitle = (!isFullMode && !isEmpty(loadedMap)) ? getUnmappedDesc(loadedTitle, loadedMap) : loadedTitle;
605
- const normalizedDesc = (!isFullMode && !isEmpty(loadedMap)) ? getUnmappedDesc(loadedDesc, loadedMap) : loadedDesc;
1797
+
1798
+ const loadedTitle = loadedTitleForMap;
1799
+ const loadedDesc = loadedDescForMap;
1800
+ const { normalizedTitle, normalizedDesc } = normalizeLibraryLoadedTitleDesc({
1801
+ loadedTitle,
1802
+ loadedDesc,
1803
+ isFullMode,
1804
+ cardVarMappedAfterHydration: hydratedCardVarMappedResult,
1805
+ rcsVarRegex,
1806
+ });
606
1807
  setTemplateTitle(normalizedTitle);
607
1808
  setTemplateDesc(normalizedDesc);
608
- setSuggestions(get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []));
609
- templateStatusHelper(details);
610
- const mediaData = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
611
- const cardSettings = get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '');
1809
+ setSuggestions(
1810
+ Array.isArray(card0.suggestions)
1811
+ ? card0.suggestions
1812
+ : get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []),
1813
+ );
1814
+ const cardForStatus = {
1815
+ ...card0,
1816
+ Status:
1817
+ card0.Status
1818
+ ?? card0.status
1819
+ ?? card0.approvalStatus
1820
+ ?? get(details, 'templateStatus')
1821
+ ?? get(details, 'approvalStatus')
1822
+ ?? get(details, 'creativeStatus')
1823
+ ?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
1824
+ ?? '',
1825
+ };
1826
+ if (isHostInfoBip) {
1827
+ setTemplateStatus('');
1828
+ } else {
1829
+ templateStatusHelper(cardForStatus);
1830
+ }
1831
+ const mediaData =
1832
+ card0.media != null && card0.media !== ''
1833
+ ? card0.media
1834
+ : get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
1835
+ const cardSettings =
1836
+ get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '')
1837
+ || get(details, 'rcsContent.cardSettings', '');
612
1838
  setMediaData(mediaData, mediaType, cardSettings);
613
1839
  if (details?.edit) {
614
1840
  const rcsAccountId = get(details, 'versions.base.content.RCS.rcsContent.accountId', '');
615
1841
  setRcsAccount(rcsAccountId);
616
1842
  }
1843
+
1844
+ const smsFallbackContent = mergeRcsSmsFallBackContentFromDetails(details);
1845
+ const base = get(smsFallbackContent, 'versions.base', {});
1846
+ const updatedEditor = base['updated-sms-editor'] ?? base['sms-editor'];
1847
+ const smsEditor = base['sms-editor'];
1848
+ const fromNested = Array.isArray(updatedEditor)
1849
+ ? updatedEditor.join('')
1850
+ : (typeof updatedEditor === 'string' ? updatedEditor : (smsEditor || ''));
1851
+ const fallbackMessage = smsFallbackContent.smsContent
1852
+ || smsFallbackContent.smsTemplateContent
1853
+ || smsFallbackContent.message
1854
+ || fromNested
1855
+ || '';
1856
+ const varMappedFromPayload = smsFallbackContent[RCS_SMS_FALLBACK_VAR_MAPPED_PROP] || {};
1857
+ const hasVarMapped = Object.keys(varMappedFromPayload).length > 0;
1858
+ const hasFallbackPayload =
1859
+ smsFallbackContent
1860
+ && Object.keys(smsFallbackContent).length > 0
1861
+ && (
1862
+ !!smsFallbackContent.smsTemplateName
1863
+ || !!fallbackMessage
1864
+ || hasVarMapped
1865
+ );
1866
+ if (hasFallbackPayload) {
1867
+ if (!fallbackMessage && !hasVarMapped && process.env.NODE_ENV !== 'production') {
1868
+ console.warn('[RCS SMS Fallback] No message text found in API response. Inspect shape:', smsFallbackContent);
1869
+ }
1870
+ const unicodeFromApi =
1871
+ typeof smsFallbackContent.unicodeValidity === 'boolean'
1872
+ ? smsFallbackContent.unicodeValidity
1873
+ : (typeof base['unicode-validity'] === 'boolean' ? base['unicode-validity'] : true);
1874
+ const registeredSenderIdsFromApi =
1875
+ extractRegisteredSenderIdsFromSmsFallbackRecord(smsFallbackContent);
1876
+ const nextSmsState = {
1877
+ templateName: smsFallbackContent.smsTemplateName || '',
1878
+ content: fallbackMessage,
1879
+ templateContent: fallbackMessage,
1880
+ unicodeValidity: unicodeFromApi,
1881
+ ...(hasVarMapped && { rcsSmsFallbackVarMapped: varMappedFromPayload }),
1882
+ ...(Array.isArray(registeredSenderIdsFromApi) && registeredSenderIdsFromApi.length > 0
1883
+ ? { registeredSenderIds: registeredSenderIdsFromApi }
1884
+ : {}),
1885
+ };
1886
+ const hydrationKey = JSON.stringify({
1887
+ creativeKey: details._id || details.name || details.creativeName || '',
1888
+ templateName: nextSmsState.templateName,
1889
+ content: nextSmsState.content,
1890
+ unicodeValidity: nextSmsState.unicodeValidity,
1891
+ varMapped: nextSmsState.rcsSmsFallbackVarMapped || {},
1892
+ senderIds:
1893
+ Array.isArray(registeredSenderIdsFromApi)
1894
+ ? registeredSenderIdsFromApi.join('\u001f')
1895
+ : '',
1896
+ });
1897
+ if (
1898
+ isFullMode
1899
+ || lastSmsFallbackHydrationKeyRef.current !== hydrationKey
1900
+ ) {
1901
+ lastSmsFallbackHydrationKeyRef.current = hydrationKey;
1902
+ setSmsFallbackData(nextSmsState);
1903
+ }
1904
+ } else if (isFullMode || lastSmsFallbackHydrationKeyRef.current !== '__EMPTY__') {
1905
+ lastSmsFallbackHydrationKeyRef.current = '__EMPTY__';
1906
+ setSmsFallbackData(null);
1907
+ }
617
1908
  }
618
- }, [rcsData, templateData, isFullMode, isEditFlow]);
619
-
1909
+ }, [rcsHydrationDetails, isFullMode, isHostInfoBip]);
620
1910
 
621
1911
  useEffect(() => {
622
1912
  if (templateType === contentType.text_message) {
623
1913
  setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
624
- setTemplateTitle('');
625
- setTemplateTitleError('');
1914
+ // Full-mode create only: switching to plain text clears draft title/media. Never clear when
1915
+ // hydrating library/edit (would wipe templateData after load) — regression seen after SMS fallback work.
626
1916
  if (!isEditFlow && isFullMode) {
1917
+ setTemplateTitle('');
1918
+ setTemplateTitleError('');
627
1919
  setUpdateRcsImageSrc('');
628
- setUpdateRcsVideoSrc({});
629
1920
  setRcsVideoSrc({});
630
1921
  setSelectedDimension(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
631
1922
  }
@@ -652,7 +1943,8 @@ export const Rcs = (props) => {
652
1943
  if (!showDltContainer) {
653
1944
  const { type, module } = location.query || {};
654
1945
  const isEmbedded = type === EMBEDDED;
655
- const context = isEmbedded ? module : DEFAULT;
1946
+ // Match TagList initial fetch (getTagsforContext('Outbound') → context "outbound") so we do not request a different context than the two TagList headers.
1947
+ const context = isEmbedded ? module : 'outbound';
656
1948
  const embedded = isEmbedded ? type : FULL;
657
1949
  const query = {
658
1950
  layout: SMS,
@@ -663,9 +1955,9 @@ export const Rcs = (props) => {
663
1955
  if (getDefaultTags) {
664
1956
  query.context = getDefaultTags;
665
1957
  }
666
- globalActions.fetchSchemaForEntity(query);
1958
+ fetchTagSchemaIfNewQuery(query);
667
1959
  }
668
- }, [showDltContainer]);
1960
+ }, [showDltContainer, fetchTagSchemaIfNewQuery]);
669
1961
 
670
1962
  useEffect(() => {
671
1963
  let tag = get(metaEntities, `tags.standard`, []);
@@ -688,16 +1980,128 @@ export const Rcs = (props) => {
688
1980
  context,
689
1981
  embedded,
690
1982
  };
1983
+ if (getDefaultTags) {
1984
+ query.context = getDefaultTags;
1985
+ }
1986
+ fetchTagSchemaIfNewQuery(query);
691
1987
  globalActions.fetchSchemaForEntity(query);
692
1988
  };
693
1989
 
694
- const onTagSelect = (data, areaId) => {
695
- if (!areaId) return;
696
- const sep = areaId.lastIndexOf('_');
1990
+ const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
1991
+ if (!templateStr || !numericVarName || !tagName) return templateStr;
1992
+ const re = buildRcsNumericMustachePlaceholderRegex(numericVarName);
1993
+ return templateStr.replace(re, `{{${tagName}}}`);
1994
+ };
1995
+
1996
+ const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
1997
+ if (!varSegmentCompositeDomId) return;
1998
+ const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
1999
+ if (underscoreIndexInCompositeId === -1) return;
2000
+ const segmentIndexSuffix = varSegmentCompositeDomId.slice(underscoreIndexInCompositeId + 1);
2001
+ if (segmentIndexSuffix === '' || isNaN(Number(segmentIndexSuffix))) return;
2002
+ const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
2003
+ const semanticOrNumericVarName = getVarNameFromToken(mustacheTokenFromCompositeId);
2004
+ if (!semanticOrNumericVarName) return;
2005
+ const isNumericPlaceholderSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(semanticOrNumericVarName));
2006
+ const templateStringForField =
2007
+ tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
2008
+ const titleOrMessageFieldType =
2009
+ tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
2010
+ const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
2011
+ varSegmentCompositeDomId,
2012
+ templateStringForField,
2013
+ titleOrMessageFieldType,
2014
+ );
2015
+ const cardVarMappedNumericSlotKey =
2016
+ globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined
2017
+ ? String(globalVarSlotIndexZeroBased + 1)
2018
+ : null;
2019
+
2020
+ setCardVarMapped((previousCardVarMapped) => {
2021
+ const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
2022
+ if (isNumericPlaceholderSlot) {
2023
+ const existingValueBeforeAppend = (
2024
+ previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
2025
+ ).toString();
2026
+ const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
2027
+ delete updatedCardVarMapped[semanticOrNumericVarName];
2028
+ updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
2029
+ } else {
2030
+ // Same semantic token (e.g. {{adv}}) in title and body must not share one map key for
2031
+ // "existing value" — that appends the new tag onto the other field. Match handleRcsVarChange:
2032
+ // read/write the global numeric slot only and drop the shared semantic key.
2033
+ const existingValueBeforeAppend = cardVarMappedNumericSlotKey
2034
+ ? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
2035
+ : String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
2036
+ const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
2037
+ delete updatedCardVarMapped[semanticOrNumericVarName];
2038
+ if (cardVarMappedNumericSlotKey) {
2039
+ updatedCardVarMapped[cardVarMappedNumericSlotKey] = mappedValueAfterAppendingTag;
2040
+ // Sync all other intra-field occurrences of the same variable (cross-field slots stay independent)
2041
+ const fieldOffset = titleOrMessageFieldType === TITLE_TEXT ? 0 : (templateTitle?.match(rcsVarRegex) ?? []).length;
2042
+ const fieldSegments = splitTemplateVarStringRcs(templateStringForField ?? '');
2043
+ let varOrdinal = 0;
2044
+ for (let i = 0; i < fieldSegments.length; i++) {
2045
+ if (rcsVarTestRegex.test(fieldSegments[i])) {
2046
+ const slotVarName = getVarNameFromToken(fieldSegments[i]);
2047
+ const globalSlot = fieldOffset + varOrdinal;
2048
+ varOrdinal += 1;
2049
+ if (slotVarName === semanticOrNumericVarName && globalSlot !== globalVarSlotIndexZeroBased) {
2050
+ updatedCardVarMapped[String(globalSlot + 1)] = mappedValueAfterAppendingTag;
2051
+ }
2052
+ }
2053
+ }
2054
+ } else {
2055
+ updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
2056
+ }
2057
+ }
2058
+ return updatedCardVarMapped;
2059
+ });
2060
+
2061
+ if (
2062
+ isNumericPlaceholderSlot
2063
+ && (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC)
2064
+ ) {
2065
+ if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
2066
+ setTemplateTitle((previousTitle) => {
2067
+ const titleAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
2068
+ previousTitle || '',
2069
+ semanticOrNumericVarName,
2070
+ selectedTagNameFromPicker,
2071
+ );
2072
+ if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
2073
+ setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
2074
+ // 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
2075
+ setRcsVarSegmentEditorRemountKey((k) => k + 1);
2076
+ return titleAfterReplacingNumericPlaceholder;
2077
+ });
2078
+ } else {
2079
+ setTemplateDesc((previousDescription) => {
2080
+ const descriptionAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
2081
+ previousDescription || '',
2082
+ semanticOrNumericVarName,
2083
+ selectedTagNameFromPicker,
2084
+ );
2085
+ if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
2086
+ return previousDescription;
2087
+ }
2088
+ setTemplateDescError(variableErrorHandling(descriptionAfterReplacingNumericPlaceholder));
2089
+ setRcsVarSegmentEditorRemountKey((k) => k + 1);
2090
+ return descriptionAfterReplacingNumericPlaceholder;
2091
+ });
2092
+ }
2093
+ }
2094
+ };
2095
+
2096
+ const onTitleTagSelect = (tagName) => onTagSelect(tagName, titleTextAreaId, RCS_TAG_AREA_FIELD_TITLE);
2097
+
2098
+ const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
2099
+
2100
+ const onCarouselTagSelect = (data) => {
2101
+ if (!carouselFocusedVarId) return;
2102
+ const sep = carouselFocusedVarId.lastIndexOf('_');
697
2103
  if (sep === -1) return;
698
- const numId = Number(areaId.slice(sep + 1));
699
- if (isNaN(numId)) return;
700
- const token = areaId.slice(0, sep);
2104
+ const token = carouselFocusedVarId.slice(0, sep);
701
2105
  const variableName = getVarNameFromToken(token);
702
2106
  if (!variableName) return;
703
2107
  setCardVarMapped((prev) => {
@@ -710,10 +2114,6 @@ export const Rcs = (props) => {
710
2114
  });
711
2115
  };
712
2116
 
713
- const onTitleTagSelect = (data) => onTagSelect(data, titleTextAreaId);
714
-
715
- const onDescTagSelect = (data) => onTagSelect(data, descTextAreaId);
716
-
717
2117
  const onTagSelectFallback = (data) => {
718
2118
  const tempMsg = `${fallbackMessage}{{${data}}}`;
719
2119
  const error = fallbackMessageErrorHandler(tempMsg);
@@ -733,15 +2133,14 @@ export const Rcs = (props) => {
733
2133
  };
734
2134
  // tag Code end
735
2135
 
736
- const renderLabel = (value, showLabel, desc) => {
737
- const isTemplateApproved = (templateStatus === RCS_STATUSES.approved);
2136
+ const renderLabel = (value, desc) => {
738
2137
  return (
739
2138
  <>
740
- <RcsLabel>
2139
+ <div className="rcs-form-section-heading">
741
2140
  <CapHeading type="h4">{formatMessage(messages[value])}</CapHeading>
742
- </RcsLabel>
2141
+ </div>
743
2142
  {desc && (
744
- <CapLabel type="label3" style={{ marginBottom: '17px' }}>
2143
+ <CapLabel type="label3" className="rcs-form-field-caption">
745
2144
  {formatMessage(messages[desc])}
746
2145
  </CapLabel>
747
2146
  )}
@@ -760,13 +2159,8 @@ export const Rcs = (props) => {
760
2159
  },
761
2160
  {
762
2161
  value: contentType.carousel,
763
- label: (
764
- <CapTooltip title={formatMessage(messages.disabledCarouselTooltip)}>
765
- {formatMessage(messages.carousel)}
766
- </CapTooltip>
767
- ),
768
- disabled: true,
769
- },
2162
+ label: formatMessage(messages.carousel),
2163
+ }
770
2164
  ];
771
2165
 
772
2166
  const onTemplateNameChange = ({ target: { value } }) => {
@@ -777,6 +2171,10 @@ export const Rcs = (props) => {
777
2171
 
778
2172
  const onTemplateTypeChange = ({ target: { value } }) => {
779
2173
  setTemplateType(value);
2174
+ // Carousel has per-card media; keep template-level media type neutral.
2175
+ if (value === contentType.carousel) {
2176
+ setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
2177
+ }
780
2178
  };
781
2179
 
782
2180
 
@@ -797,8 +2195,39 @@ export const Rcs = (props) => {
797
2195
  const onTemplateMediaTypeChange = ({ target: { value } }) => {
798
2196
  setTemplateMediaType(value);
799
2197
  };
800
-
801
-
2198
+ const renderedRCSEditMessage = (descArray, type) => {
2199
+ const renderArray = [];
2200
+ if (descArray?.length) {
2201
+ descArray.forEach((elem, index) => {
2202
+ if (rcsVarTestRegex.test(elem)) {
2203
+ // Variable input
2204
+ renderArray.push(
2205
+ <TextArea
2206
+ id={`${elem}_${index}`}
2207
+ key={`${elem}_${index}`}
2208
+ placeholder={`enter the value for ${elem}`}
2209
+ autosize={{ minRows: 1, maxRows: 3 }}
2210
+ onChange={e => textAreaValueChange(e, type)}
2211
+ value={textAreaValue(index, type)}
2212
+ onFocus={(e) => setTextAreaId(e, type)}
2213
+ />
2214
+ );
2215
+ } else if (elem) {
2216
+ // Static text
2217
+ renderArray.push(
2218
+ <TextArea
2219
+ key={`static_${index}`}
2220
+ value={elem}
2221
+ autosize={{ minRows: 1, maxRows: 3 }}
2222
+ disabled
2223
+ className="rcs-edit-template-message-static-textarea"
2224
+ />
2225
+ );
2226
+ }
2227
+ });
2228
+ }
2229
+ return renderArray;
2230
+ };
802
2231
  const onTemplateTitleChange = ({ target: { value } }) => {
803
2232
  let errorMessage = false;
804
2233
  if (templateType === contentType.rich_card && !value.trim()) {
@@ -814,7 +2243,7 @@ export const Rcs = (props) => {
814
2243
 
815
2244
  const onTemplateDescChange = ({ target: { value } }) => {
816
2245
  let errorMessage = false;
817
- if(templateType === contentType.text_message && value?.length > RCS_TEXT_MESSAGE_MAX_LENGTH){
2246
+ if(templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)){
818
2247
  errorMessage = formatMessage(messages.templateMessageLengthError);
819
2248
  } else if(templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH){
820
2249
  errorMessage = formatMessage(messages.templateMessageLengthError);
@@ -830,16 +2259,16 @@ export const Rcs = (props) => {
830
2259
 
831
2260
  const templateDescErrorHandler = (value) => {
832
2261
  let errorMessage = false;
833
- const { isBraceError } = validateTags({
2262
+ const { unsupportedTags, isBraceError } = validateTags({
834
2263
  content: value,
835
2264
  tagsParam: tags,
2265
+ injectedTagsParams: injectedTags,
836
2266
  location,
837
2267
  tagModule: getDefaultTags,
838
- isFullMode,
839
2268
  }) || {};
840
2269
 
841
2270
  const maxLength = templateType === contentType.text_message
842
- ? RCS_TEXT_MESSAGE_MAX_LENGTH
2271
+ ? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)
843
2272
  : RCS_RICH_CARD_MAX_LENGTH;
844
2273
 
845
2274
  if (value === '' && isMediaTypeText) {
@@ -862,10 +2291,25 @@ export const Rcs = (props) => {
862
2291
  };
863
2292
 
864
2293
  const fallbackMessageErrorHandler = (value) => {
2294
+ let errorMessage = false;
2295
+ const { unsupportedTags } = validateTags({
2296
+ content: value,
2297
+ tagsParam: tags,
2298
+ injectedTagsParams: injectedTags,
2299
+ location,
2300
+ tagModule: getDefaultTags,
2301
+ }) || {};
865
2302
  if (value?.length > FALLBACK_MESSAGE_MAX_LENGTH) {
866
- return formatMessage(messages.fallbackMsgLenError);
2303
+ errorMessage = formatMessage(messages.fallbackMsgLenError);
2304
+ } else if (unsupportedTags?.length > 0) {
2305
+ errorMessage = formatMessage(
2306
+ globalMessages.unsupportedTagsValidationError,
2307
+ {
2308
+ unsupportedTags,
2309
+ },
2310
+ );
867
2311
  }
868
- return false;
2312
+ return errorMessage;
869
2313
  };
870
2314
 
871
2315
  // Check for forbidden characters: square brackets [] and single curly braces {}
@@ -912,53 +2356,43 @@ export const Rcs = (props) => {
912
2356
  if(!isFullMode){
913
2357
  return false;
914
2358
  }
915
- if (!/^\w+$/.test(paramName)) {
2359
+ // Allow Liquid-style param names: letters, digits, underscore, dots (e.g. dynamic_expiry_date_after_3_days.FORMAT_1)
2360
+ if (!/^[\w.]+$/.test(paramName)) {
916
2361
  return formatMessage(messages.unknownCharactersError);
917
2362
  }
918
2363
  }
919
2364
  return false;
920
2365
  };
921
-
922
- const templateHeaderErrorHandler = (value) => {
923
- let errorMessage = false;
924
- if (value?.length > TEMPLATE_HEADER_MAX_LENGTH) {
925
- errorMessage = formatMessage(messages.templateHeaderLengthError);
926
- } else {
927
- errorMessage = variableErrorHandling(value);
928
- }
929
- return errorMessage;
930
- };
931
-
932
-
933
- const templateMessageErrorHandler = (value) => {
934
- let errorMessage = false;
935
- if (value === '') {
936
- errorMessage = formatMessage(messages.emptyTemplateMessageErrorMessage);
937
- } else if (
938
- value?.length
939
- > TEMPLATE_MESSAGE_MAX_LENGTH
940
- ) {
941
- errorMessage = formatMessage(messages.templateMessageLengthError);
942
- } else {
943
- errorMessage = variableErrorHandling(value);
944
- }
945
- return errorMessage;
946
- };
947
-
948
2366
 
949
2367
  const onMessageAddVar = () => {
950
- onAddVar(MESSAGE_TEXT, templateDesc, rcsVarRegex);
2368
+ onAddVar(templateDesc);
951
2369
  };
952
2370
 
953
- const onAddVar = (type, messageContent, regex) => {
954
- // Always append the next variable at the end, like WhatsApp
955
- const existingVars = messageContent.match(/\{\{(\d+)\}\}/g) || [];
956
- const existingNumbers = existingVars.map(v => parseInt(v.match(/\d+/)[0], 10));
2371
+ /**
2372
+ * Returns the smallest positive integer not already used as a `{{N}}` variable
2373
+ * in either the title or description fields, or null if the limit (19) is reached.
2374
+ * Scans both fields so title and description vars never share the same number
2375
+ * (duplicate numbers would share a cardVarMapped key and bleed values across fields).
2376
+ */
2377
+ const getNextRcsNumericVarNumber = (titleStr, descStr) => {
2378
+ const allExistingVars = [
2379
+ ...(titleStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
2380
+ ...(descStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
2381
+ ];
2382
+ const existingNumbers = allExistingVars.flatMap(v => {
2383
+ const m = v.match(/\d+/);
2384
+ return m ? [parseInt(m[0], 10)] : [];
2385
+ });
957
2386
  let nextNumber = 1;
958
2387
  while (existingNumbers.includes(nextNumber)) {
959
2388
  nextNumber++;
960
2389
  }
961
- if (nextNumber > 19) {
2390
+ return nextNumber > 19 ? null : nextNumber;
2391
+ };
2392
+
2393
+ const onAddVar = (messageContent) => {
2394
+ const nextNumber = getNextRcsNumericVarNumber(templateTitle, messageContent);
2395
+ if (nextNumber === null) {
962
2396
  return;
963
2397
  }
964
2398
  const nextVar = `{{${nextNumber}}}`;
@@ -970,15 +2404,13 @@ const onAddVar = (type, messageContent, regex) => {
970
2404
  };
971
2405
 
972
2406
  const onTitleAddVar = () => {
973
- // Always append the next variable at the end, like WhatsApp
2407
+ // Scan both title AND description so the new title var number doesn't
2408
+ // duplicate a number already used in the description. Duplicate numeric
2409
+ // names would share the same cardVarMapped semantic key, causing the
2410
+ // description slot to reflect the title slot value and vice-versa.
974
2411
  const messageContent = templateTitle;
975
- const existingVars = messageContent.match(/\{\{(\d+)\}\}/g) || [];
976
- const existingNumbers = existingVars.map(v => parseInt(v.match(/\d+/)[0], 10));
977
- let nextNumber = 1;
978
- while (existingNumbers.includes(nextNumber)) {
979
- nextNumber++;
980
- }
981
- if (nextNumber > 19) {
2412
+ const nextNumber = getNextRcsNumericVarNumber(templateTitle, templateDesc);
2413
+ if (nextNumber === null) {
982
2414
  return;
983
2415
  }
984
2416
  const nextVar = `{{${nextNumber}}}`;
@@ -990,26 +2422,35 @@ const onTitleAddVar = () => {
990
2422
  setTemplateTitleError(error);
991
2423
  };
992
2424
 
993
-
994
- const splitTemplateVarString = (str) => {
995
- if (!str) return [];
996
- const validVarArr = str.match(rcsVarRegex) || [];
997
- const templateVarArray = [];
998
- let content = str;
999
- while (content?.length !== 0) {
1000
- const index = content.indexOf(validVarArr?.[0]);
1001
- if (index !== -1) {
1002
- templateVarArray.push(content.substring(0, index));
1003
- templateVarArray.push(validVarArr?.[0]);
1004
- content = content.substring(index + validVarArr?.[0]?.length, content?.length);
1005
- validVarArr?.shift();
1006
- } else {
1007
- templateVarArray.push(content);
1008
- break;
1009
- }
1010
- }
1011
- return templateVarArray.filter(Boolean);
1012
- };
2425
+ // Carousel: global variables across the whole carousel (all cards, title+body)
2426
+ const getNextCarouselVarToken = () => {
2427
+ const nums = [];
2428
+ (carouselData || []).forEach((c = {}) => {
2429
+ const titleMatches = (c.title || '').match(RCS_NUMERIC_VAR_TOKEN_REGEX) || [];
2430
+ const descMatches = (c.description || '').match(RCS_NUMERIC_VAR_TOKEN_REGEX) || [];
2431
+ [...titleMatches, ...descMatches].forEach((tok) => {
2432
+ const n = parseInt((tok.match(/\d+/) || [])[0], 10);
2433
+ if (!Number.isNaN(n)) nums.push(n);
2434
+ });
2435
+ });
2436
+ const existing = new Set(nums);
2437
+ let nextNumber = 1;
2438
+ while (existing.has(nextNumber)) nextNumber++;
2439
+ if (nextNumber > 19) return '';
2440
+ return `{{${nextNumber}}}`;
2441
+ };
2442
+
2443
+ const appendVarToCarouselField = (cardIndex, fieldName) => {
2444
+ const token = getNextCarouselVarToken();
2445
+ if (!token) return;
2446
+ setCarouselData((prev = []) => {
2447
+ const updated = cloneDeep(prev);
2448
+ if (!updated[cardIndex]) return prev;
2449
+ const current = (updated[cardIndex][fieldName] || '').toString();
2450
+ updated[cardIndex][fieldName] = `${current}${token}`;
2451
+ return updated;
2452
+ });
2453
+ };
1013
2454
 
1014
2455
  const textAreaValue = (idValue, type) => {
1015
2456
  if (idValue >= 0) {
@@ -1025,6 +2466,46 @@ const splitTemplateVarString = (str) => {
1025
2466
  return "";
1026
2467
  };
1027
2468
 
2469
+ // Carousel: render variable-value editor for a given template string (title/description).
2470
+ // This matches rich-card/text edit behavior: static pieces are read-only, variable tokens are editable.
2471
+ const renderCarouselEditMessage = (templateStr) => {
2472
+ const renderArray = [];
2473
+ const templateArr = splitTemplateVarString(templateStr);
2474
+ if (templateArr?.length) {
2475
+ templateArr.forEach((elem, index) => {
2476
+ if (rcsVarTestRegex.test(elem)) {
2477
+ const varName = getVarNameFromToken(elem);
2478
+ renderArray.push(
2479
+ <div key={`${elem}_${index}`} className="var-segment-message-editor__var-slot">
2480
+ <TextArea
2481
+ id={`${elem}_${index}`}
2482
+ placeholder={`enter the value for ${elem}`}
2483
+ autosize={{ minRows: 1, maxRows: 3 }}
2484
+ onChange={(e) => textAreaValueChange(e, TITLE_TEXT)}
2485
+ value={varName ? ((cardVarMapped?.[varName] ?? '').toString()) : ''}
2486
+ onFocus={(e) => {
2487
+ const id = e?.target?.id || e?.currentTarget?.id || '';
2488
+ setCarouselFocusedVarId(id);
2489
+ }}
2490
+ />
2491
+ </div>
2492
+ );
2493
+ } else if (elem) {
2494
+ renderArray.push(
2495
+ <CapHeading
2496
+ key={`static_${index}`}
2497
+ type="h4"
2498
+ className="rcs-edit-template-message-split"
2499
+ >
2500
+ {elem}
2501
+ </CapHeading>
2502
+ );
2503
+ }
2504
+ });
2505
+ }
2506
+ return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
2507
+ };
2508
+
1028
2509
  const textAreaValueChange = (e, type) => {
1029
2510
  const value = e?.target?.value ?? '';
1030
2511
  const id = e?.target?.id || e?.currentTarget?.id || '';
@@ -1044,7 +2525,9 @@ const splitTemplateVarString = (str) => {
1044
2525
  };
1045
2526
 
1046
2527
  const setTextAreaId = (e, type) => {
1047
- const id = e?.target?.id || e?.currentTarget?.id || '';
2528
+ // VarSegmentMessageEditor calls onFocus(id) with a plain string; DOM events
2529
+ // have an `.target.id` shape. Support both.
2530
+ const id = typeof e === 'string' ? e : (e?.target?.id || e?.currentTarget?.id || '');
1048
2531
  if (!id) return;
1049
2532
  if (type === TITLE_TEXT) setTitleTextAreaId(id);
1050
2533
  else setDescTextAreaId(id);
@@ -1063,10 +2546,12 @@ const splitTemplateVarString = (str) => {
1063
2546
  </CapRow>
1064
2547
  }
1065
2548
  description={
1066
- <CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
2549
+ !isEditFlow && (
2550
+ <CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
2551
+ )
1067
2552
  }
1068
2553
  />
1069
-
2554
+
1070
2555
  <CapActionButton
1071
2556
  buttonType={buttonType}
1072
2557
  updateButtonChange={updateButtonChange}
@@ -1075,44 +2560,85 @@ const splitTemplateVarString = (str) => {
1075
2560
  isEditFlow={isEditFlow}
1076
2561
  isFullMode={isFullMode}
1077
2562
  maxButtons={MAX_BUTTONS}
1078
- />
2563
+ host={hostName}
2564
+ />
1079
2565
  </>
1080
2566
  );
1081
2567
  };
1082
2568
 
1083
- const renderedRCSEditMessage = (descArray, type) => {
1084
- const renderArray = [];
1085
- if (descArray?.length) {
1086
- descArray.forEach((elem, index) => {
1087
- if (rcsVarTestRegex.test(elem)) {
1088
- // Variable input
1089
- renderArray.push(
1090
- <TextArea
1091
- id={`${elem}_${index}`}
1092
- key={`${elem}_${index}`}
1093
- placeholder={`enter the value for ${elem}`}
1094
- autosize={{ minRows: 1, maxRows: 3 }}
1095
- onChange={e => textAreaValueChange(e, type)}
1096
- value={textAreaValue(index, type)}
1097
- onFocus={(e) => setTextAreaId(e, type)}
1098
- />
1099
- );
1100
- } else if (elem) {
1101
- // Static text
1102
- renderArray.push(
1103
- <TextArea
1104
- key={`static_${index}`}
1105
- value={elem}
1106
- autosize={{ minRows: 1, maxRows: 3 }}
1107
- disabled
1108
- className="rcs-edit-template-message-static-textarea"
1109
- style={{ background: '#fafafa', color: '#888' }}
1110
- />
1111
- );
2569
+ const getRcsValueMap = (fieldTemplateString, fieldType) => {
2570
+ if (!fieldTemplateString) return {};
2571
+ const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
2572
+ const slotOffset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
2573
+ const templateSegments = splitTemplateVarStringRcs(fieldTemplateString);
2574
+ const segmentIdToResolvedValue = {};
2575
+ let varOrdinal = 0;
2576
+ templateSegments.forEach((segmentToken, segmentIndexInField) => {
2577
+ if (rcsVarTestRegex.test(segmentToken)) {
2578
+ const varSegmentCompositeId = `${segmentToken}_${segmentIndexInField}`;
2579
+ const varName = getVarNameFromToken(segmentToken);
2580
+ const globalSlot = slotOffset + varOrdinal;
2581
+ varOrdinal += 1;
2582
+ segmentIdToResolvedValue[varSegmentCompositeId] = resolveCardVarMappedSlotValue(
2583
+ cardVarMapped,
2584
+ varName,
2585
+ globalSlot,
2586
+ isEditLike,
2587
+ rcsSpanningSemanticVarNames.has(varName),
2588
+ );
2589
+ }
2590
+ });
2591
+ return segmentIdToResolvedValue;
2592
+ };
2593
+
2594
+ const titleVarSegmentValueMapById = useMemo(
2595
+ () => getRcsValueMap(templateTitle, TITLE_TEXT),
2596
+ [templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
2597
+ );
2598
+ const descriptionVarSegmentValueMapById = useMemo(
2599
+ () => getRcsValueMap(templateDesc, MESSAGE_TEXT),
2600
+ [templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
2601
+ );
2602
+
2603
+ const handleRcsVarChange = (varSegmentCompositeDomId, value, type) => {
2604
+ const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
2605
+ if (underscoreIndexInCompositeId === -1) return;
2606
+ const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
2607
+ const variableName = getVarNameFromToken(mustacheTokenFromCompositeId);
2608
+ if (variableName === undefined || variableName === null || variableName === '') return;
2609
+ const isInvalidValue = value?.trim() === '';
2610
+ const coercedSlotValue = isInvalidValue ? '' : value;
2611
+ const templateStringForField = type === TITLE_TEXT ? templateTitle : templateDesc;
2612
+ const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
2613
+ varSegmentCompositeDomId,
2614
+ templateStringForField,
2615
+ type,
2616
+ );
2617
+ const fieldOffset = type === TITLE_TEXT ? 0 : (templateTitle?.match(rcsVarRegex) ?? []).length;
2618
+ setCardVarMapped((previousCardVarMapped) => {
2619
+ const updatedCardVarMapped = { ...previousCardVarMapped };
2620
+ // Remove stale semantic key: keeping it causes every other slot sharing the same
2621
+ // variable name (e.g. {{adv}} in both title and description) to read the same value
2622
+ // via the semantic-key fallback in resolveCardVarMappedSlotValue.
2623
+ delete updatedCardVarMapped[variableName];
2624
+ if (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined) {
2625
+ updatedCardVarMapped[String(globalVarSlotIndexZeroBased + 1)] = coercedSlotValue;
2626
+ // Sync all other intra-field occurrences of the same variable (cross-field slots stay independent)
2627
+ const fieldSegments = splitTemplateVarStringRcs(templateStringForField ?? '');
2628
+ let varOrdinal = 0;
2629
+ for (let i = 0; i < fieldSegments.length; i++) {
2630
+ if (rcsVarTestRegex.test(fieldSegments[i])) {
2631
+ const slotVarName = getVarNameFromToken(fieldSegments[i]);
2632
+ const globalSlot = fieldOffset + varOrdinal;
2633
+ varOrdinal += 1;
2634
+ if (slotVarName === variableName && globalSlot !== globalVarSlotIndexZeroBased) {
2635
+ updatedCardVarMapped[String(globalSlot + 1)] = coercedSlotValue;
2636
+ }
2637
+ }
1112
2638
  }
1113
- });
1114
- }
1115
- return renderArray;
2639
+ }
2640
+ return updatedCardVarMapped;
2641
+ });
1116
2642
  };
1117
2643
 
1118
2644
  const renderTextComponent = () => {
@@ -1132,6 +2658,7 @@ const splitTemplateVarString = (str) => {
1132
2658
  }
1133
2659
  suffix={
1134
2660
  <>
2661
+
1135
2662
  {(isEditFlow || !isFullMode) ? (
1136
2663
  <TagList
1137
2664
  label={formatMessage(globalMessages.addLabels)}
@@ -1148,18 +2675,28 @@ const splitTemplateVarString = (str) => {
1148
2675
  type="flat"
1149
2676
  isAddBtn
1150
2677
  onClick={onTitleAddVar}
1151
- disabled={!!templateTitleError}
2678
+ disabled={templateTitleError}
1152
2679
  >
1153
2680
  {formatMessage(messages.addVar)}
1154
2681
  </CapButton>
1155
- )}
2682
+ )}
1156
2683
  </>
1157
- }
2684
+ }
2685
+ />
2686
+
2687
+ {(isEditFlow || !isFullMode) ? (
2688
+ <VarSegmentMessageEditor
2689
+ key={`rcs-title-vars-${rcsVarSegmentEditorRemountKey}`}
2690
+ templateString={templateTitle}
2691
+ valueMap={titleVarSegmentValueMapById}
2692
+ onChange={(id, value) => handleRcsVarChange(id, value, TITLE_TEXT)}
2693
+ onFocus={(id) => setTitleTextAreaId(id)}
2694
+ varRegex={rcsVarRegex}
2695
+ placeholderPrefix=""
2696
+ getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
1158
2697
  />
1159
- <div className="rcs_text_area_wrapper">
1160
- {(isEditFlow || !isFullMode) ? (
1161
- renderedRCSEditMessage(splitTemplateVarString(templateTitle), TITLE_TEXT)
1162
2698
  ) : (
2699
+ <div className="rcs_text_area_wrapper">
1163
2700
  <CapInput
1164
2701
  className={`rcs-template-title-input ${
1165
2702
  !isTemplateApproved ? "rcs-edit-disabled" : ""
@@ -1172,8 +2709,8 @@ const splitTemplateVarString = (str) => {
1172
2709
  errorMessage={templateTitleError}
1173
2710
  disabled={isEditFlow || !isFullMode}
1174
2711
  />
2712
+ </div>
1175
2713
  )}
1176
- </div>
1177
2714
  {(isEditFlow || !isFullMode) && templateTitleError && (
1178
2715
  <CapError className="rcs-template-title-error">
1179
2716
  {templateTitleError}
@@ -1181,7 +2718,7 @@ const splitTemplateVarString = (str) => {
1181
2718
  )}
1182
2719
  {!isEditFlow && isFullMode && renderTitleCharacterCount()}
1183
2720
  </>
1184
- )}
2721
+ )}
1185
2722
 
1186
2723
  {/* Template Message */}
1187
2724
  <CapRow useLegacy id="rcs-template-message-label">
@@ -1219,9 +2756,21 @@ const splitTemplateVarString = (str) => {
1219
2756
  />
1220
2757
  </CapRow>
1221
2758
  <CapRow useLegacy className="rcs-create-template-message-input">
1222
- <div className="rcs_text_area_wrapper">
1223
- {(isEditFlow || !isFullMode)
1224
- ? renderedRCSEditMessage(splitTemplateVarString(templateDesc), MESSAGE_TEXT)
2759
+ {/* 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. */}
2760
+ <CapRow className="rcs_text_area_wrapper">
2761
+ {(isEditFlow || !isFullMode)?
2762
+ (
2763
+ <VarSegmentMessageEditor
2764
+ key={`rcs-desc-vars-${rcsVarSegmentEditorRemountKey}`}
2765
+ templateString={templateDesc}
2766
+ valueMap={descriptionVarSegmentValueMapById}
2767
+ onChange={(id, value) => handleRcsVarChange(id, value, MESSAGE_TEXT)}
2768
+ onFocus={(id) => setDescTextAreaId(id)}
2769
+ varRegex={rcsVarRegex}
2770
+ placeholderPrefix=""
2771
+ getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
2772
+ />
2773
+ )
1225
2774
  : (
1226
2775
  <>
1227
2776
  <TextArea
@@ -1259,13 +2808,15 @@ const splitTemplateVarString = (str) => {
1259
2808
  </>
1260
2809
  )
1261
2810
  }
1262
- </div>
2811
+ </CapRow>
1263
2812
  {(isEditFlow || !isFullMode) && templateDescError && (
1264
2813
  <CapError className="rcs-template-message-error">
1265
2814
  {templateDescError}
1266
2815
  </CapError>
1267
2816
  )}
1268
- {!isEditFlow && isFullMode && renderDescriptionCharacterCount()}
2817
+ {(isEditFlow || !isFullMode)
2818
+ ? renderDescriptionCharacterCount('rcs-character-count rcs-character-count--compact')
2819
+ : (!isEditFlow && isFullMode && renderDescriptionCharacterCount())}
1269
2820
  {!isFullMode && hasTag() && (
1270
2821
  <CapAlert
1271
2822
  message={
@@ -1279,24 +2830,13 @@ const splitTemplateVarString = (str) => {
1279
2830
  />
1280
2831
  )}
1281
2832
  </CapRow>
1282
- {renderButtonComponent()}
2833
+ {((!isEditFlow && isFullMode) || (isEditFlow && (suggestions?.length ?? 0) > 0)) &&
2834
+ renderButtonComponent()}
1283
2835
  </>
1284
2836
 
1285
2837
  );
1286
2838
  };
1287
2839
 
1288
-
1289
- const fallbackSmsLength = () => (
1290
- <CapLabel type="label1" className="fallback-sms-length">
1291
- {formatMessage(messages.totalCharacters, {
1292
- smsCount: Math.ceil(
1293
- fallbackMessage?.length / FALLBACK_MESSAGE_MAX_LENGTH,
1294
- ),
1295
- number: fallbackMessage?.length,
1296
- })}
1297
- </CapLabel>
1298
- );
1299
-
1300
2840
  // Get character count for title (rich card only)
1301
2841
  const getTitleCharacterCount = () => {
1302
2842
  if (templateType === contentType.text_message) return 0;
@@ -1311,7 +2851,7 @@ const splitTemplateVarString = (str) => {
1311
2851
  // Get max length for description based on template type
1312
2852
  const getDescriptionMaxLength = () => {
1313
2853
  return templateType === contentType.text_message
1314
- ? RCS_TEXT_MESSAGE_MAX_LENGTH // 160 for text message
2854
+ ? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH) // 160 for text message
1315
2855
  : RCS_RICH_CARD_MAX_LENGTH; // 2000 for rich card description
1316
2856
  };
1317
2857
 
@@ -1337,6 +2877,58 @@ const splitTemplateVarString = (str) => {
1337
2877
  );
1338
2878
  };
1339
2879
 
2880
+ const rcsDltCardDeleteHandler = () => {
2881
+ closeDltContainerHandler();
2882
+ setDltEditData({});
2883
+ setFallbackMessage('');
2884
+ setFallbackMessageError(false);
2885
+ };
2886
+
2887
+ const dltFallbackListingPreviewhandler = (data) => {
2888
+ const {
2889
+ 'updated-sms-editor': updatedSmsEditor = [],
2890
+ 'sms-editor': smsEditor = '',
2891
+ } = data.versions.base || {};
2892
+ };
2893
+
2894
+ const getDltContentCardList = (content, channel) => {
2895
+ const extra = [
2896
+ <CapIcon
2897
+ type="edit"
2898
+ className="rcs-dlt-edit-icon"
2899
+ onClick={() => rcsDltEditSelectHandler(dltEditData)}
2900
+ />,
2901
+ <CapDropdown
2902
+ overlay={(
2903
+ <CapMenu>
2904
+ <>
2905
+ <CapMenu.Item
2906
+ className="ant-dropdown-menu-item"
2907
+ onClick={() => {}}
2908
+ >
2909
+ {formatMessage(globalMessages.preview)}
2910
+ </CapMenu.Item>
2911
+ <CapMenu.Item
2912
+ className="ant-dropdown-menu-item"
2913
+ onClick={rcsDltCardDeleteHandler}
2914
+ >
2915
+ {formatMessage(globalMessages.delete)}
2916
+ </CapMenu.Item>
2917
+ </>
2918
+ </CapMenu>
2919
+ )}
2920
+ >
2921
+ <CapIcon type="more" />
2922
+ </CapDropdown>,
2923
+ ];
2924
+ return {
2925
+ title: channel,
2926
+ content,
2927
+ cardType: channel,
2928
+ extra,
2929
+ };
2930
+ };
2931
+
1340
2932
  // Render character count for description/message
1341
2933
  const renderDescriptionCharacterCount = (className = "rcs-character-count") => {
1342
2934
  const currentLength = getDescriptionCharacterCount();
@@ -1352,6 +2944,26 @@ const splitTemplateVarString = (str) => {
1352
2944
  );
1353
2945
  };
1354
2946
 
2947
+ // Carousel: per-card character counts (same limits as rich card)
2948
+ const getCarouselTitleCharacterCount = (cardIndex) => {
2949
+ const titleText = carouselData?.[cardIndex]?.title || '';
2950
+ return titleText ? titleText.length : 0;
2951
+ };
2952
+
2953
+ const getCarouselDescriptionCharacterCount = (cardIndex) => {
2954
+ const descriptionText = carouselData?.[cardIndex]?.description || '';
2955
+ return descriptionText ? descriptionText.length : 0;
2956
+ };
2957
+
2958
+ const renderCarouselCharacterCount = (currentLength, maxLength, className = "rcs-character-count") => (
2959
+ <CapLabel type="label1" className={className}>
2960
+ {formatMessage(messages.templateMessageLength, {
2961
+ currentLength,
2962
+ maxLength,
2963
+ })}
2964
+ </CapLabel>
2965
+ );
2966
+
1355
2967
  // Check if any RCS variables contain tags (similar to Zalo hasTag logic)
1356
2968
  const hasTag = () => {
1357
2969
  // Check cardVarMapped values for tags
@@ -1389,83 +3001,32 @@ const splitTemplateVarString = (str) => {
1389
3001
  };
1390
3002
 
1391
3003
  const rcsDltEditSelectHandler = (data) => {
1392
- setShowDltContainer(true);
1393
- setDltMode(RCS_DLT_MODE.EDIT);
1394
- setDltEditData(data);
1395
- };
1396
-
1397
- const rcsDltEditCompletionHandler = (data) => {
1398
- const tempData = {
1399
- versions: {
1400
- // eslint-disable-next-line no-undef
1401
- base: data?.value?.base || {},
1402
- },
1403
- };
1404
- const fallMsg = get(tempData, `versions.base.updated-sms-editor`, []).join(
1405
- '',
1406
- );
1407
- closeDltContainerHandler();
1408
- setDltEditData(tempData);
1409
- setFallbackMessage(fallMsg);
1410
- setFallbackMessageError(fallMsg?.length > FALLBACK_MESSAGE_MAX_LENGTH);
1411
- setShowDltCard(true);
1412
- };
1413
-
1414
- const rcsDltCardDeleteHandler = () => {
1415
- closeDltContainerHandler();
1416
- setDltEditData({});
1417
- setFallbackMessage('');
1418
- setFallbackMessageError(false);
1419
- setShowDltCard(false);
1420
- };
1421
-
1422
- const dltFallbackListingPreviewhandler = (data) => {
1423
- const {
1424
- 'updated-sms-editor': updatedSmsEditor = [],
1425
- 'sms-editor': smsEditor = '',
1426
- } = data.versions.base || {};
1427
- setFallbackPreviewmode(true);
1428
- setDltPreviewData(
1429
- updatedSmsEditor === '' ? smsEditor : updatedSmsEditor.join(''),
1430
- );
3004
+ setShowDltContainer(true);
3005
+ setDltMode(RCS_DLT_MODE.EDIT);
3006
+ setDltEditData(data);
1431
3007
  };
1432
3008
 
1433
- const getDltContentCardList = (content, channel) => {
1434
- const extra = [
1435
- <CapIcon
1436
- type="edit"
1437
- style={{ marginRight: '8px' }}
1438
- onClick={() => rcsDltEditSelectHandler(dltEditData)}
1439
- />,
1440
- <CapDropdown
1441
- overlay={(
1442
- <CapMenu>
1443
- <>
1444
- <CapMenu.Item
1445
- className="ant-dropdown-menu-item"
1446
- onClick={() => setFallbackPreviewmode(true)}
1447
- >
1448
- {formatMessage(globalMessages.preview)}
1449
- </CapMenu.Item>
1450
- <CapMenu.Item
1451
- className="ant-dropdown-menu-item"
1452
- onClick={rcsDltCardDeleteHandler}
1453
- >
1454
- {formatMessage(globalMessages.delete)}
1455
- </CapMenu.Item>
1456
- </>
1457
- </CapMenu>
1458
- )}
1459
- >
1460
- <CapIcon type="more" />
1461
- </CapDropdown>,
1462
- ];
1463
- return {
1464
- title: channel,
1465
- content,
1466
- cardType: channel,
1467
- extra,
3009
+ const rcsDltEditCompletionHandler = (data) => {
3010
+ const tempData = {
3011
+ versions: {
3012
+ // eslint-disable-next-line no-undef
3013
+ base: data?.value?.base || {},
3014
+ },
1468
3015
  };
3016
+ const fallMsg = get(tempData, `versions.base.updated-sms-editor`, []).join(
3017
+ '',
3018
+ );
3019
+ const templateNameFromDlt = get(dltEditData, 'name', '')
3020
+ || get(tempData, 'versions.base.name', '')
3021
+ || '';
3022
+ closeDltContainerHandler();
3023
+ setDltEditData(tempData);
3024
+ const unicodeFromDlt = get(tempData, 'versions.base.unicode-validity');
3025
+ setSmsFallbackData({
3026
+ templateName: templateNameFromDlt,
3027
+ content: fallMsg,
3028
+ ...(typeof unicodeFromDlt === 'boolean' ? { unicodeValidity: unicodeFromDlt } : {}),
3029
+ });
1469
3030
  };
1470
3031
 
1471
3032
  const getDltSlideBoxContent = () => {
@@ -1513,148 +3074,34 @@ const splitTemplateVarString = (str) => {
1513
3074
  return { dltHeader, dltContent };
1514
3075
  };
1515
3076
 
1516
- const renderFallBackSmsComponent = () => {
1517
- // Completely disable fallback functionality when DLT is disabled
1518
- return null;
1519
-
1520
- // const contentArr = [];
1521
- // const showAddCreativeBtnForDlt = isDltEnabled && !showDltCard;
1522
- // const showCardForDlt = isDltEnabled && showDltCard;
1523
- // const showNonDltFallbackComp = !showAddCreativeBtnForDlt && !showCardForDlt;
1524
- // //pushing common fallback sms headings
1525
- // contentArr.push(
1526
- // <CapRow
1527
- // style={{
1528
- // marginBottom: isDltEnabled ? '20px' : '10px',
1529
- // }}
1530
- // >
1531
- // <CapHeader
1532
- // title={(
1533
- // <CapRow type="flex">
1534
- // <CapHeading type="h4">
1535
- // {formatMessage(messages.fallbackLabel)}
1536
- // </CapHeading>
1537
- // <CapTooltipWithInfo
1538
- // placement="right"
1539
- // infoIconProps={{
1540
- // style: { marginLeft: '4px', marginTop: '3px' },
1541
- // }}
1542
- // title={formatMessage(messages.fallbackToolTip)}
1543
- // />
1544
- // </CapRow>
1545
- // )}
1546
- // description={formatMessage(messages.fallbackDesc)}
1547
- // suffix={
1548
- // isDltEnabled ? null : (
1549
- // <CapButton
1550
- // type="flat"
1551
- // className="fallback-preview-btn"
1552
- // prefix={<CapIcon type="eye" />}
1553
- // style={{ color: CAP_SECONDARY.base }}
1554
- // onClick={() => setFallbackPreviewmode(true)}
1555
- // disabled={fallbackMessage === '' || fallbackMessageError}
1556
- // >
1557
- // {formatMessage(globalMessages.preview)}
1558
- // </CapButton>
1559
- // )
1560
- // }
1561
- // />
1562
- // </CapRow>,
1563
- // );
1564
-
1565
- //dlt is enabled, and dlt content is not yet added, show button to add dlt creative
1566
- // showAddCreativeBtnForDlt
1567
- // && contentArr.push(
1568
- // <CapCard className="rcs-dlt-fallback-card">
1569
- // <CapRow type="flex" justify="center" align="middle">
1570
- // <CapColumn span={10}>
1571
- // <CapImage src={addCreativesIcon} />
1572
- // </CapColumn>
1573
- // <CapColumn span={14}>
1574
- // <CapButton
1575
- // className="add-dlt-btn"
1576
- // type="secondary"
1577
- // onClick={addDltMsgHandler}
1578
- // >
1579
- // {formatMessage(messages.addSmsCreative)}
1580
- // </CapButton>
1581
- // </CapColumn>
1582
- // </CapRow>
1583
- // </CapCard>,
1584
- // );
1585
-
1586
- // //dlt is enabled and dlt content is added, show it in a card
1587
- // showCardForDlt
1588
- // && contentArr.push(
1589
- // <CapCustomCardList
1590
- // cardList={[getDltContentCardList(fallbackMessage, SMS)]}
1591
- // className="rcs-dlt-card"
1592
- // />,
1593
- // fallbackMessageError && (
1594
- // <CapError className="rcs-fallback-len-error">
1595
- // {formatMessage(messages.fallbackMsgLenError)}
1596
- // </CapError>
1597
- // ),
1598
- // );
1599
-
1600
- // //dlt is not enabled, show non dlt text area
1601
- // showNonDltFallbackComp
1602
- // && contentArr.push(
1603
- // <>
1604
- // <CapRow>
1605
- // <CapHeader
1606
- // title={(
1607
- // <CapHeading type="h4">
1608
- // {formatMessage(messages.fallbackTextAreaLabel)}
1609
- // </CapHeading>
1610
- // )}
1611
- // suffix={(
1612
- // <TagList
1613
- // label={formatMessage(globalMessages.addLabels)}
1614
- // onTagSelect={onTagSelectFallback}
1615
- // location={location}
1616
- // tags={tags || []}
1617
- // onContextChange={handleOnTagsContextChange}
1618
- // injectedTags={injectedTags || {}}
1619
- // selectedOfferDetails={selectedOfferDetails}
1620
- // />
1621
- // )}
1622
- // />
1623
- // </CapRow>
1624
- // <div className="rcs_fallback_msg_textarea_wrapper">
1625
- // <TextArea
1626
- // id="rcs_fallback_message_textarea"
1627
- // autosize={{ minRows: 3, maxRows: 5 }}
1628
- // placeholder={formatMessage(messages.fallbackMsgPlaceholder)}
1629
- // onChange={onFallbackMessageChange}
1630
- // errorMessage={fallbackMessageError}
1631
- // value={fallbackMessage || ""}
1632
- // />
1633
- // {!aiContentBotDisabled && (
1634
- // <CapAskAira.ContentGenerationBot
1635
- // text={fallbackMessage || ""}
1636
- // setText={(text) => {
1637
- // onFallbackMessageChange({ target: { value: text } });
1638
- // }}
1639
- // iconPlacement="float-br"
1640
- // rootStyle={{
1641
- // bottom: "0.5rem",
1642
- // right: "0.5rem",
1643
- // position: "absolute",
1644
- // }}
1645
- // />
1646
- // )}
1647
- // </div>
1648
- // <CapRow>{fallbackSmsLength()}</CapRow>
1649
- // </>
1650
- // );
1651
-
1652
- // return <>{contentArr}</>;
1653
- };
3077
+ const renderFallBackSmsComponent = () => (
3078
+ <SmsFallback
3079
+ value={smsFallbackData}
3080
+ onChange={setSmsFallbackData}
3081
+ parentLocation={location}
3082
+ smsRegister={smsRegister}
3083
+ isFullMode={isFullMode}
3084
+ selectedOfferDetails={selectedOfferDetails}
3085
+ channelsToHide={CHANNELS_TO_HIDE_FOR_SMS_ONLY}
3086
+ sectionTitle={
3087
+ smsFallbackData
3088
+ ? formatMessage(messages.fallbackLabel)
3089
+ : formatMessage(messages.smsFallbackOptional)
3090
+ }
3091
+ templateListTitle={formatMessage(creativesMessages.creativeTemplates)}
3092
+ templateListDescription={formatMessage(creativesMessages.creativeTemplatesDesc)}
3093
+ /* Full-mode: card layout only while drafting a new template; after send for approval or when a template is loaded, use inline layout. */
3094
+ showAsCard={isFullMode && !isEditFlow && templateStatus === ''}
3095
+ disableSelectTemplate={isEditFlow}
3096
+ eventContextTags={eventContextTags}
3097
+ onRcsFallbackEditorStateChange={handleSmsFallbackEditorStateChange}
3098
+ isRcsEditFlow={isEditFlow}
3099
+ />
3100
+ );
1654
3101
 
1655
3102
  const uploadRcsImage = useCallback((file, type, fileParams, index) => {
1656
3103
  setImageError(null);
1657
- const isRcsThumbnail = index === 1;
3104
+ const isRcsThumbnail = isThumbnailAssetIndex(index);
1658
3105
  actions.uploadRcsAsset(file, type, {
1659
3106
  isRcsThumbnail,
1660
3107
  ...fileParams,
@@ -1692,10 +3139,7 @@ const splitTemplateVarString = (str) => {
1692
3139
  const updateOnRcsImageReUpload = useCallback(() => {
1693
3140
  setUpdateRcsImageSrc('');
1694
3141
  }, []);
1695
-
1696
-
1697
3142
  const uploadRcsVideo = (file, type, fileParams) => {
1698
- setImageError(null);
1699
3143
  actions.uploadRcsAsset(file, type, {
1700
3144
  ...fileParams,
1701
3145
  type: 'video',
@@ -1704,9 +3148,6 @@ const splitTemplateVarString = (str) => {
1704
3148
  });
1705
3149
  };
1706
3150
 
1707
- const updateRcsVideoSrc = (val) => {
1708
- setRcsVideoSrc(val);
1709
- };
1710
3151
  const setUpdateRcsVideoSrc = useCallback((index, val) => {
1711
3152
  setRcsVideoSrc(val);
1712
3153
  setAssetList(val);
@@ -1729,13 +3170,13 @@ const splitTemplateVarString = (str) => {
1729
3170
  updateRcsThumbnailSrc('');
1730
3171
  };
1731
3172
 
1732
- const renderThumbnailComponent = () => {
3173
+ const renderThumbnailComponent = () => {
1733
3174
  const currentDimension = selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
1734
3175
  return !isEditFlow && (
1735
3176
  <>
1736
3177
  <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
1737
3178
  <CapImageUpload
1738
- style={{ paddingTop: '20px' }}
3179
+ className="rcs-image-upload-with-top-padding"
1739
3180
  allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
1740
3181
  imgWidth={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].width}
1741
3182
  imgHeight={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].height}
@@ -1753,6 +3194,7 @@ const splitTemplateVarString = (str) => {
1753
3194
  channel={RCS}
1754
3195
  channelSpecificStyle={!isFullMode}
1755
3196
  skipDimensionValidation={true}
3197
+ showReUploadButton={!isEditFlow && isFullMode}
1756
3198
  />
1757
3199
  </>
1758
3200
  )
@@ -1778,7 +3220,7 @@ const splitTemplateVarString = (str) => {
1778
3220
  value: dim.type,
1779
3221
  label: `${dim.label}`
1780
3222
  }))}
1781
- style={{ marginBottom: '20px' }}
3223
+ className="rcs-dimension-select--bottom-spacing"
1782
3224
  />
1783
3225
  </>
1784
3226
  )}
@@ -1792,7 +3234,7 @@ const splitTemplateVarString = (str) => {
1792
3234
  </div>
1793
3235
  ) : (
1794
3236
  <CapImageUpload
1795
- style={{ paddingTop: '20px' }}
3237
+ className="rcs-image-upload-with-top-padding"
1796
3238
  allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
1797
3239
  imgWidth={RCS_IMAGE_DIMENSIONS[selectedDimension].width}
1798
3240
  imgHeight={RCS_IMAGE_DIMENSIONS[selectedDimension].height}
@@ -1803,7 +3245,7 @@ const splitTemplateVarString = (str) => {
1803
3245
  updateImageSrc={setUpdateRcsImageSrc}
1804
3246
  updateOnReUpload={updateOnRcsImageReUpload}
1805
3247
  index={0}
1806
- className="cap-custom-image-upload"
3248
+ className="cap-custom-image-upload rcs-image-upload--top-spacing"
1807
3249
  key={`rcs-uploaded-image-${selectedDimension}`}
1808
3250
  imageData={rcsData}
1809
3251
  channel={RCS}
@@ -1814,7 +3256,7 @@ const splitTemplateVarString = (str) => {
1814
3256
 
1815
3257
  </>
1816
3258
  );
1817
- }
3259
+ }
1818
3260
 
1819
3261
  const renderVideoComponent = () => {
1820
3262
  const currentDimension =selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
@@ -1833,7 +3275,7 @@ const splitTemplateVarString = (str) => {
1833
3275
  value: dim.type,
1834
3276
  label: `${dim.label}`
1835
3277
  }))}
1836
- style={{ marginBottom: '20px' }}
3278
+ className="rcs-dimension-select--bottom-spacing"
1837
3279
  />
1838
3280
  )}
1839
3281
  {(isEditFlow || !isFullMode) ? (
@@ -1891,10 +3333,48 @@ const splitTemplateVarString = (str) => {
1891
3333
  };
1892
3334
 
1893
3335
  const getRcsPreview = () => {
3336
+
3337
+ if (templateType === contentType.carousel) {
3338
+ const cardsForPreview = buildCarouselCardsForPreview(carouselData);
3339
+ const carouselDimKey = getCarouselDimensionKey();
3340
+ const carouselImgDims =
3341
+ RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
3342
+ const carouselVidDims =
3343
+ RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
3344
+ || RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
3345
+ // Debug log for embedded/library mode preview payload (carousel)
3346
+ // eslint-disable-next-line no-console
3347
+ return (
3348
+ <UnifiedPreview
3349
+ channel={RCS}
3350
+ content={{
3351
+ carouselData: cardsForPreview,
3352
+ carouselPreviewDimensions: {
3353
+ imageWidth: carouselImgDims.width,
3354
+ imageHeight: carouselImgDims.height,
3355
+ videoThumbWidth: carouselVidDims.width,
3356
+ videoThumbHeight: carouselVidDims.height,
3357
+ },
3358
+ }}
3359
+ device={ANDROID}
3360
+ showDeviceToggle={false}
3361
+ showHeader={false}
3362
+ formatMessage={formatMessage}
3363
+ />
3364
+ );
3365
+ }
1894
3366
 
1895
3367
  const dimensionObj = RCS_IMAGE_DIMENSIONS[selectedDimension];
1896
- const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
1897
- const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
3368
+ const isSlotMappingMode = isEditFlow || !isFullMode;
3369
+ const titleVarCountForResolve = isMediaTypeText
3370
+ ? 0
3371
+ : ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
3372
+ const resolvedTitle = isMediaTypeText
3373
+ ? ''
3374
+ : (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
3375
+ const resolvedDesc = isSlotMappingMode
3376
+ ? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
3377
+ : templateDesc;
1898
3378
  return (
1899
3379
  <UnifiedPreview
1900
3380
  channel={RCS}
@@ -1917,51 +3397,108 @@ const splitTemplateVarString = (str) => {
1917
3397
  );
1918
3398
  };
1919
3399
 
1920
- const getUnmappedDesc = (str, mapping) => {
1921
- if (!str) return '';
1922
- if (!mapping || Object.keys(mapping).length === 0) return str;
1923
- let result = str;
1924
- const replacements = [];
1925
- Object.entries(mapping).forEach(([key, value]) => {
1926
- const raw = (value ?? '').toString();
1927
- if (!raw || raw?.trim?.() === '') return;
1928
- const braced = /^\{\{[\s\S]*\}\}$/.test(raw) ? raw : `{{${raw}}}`;
1929
- replacements.push({ key, needle: raw });
1930
- if (braced !== raw) replacements.push({ key, needle: braced });
1931
- });
1932
- const seen = new Set();
1933
- const uniq = replacements
1934
- .filter(({ key, needle }) => {
1935
- const id = `${key}::${needle}`;
1936
- if (seen.has(id)) return false;
1937
- seen.add(id);
1938
- return true;
1939
- })
1940
- .sort((a, b) => (b.needle.length - a.needle.length));
1941
-
1942
- uniq.forEach(({ key, needle }) => {
1943
- if (!needle) return;
1944
- const escaped = needle.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
1945
- const regex = new RegExp(escaped, 'g');
1946
- result = result.replace(regex, `{{${key}}}`);
1947
- });
1948
- return result;
1949
- };
1950
-
1951
3400
  const createPayload = () => {
1952
- const base = get(dltEditData, `versions.base`, {});
1953
- const {
1954
- template_id: templateId = '',
1955
- template_name = '',
1956
- 'sms-editor': template = '',
1957
- header: registeredSenderIds = [],
1958
- } = base;
1959
- const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
1960
- const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
3401
+ const isSlotMappingMode = isEditFlow || !isFullMode;
1961
3402
  const alignment = isMediaTypeImage
1962
3403
  ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.alignment
1963
3404
  : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.alignment;
1964
3405
 
3406
+ const heightTypeForCardWidth = isMediaTypeText
3407
+ ? undefined
3408
+ : isMediaTypeImage
3409
+ ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType
3410
+ : isMediaTypeVideo
3411
+ ? RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType
3412
+ : undefined;
3413
+ const cardWidthFromSelection =
3414
+ heightTypeForCardWidth === MEDIUM ? MEDIUM : SMALL;
3415
+
3416
+ /** Library: merge props + state so SMS fallback is not dropped when local state is empty but templateData has consumer data. */
3417
+ const smsFromApiShape = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
3418
+ const smsFallbackMerged = !isFullMode
3419
+ ? (() => {
3420
+ const local =
3421
+ smsFallbackData && typeof smsFallbackData === 'object' ? smsFallbackData : {};
3422
+ return {
3423
+ ...smsFromApiShape,
3424
+ ...local,
3425
+ rcsSmsFallbackVarMapped: mergeRcsSmsFallbackVarMapLayers(smsFromApiShape, local),
3426
+ };
3427
+ })()
3428
+ : (smsFallbackData || {});
3429
+ const smsFallbackForPayload = (() => {
3430
+ if (isFullMode) {
3431
+ return hasMeaningfulSmsFallbackShape(smsFallbackData) ? smsFallbackData : null;
3432
+ }
3433
+ const mapped = {
3434
+ templateName:
3435
+ smsFallbackMerged.templateName
3436
+ || smsFallbackMerged.smsTemplateName
3437
+ || '',
3438
+ // Use `||` so empty `content` does not block campaign/API `message` (common in embedded flows).
3439
+ content:
3440
+ smsFallbackMerged.content
3441
+ || smsFallbackMerged.smsContent
3442
+ || smsFallbackMerged.smsTemplateContent
3443
+ || smsFallbackMerged.message
3444
+ || '',
3445
+ templateContent:
3446
+ pickFirstSmsFallbackTemplateString(smsFallbackMerged)
3447
+ || '',
3448
+ ...(typeof smsFallbackMerged.unicodeValidity === 'boolean'
3449
+ && { unicodeValidity: smsFallbackMerged.unicodeValidity }),
3450
+ ...(smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]
3451
+ && Object.keys(smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]).length > 0 && {
3452
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]:
3453
+ smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP],
3454
+ }),
3455
+ };
3456
+ return hasMeaningfulSmsFallbackShape(mapped) ? mapped : null;
3457
+ })();
3458
+
3459
+ const carouselCardContent = isCarouselType
3460
+ ? (carouselData || []).map((card = {}) => {
3461
+ const cardMediaType = card.mediaType;
3462
+ const isCardVideo = cardMediaType === RCS_MEDIA_TYPES.VIDEO;
3463
+ const isCardImage = cardMediaType === RCS_MEDIA_TYPES.IMAGE;
3464
+ const mediaUrl = isCardVideo
3465
+ ? (card.videoAsset?.videoSrc || '')
3466
+ : (card.imageSrc || '');
3467
+ const thumbnailUrl = isCardVideo
3468
+ ? (card.thumbnailSrc || card.videoAsset?.videoThumbnail || '')
3469
+ : '';
3470
+ const cardVarTokens = [
3471
+ ...((card.title || '').match(rcsVarRegex) ?? []),
3472
+ ...((card.description || '').match(rcsVarRegex) ?? []),
3473
+ ];
3474
+ const cardVarMappedForCard = {};
3475
+ if (isSlotMappingMode && cardVarTokens.length > 0) {
3476
+ cardVarTokens.forEach((token) => {
3477
+ const varName = getVarNameFromToken(token);
3478
+ if (!varName) return;
3479
+ cardVarMappedForCard[varName] = sanitizeCardVarMappedValue(cardVarMapped?.[varName] ?? '');
3480
+ });
3481
+ }
3482
+ return {
3483
+ title: card.title || '',
3484
+ description: card.description || '',
3485
+ mediaType: cardMediaType,
3486
+ ...((isCardImage || isCardVideo) && {
3487
+ media: {
3488
+ mediaUrl,
3489
+ thumbnailUrl,
3490
+ height: selectedCarouselHeight || MEDIUM,
3491
+ ...(isCardVideo && card.videoAsset?.videoName && { videoName: card.videoAsset.videoName }),
3492
+ },
3493
+ }),
3494
+ ...(Array.isArray(card.suggestions) && card.suggestions.length > 0 && {
3495
+ suggestions: card.suggestions,
3496
+ }),
3497
+ ...(Object.keys(cardVarMappedForCard).length > 0 && { cardVarMapped: cardVarMappedForCard }),
3498
+ };
3499
+ })
3500
+ : null;
3501
+
1965
3502
  const payload = {
1966
3503
  name: templateName,
1967
3504
  versions: {
@@ -1970,16 +3507,22 @@ const splitTemplateVarString = (str) => {
1970
3507
  RCS: {
1971
3508
  rcsContent: {
1972
3509
  ...(rcsAccount && !isFullMode && { accountId: rcsAccount }),
1973
- cardType: STANDALONE,
1974
- cardSettings: {
1975
- cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
1976
- ...(alignment && { mediaAlignment: alignment }),
1977
- cardWidth: SMALL,
1978
- },
1979
- cardContent: [
3510
+ cardType: isCarouselType ? contentType.carousel : STANDALONE,
3511
+ cardSettings: isCarouselType
3512
+ ? { cardOrientation: VERTICAL, cardWidth: selectedCarouselWidth || SMALL }
3513
+ : {
3514
+ cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
3515
+ ...(alignment && { mediaAlignment: alignment }),
3516
+ cardWidth: SMALL,
3517
+ },
3518
+ cardContent: isCarouselType
3519
+ ? carouselCardContent
3520
+ : [
1980
3521
  {
1981
- title: resolvedTitle,
1982
- description: resolvedDesc,
3522
+ // Persist raw template copy + cardVarMapped — not resolveTemplateWithMap output — so library
3523
+ // / getFormData round-trip keeps {{…}} and slot values (resolved strings broke reopen hydration).
3524
+ title: templateTitle,
3525
+ description: templateDesc,
1983
3526
  mediaType: templateMediaType,
1984
3527
  ...(!isMediaTypeText && {media: {
1985
3528
  mediaUrl: rcsImageSrc || rcsVideoSrc.videoSrc || '',
@@ -1988,23 +3531,32 @@ const splitTemplateVarString = (str) => {
1988
3531
  ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType || MEDIUM
1989
3532
  : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType || MEDIUM,
1990
3533
  }}),
1991
- ...(!isFullMode && (() => {
1992
- const tokens = [
1993
- ...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
1994
- ...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
3534
+ ...(isSlotMappingMode && (() => {
3535
+ const templateVarTokens = [
3536
+ ...(templateTitle?.match(rcsVarRegex) ?? []),
3537
+ ...(templateDesc?.match(rcsVarRegex) ?? []),
1995
3538
  ];
1996
- const allowedKeys = tokens
1997
- .map((t) => getVarNameFromToken(t))
1998
- .filter(Boolean);
1999
- const nextMap = {};
2000
- allowedKeys.forEach((k) => {
2001
- if (Object.prototype.hasOwnProperty.call(cardVarMapped || {}, k)) {
2002
- nextMap[k] = cardVarMapped[k];
2003
- } else {
2004
- nextMap[k] = '';
2005
- }
3539
+ const cardVarMappedForRcsCardOnly = pickRcsCardVarMappedEntries(
3540
+ cardVarMapped,
3541
+ );
3542
+ // Persist numeric slot keys only ("1","2",…) — avoids duplicating the same value under
3543
+ // semantic names (e.g. both "1" and dynamic_expiry_date_after_3_days.FORMAT_1). Hydration
3544
+ // and coalesceCardVarMappedToTemplate still resolve from numeric keys + template tokens.
3545
+ const persistedSlotVarMap = {};
3546
+ templateVarTokens.forEach((token, slotIndexZeroBased) => {
3547
+ const varName = getVarNameFromToken(token);
3548
+ if (!varName) return;
3549
+ const resolvedRawValue = resolveCardVarMappedSlotValue(
3550
+ cardVarMappedForRcsCardOnly,
3551
+ varName,
3552
+ slotIndexZeroBased,
3553
+ isSlotMappingMode,
3554
+ rcsSpanningSemanticVarNames.has(varName),
3555
+ );
3556
+ const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
3557
+ persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
2006
3558
  });
2007
- return { cardVarMapped: nextMap };
3559
+ return { cardVarMapped: persistedSlotVarMap };
2008
3560
  })()),
2009
3561
  ...(suggestions.length > 0 && { suggestions }),
2010
3562
  }
@@ -2012,17 +3564,109 @@ const splitTemplateVarString = (str) => {
2012
3564
  contentType: isFullMode ? templateType : RICHCARD,
2013
3565
  ...(isFullMode && {accountId:accountId, accessToken: accessToken, accountName: accountName, hostName: hostName}),
2014
3566
  },
2015
- smsFallBackContent: {
2016
- message: fallbackMessage,
2017
- ...(isDltEnabled && {
2018
- templateConfigs: {
2019
- templateId,
2020
- templateName: template_name,
2021
- template,
2022
- registeredSenderIds,
2023
- },
2024
- }),
2025
- },
3567
+ ...(smsFallbackForPayload && (() => {
3568
+ const smsBodyText =
3569
+ smsFallbackForPayload.content
3570
+ || smsFallbackForPayload.templateContent
3571
+ || smsFallbackForPayload.message
3572
+ || smsFallbackForPayload.smsContent
3573
+ || '';
3574
+ /**
3575
+ * Campaigns `getTraiSenderIds` / Iris read `smsFallBackContent.templateConfigs.registeredSenderIds`.
3576
+ * Library `smsFallbackForPayload` omits ids — use merged state (`smsFallbackMerged`) like test preview.
3577
+ */
3578
+ const m = smsFallbackMerged || {};
3579
+ const tcSibling = m.templateConfigs && typeof m.templateConfigs === 'object'
3580
+ ? m.templateConfigs
3581
+ : {};
3582
+ const smsFallbackTemplateId =
3583
+ (m.smsTemplateId != null && String(m.smsTemplateId).trim() !== ''
3584
+ ? String(m.smsTemplateId)
3585
+ : '')
3586
+ || (tcSibling.templateId != null && String(tcSibling.templateId).trim() !== ''
3587
+ ? String(tcSibling.templateId)
3588
+ : '');
3589
+ const smsFallbackTemplateStr =
3590
+ pickFirstSmsFallbackTemplateString(m)
3591
+ || (typeof m.templateContent === 'string' ? m.templateContent : '')
3592
+ || (typeof tcSibling.template === 'string' ? tcSibling.template : '')
3593
+ || '';
3594
+ const smsFallbackTemplateName =
3595
+ m.templateName
3596
+ || m.smsTemplateName
3597
+ || tcSibling.templateName
3598
+ || tcSibling.name
3599
+ || '';
3600
+ const registeredSenderIdsForPayload = Array.isArray(m.registeredSenderIds)
3601
+ ? m.registeredSenderIds
3602
+ : Array.isArray(tcSibling.registeredSenderIds)
3603
+ ? tcSibling.registeredSenderIds
3604
+ : Array.isArray(tcSibling.header)
3605
+ ? tcSibling.header
3606
+ : null;
3607
+ const hasRegisteredSenderIds = Array.isArray(registeredSenderIdsForPayload);
3608
+ const smsFallbackTemplateConfigs =
3609
+ smsFallbackTemplateId || hasRegisteredSenderIds
3610
+ ? {
3611
+ ...(smsFallbackTemplateId && { templateId: smsFallbackTemplateId }),
3612
+ ...(smsFallbackTemplateStr && { template: smsFallbackTemplateStr }),
3613
+ ...(smsFallbackTemplateName && {
3614
+ templateName: smsFallbackTemplateName,
3615
+ }),
3616
+ ...(hasRegisteredSenderIds && {
3617
+ registeredSenderIds: registeredSenderIdsForPayload,
3618
+ }),
3619
+ }
3620
+ : null;
3621
+ const isDltCampaign = !isFullMode && isTraiDLTEnable(isFullMode, smsRegister);
3622
+ return {
3623
+ smsFallBackContent: isFullMode
3624
+ ? {
3625
+ smsTemplateName: smsFallbackForPayload.templateName || '',
3626
+ smsContent: smsBodyText,
3627
+ // cap-campaigns-v2 `normalizeRcsMessageContentForApi` only serializes `message` (+ templateConfigs); without this key SMS fallback is dropped on send.
3628
+ message: smsBodyText,
3629
+ ...(typeof smsFallbackForPayload.unicodeValidity === 'boolean' && {
3630
+ unicodeValidity: smsFallbackForPayload.unicodeValidity,
3631
+ }),
3632
+ ...(smsFallbackForPayload.rcsSmsFallbackVarMapped
3633
+ && Object.keys(smsFallbackForPayload.rcsSmsFallbackVarMapped).length > 0 && {
3634
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackForPayload.rcsSmsFallbackVarMapped,
3635
+ }),
3636
+ ...(smsFallbackTemplateConfigs && {
3637
+ templateConfigs: smsFallbackTemplateConfigs,
3638
+ }),
3639
+ }
3640
+ : {
3641
+ // Round-trip storage: full shape so reopening the editor restores template
3642
+ // name, sender IDs, and var mappings. normalizeRcsMessageContentForApi
3643
+ // (called in CreativesContainer getCreativesData) strips this to
3644
+ // { message, templateConfigs } before the API call — the extra fields here
3645
+ // are only used for re-hydration, never sent to the API.
3646
+ message: smsBodyText,
3647
+ ...(smsFallbackForPayload.templateName && {
3648
+ smsTemplateName: smsFallbackForPayload.templateName,
3649
+ }),
3650
+ ...(smsFallbackForPayload.templateContent && {
3651
+ templateContent: smsFallbackForPayload.templateContent,
3652
+ }),
3653
+ ...(typeof smsFallbackForPayload.unicodeValidity === 'boolean' && {
3654
+ unicodeValidity: smsFallbackForPayload.unicodeValidity,
3655
+ }),
3656
+ ...(Array.isArray(registeredSenderIdsForPayload) && {
3657
+ registeredSenderIds: registeredSenderIdsForPayload,
3658
+ }),
3659
+ ...(smsFallbackForPayload[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]
3660
+ && Object.keys(smsFallbackForPayload[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]).length > 0 && {
3661
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]:
3662
+ smsFallbackForPayload[RCS_SMS_FALLBACK_VAR_MAPPED_PROP],
3663
+ }),
3664
+ ...(isDltCampaign && smsFallbackTemplateConfigs && {
3665
+ templateConfigs: smsFallbackTemplateConfigs,
3666
+ }),
3667
+ },
3668
+ };
3669
+ })()),
2026
3670
  },
2027
3671
  },
2028
3672
  },
@@ -2032,6 +3676,141 @@ const splitTemplateVarString = (str) => {
2032
3676
  return payload;
2033
3677
  };
2034
3678
 
3679
+ /** Shape expected by CommonTestAndPreview buildRcsTestMessagePayload (versions.base.content.RCS). */
3680
+ const testPreviewFormData = useMemo(() => {
3681
+ const payload = createPayload();
3682
+ const rcs = payload?.versions?.base?.content?.RCS;
3683
+ if (!rcs) return null;
3684
+ // createMessageMeta uses WeCRM `id` when present; else template API account id (sourceAccountIdentifier).
3685
+ const accountIdForCreateMessageMeta =
3686
+ (wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
3687
+ ? String(wecrmAccountId)
3688
+ : accountId;
3689
+ const isSlotMappingModeForPreview = isEditFlow || !isFullMode;
3690
+ let rcsForTest = {
3691
+ ...rcs,
3692
+ rcsContent: {
3693
+ ...rcs.rcsContent,
3694
+ ...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
3695
+ },
3696
+ };
3697
+ /** Approval payload keeps numeric-only `cardVarMapped`; preview APIs still need semantic keys. */
3698
+ const cardContent = rcsForTest.rcsContent?.cardContent;
3699
+ if (Array.isArray(cardContent) && cardContent[0]) {
3700
+ if (isCarouselType) {
3701
+ // Carousel: resolve {{N}} slot tokens to the actual tag expressions / static values the
3702
+ // user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
3703
+ // - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
3704
+ // - prepareTagExtractionPayload can extract tag metadata from the card content.
3705
+ // Track cumulative slotOffset across cards: each card's title/description vars occupy
3706
+ // sequential global slot indices ({{1}},{{2}} in card 0; {{3}},{{4}} in card 1, …).
3707
+ // Without the offset, every card restarts at slotKey="1" and resolves against card 0's
3708
+ // mappings, causing tags from card 1+ to be missing or wrong.
3709
+ let carouselSlotOffset = 0;
3710
+ rcsForTest = {
3711
+ ...rcsForTest,
3712
+ rcsContent: {
3713
+ ...rcsForTest.rcsContent,
3714
+ cardContent: cardContent.map((card) => {
3715
+ const rawTitle = card.title || '';
3716
+ const rawDesc = card.description || '';
3717
+ const titleVarCount = (rawTitle.match(rcsVarRegex) || []).length;
3718
+ const descVarCount = (rawDesc.match(rcsVarRegex) || []).length;
3719
+ const resolvedTitle = resolveTemplateWithMap(rawTitle, carouselSlotOffset);
3720
+ const resolvedDesc = resolveTemplateWithMap(rawDesc, carouselSlotOffset + titleVarCount);
3721
+ carouselSlotOffset += titleVarCount + descVarCount;
3722
+ return { ...card, title: resolvedTitle, description: resolvedDesc };
3723
+ }),
3724
+ },
3725
+ };
3726
+ } else if (isSlotMappingModeForPreview) {
3727
+ // Standalone card: coalesce cardVarMapped with the template's slot names so the preview
3728
+ // API receives a correctly-keyed var map for non-carousel templates.
3729
+ const fullCardVarMapped = coalesceCardVarMappedToTemplate(
3730
+ pickRcsCardVarMappedEntries(cardVarMapped),
3731
+ templateTitle,
3732
+ templateDesc,
3733
+ rcsVarRegex,
3734
+ );
3735
+ rcsForTest = {
3736
+ ...rcsForTest,
3737
+ rcsContent: {
3738
+ ...rcsForTest.rcsContent,
3739
+ cardContent: [
3740
+ { ...cardContent[0], cardVarMapped: fullCardVarMapped },
3741
+ ...cardContent.slice(1),
3742
+ ],
3743
+ },
3744
+ };
3745
+ }
3746
+ }
3747
+ const out = {
3748
+ versions: {
3749
+ base: {
3750
+ content: {
3751
+ RCS: rcsForTest,
3752
+ },
3753
+ },
3754
+ },
3755
+ };
3756
+ // Use the smsFallbackTemplateConfigs already built by createPayload() — it uses smsFallbackMerged
3757
+ // which merges smsFromApiShape (API baseline with templateId, registeredSenderIds, templateName, etc.)
3758
+ // with local smsFallbackData state. Reading raw smsFallbackData here would miss the API-sourced
3759
+ // smsTemplateId in campaign mode (smsFallbackMerged = { ...smsFromApiShape, ...local } there).
3760
+ // This also keeps templateName (and all other fields) in sync with the approval payload automatically.
3761
+ const smsFallbackTcFromPayload = rcs?.rcsContent?.smsFallBackContent?.templateConfigs;
3762
+ if (smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object') {
3763
+ out.templateConfigs = {
3764
+ ...smsFallbackTcFromPayload,
3765
+ traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister),
3766
+ };
3767
+ }
3768
+ return out;
3769
+ }, [
3770
+ templateName,
3771
+ templateTitle,
3772
+ templateDesc,
3773
+ templateMediaType,
3774
+ cardVarMapped,
3775
+ carouselData,
3776
+ suggestions,
3777
+ rcsImageSrc,
3778
+ rcsVideoSrc,
3779
+ rcsThumbnailSrc,
3780
+ selectedDimension,
3781
+ smsFallbackData,
3782
+ isFullMode,
3783
+ isEditFlow,
3784
+ templateType,
3785
+ accountId,
3786
+ wecrmAccountId,
3787
+ accessToken,
3788
+ accountName,
3789
+ hostName,
3790
+ smsRegister,
3791
+ ]);
3792
+
3793
+ /**
3794
+ * Library/campaign: `createPayload` merges root + nested `smsFallBackContent` from `templateData`
3795
+ * with `smsFallbackData`. Done/slot validation must use the same merge — otherwise local state can
3796
+ * miss `templateContent` / var map while the parent payload still has them (DLT campaigns).
3797
+ */
3798
+ const librarySmsFallbackMergedForValidation = useMemo(() => {
3799
+ if (isFullMode) {
3800
+ return smsFallbackData;
3801
+ }
3802
+ const smsFromApiShape = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
3803
+ const local =
3804
+ smsFallbackData && typeof smsFallbackData === 'object' ? smsFallbackData : {};
3805
+ return {
3806
+ ...smsFromApiShape,
3807
+ ...local,
3808
+ rcsSmsFallbackVarMapped: mergeRcsSmsFallbackVarMapLayers(smsFromApiShape, local),
3809
+ };
3810
+ }, [isFullMode, templateData, smsFallbackData]);
3811
+
3812
+
3813
+
2035
3814
  const actionCallback = ({ errorMessage, resp }, isEdit) => {
2036
3815
  // eslint-disable-next-line no-undef
2037
3816
  const error = errorMessage?.message || errorMessage;
@@ -2061,6 +3840,9 @@ const splitTemplateVarString = (str) => {
2061
3840
  _id: params?.id,
2062
3841
  validity: true,
2063
3842
  type: RCS,
3843
+ // CreativesContainer closes the slide box *after* getCreativesData runs so the parent receives
3844
+ // the RCS payload first (closing immediately used to skip getCreativesData → empty "Add creative").
3845
+ closeSlideBoxAfterSubmit: !isFullMode,
2064
3846
  };
2065
3847
  getFormData(formDataParams);
2066
3848
  };
@@ -2074,6 +3856,7 @@ const splitTemplateVarString = (str) => {
2074
3856
  actionCallback({ resp, errorMessage });
2075
3857
  setSpin(false); // Always turn off spinner
2076
3858
  if (!errorMessage) {
3859
+ setTemplateStatus(RCS_STATUSES.pending);
2077
3860
  onCreateComplete();
2078
3861
  }
2079
3862
  });
@@ -2085,6 +3868,91 @@ const splitTemplateVarString = (str) => {
2085
3868
  }
2086
3869
  };
2087
3870
 
3871
+ /** When a fallback SMS row exists, require non-empty body (trimmed) and filled var slots (DLT). */
3872
+ const smsFallbackBlocksDone = () => {
3873
+ // Non-DLT library: user removed SMS fallback (local null) but template still carries fallback — block Done.
3874
+ if (
3875
+ !isFullMode
3876
+ && !isTraiDLTEnable(isFullMode, smsRegister)
3877
+ && smsFallbackData == null
3878
+ && hasMeaningfulSmsFallbackShape(
3879
+ getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
3880
+ )
3881
+ ) {
3882
+ return true;
3883
+ }
3884
+ if (!smsFallbackData) return false;
3885
+ // Full-mode (Send for approval): SMS fallback is optional. Tag-slot mapping is a display/preview
3886
+ // concern, not a structural requirement for approval — the registered SMS template body stands on
3887
+ // its own. Never block the Send for approval button due to missing or unfilled fallback var slots.
3888
+ if (isFullMode) return false;
3889
+ const merged = librarySmsFallbackMergedForValidation;
3890
+ const templateText = pickFirstSmsFallbackTemplateString(merged);
3891
+ if (!templateText) {
3892
+ return true;
3893
+ }
3894
+ const rawVarMap =
3895
+ merged.rcsSmsFallbackVarMapped
3896
+ || merged['rcs-sms-fallback-var-mapped'];
3897
+ const varMap =
3898
+ rawVarMap != null && typeof rawVarMap === 'object' ? rawVarMap : {};
3899
+ return !areAllRcsSmsFallbackVarSlotsFilled(templateText, varMap);
3900
+ };
3901
+
3902
+ /**
3903
+ * Library / campaigns (`!isFullMode`): card slots are often stored on numeric keys (`1`,`2`,…) while
3904
+ * semantic keys stay `""` from API round-trip. `resolveCardVarMappedSlotValue` matches createPayload
3905
+ * / preview — naive `cardVarMapped[name]` wrongly kept Done disabled for DLT.
3906
+ */
3907
+ const isLibraryCampaignCardVarMappingIncomplete = () => {
3908
+ if (isFullMode) return false;
3909
+ const titleTokens = splitTemplateVarStringRcs(templateTitle).filter((elem) =>
3910
+ rcsVarTestRegex.test(elem),
3911
+ );
3912
+ const descTokens = splitTemplateVarStringRcs(templateDesc).filter((elem) =>
3913
+ rcsVarTestRegex.test(elem),
3914
+ );
3915
+ const orderedVarNames = [
3916
+ ...titleTokens.map((t) => t.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '')),
3917
+ ...descTokens.map((t) => t.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '')),
3918
+ ];
3919
+ if (orderedVarNames.length > 0 && isEmpty(cardVarMapped)) {
3920
+ return true;
3921
+ }
3922
+ return orderedVarNames.some((name, globalIdx) => {
3923
+ const v = resolveCardVarMappedSlotValue(
3924
+ cardVarMapped,
3925
+ name,
3926
+ globalIdx,
3927
+ true,
3928
+ rcsSpanningSemanticVarNames.has(name),
3929
+ );
3930
+ const s = v == null ? '' : String(v);
3931
+ return s.trim() === '';
3932
+ });
3933
+ };
3934
+
3935
+ const isCarouselLibraryIncomplete = () => {
3936
+ if (!isCarouselType || isFullMode) return false;
3937
+ if ((carouselErrors || []).some((err) => err?.title || err?.description)) return true;
3938
+ // Block if any card's title or description has no content at all (no static text and no variable tokens).
3939
+ const hasEmptyField = (carouselData || []).some((card) =>
3940
+ ['title', 'description'].some((field) => !(card?.[field] || '').trim())
3941
+ );
3942
+ if (hasEmptyField) return true;
3943
+ const unfilledVar = (carouselData || []).some((card) =>
3944
+ ['title', 'description'].some((field) => {
3945
+ const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((t) => rcsVarTestRegex.test(t));
3946
+ return tokens.some((t) => {
3947
+ const name = t.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
3948
+ const v = cardVarMapped?.[name];
3949
+ return v == null || String(v).trim() === '';
3950
+ });
3951
+ })
3952
+ );
3953
+ return unfilledVar;
3954
+ };
3955
+
2088
3956
  const isDisableDone = () => {
2089
3957
  if(isEditFlow){
2090
3958
  return false;
@@ -2095,46 +3963,26 @@ const splitTemplateVarString = (str) => {
2095
3963
  }
2096
3964
  }
2097
3965
 
2098
- if(!isFullMode){
2099
- const titleVars = splitTemplateVarString(templateTitle).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2100
- const descVars = splitTemplateVarString(templateDesc).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2101
- const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
2102
-
2103
- if (allVars.length > 0 && isEmpty(cardVarMapped)) {
2104
- return true;
2105
- }
2106
-
2107
- const hasEmptyMapping =
2108
- cardVarMapped &&
2109
- Object.keys(cardVarMapped).length > 0 &&
2110
- Object.entries(cardVarMapped).some(([_, v]) => {
2111
- if (typeof v !== 'string') return !v; // null/undefined
2112
- return v.trim() === ''; // empty string
2113
- });
2114
-
2115
- if (hasEmptyMapping) {
2116
- return true;
2117
- }
3966
+ if (isCarouselLibraryIncomplete()) {
3967
+ return true;
3968
+ }
2118
3969
 
2119
- const anyMissing = allVars.some(name => {
2120
- const v = cardVarMapped?.[name];
2121
- if (typeof v !== 'string') return !v;
2122
- return v.trim() === '';
2123
- });
2124
- if (anyMissing) {
2125
- return true;
2126
- }
3970
+ if (isLibraryCampaignCardVarMappingIncomplete()) {
3971
+ return true;
2127
3972
  }
2128
3973
 
2129
- if (isMediaTypeText && templateDesc.trim() === '') {
3974
+ if (smsFallbackBlocksDone()) {
2130
3975
  return true;
3976
+ }
2131
3977
 
3978
+ if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
3979
+ return true;
2132
3980
  }
2133
- if (isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
3981
+ if (!isCarouselType && isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
2134
3982
  return true;
2135
3983
  }
2136
3984
 
2137
- if (isMediaTypeVideo && (rcsVideoSrc.videoSrc === '' || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
3985
+ if (!isCarouselType && isMediaTypeVideo && (!rcsVideoSrc.videoSrc || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
2138
3986
  return true;
2139
3987
  }
2140
3988
  if (buttonType.includes(CTA)) {
@@ -2146,72 +3994,60 @@ const splitTemplateVarString = (str) => {
2146
3994
  return true;
2147
3995
  }
2148
3996
  }
2149
- if (templateDescError || templateTitleError || fallbackMessageError) {
3997
+ if (templateDescError || templateTitleError) {
3998
+ return true;
3999
+ }
4000
+ if (
4001
+ smsFallbackData?.content
4002
+ && smsFallbackData.content.length > FALLBACK_MESSAGE_MAX_LENGTH
4003
+ ) {
2150
4004
  return true;
2151
4005
  }
2152
4006
  return false;
2153
4007
  };
2154
4008
 
2155
4009
  const isEditDisableDone = () => {
2156
-
2157
- if (templateStatus !== RCS_STATUSES.approved) {
4010
+ if (isFullMode && !isHostInfoBip && templateStatus !== RCS_STATUSES.approved) {
2158
4011
  return true;
2159
4012
  }
2160
4013
 
2161
- if (!isFullMode) {
2162
- if (templateName.trim() === '' || templateNameError) {
2163
- return true;
2164
- }
4014
+ if (isCarouselLibraryIncomplete()) {
4015
+ return true;
2165
4016
  }
2166
- if(!isFullMode){
2167
- const titleVars = splitTemplateVarString(templateTitle).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2168
- const descVars = splitTemplateVarString(templateDesc).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2169
- const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
2170
-
2171
- if (allVars.length > 0 && isEmpty(cardVarMapped)) {
2172
- return true;
2173
- }
2174
-
2175
- const hasEmptyMapping =
2176
- cardVarMapped &&
2177
- Object.keys(cardVarMapped).length > 0 &&
2178
- Object.entries(cardVarMapped).some(([_, v]) => {
2179
- if (typeof v !== 'string') return !v; // null/undefined
2180
- return v.trim() === ''; // empty string
2181
- });
2182
4017
 
2183
- if (hasEmptyMapping) {
2184
- return true;
2185
- }
4018
+ if (isLibraryCampaignCardVarMappingIncomplete()) {
4019
+ return true;
4020
+ }
2186
4021
 
2187
- const anyMissing = allVars.some(name => {
2188
- const v = cardVarMapped?.[name];
2189
- if (typeof v !== 'string') return !v;
2190
- return v.trim() === '';
2191
- });
2192
- if (anyMissing) {
2193
- return true;
2194
- }
4022
+ if (smsFallbackBlocksDone()) {
4023
+ return true;
2195
4024
  }
2196
- if (isMediaTypeText && templateDesc.trim() === '') {
4025
+
4026
+ if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
2197
4027
  return true;
2198
4028
  }
2199
- if (isMediaTypeImage && rcsImageSrc === '') {
4029
+ if (!isCarouselType && isMediaTypeImage && rcsImageSrc === '') {
2200
4030
  return true;
2201
4031
  }
2202
- if(isMediaTypeVideo && (rcsThumbnailSrc === '' || rcsVideoSrc.videoSrc === '')) {
4032
+ if (!isCarouselType && isMediaTypeVideo && (rcsThumbnailSrc === '' || rcsVideoSrc.videoSrc === '')) {
2203
4033
  return true;
2204
4034
  }
2205
4035
 
2206
4036
  if (buttonType.includes(CTA)) {
2207
- const hasValidButtons = suggestions.every(suggestion =>
4037
+ const hasValidButtons = suggestions.every(suggestion =>
2208
4038
  suggestion.text && suggestion.url && !suggestionError && !forbiddenCharactersValidation(suggestion.text)
2209
4039
  );
2210
4040
  if (!hasValidButtons) {
2211
4041
  return true;
2212
4042
  }
2213
4043
  }
2214
- if (templateTitleError || templateDescError || fallbackMessageError) {
4044
+ if (templateTitleError || templateDescError) {
4045
+ return true;
4046
+ }
4047
+ if (
4048
+ smsFallbackData?.content
4049
+ && smsFallbackData.content.length > FALLBACK_MESSAGE_MAX_LENGTH
4050
+ ) {
2215
4051
  return true;
2216
4052
  }
2217
4053
  return false;
@@ -2261,85 +4097,111 @@ const splitTemplateVarString = (str) => {
2261
4097
  };
2262
4098
 
2263
4099
  const getMainContent = () => {
2264
- if (showDltContainer && !fallbackPreviewmode) {
2265
- const dltSlideBoxContent = showDltContainer && getDltSlideBoxContent();
2266
- const { dltHeader = '', dltContent = '' } = dltSlideBoxContent;
2267
- return (
2268
- <CapSlideBox
2269
- show={showDltContainer}
2270
- header={dltHeader}
2271
- content={dltContent}
2272
- handleClose={closeDltContainerHandler}
2273
- size="size-xl"
2274
- />
2275
- );
2276
- }
2277
-
4100
+ // Slideboxes are rendered outside the page-level spinner to avoid
4101
+ // stacking/blur issues during initial loads.
4102
+ if (showDltContainer) return null;
2278
4103
  return (
2279
4104
  <>
2280
- {templateStatus !== '' && (<CapRow useLegacy className="template-status-container">
2281
- <CapLabel type="label2">
2282
- {formatMessage(messages.templateStatusLabel)}
2283
- </CapLabel>
2284
-
2285
- {templateStatus && (
2286
- <CapAlert
2287
- message={getTemplateStatusMessage()}
2288
- type={getTemplateStatusType(templateStatus)}
2289
- />
2290
- )}
2291
- </CapRow>
4105
+ {templateStatus !== '' && (
4106
+ <CapRow useLegacy className="template-status-container">
4107
+ <CapColumn span={14}>
4108
+ <CapLabel type="label2">
4109
+ {formatMessage(messages.templateStatusLabel)}
4110
+ </CapLabel>
4111
+
4112
+ {!isHostInfoBip && templateStatus && (
4113
+ <CapAlert
4114
+ message={getTemplateStatusMessage()}
4115
+ type={getTemplateStatusType(templateStatus)}
4116
+ />
4117
+ )}
4118
+ </CapColumn>
4119
+ </CapRow>
2292
4120
  )}
2293
- <CapRow className="cap-rcs-creatives">
4121
+ <CapRow className={`cap-rcs-creatives ${isEditLike ? 'rcs-edit-mode' : ''}`}>
2294
4122
  <CapColumn span={14}>
2295
4123
  {/* template name */}
2296
4124
  {isFullMode && (
2297
- <CapInput
2298
- id="rcs_template_name_input"
2299
- data-testid="template_name"
2300
- onChange={onTemplateNameChange}
2301
- errorMessage={templateNameError}
2302
- placeholder={formatMessage(
2303
- globalMessages.templateNamePlaceholder,
2304
- )}
2305
- value={templateName || ''}
2306
- size="default"
2307
- label={formatMessage(globalMessages.creativeNameLabel)}
2308
- disabled={(isEditFlow || !isFullMode)}
2309
- />
4125
+ isEditFlow ? (
4126
+ <div className="rcs-creative-name-readonly">
4127
+ <CapHeading type="h4">
4128
+ {formatMessage(globalMessages.creativeNameLabel)}
4129
+ </CapHeading>
4130
+ <CapHeading type="h5" className="rcs-creative-name-value">
4131
+ {templateName || '-'}
4132
+ </CapHeading>
4133
+ </div>
4134
+ ) : (
4135
+ <CapInput
4136
+ id="rcs_template_name_input"
4137
+ data-testid="template_name"
4138
+ onChange={onTemplateNameChange}
4139
+ errorMessage={templateNameError}
4140
+ placeholder={formatMessage(
4141
+ globalMessages.templateNamePlaceholder,
4142
+ )}
4143
+ value={templateName || ''}
4144
+ size="default"
4145
+ label={formatMessage(globalMessages.creativeNameLabel)}
4146
+ disabled={(isEditFlow || !isFullMode)}
4147
+ />
4148
+ )
4149
+ )}
4150
+ {isEditFlow ? (
4151
+ <div className="rcs-creative-name-readonly">
4152
+ <CapHeading type="h4">
4153
+ {formatMessage(messages.templateTypeLabel)}
4154
+ </CapHeading>
4155
+ <CapHeading type="h5" className="rcs-creative-name-value">
4156
+ {TEMPLATE_TYPE_OPTIONS.find((option) => option.value === templateType)?.label || '-'}
4157
+ </CapHeading>
4158
+ </div>
4159
+ ) : (
4160
+ <>
4161
+ {renderLabel('templateTypeLabel')}
4162
+ <ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
4163
+ <CapRadioGroup
4164
+ id="select-rcs-template-type"
4165
+ options={TEMPLATE_TYPE_OPTIONS}
4166
+ onChange={onTemplateTypeChange}
4167
+ value={templateType}
4168
+ disabled={!isFullMode}
4169
+ />
4170
+ </ConfigProvider>
4171
+ </>
2310
4172
  )}
2311
- {renderLabel('templateTypeLabel')}
2312
- <ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
2313
- <CapRadioGroup
2314
- id="select-rcs-template-type"
2315
- options={TEMPLATE_TYPE_OPTIONS}
2316
- onChange={onTemplateTypeChange}
2317
- value={templateType}
2318
- disabled={(isEditFlow || !isFullMode)}
2319
- />
2320
- </ConfigProvider>
2321
4173
 
2322
- {/* Show media only for rich_card or carousel */}
2323
- {(templateType === contentType.rich_card || templateType === contentType.carousel) && (
4174
+ {templateType === contentType.carousel ? (
4175
+ renderCarouselSection()
4176
+ ) : (
2324
4177
  <>
2325
- {renderLabel('mediaLabel')}
2326
- <ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
4178
+ {/* Show media only for rich_card */}
4179
+ {templateType === contentType.rich_card && (
4180
+ <>
4181
+ {renderLabel('mediaLabel')}
4182
+ <ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
2327
4183
  <CapRadioGroup
2328
- options={mediaRadioOptions || []}
2329
- value={templateMediaType}
2330
- onChange={onTemplateMediaTypeChange}
2331
- disabled={(isEditFlow || !isFullMode)}
2332
- className="rcs-radio"
2333
- />
2334
- </ConfigProvider>
4184
+ options={mediaRadioOptions || []}
4185
+ value={templateMediaType}
4186
+ onChange={onTemplateMediaTypeChange}
4187
+ disabled={(isEditFlow || !isFullMode)}
4188
+ className="rcs-radio"
4189
+ />
4190
+ </ConfigProvider>
2335
4191
  <div className="rcs-container-image">
2336
- {getMediaBasedComponent()}
2337
- </div>
4192
+ {getMediaBasedComponent()}
4193
+ </div>
4194
+ </>
4195
+ )}
4196
+ {renderTextComponent()}
4197
+ </>
4198
+ )}
4199
+ {(!isEditFlow || hasMeaningfulSmsFallbackShape(smsFallbackData)) && (
4200
+ <>
4201
+ <CapDivider className="rcs-fallback-section-divider" />
4202
+ {renderFallBackSmsComponent()}
2338
4203
  </>
2339
4204
  )}
2340
- {renderTextComponent()}
2341
- <CapDivider style={{ margin: `${CAP_SPACE_28} 0` }} />
2342
- {renderFallBackSmsComponent()}
2343
4205
  <div className="rcs-scroll-div" />
2344
4206
  </CapColumn>
2345
4207
  <CapColumn span={8} offset={2}>
@@ -2350,7 +4212,8 @@ const splitTemplateVarString = (str) => {
2350
4212
 
2351
4213
 
2352
4214
  <div className="rcs-footer">
2353
- {!isEditFlow && (
4215
+ {/* Full-mode create only: send-for-approval + disabled test/preview. Library mode uses Done below — onDoneCallback() is undefined when !isFullMode, so do not render this row (avoids a no-op primary button). */}
4216
+ {!isEditFlow && isFullMode && (
2354
4217
  <>
2355
4218
  <div className="button-disabled-tooltip-wrapper">
2356
4219
  <CapButton
@@ -2358,7 +4221,9 @@ const splitTemplateVarString = (str) => {
2358
4221
  disabled={isDisableDone()}
2359
4222
  className="rcs-done-btn"
2360
4223
  >
2361
- <FormattedMessage {...messages.sendForApprovalButtonLabel} />
4224
+ <FormattedMessage
4225
+ {...(isHostInfoBip ? messages.doneButtonLabel : messages.sendForApprovalButtonLabel)}
4226
+ />
2362
4227
  </CapButton>
2363
4228
  </div>
2364
4229
  <CapTooltip
@@ -2371,7 +4236,6 @@ const splitTemplateVarString = (str) => {
2371
4236
  className="rcs-test-preview-btn"
2372
4237
  type="secondary"
2373
4238
  disabled={true}
2374
- style={{ marginLeft: "8px" }}
2375
4239
  >
2376
4240
  <FormattedMessage {...creativesMessages.testAndPreview} />
2377
4241
  </CapButton>
@@ -2392,7 +4256,7 @@ const splitTemplateVarString = (str) => {
2392
4256
  </div>
2393
4257
  </>
2394
4258
  )}
2395
- {isEditFlow && templateStatus === RCS_STATUSES.approved && (
4259
+ {isEditFlow && (isHostInfoBip || templateStatus === RCS_STATUSES.approved) && (
2396
4260
  <>
2397
4261
  <CapButton
2398
4262
  onClick={handleTestAndPreview}
@@ -2404,51 +4268,6 @@ const splitTemplateVarString = (str) => {
2404
4268
  </>
2405
4269
  )}
2406
4270
  </div>
2407
-
2408
-
2409
- {fallbackPreviewmode && (
2410
- <CapSlideBox
2411
- className="rcs-fallback-preview"
2412
- show={fallbackPreviewmode}
2413
- header={(
2414
- <CapHeading type="h7" style={{ color: CAP_G01 }}>
2415
- {formatMessage(messages.fallbackPreviewtitle)}
2416
- </CapHeading>
2417
- )}
2418
- content={(
2419
- <>
2420
- <UnifiedPreview
2421
- channel={RCS}
2422
- content={{
2423
- rcsPreviewContent: {
2424
- rcsDesc: tempMsg,
2425
- },
2426
- }}
2427
- device={ANDROID}
2428
- showDeviceToggle={false}
2429
- showHeader={false}
2430
- formatMessage={formatMessage}
2431
- />
2432
- <CapHeading
2433
- type="h3"
2434
- style={{ textAlign: 'center', width: '100%' }}
2435
- className="margin-t-16"
2436
- >
2437
- {formatMessage(messages.totalCharacters, {
2438
- smsCount: Math.ceil(
2439
- tempMsg?.length / FALLBACK_MESSAGE_MAX_LENGTH,
2440
- ),
2441
- number: tempMsg.length,
2442
- })}
2443
- </CapHeading>
2444
- </>
2445
- )}
2446
- handleClose={() => {
2447
- setFallbackPreviewmode(false);
2448
- setDltPreviewData('');
2449
- }}
2450
- />
2451
- )}
2452
4271
  </>
2453
4272
  );
2454
4273
  };
@@ -2457,23 +4276,57 @@ const splitTemplateVarString = (str) => {
2457
4276
  <CapSpin spinning={loadingTags || spin}>
2458
4277
  {getMainContent()}
2459
4278
  </CapSpin>
4279
+
4280
+ {showDltContainer && (() => {
4281
+ const { dltHeader = '', dltContent = '' } = getDltSlideBoxContent() || {};
4282
+ return (
4283
+ <CapSlideBox
4284
+ show={showDltContainer}
4285
+ header={dltHeader}
4286
+ content={dltContent}
4287
+ handleClose={closeDltContainerHandler}
4288
+ size="size-xl"
4289
+ />
4290
+ );
4291
+ })()}
4292
+
2460
4293
  <TestAndPreviewSlidebox
2461
4294
  show={propsShowTestAndPreviewSlidebox || showTestAndPreviewSlidebox}
2462
4295
  onClose={handleCloseTestAndPreview}
2463
- formData={null} // RCS doesn't use formData structure like SMS
2464
- content={getTemplateContent()}
4296
+ formData={testPreviewFormData}
4297
+ content={testAndPreviewContent}
2465
4298
  currentChannel={RCS}
4299
+ orgUnitId={orgUnitId}
4300
+ rcsTestPreviewOptions={{ isLibraryMode: !isFullMode }}
4301
+ smsFallbackContent={
4302
+ smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
4303
+ ? {
4304
+ templateContent:
4305
+ smsFallbackData.templateContent || smsFallbackData.content || '',
4306
+ templateName: smsFallbackData.templateName || '',
4307
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: !isFullMode
4308
+ ? mergeRcsSmsFallbackVarMapLayers(
4309
+ getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
4310
+ smsFallbackData,
4311
+ )
4312
+ : mergeRcsSmsFallbackVarMapLayers({}, smsFallbackData),
4313
+ }
4314
+ : null
4315
+ }
4316
+ smsRegister={smsRegister}
2466
4317
  />
2467
4318
  </>
2468
4319
  );
2469
4320
  };
2470
4321
 
4322
+
2471
4323
  const mapStateToProps = createStructuredSelector({
2472
4324
  rcsData: makeSelectRcs(),
2473
4325
  accountData: makeSelectAccount(),
2474
4326
  metaEntities: makeSelectMetaEntities(),
2475
4327
  loadingTags: isLoadingMetaEntities(),
2476
4328
  injectedTags: setInjectedTags(),
4329
+ currentOrgDetails: selectCurrentOrgDetails(),
2477
4330
  });
2478
4331
 
2479
4332
  const mapDispatchToProps = (dispatch) => ({