@capillarytech/creatives-library 8.0.318 → 8.0.320

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