@capillarytech/creatives-library 9.0.15-alpha.1 → 9.0.16

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