@capillarytech/creatives-library 8.0.319 → 8.0.321

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 (139) hide show
  1. package/constants/unified.js +14 -0
  2. package/package.json +1 -1
  3. package/utils/templateVarUtils.js +172 -0
  4. package/utils/tests/tagValidations.test.js +34 -0
  5. package/utils/tests/templateVarUtils.test.js +160 -0
  6. package/v2Components/CapTagList/index.js +25 -22
  7. package/v2Components/CapTagList/style.scss +48 -0
  8. package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +63 -0
  9. package/v2Components/CapTagListWithInput/index.js +4 -0
  10. package/v2Components/CapWhatsappCTA/index.js +2 -0
  11. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  13. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  14. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  15. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  16. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  18. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  19. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  20. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  21. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  22. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  23. package/v2Components/CommonTestAndPreview/index.js +693 -155
  24. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  25. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  26. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  27. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
  28. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  29. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  30. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  31. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  32. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  33. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  34. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  35. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  36. package/v2Components/FormBuilder/index.js +14 -1
  37. package/v2Components/HtmlEditor/HTMLEditor.js +6 -1
  38. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  39. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +927 -2
  40. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +3 -0
  41. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  42. package/v2Components/SmsFallback/constants.js +73 -0
  43. package/v2Components/SmsFallback/index.js +956 -0
  44. package/v2Components/SmsFallback/index.scss +265 -0
  45. package/v2Components/SmsFallback/messages.js +78 -0
  46. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  47. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  48. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  49. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  50. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  51. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  52. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  53. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  54. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  55. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  56. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  57. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  58. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  59. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  60. package/v2Containers/BeeEditor/index.js +3 -0
  61. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  62. package/v2Containers/CreativesContainer/SlideBoxContent.js +64 -5
  63. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  64. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  65. package/v2Containers/CreativesContainer/constants.js +9 -0
  66. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  67. package/v2Containers/CreativesContainer/index.js +292 -99
  68. package/v2Containers/CreativesContainer/index.scss +51 -1
  69. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  70. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
  71. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
  72. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  73. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
  74. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  75. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  76. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  77. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  78. package/v2Containers/Email/index.js +1 -0
  79. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +7 -1
  80. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +3 -0
  81. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -2
  82. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +16 -1
  83. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +3 -0
  84. package/v2Containers/EmailWrapper/index.js +4 -0
  85. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +1 -0
  86. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +9 -0
  87. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +19 -0
  88. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +3 -0
  89. package/v2Containers/InAppWrapper/index.js +3 -0
  90. package/v2Containers/MobilePush/Create/index.js +2 -0
  91. package/v2Containers/MobilePush/Edit/index.js +2 -0
  92. package/v2Containers/MobilepushWrapper/index.js +3 -1
  93. package/v2Containers/Rcs/constants.js +32 -1
  94. package/v2Containers/Rcs/index.js +951 -873
  95. package/v2Containers/Rcs/index.scss +85 -6
  96. package/v2Containers/Rcs/messages.js +10 -1
  97. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  98. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  99. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  100. package/v2Containers/Rcs/tests/index.test.js +41 -38
  101. package/v2Containers/Rcs/tests/mockData.js +38 -0
  102. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  103. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  104. package/v2Containers/Rcs/utils.js +358 -10
  105. package/v2Containers/Sms/Create/index.js +83 -36
  106. package/v2Containers/Sms/Edit/index.js +2 -0
  107. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  108. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  109. package/v2Containers/SmsTrai/Create/index.js +9 -4
  110. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  111. package/v2Containers/SmsTrai/Edit/index.js +611 -128
  112. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  113. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  114. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  115. package/v2Containers/SmsWrapper/index.js +39 -8
  116. package/v2Containers/TagList/index.js +47 -2
  117. package/v2Containers/TagList/messages.js +4 -0
  118. package/v2Containers/TagList/tests/TagList.test.js +122 -20
  119. package/v2Containers/TagList/tests/mockdata.js +17 -0
  120. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  121. package/v2Containers/Templates/_templates.scss +61 -2
  122. package/v2Containers/Templates/actions.js +11 -0
  123. package/v2Containers/Templates/constants.js +2 -0
  124. package/v2Containers/Templates/index.js +90 -40
  125. package/v2Containers/Templates/sagas.js +57 -12
  126. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  127. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  128. package/v2Containers/Templates/tests/sagas.test.js +193 -12
  129. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  130. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  131. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  132. package/v2Containers/TemplatesV2/index.js +86 -23
  133. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  134. package/v2Containers/Viber/index.js +5 -0
  135. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +0 -2
  136. package/v2Containers/WebPush/Create/index.js +9 -1
  137. package/v2Containers/Whatsapp/index.js +8 -20
  138. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +598 -34
  139. package/v2Containers/Zalo/index.js +2 -0
@@ -1,13 +1,12 @@
1
1
  /* eslint-disable no-unused-expressions */
2
- import React, { useState, useEffect, useCallback } from 'react';
2
+ import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
3
3
  import { bindActionCreators } from 'redux';
4
4
  import { createStructuredSelector } from 'reselect';
5
- import { injectIntl, FormattedMessage } from 'react-intl';
5
+ import { FormattedMessage } from 'react-intl';
6
6
  import get from 'lodash/get';
7
7
  import isEmpty from 'lodash/isEmpty';
8
8
  import cloneDeep from 'lodash/cloneDeep';
9
9
  import isNil from 'lodash/isNil';
10
- import styled from 'styled-components';
11
10
  import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
12
11
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
13
12
  import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
@@ -22,35 +21,16 @@ import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
22
21
  import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
23
22
  import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
24
23
  import CapImage from '@capillarytech/cap-ui-library/CapImage';
25
- import CapCard from '@capillarytech/cap-ui-library/CapCard';
26
24
  import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
27
25
  import CapSelect from '@capillarytech/cap-ui-library/CapSelect';
28
- import CapCustomCard from '@capillarytech/cap-ui-library/CapCustomCard';
29
- import CapDropdown from '@capillarytech/cap-ui-library/CapDropdown';
30
- import CapMenu from '@capillarytech/cap-ui-library/CapMenu';
31
26
  import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
32
- import CapTooltipWithInfo from '@capillarytech/cap-ui-library/CapTooltipWithInfo';
33
27
  import CapError from '@capillarytech/cap-ui-library/CapError';
34
- import CapCheckbox from '@capillarytech/cap-ui-library/CapCheckbox';
35
28
  import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
36
- import CapLink from '@capillarytech/cap-ui-library/CapLink';
37
-
38
- import {
39
- CAP_G01,
40
- CAP_SPACE_04,
41
- CAP_SPACE_16,
42
- CAP_SPACE_24,
43
- CAP_SPACE_28,
44
- CAP_SPACE_32,
45
- CAP_WHITE,
46
- CAP_SECONDARY,
47
- } from '@capillarytech/cap-ui-library/styled/variables';
48
29
 
49
30
  import CapVideoUpload from '../../v2Components/CapVideoUpload';
50
31
  import * as globalActions from '../Cap/actions';
51
32
  import CapActionButton from '../../v2Components/CapActionButton';
52
33
  import { makeSelectRcs, makeSelectAccount } from './selectors';
