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

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