@capillarytech/creatives-library 8.0.353-alpha.6 → 8.0.354

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