53
- import { DATE_DISPLAY_FORMAT, TIME_DISPLAY_FORMAT } from '../App/constants';
54
34
  import {
55
35
  isLoadingMetaEntities,
56
36
  makeSelectMetaEntities,
@@ -60,6 +40,15 @@ import * as RcsActions from './actions';
60
40
  import { isAiContentBotDisabled } from '../../utils/common';
61
41
  import * as TemplatesActions from '../Templates/actions';
62
42
  import './index.scss';
43
+ import {
44
+ normalizeLibraryLoadedTitleDesc,
45
+ mergeRcsSmsFallBackContentFromDetails,
46
+ mergeRcsSmsFallbackVarMapLayers,
47
+ pickFirstSmsFallbackTemplateString,
48
+ syncCardVarMappedSemanticsFromSlots,
49
+ hasMeaningfulSmsFallbackShape,
50
+ getLibrarySmsFallbackApiBaselineFromTemplateData,
51
+ } from './rcsLibraryHydrationUtils';
63
52
  import {
64
53
  RCS,
65
54
  SMS,
@@ -81,8 +70,6 @@ import {
81
70
  MESSAGE_TEXT,
82
71
  ALLOWED_EXTENSIONS_VIDEO_REGEX,
83
72
  RCS_VIDEO_SIZE,
84
- TEMPLATE_HEADER_MAX_LENGTH,
85
- TEMPLATE_MESSAGE_MAX_LENGTH,
86
73
  RCS_THUMBNAIL_MIN_SIZE,
87
74
  RCS_THUMBNAIL_MAX_SIZE,
88
75
  contentType,
@@ -95,35 +82,45 @@ import {
95
82
  MAX_BUTTONS,
96
83
  INITIAL_SUGGESTIONS_DATA_STOP,
97
84
  RCS_BUTTON_TYPES,
98
- titletype,
99
- descType,
100
85
  STANDALONE,
101
86
  VERTICAL,
102
87
  SMALL,
103
88
  MEDIUM,
104
89
  RICHCARD,
90
+ RCS_NUMERIC_VAR_NAME_REGEX,
91
+ RCS_TAG_AREA_FIELD_TITLE,
92
+ RCS_TAG_AREA_FIELD_DESC,
105
93
  } from './constants';
106
94
  import globalMessages from '../Cap/messages';
107
95
  import messages from './messages';
108
96
  import creativesMessages from '../CreativesContainer/messages';
109
97
  import withCreatives from '../../hoc/withCreatives';
110
98
  import UnifiedPreview from '../../v2Components/CommonTestAndPreview/UnifiedPreview';
111
- import { ANDROID } from '../../v2Components/CommonTestAndPreview/constants';
99
+ import VarSegmentMessageEditor from '../../v2Components/VarSegmentMessageEditor';
100
+ import { ANDROID, RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
112
101
  import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
102
+ import { splitTemplateVarString } from '../../utils/templateVarUtils';
113
103
  import CapImageUpload from '../../v2Components/CapImageUpload';
114
- import addCreativesIcon from '../Assets/images/addCreativesIllustration.svg';
115
104
  import Templates from '../Templates';
116
105
  import SmsTraiEdit from '../SmsTrai/Edit';
106
+ import SmsFallback from '../../v2Components/SmsFallback';
107
+ import { CHANNELS_TO_HIDE_FOR_SMS_ONLY } from '../../v2Components/SmsFallback/constants';
117
108
  import TagList from '../TagList';
118
109
  import { validateTags } from '../../utils/tagValidations';
119
- import { getCdnUrl } from '../../utils/cdnTransformation';
110
+ import { isTraiDLTEnable } from '../../utils/common';
120
111
  import { isTagIncluded } from '../../utils/commonUtils';
121
112
  import injectReducer from '../../utils/injectReducer';
122
113
  import v2RcsReducer from './reducer';
123
- import { getTemplateStatusType } from './utils';
124
-
114
+ import {
115
+ areAllRcsSmsFallbackVarSlotsFilled,
116
+ buildRcsNumericMustachePlaceholderRegex,
117
+ getTemplateStatusType,
118
+ normalizeCardVarMapped,
119
+ coalesceCardVarMappedToTemplate,
120
+ resolveCardVarMappedSlotValue,
121
+ sanitizeCardVarMappedValue,
122
+ } from './utils';
125
123
 
126
- const { Group: CapCheckboxGroup } = CapCheckbox;
127
124
  export const Rcs = (props) => {
128
125
  const {
129
126
  intl,
@@ -137,17 +134,17 @@ export const Rcs = (props) => {
137
134
  templatesActions,
138
135
  globalActions,
139
136
  location,
140
- handleClose,
141
137
  getDefaultTags,
142
138
  supportedTags,
143
139
  metaEntities,
144
140
  injectedTags,
145
141
  loadingTags,
146
142
  getFormData,
147
- isDltEnabled,
148
143
  smsRegister,
144
+ orgUnitId,
149
145
  selectedOfferDetails,
150
146
  eventContextTags,
147
+ waitEventContextTags,
151
148
  accountData = {},
152
149
  // TestAndPreviewSlidebox props
153
150
  showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
@@ -156,8 +153,8 @@ export const Rcs = (props) => {
156
153
  } = props || {};
157
154
  const { formatMessage } = intl;
158
155
  const { TextArea } = CapInput;
159
- const { CapCustomCardList } = CapCustomCard;
160
156
  const [isEditFlow, setEditFlow] = useState(false);
157
+ const isEditLike = isEditFlow || !isFullMode;
161
158
  const [tags, updateTags] = useState([]);
162
159
  const [spin, setSpin] = useState(false);
163
160
  //template
@@ -166,112 +163,71 @@ export const Rcs = (props) => {
166
163
  const [templateMediaType, setTemplateMediaType] = useState(
167
164
  RCS_MEDIA_TYPES.NONE,
168
165
  );
169
- const [templateRejectionReason, setTemplateRejectionReason] = useState(null);
170
166
  const [templateTitle, setTemplateTitle] = useState('');
171
167
  const [templateDesc, setTemplateDesc] = useState('');
172
168
  const [templateDescError, setTemplateDescError] = useState(false);
173
169
  const [templateStatus, setTemplateStatus] = useState('');
174
- const [templateDate, setTemplateDate] = useState('');
175
- //fallback
176
- const [fallbackMessage, setFallbackMessage] = useState('');
177
- const [fallbackMessageError, setFallbackMessageError] = useState(false);
178
170
  //fallback dlt
179
171
  const [showDltContainer, setShowDltContainer] = useState(false);
180
172
  const [dltMode, setDltMode] = useState('');
181
173
  const [dltEditData, setDltEditData] = useState({});
182
- const [showDltCard, setShowDltCard] = useState(false);
183
- const [fallbackPreviewmode, setFallbackPreviewmode] = useState(false);
184
- const [dltPreviewData, setDltPreviewData] = useState('');
174
+ /** `undefined` = not hydrated yet; `null` = no fallback / user removed template; object = selected fallback */
175
+ const [smsFallbackData, setSmsFallbackData] = useState(undefined);
185
176
  const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS_DATA_STOP);
186
- const [buttonType, setButtonType] = useState(RCS_BUTTON_TYPES.NONE);
177
+ const buttonType = RCS_BUTTON_TYPES.NONE;
187
178
  const [suggestionError, setSuggestionError] = useState(true);
188
179
  const [templateType, setTemplateType] = useState('text_message');
189
- const [templateHeader, setTemplateHeader] = useState('');
190
- const [templateMessage, setTemplateMessage] = useState('');
191
- const [templateHeaderError, setTemplateHeaderError] = useState('');
192
- const [templateMessageError, setTemplateMessageError] = useState('');
193
- const validVarRegex = /\{\{(\d+)\}\}/g;
194
- const [updatedTitleData, setUpdatedTitleData] = useState([]);
195
- const [updatedDescData, setUpdatedDescData] = useState([]);
196
180
  const [titleVarMappedData, setTitleVarMappedData] = useState({});
197
181
  const [descVarMappedData, setDescVarMappedData] = useState({});
198
182
  const [titleTextAreaId, setTitleTextAreaId] = useState();
199
183
  const [descTextAreaId, setDescTextAreaId] = useState();
200
184
  const [assetList, setAssetList] = useState({});
201
- const [assetListImage, setAssetListImage] = useState('');
202
185
  const [rcsImageSrc, updateRcsImageSrc] = useState('');
203
186
  const [rcsVideoSrc, setRcsVideoSrc] = useState({});
204
187
  const [rcsThumbnailSrc, setRcsThumbnailSrc] = useState('');
205
188
  const [selectedDimension, setSelectedDimension] = useState(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
206
- const [imageError, setImageError] = useState(null);
207
189
  const [templateTitleError, setTemplateTitleError] = useState(false);
208
190
  const [cardVarMapped, setCardVarMapped] = useState({});
191
+ /** Bump when hydrated cardVarMapped payload changes so VarSegment TextAreas remount with fresh valueMap (controlled-input sync). */
192
+ const [rcsVarSegmentEditorRemountKey, setRcsVarSegmentEditorRemountKey] = useState(0);
193
+ const lastHydratedRcsCardVarSignatureRef = useRef(null);
194
+
195
+ /**
196
+ * Hydrate only from template payload — not from full `rcsData`. Uploads/asset updates change `rcsData`
197
+ * without changing `templateDetails`; re-running hydration then cleared `smsFallbackData`, so the SMS
198
+ * fallback card / content stopped appearing until re-selected.
199
+ */
200
+ const rcsHydrationDetails = useMemo(
201
+ () => (isFullMode ? rcsData?.templateDetails : templateData),
202
+ [isFullMode, rcsData?.templateDetails, templateData],
203
+ );
209
204
 
210
- // TestAndPreviewSlidebox state
211
- const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
212
- const [isTestAndPreviewMode, setIsTestAndPreviewMode] = useState(false);
213
-
214
- const tempMsg = dltPreviewData === '' ? fallbackMessage : dltPreviewData;
215
-
216
- // Get template content for TestAndPreviewSlidebox
217
- // Reference: Based on getRcsPreview() function (lines 2087-2111) which prepares content for TemplatePreview
218
- // getRcsPreview ALWAYS uses templateTitle and templateDesc for ALL template types (text_message, rich_card, carousel)
219
- // renderTextComponent (lines 1317-1485) also uses templateTitle and templateDesc
220
- // Note: templateHeader and templateMessage are defined but NOT used in the component
221
- const getTemplateContent = useCallback(() => {
222
- const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
223
- const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
224
- const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
225
-
226
- // Build media preview object (same pattern as getRcsPreview)
227
- const mediaPreview = {};
228
- if (isMediaTypeImage && rcsImageSrc) {
229
- mediaPreview.rcsImageSrc = rcsImageSrc;
230
- }
231
- if (isMediaTypeVideo && !isMediaTypeText) {
232
- // For video, use thumbnailSrc as rcsVideoSrc (same as getRcsPreview line 2104)
233
- if (rcsThumbnailSrc) {
234
- mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
235
- } else if (rcsVideoSrc?.videoSrc) {
236
- mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
237
- }
238
- // Also include thumbnailSrc separately if available
239
- if (rcsThumbnailSrc) {
240
- mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
205
+ /** 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'). */
206
+ const lastTagSchemaQueryKeyRef = useRef(null);
207
+ /**
208
+ * Library: parent often passes a new `templateData` object reference every render. Re-applying the same
209
+ * SMS fallback snapshot was resetting `smsFallbackData` and caused VarSegment inputs to flicker old/new.
210
+ */
211
+ const lastSmsFallbackHydrationKeyRef = useRef(null);
212
+
213
+ const fetchTagSchemaIfNewQuery = useCallback(
214
+ (query) => {
215
+ const key = JSON.stringify(query);
216
+ if (lastTagSchemaQueryKeyRef.current === key) {
217
+ return;
241
218
  }
242
- }
243
-
244
- // Build content object
245
- // Reference: getRcsPreview (line 2091-2092) uses templateTitle and templateDesc for ALL cases
246
- // templateTitle is used for rich_card/carousel title, empty for text_message
247
- // templateDesc is used for ALL types (text message body or rich card description)
248
- // For UnifiedPreview, we map templateTitle -> templateHeader and templateDesc -> templateMessage
249
- const contentObj = {
250
- // Map templateTitle to templateHeader and templateDesc to templateMessage
251
- templateHeader: templateTitle,
252
- templateMessage: templateDesc,
253
- ...mediaPreview,
254
- ...(suggestions.length > 0 && {
255
- suggestions: suggestions,
256
- }),
257
- };
219
+ lastTagSchemaQueryKeyRef.current = key;
220
+ globalActions.fetchSchemaForEntity(query);
221
+ },
222
+ [globalActions],
223
+ );
258
224
 
259
- return contentObj;
260
- }, [
261
- templateMediaType,
262
- templateTitle,
263
- templateDesc,
264
- rcsImageSrc,
265
- rcsVideoSrc,
266
- rcsThumbnailSrc,
267
- suggestions,
268
- selectedDimension,
269
- ]);
225
+ // TestAndPreviewSlidebox state
226
+ const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
270
227
 
271
228
  // Handle Test and Preview button click
272
229
  const handleTestAndPreview = useCallback(() => {
273
230
  setShowTestAndPreviewSlidebox(true);
274
- setIsTestAndPreviewMode(true);
275
231
  if (propsHandleTestAndPreview) {
276
232
  propsHandleTestAndPreview();
277
233
  }
@@ -280,23 +236,24 @@ export const Rcs = (props) => {
280
236
  // Handle close Test and Preview slidebox
281
237
  const handleCloseTestAndPreview = useCallback(() => {
282
238
  setShowTestAndPreviewSlidebox(false);
283
- setIsTestAndPreviewMode(false);
284
239
  if (propsHandleCloseTestAndPreview) {
285
240
  propsHandleCloseTestAndPreview();
286
241
  }
287
242
  }, [propsHandleCloseTestAndPreview]);
288
243
 
289
- // Helper to get RCS orientation from selectedDimension
290
- const getRcsOrientation = () => {
291
- const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
292
- if (isMediaTypeImage) {
293
- return RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
294
- }
295
- // For video
296
- return RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
297
- };
244
+ /** Merge editor slot map into `smsFallbackData` (like `cardVarMapped` for title/desc). */
245
+ const handleSmsFallbackEditorStateChange = useCallback((patch) => {
246
+ setSmsFallbackData((prev) => {
247
+ if (!patch || typeof patch !== 'object') return prev;
248
+ // Merge even when `prev` is null so tag/slot updates apply on top of `smsFromApiShape` in createPayload.
249
+ return { ...(prev || {}), ...patch };
250
+ });
251
+ }, []);
298
252
 
253
+ /** RCS template save / edit API: `rcsContent.accountId` must stay `sourceAccountIdentifier` (pairs with accessToken). */
299
254
  const [accountId, setAccountId] = useState('');
255
+ /** WeCRM list row `id` — only for CommonTestAndPreview → createMessageMeta payload, not for template save. */
256
+ const [wecrmAccountId, setWecrmAccountId] = useState('');
300
257
  const [accessToken, setAccessToken] = useState('');
301
258
  const [hostName, setHostName] = useState('');
302
259
  const [accountName, setAccountName] = useState('');
@@ -305,15 +262,25 @@ export const Rcs = (props) => {
305
262
  const accountObj = accountData.selectedRcsAccount || {};
306
263
  if (!isEmpty(accountObj)) {
307
264
  const {
265
+ id: wecrmId,
308
266
  sourceAccountIdentifier = '',
309
267
  configs = {},
310
268
  } = accountObj;
311
-
312
269
  setAccountId(sourceAccountIdentifier);
270
+ setWecrmAccountId(
271
+ wecrmId != null && String(wecrmId).trim() !== '' ? String(wecrmId) : '',
272
+ );
313
273
  setAccessToken(configs.accessToken || '');
314
274
  setHostName(accountObj.hostName || '');
315
275
  setAccountName(accountObj.name || '');
316
276
  setRcsAccount(accountObj.id || '');
277
+ } else {
278
+ setAccountId('');
279
+ setWecrmAccountId('');
280
+ setAccessToken('');
281
+ setHostName('');
282
+ setAccountName('');
283
+ setRcsAccount('');
317
284
  }
318
285
  }, [accountData.selectedRcsAccount]);
319
286
 
@@ -369,7 +336,9 @@ export const Rcs = (props) => {
369
336
  if (isFullMode) return;
370
337
  if (loadingTags || !tags || tags.length === 0) return;
371
338
  const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
372
- const resolved = resolveTemplateWithMap(templateStr); // placeholders -> mapped value (or '')
339
+ const slotOffset =
340
+ type === TITLE_TEXT ? 0 : (templateTitle ? templateTitle.match(rcsVarRegex) || [] : []).length;
341
+ const resolved = resolveTemplateWithMap(templateStr, slotOffset); // placeholders -> mapped value (or '')
373
342
  if (!resolved) {
374
343
  if (type === TITLE_TEXT) setTemplateTitleError(false);
375
344
  if (type === MESSAGE_TEXT) setTemplateDescError(false);
@@ -414,13 +383,41 @@ export const Rcs = (props) => {
414
383
 
415
384
  const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
416
385
 
417
- const resolveTemplateWithMap = (str = '') => {
386
+ const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
387
+
388
+ /** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
389
+ const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
390
+ const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
391
+ const offset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
392
+ const templateSegments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
393
+ let varOrdinal = 0;
394
+ for (let segmentIndexInField = 0; segmentIndexInField < templateSegments.length; segmentIndexInField += 1) {
395
+ const segmentToken = templateSegments[segmentIndexInField];
396
+ if (rcsVarTestRegex.test(segmentToken)) {
397
+ if (`${segmentToken}_${segmentIndexInField}` === varSegmentCompositeId) {
398
+ return offset + varOrdinal;
399
+ }
400
+ varOrdinal += 1;
401
+ }
402
+ }
403
+ return null;
404
+ };
405
+
406
+ const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
418
407
  if (!str) return '';
419
- const arr = splitTemplateVarString(str);
408
+ const arr = splitTemplateVarStringRcs(str);
409
+ let varOrdinal = 0;
420
410
  return arr.map((elem) => {
421
411
  if (rcsVarTestRegex.test(elem)) {
422
412
  const key = getVarNameFromToken(elem);
423
- const v = cardVarMapped?.[key];
413
+ const globalSlot = slotOffset + varOrdinal;
414
+ varOrdinal += 1;
415
+ const v = resolveCardVarMappedSlotValue(
416
+ cardVarMapped,
417
+ key,
418
+ globalSlot,
419
+ isEditLike,
420
+ );
424
421
  if (isNil(v) || String(v)?.trim?.() === '') return elem;
425
422
  return String(v);
426
423
  }
@@ -428,107 +425,154 @@ export const Rcs = (props) => {
428
425
  }).join('');
429
426
  };
430
427
 
428
+ /**
429
+ * Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
430
+ * (VarSegmentMessageEditor: isEditFlow || !isFullMode). Full-mode create without edit uses plain
431
+ * TextArea only — no mapping. Full-mode + edit uses slots; !isFullMode alone is not enough.
432
+ */
433
+ const getTemplateContent = useCallback(() => {
434
+ const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
435
+ const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
436
+ const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
431
437
 
432
- useEffect(() => {
433
- if (isFullMode || isEditFlow) return;
434
- const tokens = [
435
- ...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
436
- ...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
437
- ];
438
- if (!tokens.length) return;
439
- setCardVarMapped((prev) => {
440
- const next = { ...(prev || {}) };
441
- let changed = false;
442
- tokens.forEach((t) => {
443
- const name = getVarNameFromToken(t);
444
- if (name && !Object.prototype.hasOwnProperty.call(next, name)) {
445
- next[name] = '';
446
- changed = true;
447
- }
448
- });
449
- return changed ? next : prev;
450
- });
451
- }, [isFullMode, templateTitle, templateDesc]);
438
+ const isSlotMappingMode = isEditFlow || !isFullMode;
439
+ const titleVarCountForResolve = isMediaTypeText
440
+ ? 0
441
+ : ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
442
+ const resolvedTitle = isMediaTypeText
443
+ ? ''
444
+ : (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
445
+ const resolvedDesc = isSlotMappingMode
446
+ ? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
447
+ : templateDesc;
452
448
 
449
+ const mediaPreview = {};
450
+ if (isMediaTypeImage && rcsImageSrc) {
451
+ mediaPreview.rcsImageSrc = rcsImageSrc;
452
+ }
453
+ if (isMediaTypeVideo && !isMediaTypeText) {
454
+ if (rcsThumbnailSrc) {
455
+ mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
456
+ } else if (rcsVideoSrc?.videoSrc) {
457
+ mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
458
+ }
459
+ if (rcsThumbnailSrc) {
460
+ mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
461
+ }
462
+ }
463
+
464
+ const contentObj = {
465
+ templateHeader: resolvedTitle,
466
+ templateMessage: resolvedDesc,
467
+ ...mediaPreview,
468
+ ...(suggestions.length > 0 && {
469
+ suggestions: suggestions,
470
+ }),
471
+ };
472
+
473
+ return contentObj;
474
+ }, [
475
+ templateMediaType,
476
+ templateTitle,
477
+ templateDesc,
478
+ rcsImageSrc,
479
+ rcsVideoSrc,
480
+ rcsThumbnailSrc,
481
+ suggestions,
482
+ selectedDimension,
483
+ isFullMode,
484
+ isEditFlow,
485
+ cardVarMapped,
486
+ ]);
487
+
488
+ const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
453
489
 
454
- const RcsLabel = styled.div`
455
- display: flex;
456
- margin-top: 20px;
457
- `;
458
490
  const paramObj = params || {};
459
491
  useEffect(() => {
460
- const { id } = paramObj;
461
- if (id && isFullMode) {
462
- setSpin(true);
463
- actions.getTemplateDetails(id, setSpin);
464
- }
465
- return () => {
466
- actions.clearEditResponse();
467
- };
468
- }, [paramObj.id]);
492
+ const { id } = paramObj;
493
+ if (id && isFullMode) {
494
+ setSpin(true);
495
+ actions.getTemplateDetails(id, setSpin);
496
+ }
497
+ return () => {
498
+ actions.clearEditResponse();
499
+ };
500
+ }, [paramObj.id, isFullMode]);
469
501
 
470
502
  useEffect(() => {
471
- if (!(isEditFlow || !isFullMode)) return;
472
-
473
- const initField = (targetString, currentVarMap, setVarMap, setUpdated) => {
474
- const arr = splitTemplateVarString(targetString);
475
- if (!arr?.length) {
476
- setVarMap({});
477
- setUpdated([]);
478
- return;
503
+ if (!(isEditFlow || !isFullMode)) return;
504
+
505
+ const titleTokenCount = (templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length;
506
+
507
+ const initField = (targetString, setVarMap, slotOffset) => {
508
+ const arr = splitTemplateVarStringRcs(targetString);
509
+ if (!arr?.length) {
510
+ setVarMap({});
511
+ return;
512
+ }
513
+ const nextVarMap = {};
514
+ let varOrdinal = 0;
515
+ arr.forEach((elem, idx) => {
516
+ // RCS placeholders are alphanumeric/underscore (e.g. {{user_name}}), not numeric-only
517
+ if (rcsVarTestRegex.test(elem)) {
518
+ const id = `${elem}_${idx}`;
519
+ const varName = getVarNameFromToken(elem);
520
+ const globalSlot = slotOffset + varOrdinal;
521
+ varOrdinal += 1;
522
+ const mappedValue = resolveCardVarMappedSlotValue(
523
+ cardVarMapped,
524
+ varName,
525
+ globalSlot,
526
+ isEditLike,
527
+ );
528
+ nextVarMap[id] = mappedValue;
479
529
  }
480
- const nextVarMap = {};
481
- const nextUpdated = [...arr];
482
- arr.forEach((elem, idx) => {
483
- // RCS placeholders are alphanumeric/underscore (e.g. {{user_name}}), not numeric-only
484
- if (rcsVarTestRegex.test(elem)) {
485
- const id = `${elem}_${idx}`;
486
- const varName = getVarNameFromToken(elem);
487
- const mappedValue = (cardVarMapped?.[varName] ?? '').toString();
488
- nextVarMap[id] = mappedValue;
489
- if (mappedValue !== '') {
490
- nextUpdated[idx] = mappedValue;
491
- } else {
492
- nextUpdated[idx] = elem;
493
- }
494
- }
495
- });
496
- setVarMap(nextVarMap);
497
- setUpdated(nextUpdated);
498
- };
530
+ });
531
+ setVarMap(nextVarMap);
532
+ };
499
533
 
500
- initField(templateTitle, titleVarMappedData, setTitleVarMappedData, setUpdatedTitleData);
501
- initField(templateDesc, descVarMappedData, setDescVarMappedData, setUpdatedDescData);
502
- }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode]);
503
-
504
- useEffect(() => {
505
- if(!isEditFlow && isFullMode){
534
+ initField(templateTitle, setTitleVarMappedData, 0);
535
+ initField(templateDesc, setDescVarMappedData, titleTokenCount);
536
+ }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode]);
537
+
538
+ useEffect(() => {
539
+ if (!isEditFlow && isFullMode) {
506
540
  setRcsVideoSrc({});
507
541
  updateRcsImageSrc('');
508
542
  setUpdateRcsImageSrc('');
509
543
  updateRcsThumbnailSrc('');
510
544
  setAssetList({});
511
- }
512
- }, [templateMediaType]);
545
+ }
546
+ }, [templateMediaType]);
513
547
 
514
- const templateStatusHelper = (details) => {
515
- const status = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
516
- switch (status) {
517
- case RCS_STATUSES.approved:
548
+ /** Status on first card — same merged card as title/description/cardVarMapped (library may only set rcsContent at root). */
549
+ const templateStatusHelper = (cardContentFirst) => {
550
+ const raw =
551
+ cardContentFirst?.Status
552
+ ?? cardContentFirst?.status
553
+ ?? cardContentFirst?.approvalStatus
554
+ ?? '';
555
+ const status = typeof raw === 'string' ? raw.trim() : String(raw);
556
+ const n = status.toLowerCase();
557
+ switch (n) {
558
+ case 'approved':
518
559
  setTemplateStatus(RCS_STATUSES.approved);
519
560
  break;
520
- case RCS_STATUSES.pending:
561
+ case 'pending':
521
562
  setTemplateStatus(RCS_STATUSES.pending);
522
563
  break;
523
- case RCS_STATUSES.awaitingApproval:
564
+ case 'awaitingapproval':
524
565
  setTemplateStatus(RCS_STATUSES.awaitingApproval);
525
566
  break;
526
- case RCS_STATUSES.unavailable:
567
+ case 'unavailable':
527
568
  setTemplateStatus(RCS_STATUSES.unavailable);
528
569
  break;
529
- case RCS_STATUSES.rejected:
570
+ case 'rejected':
530
571
  setTemplateStatus(RCS_STATUSES.rejected);
531
572
  break;
573
+ case 'created':
574
+ setTemplateStatus(RCS_STATUSES.created);
575
+ break;
532
576
  default:
533
577
  setTemplateStatus(status);
534
578
  break;
@@ -539,7 +583,6 @@ export const Rcs = (props) => {
539
583
  if (mediaType) {
540
584
  setTemplateMediaType(mediaType);
541
585
  }
542
- const tempOrientation = cardSettings.cardOrientation;
543
586
  const tempAlignment = cardSettings.mediaAlignment;
544
587
  const tempHeight = mediaData.height;
545
588
 
@@ -582,48 +625,200 @@ export const Rcs = (props) => {
582
625
  };
583
626
 
584
627
  useEffect(() => {
585
- const details = isFullMode ? rcsData?.templateDetails : templateData;
628
+ const details = rcsHydrationDetails;
586
629
  if (details && Object.keys(details).length > 0) {
587
- if (!isFullMode) {
588
- const tempCardVarMapped = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped', {});
589
- setCardVarMapped(tempCardVarMapped);
630
+ // Library/campaign: match SMS fallback — read from versions… and from top-level rcsContent (getCreativesData / parent shape).
631
+ const cardFromVersions = get(
632
+ details,
633
+ 'versions.base.content.RCS.rcsContent.cardContent[0]',
634
+ );
635
+ const cardFromTop = get(details, 'rcsContent.cardContent[0]');
636
+ const card0 = { ...(cardFromTop || {}), ...(cardFromVersions || {}) };
637
+ const cardVarMappedFromCardContent =
638
+ card0?.cardVarMapped != null && typeof card0.cardVarMapped === 'object'
639
+ ? card0.cardVarMapped
640
+ : {};
641
+ const cardVarMappedFromRootMirror =
642
+ details?.rcsCardVarMapped != null && typeof details.rcsCardVarMapped === 'object'
643
+ ? details.rcsCardVarMapped
644
+ : {};
645
+ // Root mirror from getCreativesData / getFormData — campaigns often preserve flat fields when
646
+ // nested versions.cardContent[0].cardVarMapped is dropped on reload.
647
+ const mergedCardVarMappedFromPayload = {
648
+ ...cardVarMappedFromRootMirror,
649
+ ...cardVarMappedFromCardContent,
650
+ };
651
+ const loadedTitleForMap = card0?.title != null ? String(card0.title) : '';
652
+ const loadedDescForMap = card0?.description != null ? String(card0.description) : '';
653
+ const hydratedCardVarPayloadSignature = `${loadedTitleForMap}\u0000${loadedDescForMap}\u0000${JSON.stringify(
654
+ Object.keys(mergedCardVarMappedFromPayload)
655
+ .sort()
656
+ .reduce((accumulator, mapKey) => {
657
+ accumulator[mapKey] = mergedCardVarMappedFromPayload[mapKey];
658
+ return accumulator;
659
+ }, {}),
660
+ )}`;
661
+ if (lastHydratedRcsCardVarSignatureRef.current !== hydratedCardVarPayloadSignature) {
662
+ lastHydratedRcsCardVarSignatureRef.current = hydratedCardVarPayloadSignature;
663
+ setRcsVarSegmentEditorRemountKey((previousKey) => previousKey + 1);
590
664
  }
591
- const mediaType = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
665
+ const tokenListForMap = [
666
+ ...(loadedTitleForMap ? loadedTitleForMap.match(rcsVarRegex) ?? [] : []),
667
+ ...(loadedDescForMap ? loadedDescForMap.match(rcsVarRegex) ?? [] : []),
668
+ ];
669
+ const orderedTagNamesForMap = tokenListForMap.map((token) => getVarNameFromToken(token)).filter(Boolean);
670
+ // Full-mode library/API payloads need normalize for legacy slot shapes. Campaign round-trip from
671
+ // getFormData already stores TagList values as {{TagName}}; normalize can strip or remap them.
672
+ const cardVarMappedBeforeCoalesce = isFullMode
673
+ ? normalizeCardVarMapped(mergedCardVarMappedFromPayload, orderedTagNamesForMap)
674
+ : { ...mergedCardVarMappedFromPayload };
675
+ const cardVarMappedAfterCoalesce = coalesceCardVarMappedToTemplate(
676
+ cardVarMappedBeforeCoalesce,
677
+ loadedTitleForMap,
678
+ loadedDescForMap,
679
+ rcsVarRegex,
680
+ );
681
+ const cardVarMappedAfterNumericSlotSync = !isFullMode
682
+ ? syncCardVarMappedSemanticsFromSlots(
683
+ cardVarMappedAfterCoalesce,
684
+ loadedTitleForMap,
685
+ loadedDescForMap,
686
+ rcsVarRegex,
687
+ )
688
+ : cardVarMappedAfterCoalesce;
689
+ const hydratedCardVarMappedResult = { ...cardVarMappedAfterNumericSlotSync };
690
+ // Pre-populate variable/tag mappings while opening an existing template in edit flows
691
+ setCardVarMapped((previousVarMapState) => {
692
+ const previousVarMap = previousVarMapState ?? {};
693
+ if (previousVarMap === hydratedCardVarMappedResult) return previousVarMapState;
694
+ const previousVarMapKeys = Object.keys(previousVarMap);
695
+ const nextVarMapKeys = Object.keys(hydratedCardVarMappedResult);
696
+ if (previousVarMapKeys.length === nextVarMapKeys.length) {
697
+ const allSlotValuesMatchPrevious = previousVarMapKeys.every(
698
+ (key) => previousVarMap[key] === hydratedCardVarMappedResult[key],
699
+ );
700
+ if (allSlotValuesMatchPrevious) return previousVarMapState;
701
+ }
702
+ return hydratedCardVarMappedResult;
703
+ });
704
+ const mediaType =
705
+ card0.mediaType
706
+ || get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
592
707
  if (mediaType === RCS_MEDIA_TYPES.NONE) {
593
708
  setTemplateType(contentType.text_message);
594
709
  } else {
595
710
  setTemplateType(contentType.rich_card);
596
711
  }
597
712
  setEditFlow(true);
598
- setTemplateName(details.name || '');
599
- const loadedTitle = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].title', '');
600
- const loadedDesc = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].description', '');
601
- const loadedMap = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped', {});
602
- const normalizedTitle = (!isFullMode && !isEmpty(loadedMap)) ? getUnmappedDesc(loadedTitle, loadedMap) : loadedTitle;
603
- const normalizedDesc = (!isFullMode && !isEmpty(loadedMap)) ? getUnmappedDesc(loadedDesc, loadedMap) : loadedDesc;
713
+ setTemplateName(details?.name || details?.creativeName || '');
714
+ const loadedTitle = loadedTitleForMap;
715
+ const loadedDesc = loadedDescForMap;
716
+ const { normalizedTitle, normalizedDesc } = normalizeLibraryLoadedTitleDesc({
717
+ loadedTitle,
718
+ loadedDesc,
719
+ isFullMode,
720
+ cardVarMappedAfterHydration: hydratedCardVarMappedResult,
721
+ rcsVarRegex,
722
+ });
604
723
  setTemplateTitle(normalizedTitle);
605
724
  setTemplateDesc(normalizedDesc);
606
- setSuggestions(get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []));
607
- templateStatusHelper(details);
608
- const mediaData = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
609
- const cardSettings = get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '');
725
+ setSuggestions(
726
+ Array.isArray(card0.suggestions)
727
+ ? card0.suggestions
728
+ : get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []),
729
+ );
730
+ const cardForStatus = {
731
+ ...card0,
732
+ Status:
733
+ card0.Status
734
+ ?? card0.status
735
+ ?? card0.approvalStatus
736
+ ?? get(details, 'templateStatus')
737
+ ?? get(details, 'approvalStatus')
738
+ ?? get(details, 'creativeStatus')
739
+ ?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
740
+ ?? '',
741
+ };
742
+ templateStatusHelper(cardForStatus);
743
+ const mediaData =
744
+ card0.media != null && card0.media !== ''
745
+ ? card0.media
746
+ : get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
747
+ const cardSettings =
748
+ get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '')
749
+ || get(details, 'rcsContent.cardSettings', '');
610
750
  setMediaData(mediaData, mediaType, cardSettings);
611
751
  if (details?.edit) {
612
752
  const rcsAccountId = get(details, 'versions.base.content.RCS.rcsContent.accountId', '');
613
753
  setRcsAccount(rcsAccountId);
614
754
  }
755
+ const smsFallbackContent = mergeRcsSmsFallBackContentFromDetails(details);
756
+ const base = get(smsFallbackContent, 'versions.base', {});
757
+ const updatedEditor = base['updated-sms-editor'] ?? base['sms-editor'];
758
+ const smsEditor = base['sms-editor'];
759
+ const fromNested = Array.isArray(updatedEditor)
760
+ ? updatedEditor.join('')
761
+ : (typeof updatedEditor === 'string' ? updatedEditor : (smsEditor || ''));
762
+ const fallbackMessage = smsFallbackContent.smsContent
763
+ || smsFallbackContent.smsTemplateContent
764
+ || smsFallbackContent.message
765
+ || fromNested
766
+ || '';
767
+ const varMappedFromPayload = smsFallbackContent[RCS_SMS_FALLBACK_VAR_MAPPED_PROP] || {};
768
+ const hasVarMapped = Object.keys(varMappedFromPayload).length > 0;
769
+ const hasFallbackPayload =
770
+ smsFallbackContent
771
+ && Object.keys(smsFallbackContent).length > 0
772
+ && (
773
+ !!smsFallbackContent.smsTemplateName
774
+ || !!fallbackMessage
775
+ || hasVarMapped
776
+ );
777
+ if (hasFallbackPayload) {
778
+ if (!fallbackMessage && !hasVarMapped && process.env.NODE_ENV !== 'production') {
779
+ console.warn('[RCS SMS Fallback] No message text found in API response. Inspect shape:', smsFallbackContent);
780
+ }
781
+ const unicodeFromApi =
782
+ typeof smsFallbackContent.unicodeValidity === 'boolean'
783
+ ? smsFallbackContent.unicodeValidity
784
+ : (typeof base['unicode-validity'] === 'boolean' ? base['unicode-validity'] : true);
785
+ const nextSmsState = {
786
+ templateName: smsFallbackContent.smsTemplateName || '',
787
+ content: fallbackMessage,
788
+ templateContent: fallbackMessage,
789
+ unicodeValidity: unicodeFromApi,
790
+ ...(hasVarMapped && { rcsSmsFallbackVarMapped: varMappedFromPayload }),
791
+ };
792
+ const hydrationKey = JSON.stringify({
793
+ creativeKey: details._id || details.name || details.creativeName || '',
794
+ templateName: nextSmsState.templateName,
795
+ content: nextSmsState.content,
796
+ unicodeValidity: nextSmsState.unicodeValidity,
797
+ varMapped: nextSmsState.rcsSmsFallbackVarMapped || {},
798
+ });
799
+ if (
800
+ isFullMode
801
+ || lastSmsFallbackHydrationKeyRef.current !== hydrationKey
802
+ ) {
803
+ lastSmsFallbackHydrationKeyRef.current = hydrationKey;
804
+ setSmsFallbackData(nextSmsState);
805
+ }
806
+ } else if (isFullMode || lastSmsFallbackHydrationKeyRef.current !== '__EMPTY__') {
807
+ lastSmsFallbackHydrationKeyRef.current = '__EMPTY__';
808
+ setSmsFallbackData(null);
809
+ }
615
810
  }
616
- }, [rcsData, templateData, isFullMode, isEditFlow]);
617
-
811
+ }, [rcsHydrationDetails, isFullMode]);
618
812
 
619
813
  useEffect(() => {
620
814
  if (templateType === contentType.text_message) {
621
815
  setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
622
- setTemplateTitle('');
623
- setTemplateTitleError('');
816
+ // Full-mode create only: switching to plain text clears draft title/media. Never clear when
817
+ // hydrating library/edit (would wipe templateData after load) — regression seen after SMS fallback work.
624
818
  if (!isEditFlow && isFullMode) {
819
+ setTemplateTitle('');
820
+ setTemplateTitleError('');
625
821
  setUpdateRcsImageSrc('');
626
- setUpdateRcsVideoSrc({});
627
822
  setRcsVideoSrc({});
628
823
  setSelectedDimension(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
629
824
  }
@@ -650,7 +845,8 @@ export const Rcs = (props) => {
650
845
  if (!showDltContainer) {
651
846
  const { type, module } = location.query || {};
652
847
  const isEmbedded = type === EMBEDDED;
653
- const context = isEmbedded ? module : DEFAULT;
848
+ // Match TagList initial fetch (getTagsforContext('Outbound') → context "outbound") so we do not request a different context than the two TagList headers.
849
+ const context = isEmbedded ? module : 'outbound';
654
850
  const embedded = isEmbedded ? type : FULL;
655
851
  const query = {
656
852
  layout: SMS,
@@ -661,9 +857,9 @@ export const Rcs = (props) => {
661
857
  if (getDefaultTags) {
662
858
  query.context = getDefaultTags;
663
859
  }
664
- globalActions.fetchSchemaForEntity(query);
860
+ fetchTagSchemaIfNewQuery(query);
665
861
  }
666
- }, [showDltContainer]);
862
+ }, [showDltContainer, fetchTagSchemaIfNewQuery]);
667
863
 
668
864
  useEffect(() => {
669
865
  let tag = get(metaEntities, `tags.standard`, []);
@@ -686,39 +882,72 @@ export const Rcs = (props) => {
686
882
  context,
687
883
  embedded,
688
884
  };
689
- globalActions.fetchSchemaForEntity(query);
885
+ if (getDefaultTags) {
886
+ query.context = getDefaultTags;
887
+ }
888
+ fetchTagSchemaIfNewQuery(query);
889
+ };
890
+
891
+ const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
892
+ if (!templateStr || !numericVarName || !tagName) return templateStr;
893
+ const re = buildRcsNumericMustachePlaceholderRegex(numericVarName);
894
+ return templateStr.replace(re, `{{${tagName}}}`);
690
895
  };
691
896
 
692
- const onTagSelect = (data, areaId) => {
897
+ const onTagSelect = (data, areaId, field) => {
693
898
  if (!areaId) return;
694
899
  const sep = areaId.lastIndexOf('_');
695
900
  if (sep === -1) return;
696
- const numId = Number(areaId.slice(sep + 1));
697
- if (isNaN(numId)) return;
901
+ const slotSuffix = areaId.slice(sep + 1);
902
+ if (slotSuffix === '' || isNaN(Number(slotSuffix))) return;
698
903
  const token = areaId.slice(0, sep);
699
904
  const variableName = getVarNameFromToken(token);
700
905
  if (!variableName) return;
906
+ const isNumericSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(variableName));
907
+ const fieldStr = field === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
908
+ const fieldType = field === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
909
+ const globalSlotForArea = getGlobalSlotIndexForRcsFieldId(areaId, fieldStr, fieldType);
910
+
701
911
  setCardVarMapped((prev) => {
702
912
  const base = (prev?.[variableName] ?? '').toString();
703
913
  const nextVal = `${base}{{${data}}}`;
704
- return {
705
- ...(prev || {}),
706
- [variableName]: nextVal,
707
- };
914
+ const next = { ...(prev || {}) };
915
+ if (isNumericSlot) {
916
+ delete next[variableName];
917
+ next[data] = nextVal;
918
+ } else {
919
+ next[variableName] = nextVal;
920
+ // resolveCardVarMappedSlotValue prefers numeric slot keys ("1","2",…) over semantic names;
921
+ // hydration may set "1": ''. Use global slot index — suffix is segment index (includes static text), not var ordinal.
922
+ if (globalSlotForArea !== null && globalSlotForArea !== undefined) {
923
+ next[String(globalSlotForArea + 1)] = nextVal;
924
+ }
925
+ }
926
+ return next;
708
927
  });
709
- };
710
-
711
- const onTitleTagSelect = (data) => onTagSelect(data, titleTextAreaId);
712
-
713
- const onDescTagSelect = (data) => onTagSelect(data, descTextAreaId);
714
928
 
715
- const onTagSelectFallback = (data) => {
716
- const tempMsg = `${fallbackMessage}{{${data}}}`;
717
- const error = fallbackMessageErrorHandler(tempMsg);
718
- setFallbackMessage(tempMsg);
719
- setFallbackMessageError(error);
929
+ if (isNumericSlot && (field === RCS_TAG_AREA_FIELD_TITLE || field === RCS_TAG_AREA_FIELD_DESC)) {
930
+ if (field === RCS_TAG_AREA_FIELD_TITLE) {
931
+ setTemplateTitle((prev) => {
932
+ const nextStr = replaceNumericPlaceholderWithTagInTemplate(prev || '', variableName, data);
933
+ if (nextStr === prev) return prev;
934
+ setTemplateTitleError(variableErrorHandling(nextStr));
935
+ return nextStr;
936
+ });
937
+ } else {
938
+ setTemplateDesc((prev) => {
939
+ const nextStr = replaceNumericPlaceholderWithTagInTemplate(prev || '', variableName, data);
940
+ if (nextStr === prev) return prev;
941
+ setTemplateDescError(variableErrorHandling(nextStr));
942
+ return nextStr;
943
+ });
944
+ }
945
+ }
720
946
  };
721
947
 
948
+ const onTitleTagSelect = (tagName) => onTagSelect(tagName, titleTextAreaId, RCS_TAG_AREA_FIELD_TITLE);
949
+
950
+ const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
722
951
 
723
952
  //removing optout tag for rcs
724
953
  const getRcsTags = () => {
@@ -731,15 +960,14 @@ export const Rcs = (props) => {
731
960
  };
732
961
  // tag Code end
733
962
 
734
- const renderLabel = (value, showLabel, desc) => {
735
- const isTemplateApproved = (templateStatus === RCS_STATUSES.approved);
963
+ const renderLabel = (value, desc) => {
736
964
  return (
737
965
  <>
738
- <RcsLabel>
966
+ <div className="rcs-form-section-heading">
739
967
  <CapHeading type="h4">{formatMessage(messages[value])}</CapHeading>
740
- </RcsLabel>
968
+ </div>
741
969
  {desc && (
742
- <CapLabel type="label3" style={{ marginBottom: '17px' }}>
970
+ <CapLabel type="label3" className="rcs-form-field-caption">
743
971
  {formatMessage(messages[desc])}
744
972
  </CapLabel>
745
973
  )}
@@ -825,47 +1053,6 @@ export const Rcs = (props) => {
825
1053
  setTemplateDescError(error);
826
1054
  };
827
1055
 
828
-
829
- const templateDescErrorHandler = (value) => {
830
- let errorMessage = false;
831
- const { isBraceError } = validateTags({
832
- content: value,
833
- tagsParam: tags,
834
- location,
835
- tagModule: getDefaultTags,
836
- isFullMode,
837
- }) || {};
838
-
839
- const maxLength = templateType === contentType.text_message
840
- ? RCS_TEXT_MESSAGE_MAX_LENGTH
841
- : RCS_RICH_CARD_MAX_LENGTH;
842
-
843
- if (value === '' && isMediaTypeText) {
844
- errorMessage = formatMessage(messages.emptyTemplateDescErrorMessage);
845
- } else if (value?.length > maxLength) {
846
- errorMessage = formatMessage(messages.templateMessageLengthError);
847
- }
848
-
849
- if (isBraceError) {
850
- errorMessage = formatMessage(globalMessages.unbalanacedCurlyBraces);
851
- }
852
- return errorMessage;
853
- };
854
-
855
-
856
- const onFallbackMessageChange = ({ target: { value } }) => {
857
- const error = fallbackMessageErrorHandler(value);
858
- setFallbackMessage(value);
859
- setFallbackMessageError(error);
860
- };
861
-
862
- const fallbackMessageErrorHandler = (value) => {
863
- if (value?.length > FALLBACK_MESSAGE_MAX_LENGTH) {
864
- return formatMessage(messages.fallbackMsgLenError);
865
- }
866
- return false;
867
- };
868
-
869
1056
  // Check for forbidden characters: square brackets [] and single curly braces {}
870
1057
  const forbiddenCharactersValidation = (value) => {
871
1058
  if (!value) return false;
@@ -916,39 +1103,12 @@ export const Rcs = (props) => {
916
1103
  }
917
1104
  return false;
918
1105
  };
919
-
920
- const templateHeaderErrorHandler = (value) => {
921
- let errorMessage = false;
922
- if (value?.length > TEMPLATE_HEADER_MAX_LENGTH) {
923
- errorMessage = formatMessage(messages.templateHeaderLengthError);
924
- } else {
925
- errorMessage = variableErrorHandling(value);
926
- }
927
- return errorMessage;
928
- };
929
-
930
-
931
- const templateMessageErrorHandler = (value) => {
932
- let errorMessage = false;
933
- if (value === '') {
934
- errorMessage = formatMessage(messages.emptyTemplateMessageErrorMessage);
935
- } else if (
936
- value?.length
937
- > TEMPLATE_MESSAGE_MAX_LENGTH
938
- ) {
939
- errorMessage = formatMessage(messages.templateMessageLengthError);
940
- } else {
941
- errorMessage = variableErrorHandling(value);
942
- }
943
- return errorMessage;
944
- };
945
-
946
1106
 
947
1107
  const onMessageAddVar = () => {
948
- onAddVar(MESSAGE_TEXT, templateDesc, rcsVarRegex);
1108
+ onAddVar(templateDesc);
949
1109
  };
950
1110
 
951
- const onAddVar = (type, messageContent, regex) => {
1111
+ const onAddVar = (messageContent) => {
952
1112
  // Always append the next variable at the end, like WhatsApp
953
1113
  const existingVars = messageContent.match(/\{\{(\d+)\}\}/g) || [];
954
1114
  const existingNumbers = existingVars.map(v => parseInt(v.match(/\d+/)[0], 10));
@@ -988,66 +1148,6 @@ const onTitleAddVar = () => {
988
1148
  setTemplateTitleError(error);
989
1149
  };
990
1150
 
991
-
992
- const splitTemplateVarString = (str) => {
993
- if (!str) return [];
994
- const validVarArr = str.match(rcsVarRegex) || [];
995
- const templateVarArray = [];
996
- let content = str;
997
- while (content?.length !== 0) {
998
- const index = content.indexOf(validVarArr?.[0]);
999
- if (index !== -1) {
1000
- templateVarArray.push(content.substring(0, index));
1001
- templateVarArray.push(validVarArr?.[0]);
1002
- content = content.substring(index + validVarArr?.[0]?.length, content?.length);
1003
- validVarArr?.shift();
1004
- } else {
1005
- templateVarArray.push(content);
1006
- break;
1007
- }
1008
- }
1009
- return templateVarArray.filter(Boolean);
1010
- };
1011
-
1012
- const textAreaValue = (idValue, type) => {
1013
- if (idValue >= 0) {
1014
- const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
1015
- const templateArr = splitTemplateVarString(templateStr);
1016
- const token = templateArr?.[idValue] || "";
1017
- if (token && rcsVarTestRegex.test(token)) {
1018
- const varName = getVarNameFromToken(token);
1019
- return (cardVarMapped?.[varName] ?? '').toString();
1020
- }
1021
- return "";
1022
- }
1023
- return "";
1024
- };
1025
-
1026
- const textAreaValueChange = (e, type) => {
1027
- const value = e?.target?.value ?? '';
1028
- const id = e?.target?.id || e?.currentTarget?.id || '';
1029
- if (!id) return;
1030
- const sep = id.lastIndexOf('_');
1031
- if (sep === -1) return;
1032
- const isInvalidValue = value?.trim() === "";
1033
- const token = id.slice(0, sep);
1034
- const variableName = getVarNameFromToken(token);
1035
-
1036
- if (variableName) {
1037
- setCardVarMapped((prev) => ({
1038
- ...prev,
1039
- [variableName]: isInvalidValue ? "" : value,
1040
- }));
1041
- }
1042
- };
1043
-
1044
- const setTextAreaId = (e, type) => {
1045
- const id = e?.target?.id || e?.currentTarget?.id || '';
1046
- if (!id) return;
1047
- if (type === TITLE_TEXT) setTitleTextAreaId(id);
1048
- else setDescTextAreaId(id);
1049
- };
1050
-
1051
1151
  const renderButtonComponent = () => {
1052
1152
  return (
1053
1153
  <>
@@ -1078,39 +1178,56 @@ const splitTemplateVarString = (str) => {
1078
1178
  );
1079
1179
  };
1080
1180
 
1081
- const renderedRCSEditMessage = (descArray, type) => {
1082
- const renderArray = [];
1083
- if (descArray?.length) {
1084
- descArray.forEach((elem, index) => {
1085
- if (rcsVarTestRegex.test(elem)) {
1086
- // Variable input
1087
- renderArray.push(
1088
- <TextArea
1089
- id={`${elem}_${index}`}
1090
- key={`${elem}_${index}`}
1091
- placeholder={`enter the value for ${elem}`}
1092
- autosize={{ minRows: 1, maxRows: 3 }}
1093
- onChange={e => textAreaValueChange(e, type)}
1094
- value={textAreaValue(index, type)}
1095
- onFocus={(e) => setTextAreaId(e, type)}
1096
- />
1097
- );
1098
- } else if (elem) {
1099
- // Static text
1100
- renderArray.push(
1101
- <TextArea
1102
- key={`static_${index}`}
1103
- value={elem}
1104
- autosize={{ minRows: 1, maxRows: 3 }}
1105
- disabled
1106
- className="rcs-edit-template-message-static-textarea"
1107
- style={{ background: '#fafafa', color: '#888' }}
1108
- />
1109
- );
1110
- }
1111
- });
1112
- }
1113
- return renderArray;
1181
+ const getRcsValueMap = (fieldTemplateString, fieldType) => {
1182
+ if (!fieldTemplateString) return {};
1183
+ const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
1184
+ const slotOffset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
1185
+ const templateSegments = splitTemplateVarStringRcs(fieldTemplateString);
1186
+ const segmentIdToResolvedValue = {};
1187
+ let varOrdinal = 0;
1188
+ templateSegments.forEach((segmentToken, segmentIndexInField) => {
1189
+ if (rcsVarTestRegex.test(segmentToken)) {
1190
+ const varSegmentCompositeId = `${segmentToken}_${segmentIndexInField}`;
1191
+ const varName = getVarNameFromToken(segmentToken);
1192
+ const globalSlot = slotOffset + varOrdinal;
1193
+ varOrdinal += 1;
1194
+ segmentIdToResolvedValue[varSegmentCompositeId] = resolveCardVarMappedSlotValue(
1195
+ cardVarMapped,
1196
+ varName,
1197
+ globalSlot,
1198
+ isEditLike,
1199
+ );
1200
+ }
1201
+ });
1202
+ return segmentIdToResolvedValue;
1203
+ };
1204
+
1205
+ const titleVarSegmentValueMapById = useMemo(
1206
+ () => getRcsValueMap(templateTitle, TITLE_TEXT),
1207
+ [templateTitle, cardVarMapped, isEditFlow, isFullMode],
1208
+ );
1209
+ const descriptionVarSegmentValueMapById = useMemo(
1210
+ () => getRcsValueMap(templateDesc, MESSAGE_TEXT),
1211
+ [templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode],
1212
+ );
1213
+
1214
+ const handleRcsVarChange = (id, value, type) => {
1215
+ const sep = id.lastIndexOf('_');
1216
+ if (sep === -1) return;
1217
+ const token = id.slice(0, sep);
1218
+ const variableName = getVarNameFromToken(token);
1219
+ if (variableName === undefined || variableName === null || variableName === '') return;
1220
+ const isInvalidValue = value?.trim() === '';
1221
+ const coercedSlotValue = isInvalidValue ? '' : value;
1222
+ const fieldStr = type === TITLE_TEXT ? templateTitle : templateDesc;
1223
+ const globalSlot = getGlobalSlotIndexForRcsFieldId(id, fieldStr, type);
1224
+ setCardVarMapped((previousVarMap) => {
1225
+ const nextVarMap = { ...previousVarMap, [variableName]: coercedSlotValue };
1226
+ if (globalSlot !== null && globalSlot !== undefined) {
1227
+ nextVarMap[String(globalSlot + 1)] = coercedSlotValue;
1228
+ }
1229
+ return nextVarMap;
1230
+ });
1114
1231
  };
1115
1232
 
1116
1233
  const renderTextComponent = () => {
@@ -1154,10 +1271,19 @@ const splitTemplateVarString = (str) => {
1154
1271
  </>
1155
1272
  }
1156
1273
  />
1157
- <div className="rcs_text_area_wrapper">
1158
1274
  {(isEditFlow || !isFullMode) ? (
1159
- renderedRCSEditMessage(splitTemplateVarString(templateTitle), TITLE_TEXT)
1275
+ <VarSegmentMessageEditor
1276
+ key={`rcs-title-vars-${rcsVarSegmentEditorRemountKey}`}
1277
+ templateString={templateTitle}
1278
+ valueMap={titleVarSegmentValueMapById}
1279
+ onChange={(id, value) => handleRcsVarChange(id, value, TITLE_TEXT)}
1280
+ onFocus={(id) => setTitleTextAreaId(id)}
1281
+ varRegex={rcsVarRegex}
1282
+ placeholderPrefix=""
1283
+ getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
1284
+ />
1160
1285
  ) : (
1286
+ <div className="rcs_text_area_wrapper">
1161
1287
  <CapInput
1162
1288
  className={`rcs-template-title-input ${
1163
1289
  !isTemplateApproved ? "rcs-edit-disabled" : ""
@@ -1170,8 +1296,8 @@ const splitTemplateVarString = (str) => {
1170
1296
  errorMessage={templateTitleError}
1171
1297
  disabled={isEditFlow || !isFullMode}
1172
1298
  />
1299
+ </div>
1173
1300
  )}
1174
- </div>
1175
1301
  {(isEditFlow || !isFullMode) && templateTitleError && (
1176
1302
  <CapError className="rcs-template-title-error">
1177
1303
  {templateTitleError}
@@ -1219,7 +1345,18 @@ const splitTemplateVarString = (str) => {
1219
1345
  <CapRow className="rcs-create-template-message-input">
1220
1346
  <div className="rcs_text_area_wrapper">
1221
1347
  {(isEditFlow || !isFullMode)
1222
- ? renderedRCSEditMessage(splitTemplateVarString(templateDesc), MESSAGE_TEXT)
1348
+ ? (
1349
+ <VarSegmentMessageEditor
1350
+ key={`rcs-desc-vars-${rcsVarSegmentEditorRemountKey}`}
1351
+ templateString={templateDesc}
1352
+ valueMap={descriptionVarSegmentValueMapById}
1353
+ onChange={(id, value) => handleRcsVarChange(id, value, MESSAGE_TEXT)}
1354
+ onFocus={(id) => setDescTextAreaId(id)}
1355
+ varRegex={rcsVarRegex}
1356
+ placeholderPrefix=""
1357
+ getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
1358
+ />
1359
+ )
1223
1360
  : (
1224
1361
  <>
1225
1362
  <TextArea
@@ -1263,7 +1400,9 @@ const splitTemplateVarString = (str) => {
1263
1400
  {templateDescError}
1264
1401
  </CapError>
1265
1402
  )}
1266
- {!isEditFlow && isFullMode && renderDescriptionCharacterCount()}
1403
+ {(isEditFlow || !isFullMode)
1404
+ ? renderDescriptionCharacterCount('rcs-character-count rcs-character-count--compact')
1405
+ : (!isEditFlow && isFullMode && renderDescriptionCharacterCount())}
1267
1406
  {!isFullMode && hasTag() && (
1268
1407
  <CapAlert
1269
1408
  message={
@@ -1283,18 +1422,6 @@ const splitTemplateVarString = (str) => {
1283
1422
  );
1284
1423
  };
1285
1424
 
1286
-
1287
- const fallbackSmsLength = () => (
1288
- <CapLabel type="label1" className="fallback-sms-length">
1289
- {formatMessage(messages.totalCharacters, {
1290
- smsCount: Math.ceil(
1291
- fallbackMessage?.length / FALLBACK_MESSAGE_MAX_LENGTH,
1292
- ),
1293
- number: fallbackMessage?.length,
1294
- })}
1295
- </CapLabel>
1296
- );
1297
-
1298
1425
  // Get character count for title (rich card only)
1299
1426
  const getTitleCharacterCount = () => {
1300
1427
  if (templateType === contentType.text_message) return 0;
@@ -1354,7 +1481,7 @@ const splitTemplateVarString = (str) => {
1354
1481
  const hasTag = () => {
1355
1482
  // Check cardVarMapped values for tags
1356
1483
  if (cardVarMapped && Object.keys(cardVarMapped).length > 0) {
1357
- const hasTagInMapped = Object.values(cardVarMapped).some(value =>
1484
+ const hasTagInMapped = Object.values(cardVarMapped).some((value) =>
1358
1485
  isTagIncluded(value)
1359
1486
  );
1360
1487
  if (hasTagInMapped) return true;
@@ -1372,14 +1499,6 @@ const splitTemplateVarString = (str) => {
1372
1499
  return false;
1373
1500
  };
1374
1501
 
1375
- //adding creative dlt fallback sms handlers
1376
- const addDltMsgHandler = () => {
1377
- setShowDltContainer(true);
1378
- setDltMode(RCS_DLT_MODE.TEMPLATES);
1379
- setDltEditData({});
1380
- setFallbackMessage('');
1381
- };
1382
-
1383
1502
  const closeDltContainerHandler = () => {
1384
1503
  setShowDltContainer(false);
1385
1504
  setDltMode('');
@@ -1402,68 +1521,17 @@ const splitTemplateVarString = (str) => {
1402
1521
  const fallMsg = get(tempData, `versions.base.updated-sms-editor`, []).join(
1403
1522
  '',
1404
1523
  );
1524
+ const templateNameFromDlt = get(dltEditData, 'name', '')
1525
+ || get(tempData, 'versions.base.name', '')
1526
+ || '';
1405
1527
  closeDltContainerHandler();
1406
1528
  setDltEditData(tempData);
1407
- setFallbackMessage(fallMsg);
1408
- setFallbackMessageError(fallMsg?.length > FALLBACK_MESSAGE_MAX_LENGTH);
1409
- setShowDltCard(true);
1410
- };
1411
-
1412
- const rcsDltCardDeleteHandler = () => {
1413
- closeDltContainerHandler();
1414
- setDltEditData({});
1415
- setFallbackMessage('');
1416
- setFallbackMessageError(false);
1417
- setShowDltCard(false);
1418
- };
1419
-
1420
- const dltFallbackListingPreviewhandler = (data) => {
1421
- const {
1422
- 'updated-sms-editor': updatedSmsEditor = [],
1423
- 'sms-editor': smsEditor = '',
1424
- } = data.versions.base || {};
1425
- setFallbackPreviewmode(true);
1426
- setDltPreviewData(
1427
- updatedSmsEditor === '' ? smsEditor : updatedSmsEditor.join(''),
1428
- );
1429
- };
1430
-
1431
- const getDltContentCardList = (content, channel) => {
1432
- const extra = [
1433
- <CapIcon
1434
- type="edit"
1435
- style={{ marginRight: '8px' }}
1436
- onClick={() => rcsDltEditSelectHandler(dltEditData)}
1437
- />,
1438
- <CapDropdown
1439
- overlay={(
1440
- <CapMenu>
1441
- <>
1442
- <CapMenu.Item
1443
- className="ant-dropdown-menu-item"
1444
- onClick={() => setFallbackPreviewmode(true)}
1445
- >
1446
- {formatMessage(globalMessages.preview)}
1447
- </CapMenu.Item>
1448
- <CapMenu.Item
1449
- className="ant-dropdown-menu-item"
1450
- onClick={rcsDltCardDeleteHandler}
1451
- >
1452
- {formatMessage(globalMessages.delete)}
1453
- </CapMenu.Item>
1454
- </>
1455
- </CapMenu>
1456
- )}
1457
- >
1458
- <CapIcon type="more" />
1459
- </CapDropdown>,
1460
- ];
1461
- return {
1462
- title: channel,
1463
- content,
1464
- cardType: channel,
1465
- extra,
1466
- };
1529
+ const unicodeFromDlt = get(tempData, 'versions.base.unicode-validity');
1530
+ setSmsFallbackData({
1531
+ templateName: templateNameFromDlt,
1532
+ content: fallMsg,
1533
+ ...(typeof unicodeFromDlt === 'boolean' ? { unicodeValidity: unicodeFromDlt } : {}),
1534
+ });
1467
1535
  };
1468
1536
 
1469
1537
  const getDltSlideBoxContent = () => {
@@ -1490,7 +1558,6 @@ const splitTemplateVarString = (str) => {
1490
1558
  isFullMode={isFullMode}
1491
1559
  isDltFromRcs
1492
1560
  onSelectTemplate={rcsDltEditSelectHandler}
1493
- handlePeviewTemplate={dltFallbackListingPreviewhandler}
1494
1561
  />
1495
1562
  );
1496
1563
  } else if (dltMode === RCS_DLT_MODE.EDIT) {
@@ -1511,147 +1578,32 @@ const splitTemplateVarString = (str) => {
1511
1578
  return { dltHeader, dltContent };
1512
1579
  };
1513
1580
 
1514
- const renderFallBackSmsComponent = () => {
1515
- // Completely disable fallback functionality when DLT is disabled
1516
- return null;
1517
-
1518
- // const contentArr = [];
1519
- // const showAddCreativeBtnForDlt = isDltEnabled && !showDltCard;
1520
- // const showCardForDlt = isDltEnabled && showDltCard;
1521
- // const showNonDltFallbackComp = !showAddCreativeBtnForDlt && !showCardForDlt;
1522
- // //pushing common fallback sms headings
1523
- // contentArr.push(
1524
- // <CapRow
1525
- // style={{
1526
- // marginBottom: isDltEnabled ? '20px' : '10px',
1527
- // }}
1528
- // >
1529
- // <CapHeader
1530
- // title={(
1531
- // <CapRow type="flex">
1532
- // <CapHeading type="h4">
1533
- // {formatMessage(messages.fallbackLabel)}
1534
- // </CapHeading>
1535
- // <CapTooltipWithInfo
1536
- // placement="right"
1537
- // infoIconProps={{
1538
- // style: { marginLeft: '4px', marginTop: '3px' },
1539
- // }}
1540
- // title={formatMessage(messages.fallbackToolTip)}
1541
- // />
1542
- // </CapRow>
1543
- // )}
1544
- // description={formatMessage(messages.fallbackDesc)}
1545
- // suffix={
1546
- // isDltEnabled ? null : (
1547
- // <CapButton
1548
- // type="flat"
1549
- // className="fallback-preview-btn"
1550
- // prefix={<CapIcon type="eye" />}
1551
- // style={{ color: CAP_SECONDARY.base }}
1552
- // onClick={() => setFallbackPreviewmode(true)}
1553
- // disabled={fallbackMessage === '' || fallbackMessageError}
1554
- // >
1555
- // {formatMessage(globalMessages.preview)}
1556
- // </CapButton>
1557
- // )
1558
- // }
1559
- // />
1560
- // </CapRow>,
1561
- // );
1562
-
1563
- //dlt is enabled, and dlt content is not yet added, show button to add dlt creative
1564
- // showAddCreativeBtnForDlt
1565
- // && contentArr.push(
1566
- // <CapCard className="rcs-dlt-fallback-card">
1567
- // <CapRow type="flex" justify="center" align="middle">
1568
- // <CapColumn span={10}>
1569
- // <CapImage src={addCreativesIcon} />
1570
- // </CapColumn>
1571
- // <CapColumn span={14}>
1572
- // <CapButton
1573
- // className="add-dlt-btn"
1574
- // type="secondary"
1575
- // onClick={addDltMsgHandler}
1576
- // >
1577
- // {formatMessage(messages.addSmsCreative)}
1578
- // </CapButton>
1579
- // </CapColumn>
1580
- // </CapRow>
1581
- // </CapCard>,
1582
- // );
1583
-
1584
- // //dlt is enabled and dlt content is added, show it in a card
1585
- // showCardForDlt
1586
- // && contentArr.push(
1587
- // <CapCustomCardList
1588
- // cardList={[getDltContentCardList(fallbackMessage, SMS)]}
1589
- // className="rcs-dlt-card"
1590
- // />,
1591
- // fallbackMessageError && (
1592
- // <CapError className="rcs-fallback-len-error">
1593
- // {formatMessage(messages.fallbackMsgLenError)}
1594
- // </CapError>
1595
- // ),
1596
- // );
1597
-
1598
- // //dlt is not enabled, show non dlt text area
1599
- // showNonDltFallbackComp
1600
- // && contentArr.push(
1601
- // <>
1602
- // <CapRow>
1603
- // <CapHeader
1604
- // title={(
1605
- // <CapHeading type="h4">
1606
- // {formatMessage(messages.fallbackTextAreaLabel)}
1607
- // </CapHeading>
1608
- // )}
1609
- // suffix={(
1610
- // <TagList
1611
- // label={formatMessage(globalMessages.addLabels)}
1612
- // onTagSelect={onTagSelectFallback}
1613
- // location={location}
1614
- // tags={tags || []}
1615
- // onContextChange={handleOnTagsContextChange}
1616
- // injectedTags={injectedTags || {}}
1617
- // selectedOfferDetails={selectedOfferDetails}
1618
- // />
1619
- // )}
1620
- // />
1621
- // </CapRow>
1622
- // <div className="rcs_fallback_msg_textarea_wrapper">
1623
- // <TextArea
1624
- // id="rcs_fallback_message_textarea"
1625
- // autosize={{ minRows: 3, maxRows: 5 }}
1626
- // placeholder={formatMessage(messages.fallbackMsgPlaceholder)}
1627
- // onChange={onFallbackMessageChange}
1628
- // errorMessage={fallbackMessageError}
1629
- // value={fallbackMessage || ""}
1630
- // />
1631
- // {!aiContentBotDisabled && (
1632
- // <CapAskAira.ContentGenerationBot
1633
- // text={fallbackMessage || ""}
1634
- // setText={(text) => {
1635
- // onFallbackMessageChange({ target: { value: text } });
1636
- // }}
1637
- // iconPlacement="float-br"
1638
- // rootStyle={{
1639
- // bottom: "0.5rem",
1640
- // right: "0.5rem",
1641
- // position: "absolute",
1642
- // }}
1643
- // />
1644
- // )}
1645
- // </div>
1646
- // <CapRow>{fallbackSmsLength()}</CapRow>
1647
- // </>
1648
- // );
1649
-
1650
- // return <>{contentArr}</>;
1651
- };
1581
+ const renderFallBackSmsComponent = () => (
1582
+ <SmsFallback
1583
+ value={smsFallbackData}
1584
+ onChange={setSmsFallbackData}
1585
+ parentLocation={location}
1586
+ smsRegister={smsRegister}
1587
+ isFullMode={isFullMode}
1588
+ selectedOfferDetails={selectedOfferDetails}
1589
+ channelsToHide={CHANNELS_TO_HIDE_FOR_SMS_ONLY}
1590
+ sectionTitle={
1591
+ smsFallbackData
1592
+ ? formatMessage(messages.fallbackLabel)
1593
+ : formatMessage(messages.smsFallbackOptional)
1594
+ }
1595
+ templateListTitle={formatMessage(creativesMessages.creativeTemplates)}
1596
+ templateListDescription={formatMessage(creativesMessages.creativeTemplatesDesc)}
1597
+ /* Full-mode: card layout only while drafting a new template; after send for approval or when a template is loaded, use inline layout. */
1598
+ showAsCard={isFullMode && !isEditFlow && templateStatus === ''}
1599
+ disableSelectTemplate={isEditFlow}
1600
+ eventContextTags={eventContextTags}
1601
+ onRcsFallbackEditorStateChange={handleSmsFallbackEditorStateChange}
1602
+ isRcsEditFlow={isEditFlow}
1603
+ />
1604
+ );
1652
1605
 
1653
1606
  const uploadRcsImage = useCallback((file, type, fileParams, index) => {
1654
- setImageError(null);
1655
1607
  const isRcsThumbnail = index === 1;
1656
1608
  actions.uploadRcsAsset(file, type, {
1657
1609
  isRcsThumbnail,
@@ -1663,7 +1615,6 @@ const splitTemplateVarString = (str) => {
1663
1615
 
1664
1616
  const setUpdateRcsImageSrc = useCallback(
1665
1617
  (val) => {
1666
- setAssetListImage(val);
1667
1618
  updateRcsImageSrc(val);
1668
1619
  actions.clearRcsMediaAsset(0);
1669
1620
  },
@@ -1693,7 +1644,6 @@ const splitTemplateVarString = (str) => {
1693
1644
 
1694
1645
 
1695
1646
  const uploadRcsVideo = (file, type, fileParams) => {
1696
- setImageError(null);
1697
1647
  actions.uploadRcsAsset(file, type, {
1698
1648
  ...fileParams,
1699
1649
  type: 'video',
@@ -1702,9 +1652,6 @@ const splitTemplateVarString = (str) => {
1702
1652
  });
1703
1653
  };
1704
1654
 
1705
- const updateRcsVideoSrc = (val) => {
1706
- setRcsVideoSrc(val);
1707
- };
1708
1655
  const setUpdateRcsVideoSrc = useCallback((index, val) => {
1709
1656
  setRcsVideoSrc(val);
1710
1657
  setAssetList(val);
@@ -1733,7 +1680,7 @@ const splitTemplateVarString = (str) => {
1733
1680
  <>
1734
1681
  <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
1735
1682
  <CapImageUpload
1736
- style={{ paddingTop: '20px' }}
1683
+ className="cap-custom-image-upload rcs-image-upload--top-spacing"
1737
1684
  allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
1738
1685
  imgWidth={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].width}
1739
1686
  imgHeight={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].height}
@@ -1745,7 +1692,6 @@ const splitTemplateVarString = (str) => {
1745
1692
  updateOnReUpload={updateOnRcsThumbnailReUpload}
1746
1693
  minImgSize={RCS_THUMBNAIL_MIN_SIZE}
1747
1694
  index={1}
1748
- className="cap-custom-image-upload"
1749
1695
  key={`rcs-uploaded-image-${currentDimension}`}
1750
1696
  imageData={thumbnailData}
1751
1697
  channel={RCS}
@@ -1776,7 +1722,7 @@ const splitTemplateVarString = (str) => {
1776
1722
  value: dim.type,
1777
1723
  label: `${dim.label}`
1778
1724
  }))}
1779
- style={{ marginBottom: '20px' }}
1725
+ className="rcs-dimension-select--bottom-spacing"
1780
1726
  />
1781
1727
  </>
1782
1728
  )}
@@ -1790,7 +1736,6 @@ const splitTemplateVarString = (str) => {
1790
1736
  </div>
1791
1737
  ) : (
1792
1738
  <CapImageUpload
1793
- style={{ paddingTop: '20px' }}
1794
1739
  allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
1795
1740
  imgWidth={RCS_IMAGE_DIMENSIONS[selectedDimension].width}
1796
1741
  imgHeight={RCS_IMAGE_DIMENSIONS[selectedDimension].height}
@@ -1801,7 +1746,7 @@ const splitTemplateVarString = (str) => {
1801
1746
  updateImageSrc={setUpdateRcsImageSrc}
1802
1747
  updateOnReUpload={updateOnRcsImageReUpload}
1803
1748
  index={0}
1804
- className="cap-custom-image-upload"
1749
+ className="cap-custom-image-upload rcs-image-upload--top-spacing"
1805
1750
  key={`rcs-uploaded-image-${selectedDimension}`}
1806
1751
  imageData={rcsData}
1807
1752
  channel={RCS}
@@ -1831,7 +1776,7 @@ const splitTemplateVarString = (str) => {
1831
1776
  value: dim.type,
1832
1777
  label: `${dim.label}`
1833
1778
  }))}
1834
- style={{ marginBottom: '20px' }}
1779
+ className="rcs-dimension-select--bottom-spacing"
1835
1780
  />
1836
1781
  )}
1837
1782
  {(isEditFlow || !isFullMode) ? (
@@ -1891,8 +1836,16 @@ const splitTemplateVarString = (str) => {
1891
1836
  const getRcsPreview = () => {
1892
1837
 
1893
1838
  const dimensionObj = RCS_IMAGE_DIMENSIONS[selectedDimension];
1894
- const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
1895
- const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
1839
+ const isSlotMappingMode = isEditFlow || !isFullMode;
1840
+ const titleVarCountForResolve = isMediaTypeText
1841
+ ? 0
1842
+ : ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
1843
+ const resolvedTitle = isMediaTypeText
1844
+ ? ''
1845
+ : (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
1846
+ const resolvedDesc = isSlotMappingMode
1847
+ ? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
1848
+ : templateDesc;
1896
1849
  return (
1897
1850
  <UnifiedPreview
1898
1851
  channel={RCS}
@@ -1915,51 +1868,65 @@ const splitTemplateVarString = (str) => {
1915
1868
  );
1916
1869
  };
1917
1870
 
1918
- const getUnmappedDesc = (str, mapping) => {
1919
- if (!str) return '';
1920
- if (!mapping || Object.keys(mapping).length === 0) return str;
1921
- let result = str;
1922
- const replacements = [];
1923
- Object.entries(mapping).forEach(([key, value]) => {
1924
- const raw = (value ?? '').toString();
1925
- if (!raw || raw?.trim?.() === '') return;
1926
- const braced = /^\{\{[\s\S]*\}\}$/.test(raw) ? raw : `{{${raw}}}`;
1927
- replacements.push({ key, needle: raw });
1928
- if (braced !== raw) replacements.push({ key, needle: braced });
1929
- });
1930
- const seen = new Set();
1931
- const uniq = replacements
1932
- .filter(({ key, needle }) => {
1933
- const id = `${key}::${needle}`;
1934
- if (seen.has(id)) return false;
1935
- seen.add(id);
1936
- return true;
1937
- })
1938
- .sort((a, b) => (b.needle.length - a.needle.length));
1939
-
1940
- uniq.forEach(({ key, needle }) => {
1941
- if (!needle) return;
1942
- const escaped = needle.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
1943
- const regex = new RegExp(escaped, 'g');
1944
- result = result.replace(regex, `{{${key}}}`);
1945
- });
1946
- return result;
1947
- };
1948
-
1949
1871
  const createPayload = () => {
1950
- const base = get(dltEditData, `versions.base`, {});
1951
- const {
1952
- template_id: templateId = '',
1953
- template_name = '',
1954
- 'sms-editor': template = '',
1955
- header: registeredSenderIds = [],
1956
- } = base;
1957
- const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
1958
- const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
1872
+ const isSlotMappingMode = isEditFlow || !isFullMode;
1959
1873
  const alignment = isMediaTypeImage
1960
1874
  ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.alignment
1961
1875
  : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.alignment;
1962
1876
 
1877
+ const heightTypeForCardWidth = isMediaTypeText
1878
+ ? undefined
1879
+ : isMediaTypeImage
1880
+ ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType
1881
+ : isMediaTypeVideo
1882
+ ? RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType
1883
+ : undefined;
1884
+ const cardWidthFromSelection =
1885
+ heightTypeForCardWidth === MEDIUM ? MEDIUM : SMALL;
1886
+
1887
+ /** Library: merge props + state so SMS fallback is not dropped when local state is empty but templateData has consumer data. */
1888
+ const smsFromApiShape = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
1889
+ const smsFallbackMerged = !isFullMode
1890
+ ? (() => {
1891
+ const local =
1892
+ smsFallbackData && typeof smsFallbackData === 'object' ? smsFallbackData : {};
1893
+ return {
1894
+ ...smsFromApiShape,
1895
+ ...local,
1896
+ rcsSmsFallbackVarMapped: mergeRcsSmsFallbackVarMapLayers(smsFromApiShape, local),
1897
+ };
1898
+ })()
1899
+ : (smsFallbackData || {});
1900
+ const smsFallbackForPayload = (() => {
1901
+ if (isFullMode) {
1902
+ return hasMeaningfulSmsFallbackShape(smsFallbackData) ? smsFallbackData : null;
1903
+ }
1904
+ const mapped = {
1905
+ templateName:
1906
+ smsFallbackMerged.templateName
1907
+ || smsFallbackMerged.smsTemplateName
1908
+ || '',
1909
+ // Use `||` so empty `content` does not block campaign/API `message` (common in embedded flows).
1910
+ content:
1911
+ smsFallbackMerged.content
1912
+ || smsFallbackMerged.smsContent
1913
+ || smsFallbackMerged.smsTemplateContent
1914
+ || smsFallbackMerged.message
1915
+ || '',
1916
+ templateContent:
1917
+ pickFirstSmsFallbackTemplateString(smsFallbackMerged)
1918
+ || '',
1919
+ ...(typeof smsFallbackMerged.unicodeValidity === 'boolean'
1920
+ && { unicodeValidity: smsFallbackMerged.unicodeValidity }),
1921
+ ...(smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]
1922
+ && Object.keys(smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]).length > 0 && {
1923
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]:
1924
+ smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP],
1925
+ }),
1926
+ };
1927
+ return hasMeaningfulSmsFallbackShape(mapped) ? mapped : null;
1928
+ })();
1929
+
1963
1930
  const payload = {
1964
1931
  name: templateName,
1965
1932
  versions: {
@@ -1972,12 +1939,14 @@ const splitTemplateVarString = (str) => {
1972
1939
  cardSettings: {
1973
1940
  cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
1974
1941
  ...(alignment && { mediaAlignment: alignment }),
1975
- cardWidth: SMALL,
1942
+ cardWidth: cardWidthFromSelection,
1976
1943
  },
1977
1944
  cardContent: [
1978
1945
  {
1979
- title: resolvedTitle,
1980
- description: resolvedDesc,
1946
+ // Persist raw template copy + cardVarMapped — not resolveTemplateWithMap output — so library
1947
+ // / getFormData round-trip keeps {{…}} and slot values (resolved strings broke reopen hydration).
1948
+ title: templateTitle,
1949
+ description: templateDesc,
1981
1950
  mediaType: templateMediaType,
1982
1951
  ...(!isMediaTypeText && {media: {
1983
1952
  mediaUrl: rcsImageSrc || rcsVideoSrc.videoSrc || '',
@@ -1986,23 +1955,30 @@ const splitTemplateVarString = (str) => {
1986
1955
  ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType || MEDIUM
1987
1956
  : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType || MEDIUM,
1988
1957
  }}),
1989
- ...(!isFullMode && (() => {
1990
- const tokens = [
1991
- ...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
1992
- ...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
1958
+ ...(isSlotMappingMode && (() => {
1959
+ const templateVarTokens = [
1960
+ ...(templateTitle?.match(rcsVarRegex) ?? []),
1961
+ ...(templateDesc?.match(rcsVarRegex) ?? []),
1993
1962
  ];
1994
- const allowedKeys = tokens
1995
- .map((t) => getVarNameFromToken(t))
1996
- .filter(Boolean);
1997
- const nextMap = {};
1998
- allowedKeys.forEach((k) => {
1999
- if (Object.prototype.hasOwnProperty.call(cardVarMapped || {}, k)) {
2000
- nextMap[k] = cardVarMapped[k];
2001
- } else {
2002
- nextMap[k] = '';
1963
+ const persistedSlotVarMap = {};
1964
+ const seenSemanticVarNames = new Set();
1965
+ templateVarTokens.forEach((token, slotIndexZeroBased) => {
1966
+ const varName = getVarNameFromToken(token);
1967
+ if (!varName) return;
1968
+ const resolvedRawValue = resolveCardVarMappedSlotValue(
1969
+ cardVarMapped,
1970
+ varName,
1971
+ slotIndexZeroBased,
1972
+ isSlotMappingMode,
1973
+ );
1974
+ const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
1975
+ persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
1976
+ if (!seenSemanticVarNames.has(varName)) {
1977
+ seenSemanticVarNames.add(varName);
1978
+ persistedSlotVarMap[varName] = sanitizedSlotValue;
2003
1979
  }
2004
1980
  });
2005
- return { cardVarMapped: nextMap };
1981
+ return { cardVarMapped: persistedSlotVarMap };
2006
1982
  })()),
2007
1983
  ...(suggestions.length > 0 && { suggestions }),
2008
1984
  }
@@ -2010,17 +1986,29 @@ const splitTemplateVarString = (str) => {
2010
1986
  contentType: isFullMode ? templateType : RICHCARD,
2011
1987
  ...(isFullMode && {accountId:accountId, accessToken: accessToken, accountName: accountName, hostName: hostName}),
2012
1988
  },
2013
- smsFallBackContent: {
2014
- message: fallbackMessage,
2015
- ...(isDltEnabled && {
2016
- templateConfigs: {
2017
- templateId,
2018
- templateName: template_name,
2019
- template,
2020
- registeredSenderIds,
1989
+ ...(smsFallbackForPayload && (() => {
1990
+ const smsBodyText =
1991
+ smsFallbackForPayload.content
1992
+ || smsFallbackForPayload.templateContent
1993
+ || smsFallbackForPayload.message
1994
+ || smsFallbackForPayload.smsContent
1995
+ || '';
1996
+ return {
1997
+ smsFallBackContent: {
1998
+ smsTemplateName: smsFallbackForPayload.templateName || '',
1999
+ smsContent: smsBodyText,
2000
+ // cap-campaigns-v2 `normalizeRcsMessageContentForApi` only serializes `message` (+ templateConfigs); without this key SMS fallback is dropped on send.
2001
+ message: smsBodyText,
2002
+ ...(typeof smsFallbackForPayload.unicodeValidity === 'boolean' && {
2003
+ unicodeValidity: smsFallbackForPayload.unicodeValidity,
2004
+ }),
2005
+ ...(smsFallbackForPayload.rcsSmsFallbackVarMapped
2006
+ && Object.keys(smsFallbackForPayload.rcsSmsFallbackVarMapped).length > 0 && {
2007
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackForPayload.rcsSmsFallbackVarMapped,
2008
+ }),
2021
2009
  },
2022
- }),
2023
- },
2010
+ };
2011
+ })()),
2024
2012
  },
2025
2013
  },
2026
2014
  },
@@ -2030,6 +2018,84 @@ const splitTemplateVarString = (str) => {
2030
2018
  return payload;
2031
2019
  };
2032
2020
 
2021
+ /** Shape expected by CommonTestAndPreview buildRcsTestMessagePayload (versions.base.content.RCS). */
2022
+ const testPreviewFormData = useMemo(() => {
2023
+ const payload = createPayload();
2024
+ const rcs = payload?.versions?.base?.content?.RCS;
2025
+ if (!rcs) return null;
2026
+ // createMessageMeta uses WeCRM `id` when present; else template API account id (sourceAccountIdentifier).
2027
+ const accountIdForCreateMessageMeta =
2028
+ (wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
2029
+ ? String(wecrmAccountId)
2030
+ : accountId;
2031
+ const rcsForTest = {
2032
+ ...rcs,
2033
+ rcsContent: {
2034
+ ...rcs.rcsContent,
2035
+ ...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
2036
+ },
2037
+ };
2038
+ const out = {
2039
+ versions: {
2040
+ base: {
2041
+ content: {
2042
+ RCS: rcsForTest,
2043
+ },
2044
+ },
2045
+ },
2046
+ };
2047
+ const fb = smsFallbackData;
2048
+ if (fb && (fb.smsTemplateId || fb.templateContent || fb.content)) {
2049
+ out.templateConfigs = {
2050
+ templateId: fb.smsTemplateId || '',
2051
+ template: fb.templateContent || fb.content || '',
2052
+ traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister),
2053
+ registeredSenderIds: Array.isArray(fb.registeredSenderIds) ? fb.registeredSenderIds : [],
2054
+ };
2055
+ }
2056
+ return out;
2057
+ }, [
2058
+ templateName,
2059
+ templateTitle,
2060
+ templateDesc,
2061
+ templateMediaType,
2062
+ cardVarMapped,
2063
+ suggestions,
2064
+ rcsImageSrc,
2065
+ rcsVideoSrc,
2066
+ rcsThumbnailSrc,
2067
+ selectedDimension,
2068
+ smsFallbackData,
2069
+ isFullMode,
2070
+ isEditFlow,
2071
+ templateType,
2072
+ accountId,
2073
+ wecrmAccountId,
2074
+ accessToken,
2075
+ accountName,
2076
+ hostName,
2077
+ smsRegister,
2078
+ ]);
2079
+
2080
+ /**
2081
+ * Library/campaign: `createPayload` merges root + nested `smsFallBackContent` from `templateData`
2082
+ * with `smsFallbackData`. Done/slot validation must use the same merge — otherwise local state can
2083
+ * miss `templateContent` / var map while the parent payload still has them (DLT campaigns).
2084
+ */
2085
+ const librarySmsFallbackMergedForValidation = useMemo(() => {
2086
+ if (isFullMode) {
2087
+ return smsFallbackData;
2088
+ }
2089
+ const smsFromApiShape = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
2090
+ const local =
2091
+ smsFallbackData && typeof smsFallbackData === 'object' ? smsFallbackData : {};
2092
+ return {
2093
+ ...smsFromApiShape,
2094
+ ...local,
2095
+ rcsSmsFallbackVarMapped: mergeRcsSmsFallbackVarMapLayers(smsFromApiShape, local),
2096
+ };
2097
+ }, [isFullMode, templateData, smsFallbackData]);
2098
+
2033
2099
  const actionCallback = ({ errorMessage, resp }, isEdit) => {
2034
2100
  // eslint-disable-next-line no-undef
2035
2101
  const error = errorMessage?.message || errorMessage;
@@ -2059,6 +2125,9 @@ const splitTemplateVarString = (str) => {
2059
2125
  _id: params?.id,
2060
2126
  validity: true,
2061
2127
  type: RCS,
2128
+ // CreativesContainer closes the slide box *after* getCreativesData runs so the parent receives
2129
+ // the RCS payload first (closing immediately used to skip getCreativesData → empty "Add creative").
2130
+ closeSlideBoxAfterSubmit: !isFullMode,
2062
2131
  };
2063
2132
  getFormData(formDataParams);
2064
2133
  };
@@ -2072,6 +2141,7 @@ const splitTemplateVarString = (str) => {
2072
2141
  actionCallback({ resp, errorMessage });
2073
2142
  setSpin(false); // Always turn off spinner
2074
2143
  if (!errorMessage) {
2144
+ setTemplateStatus(RCS_STATUSES.pending);
2075
2145
  onCreateComplete();
2076
2146
  }
2077
2147
  });
@@ -2083,6 +2153,64 @@ const splitTemplateVarString = (str) => {
2083
2153
  }
2084
2154
  };
2085
2155
 
2156
+ /** When a fallback SMS row exists, require non-empty body (trimmed) and filled var slots (DLT). */
2157
+ const smsFallbackBlocksDone = () => {
2158
+ // Non-DLT library: user removed SMS fallback (local null) but template still carries fallback — block Done.
2159
+ if (
2160
+ !isFullMode
2161
+ && !isTraiDLTEnable(isFullMode, smsRegister)
2162
+ && smsFallbackData == null
2163
+ && hasMeaningfulSmsFallbackShape(
2164
+ getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
2165
+ )
2166
+ ) {
2167
+ return true;
2168
+ }
2169
+ if (!smsFallbackData) return false;
2170
+ // Full-mode (Send for approval): SMS fallback is optional. Tag-slot mapping is a display/preview
2171
+ // concern, not a structural requirement for approval — the registered SMS template body stands on
2172
+ // its own. Never block the Send for approval button due to missing or unfilled fallback var slots.
2173
+ if (isFullMode) return false;
2174
+ const merged = librarySmsFallbackMergedForValidation;
2175
+ const templateText = pickFirstSmsFallbackTemplateString(merged);
2176
+ if (!templateText) {
2177
+ return true;
2178
+ }
2179
+ const rawVarMap =
2180
+ merged.rcsSmsFallbackVarMapped
2181
+ || merged['rcs-sms-fallback-var-mapped'];
2182
+ const varMap =
2183
+ rawVarMap != null && typeof rawVarMap === 'object' ? rawVarMap : {};
2184
+ return !areAllRcsSmsFallbackVarSlotsFilled(templateText, varMap);
2185
+ };
2186
+
2187
+ /**
2188
+ * Library / campaigns (`!isFullMode`): card slots are often stored on numeric keys (`1`,`2`,…) while
2189
+ * semantic keys stay `""` from API round-trip. `resolveCardVarMappedSlotValue` matches createPayload
2190
+ * / preview — naive `cardVarMapped[name]` wrongly kept Done disabled for DLT.
2191
+ */
2192
+ const isLibraryCampaignCardVarMappingIncomplete = () => {
2193
+ if (isFullMode) return false;
2194
+ const titleTokens = splitTemplateVarStringRcs(templateTitle).filter((elem) =>
2195
+ rcsVarTestRegex.test(elem),
2196
+ );
2197
+ const descTokens = splitTemplateVarStringRcs(templateDesc).filter((elem) =>
2198
+ rcsVarTestRegex.test(elem),
2199
+ );
2200
+ const orderedVarNames = [
2201
+ ...titleTokens.map((t) => t.replace(/^\{\{|\}\}$/g, '')),
2202
+ ...descTokens.map((t) => t.replace(/^\{\{|\}\}$/g, '')),
2203
+ ];
2204
+ if (orderedVarNames.length > 0 && isEmpty(cardVarMapped)) {
2205
+ return true;
2206
+ }
2207
+ return orderedVarNames.some((name, globalIdx) => {
2208
+ const v = resolveCardVarMappedSlotValue(cardVarMapped, name, globalIdx, true);
2209
+ const s = v == null ? '' : String(v);
2210
+ return s.trim() === '';
2211
+ });
2212
+ };
2213
+
2086
2214
  const isDisableDone = () => {
2087
2215
  if(isEditFlow){
2088
2216
  return false;
@@ -2093,40 +2221,16 @@ const splitTemplateVarString = (str) => {
2093
2221
  }
2094
2222
  }
2095
2223
 
2096
- if(!isFullMode){
2097
- const titleVars = splitTemplateVarString(templateTitle).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2098
- const descVars = splitTemplateVarString(templateDesc).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2099
- const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
2100
-
2101
- if (allVars.length > 0 && isEmpty(cardVarMapped)) {
2102
- return true;
2103
- }
2104
-
2105
- const hasEmptyMapping =
2106
- cardVarMapped &&
2107
- Object.keys(cardVarMapped).length > 0 &&
2108
- Object.entries(cardVarMapped).some(([_, v]) => {
2109
- if (typeof v !== 'string') return !v; // null/undefined
2110
- return v.trim() === ''; // empty string
2111
- });
2112
-
2113
- if (hasEmptyMapping) {
2114
- return true;
2115
- }
2116
-
2117
- const anyMissing = allVars.some(name => {
2118
- const v = cardVarMapped?.[name];
2119
- if (typeof v !== 'string') return !v;
2120
- return v.trim() === '';
2121
- });
2122
- if (anyMissing) {
2123
- return true;
2124
- }
2224
+ if (isLibraryCampaignCardVarMappingIncomplete()) {
2225
+ return true;
2125
2226
  }
2126
2227
 
2127
- if (isMediaTypeText && templateDesc.trim() === '') {
2228
+ if (smsFallbackBlocksDone()) {
2128
2229
  return true;
2230
+ }
2129
2231
 
2232
+ if (isMediaTypeText && templateDesc.trim() === '') {
2233
+ return true;
2130
2234
  }
2131
2235
  if (isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
2132
2236
  return true;
@@ -2144,53 +2248,36 @@ const splitTemplateVarString = (str) => {
2144
2248
  return true;
2145
2249
  }
2146
2250
  }
2147
- if (templateDescError || templateTitleError || fallbackMessageError) {
2251
+ if (templateDescError || templateTitleError) {
2252
+ return true;
2253
+ }
2254
+ if (
2255
+ smsFallbackData?.content
2256
+ && smsFallbackData.content.length > FALLBACK_MESSAGE_MAX_LENGTH
2257
+ ) {
2148
2258
  return true;
2149
2259
  }
2150
2260
  return false;
2151
2261
  };
2152
2262
 
2153
2263
  const isEditDisableDone = () => {
2154
-
2155
2264
  if (templateStatus !== RCS_STATUSES.approved) {
2156
2265
  return true;
2157
2266
  }
2158
2267
 
2159
- if (!isFullMode) {
2160
- if (templateName.trim() === '' || templateNameError) {
2161
- return true;
2162
- }
2268
+ // if (!isFullMode) {
2269
+ // if (templateName.trim() === '' || templateNameError) {
2270
+ // return true;
2271
+ // }
2272
+ // }
2273
+ if (isLibraryCampaignCardVarMappingIncomplete()) {
2274
+ return true;
2163
2275
  }
2164
- if(!isFullMode){
2165
- const titleVars = splitTemplateVarString(templateTitle).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2166
- const descVars = splitTemplateVarString(templateDesc).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2167
- const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
2168
2276
 
2169
- if (allVars.length > 0 && isEmpty(cardVarMapped)) {
2170
- return true;
2171
- }
2172
-
2173
- const hasEmptyMapping =
2174
- cardVarMapped &&
2175
- Object.keys(cardVarMapped).length > 0 &&
2176
- Object.entries(cardVarMapped).some(([_, v]) => {
2177
- if (typeof v !== 'string') return !v; // null/undefined
2178
- return v.trim() === ''; // empty string
2179
- });
2180
-
2181
- if (hasEmptyMapping) {
2182
- return true;
2183
- }
2184
-
2185
- const anyMissing = allVars.some(name => {
2186
- const v = cardVarMapped?.[name];
2187
- if (typeof v !== 'string') return !v;
2188
- return v.trim() === '';
2189
- });
2190
- if (anyMissing) {
2191
- return true;
2192
- }
2277
+ if (smsFallbackBlocksDone()) {
2278
+ return true;
2193
2279
  }
2280
+
2194
2281
  if (isMediaTypeText && templateDesc.trim() === '') {
2195
2282
  return true;
2196
2283
  }
@@ -2209,7 +2296,13 @@ const splitTemplateVarString = (str) => {
2209
2296
  return true;
2210
2297
  }
2211
2298
  }
2212
- if (templateTitleError || templateDescError || fallbackMessageError) {
2299
+ if (templateTitleError || templateDescError) {
2300
+ return true;
2301
+ }
2302
+ if (
2303
+ smsFallbackData?.content
2304
+ && smsFallbackData.content.length > FALLBACK_MESSAGE_MAX_LENGTH
2305
+ ) {
2213
2306
  return true;
2214
2307
  }
2215
2308
  return false;
@@ -2259,52 +2352,56 @@ const splitTemplateVarString = (str) => {
2259
2352
  };
2260
2353
 
2261
2354
  const getMainContent = () => {
2262
- if (showDltContainer && !fallbackPreviewmode) {
2263
- const dltSlideBoxContent = showDltContainer && getDltSlideBoxContent();
2264
- const { dltHeader = '', dltContent = '' } = dltSlideBoxContent;
2265
- return (
2266
- <CapSlideBox
2267
- show={showDltContainer}
2268
- header={dltHeader}
2269
- content={dltContent}
2270
- handleClose={closeDltContainerHandler}
2271
- size="size-xl"
2272
- />
2273
- );
2274
- }
2355
+ // Slideboxes are rendered outside the page-level spinner to avoid
2356
+ // stacking/blur issues during initial loads.
2357
+ if (showDltContainer) return null;
2275
2358
 
2276
2359
  return (
2277
2360
  <>
2278
- {templateStatus !== '' && (<CapRow className="template-status-container">
2279
- <CapLabel type="label2">
2280
- {formatMessage(messages.templateStatusLabel)}
2281
- </CapLabel>
2282
-
2283
- {templateStatus && (
2284
- <CapAlert
2285
- message={getTemplateStatusMessage()}
2286
- type={getTemplateStatusType(templateStatus)}
2287
- />
2288
- )}
2289
- </CapRow>
2361
+ {templateStatus !== '' && (
2362
+ <CapRow className="template-status-container">
2363
+ <CapColumn span={14}>
2364
+ <CapLabel type="label2">
2365
+ {formatMessage(messages.templateStatusLabel)}
2366
+ </CapLabel>
2367
+
2368
+ {templateStatus && (
2369
+ <CapAlert
2370
+ message={getTemplateStatusMessage()}
2371
+ type={getTemplateStatusType(templateStatus)}
2372
+ />
2373
+ )}
2374
+ </CapColumn>
2375
+ </CapRow>
2290
2376
  )}
2291
- <CapRow className="cap-rcs-creatives">
2377
+ <CapRow className={`cap-rcs-creatives ${isEditLike ? 'rcs-edit-mode' : ''}`}>
2292
2378
  <CapColumn span={14}>
2293
2379
  {/* template name */}
2294
2380
  {isFullMode && (
2295
- <CapInput
2296
- id="rcs_template_name_input"
2297
- data-testid="template_name"
2298
- onChange={onTemplateNameChange}
2299
- errorMessage={templateNameError}
2300
- placeholder={formatMessage(
2301
- globalMessages.templateNamePlaceholder,
2302
- )}
2303
- value={templateName || ''}
2304
- size="default"
2305
- label={formatMessage(globalMessages.creativeNameLabel)}
2306
- disabled={(isEditFlow || !isFullMode)}
2307
- />
2381
+ isEditFlow ? (
2382
+ <div className="rcs-creative-name-readonly">
2383
+ <CapHeading type="h4">
2384
+ {formatMessage(globalMessages.creativeNameLabel)}
2385
+ </CapHeading>
2386
+ <CapHeading type="h5" className="rcs-creative-name-value">
2387
+ {templateName || '-'}
2388
+ </CapHeading>
2389
+ </div>
2390
+ ) : (
2391
+ <CapInput
2392
+ id="rcs_template_name_input"
2393
+ data-testid="template_name"
2394
+ onChange={onTemplateNameChange}
2395
+ errorMessage={templateNameError}
2396
+ placeholder={formatMessage(
2397
+ globalMessages.templateNamePlaceholder,
2398
+ )}
2399
+ value={templateName || ''}
2400
+ size="default"
2401
+ label={formatMessage(globalMessages.creativeNameLabel)}
2402
+ disabled={(isEditFlow || !isFullMode)}
2403
+ />
2404
+ )
2308
2405
  )}
2309
2406
  {renderLabel('templateTypeLabel')}
2310
2407
  <CapRadioGroup
@@ -2332,7 +2429,7 @@ const splitTemplateVarString = (str) => {
2332
2429
  </>
2333
2430
  )}
2334
2431
  {renderTextComponent()}
2335
- <CapDivider style={{ margin: `${CAP_SPACE_28} 0` }} />
2432
+ <CapDivider className="rcs-fallback-section-divider" />
2336
2433
  {renderFallBackSmsComponent()}
2337
2434
  <div className="rcs-scroll-div" />
2338
2435
  </CapColumn>
@@ -2344,7 +2441,8 @@ const splitTemplateVarString = (str) => {
2344
2441
 
2345
2442
 
2346
2443
  <div className="rcs-footer">
2347
- {!isEditFlow && (
2444
+ {/* 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). */}
2445
+ {!isEditFlow && isFullMode && (
2348
2446
  <>
2349
2447
  <div className="button-disabled-tooltip-wrapper">
2350
2448
  <CapButton
@@ -2365,7 +2463,6 @@ const splitTemplateVarString = (str) => {
2365
2463
  className="rcs-test-preview-btn"
2366
2464
  type="secondary"
2367
2465
  disabled={true}
2368
- style={{ marginLeft: "8px" }}
2369
2466
  >
2370
2467
  <FormattedMessage {...creativesMessages.testAndPreview} />
2371
2468
  </CapButton>
@@ -2398,51 +2495,6 @@ const splitTemplateVarString = (str) => {
2398
2495
  </>
2399
2496
  )}
2400
2497
  </div>
2401
-
2402
-
2403
- {fallbackPreviewmode && (
2404
- <CapSlideBox
2405
- className="rcs-fallback-preview"
2406
- show={fallbackPreviewmode}
2407
- header={(
2408
- <CapHeading type="h7" style={{ color: CAP_G01 }}>
2409
- {formatMessage(messages.fallbackPreviewtitle)}
2410
- </CapHeading>
2411
- )}
2412
- content={(
2413
- <>
2414
- <UnifiedPreview
2415
- channel={RCS}
2416
- content={{
2417
- rcsPreviewContent: {
2418
- rcsDesc: tempMsg,
2419
- },
2420
- }}
2421
- device={ANDROID}
2422
- showDeviceToggle={false}
2423
- showHeader={false}
2424
- formatMessage={formatMessage}
2425
- />
2426
- <CapHeading
2427
- type="h3"
2428
- style={{ textAlign: 'center' }}
2429
- className="margin-t-16"
2430
- >
2431
- {formatMessage(messages.totalCharacters, {
2432
- smsCount: Math.ceil(
2433
- tempMsg?.length / FALLBACK_MESSAGE_MAX_LENGTH,
2434
- ),
2435
- number: tempMsg.length,
2436
- })}
2437
- </CapHeading>
2438
- </>
2439
- )}
2440
- handleClose={() => {
2441
- setFallbackPreviewmode(false);
2442
- setDltPreviewData('');
2443
- }}
2444
- />
2445
- )}
2446
2498
  </>
2447
2499
  );
2448
2500
  };
@@ -2451,12 +2503,38 @@ const splitTemplateVarString = (str) => {
2451
2503
  <CapSpin spinning={loadingTags || spin}>
2452
2504
  {getMainContent()}
2453
2505
  </CapSpin>
2506
+
2507
+ {showDltContainer && (() => {
2508
+ const { dltHeader = '', dltContent = '' } = getDltSlideBoxContent() || {};
2509
+ return (
2510
+ <CapSlideBox
2511
+ show={showDltContainer}
2512
+ header={dltHeader}
2513
+ content={dltContent}
2514
+ handleClose={closeDltContainerHandler}
2515
+ size="size-xl"
2516
+ />
2517
+ );
2518
+ })()}
2519
+
2454
2520
  <TestAndPreviewSlidebox
2455
2521
  show={propsShowTestAndPreviewSlidebox || showTestAndPreviewSlidebox}
2456
2522
  onClose={handleCloseTestAndPreview}
2457
- formData={null} // RCS doesn't use formData structure like SMS
2458
- content={getTemplateContent()}
2523
+ formData={testPreviewFormData}
2524
+ content={testAndPreviewContent}
2459
2525
  currentChannel={RCS}
2526
+ orgUnitId={orgUnitId}
2527
+ smsFallbackContent={
2528
+ smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
2529
+ ? {
2530
+ templateContent:
2531
+ smsFallbackData.templateContent || smsFallbackData.content || '',
2532
+ templateName: smsFallbackData.templateName || '',
2533
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackData?.rcsSmsFallbackVarMapped ?? {},
2534
+ }
2535
+ : null
2536
+ }
2537
+ smsRegister={smsRegister}
2460
2538
  />
2461
2539
  </>
2462
2540
  );