@capillarytech/creatives-library 9.0.28 → 9.0.29-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/constants/unified.js +29 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/commonUtils.js +22 -1
  6. package/utils/rcsPayloadUtils.js +102 -0
  7. package/utils/templateVarUtils.js +198 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +295 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +21 -1
  11. package/v2Components/CapActionButton/index.js +260 -144
  12. package/v2Components/CapActionButton/index.scss +245 -14
  13. package/v2Components/CapActionButton/messages.js +32 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +74 -19
  15. package/v2Components/CapImageUpload/index.js +2 -2
  16. package/v2Components/CapTagList/index.js +9 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +58 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -22
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  28. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  29. package/v2Components/CommonTestAndPreview/constants.js +35 -2
  30. package/v2Components/CommonTestAndPreview/index.js +770 -231
  31. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  32. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  33. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  34. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +412 -257
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  39. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  40. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +108 -15
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  46. package/v2Components/CommonTestAndPreview/tests/utils.test.js +151 -0
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  49. package/v2Components/SmsFallback/constants.js +94 -0
  50. package/v2Components/SmsFallback/index.js +958 -0
  51. package/v2Components/SmsFallback/index.scss +266 -0
  52. package/v2Components/SmsFallback/messages.js +78 -0
  53. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  54. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  55. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  58. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  59. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
  60. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  61. package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
  62. package/v2Components/TemplatePreview/constants.js +2 -0
  63. package/v2Components/TemplatePreview/index.js +147 -32
  64. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  65. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  66. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  67. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  68. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  69. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  70. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  71. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  72. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  75. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +27 -0
  76. package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
  77. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  78. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  79. package/v2Containers/CreativesContainer/constants.js +11 -0
  80. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +77 -0
  81. package/v2Containers/CreativesContainer/index.js +328 -106
  82. package/v2Containers/CreativesContainer/index.scss +102 -1
  83. package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
  84. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  85. package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
  86. package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
  87. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
  88. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
  89. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
  90. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  92. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  93. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  94. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  95. package/v2Containers/Email/index.js +3 -63
  96. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +4 -9
  97. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  98. package/v2Containers/Rcs/carouselUtils.js +224 -0
  99. package/v2Containers/Rcs/components/CarouselCard.js +317 -0
  100. package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
  101. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  102. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  103. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  104. package/v2Containers/Rcs/constants.js +132 -16
  105. package/v2Containers/Rcs/index.js +1808 -949
  106. package/v2Containers/Rcs/index.scss +443 -8
  107. package/v2Containers/Rcs/messages.js +45 -22
  108. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +484 -0
  109. package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
  110. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  111. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  112. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  113. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  114. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74176 -39409
  115. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  116. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  117. package/v2Containers/Rcs/tests/index.test.js +219 -40
  118. package/v2Containers/Rcs/tests/mockData.js +38 -0
  119. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +873 -0
  120. package/v2Containers/Rcs/tests/utils.test.js +682 -30
  121. package/v2Containers/Rcs/utils.js +514 -12
  122. package/v2Containers/Sms/Create/index.js +115 -48
  123. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  124. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  125. package/v2Containers/SmsTrai/Create/index.js +9 -4
  126. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  127. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  128. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  129. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  130. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  131. package/v2Containers/SmsWrapper/index.js +37 -8
  132. package/v2Containers/TagList/index.js +5 -0
  133. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  134. package/v2Containers/Templates/_templates.scss +196 -12
  135. package/v2Containers/Templates/actions.js +11 -0
  136. package/v2Containers/Templates/constants.js +2 -0
  137. package/v2Containers/Templates/index.js +131 -59
  138. package/v2Containers/Templates/sagas.js +57 -13
  139. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  140. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  141. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  142. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  143. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  144. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  145. package/v2Containers/TemplatesV2/index.js +88 -25
  146. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  147. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  148. package/v2Containers/Whatsapp/index.js +3 -20
  149. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -44,10 +44,6 @@ class TemplateNameInputField extends React.Component {
44
44
  }
45
45
  }
46
46
  import PropTypes from 'prop-types';
47
- import {
48
- CAP_SPACE_16, CAP_SPACE_32, CAP_SPACE_56, CAP_SPACE_64,
49
- } from '@capillarytech/cap-ui-library/styled/variables';
50
-
51
47
  import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
52
48
  import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
53
49
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
@@ -56,13 +52,15 @@ import CapInput from '@capillarytech/cap-ui-library/CapInput';
56
52
  import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
57
53
  import { injectIntl, FormattedMessage } from 'react-intl';
58
54
  import classnames from 'classnames';
59
- import {
60
- isEmpty, get, forEach, cloneDeep, debounce,
61
- } from 'lodash';
55
+ import isEmpty from 'lodash/isEmpty';
56
+ import get from 'lodash/get';
57
+ import forEach from 'lodash/forEach';
58
+ import cloneDeep from 'lodash/cloneDeep';
59
+ import debounce from 'lodash/debounce';
60
+ import pick from 'lodash/pick';
62
61
  import { connect } from 'react-redux';
63
62
  import { createStructuredSelector } from 'reselect';
64
63
  import { bindActionCreators, compose } from 'redux';
65
- import styled from 'styled-components';
66
64
  import { GA } from '@capillarytech/cap-ui-utils';
67
65
  import { DAEMON } from '@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes';
68
66
 
@@ -76,6 +74,7 @@ import SlideBoxContent from './SlideBoxContent';
76
74
  import * as constants from './constants';
77
75
  import * as commonUtil from '../../utils/common';
78
76
  import { gtmPush } from '../../utils/gtmTrackers';
77
+ import { normalizeRcsMessageContentForApi } from '../../utils/rcsPayloadUtils';
79
78
  import './index.scss';
80
79
  import * as templateActions from '../Templates/actions';
81
80
  import * as globalActions from '../Cap/actions';
@@ -90,7 +89,10 @@ import {
90
89
  } from '../Line/Container/constants';
91
90
  import {EXTERNAL_URL, SITE_URL, WEBPUSH_MEDIA_TYPES} from '../WebPush/constants';
92
91
  import { IMAGE, VIDEO } from '../Facebook/Advertisement/constant';
93
- import { RCS_STATUSES } from '../Rcs/constants';
92
+ import { RCS_STATUSES, contentType as RCS_CONTENT_TYPE, EMBEDDED } from '../Rcs/constants';
93
+ import { mapRcsCardContentForConsumerWithResolvedTags } from '../Rcs/utils';
94
+ import { pickRcsCardVarMappedEntries } from '../Rcs/rcsLibraryHydrationUtils';
95
+ import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
94
96
  import { CREATIVE } from '../Facebook/constants';
95
97
  import { LOYALTY } from '../App/constants';
96
98
  import {
@@ -131,6 +133,11 @@ import { capSagaForFetchSchemaForEntity, capSagaLiquidEntity } from '../Cap/saga
131
133
  import { v2TemplateSagaWatchGetDefaultBeeTemplates } from '../Templates/sagas';
132
134
  import { DYNAMIC_URL } from '../../v2Components/CapWhatsappCTA/constants';
133
135
  import ErrorInfoNote from '../../v2Components/ErrorInfoNote';
136
+ import SlideBoxWrapper from './CreativesSlideBoxWrapper';
137
+ import {
138
+ computeLiquidFooterUpdateFromFormBuilder,
139
+ getSlideBoxWrapperMarginFromLiquidErrors,
140
+ } from './embeddedSlideboxUtils';
134
141
 
135
142
  import {
136
143
  transformChannelPayload,
@@ -139,51 +146,24 @@ import {
139
146
  import { MANUAL_CAROUSEL } from '../MobilePushNew/constants';
140
147
  import { BIG_HTML } from '../InApp/constants';
141
148
 
142
- /**
143
- * Returns true if value is "deep empty": no errors present.
144
- * - null/undefined: empty
145
- * - string: empty if length === 0
146
- * - array: empty if length === 0
147
- * - plain object (e.g. { android: [], ios: [], generic: [] }): empty only if every value is deep-empty
148
- */
149
- function isDeepEmpty(value) {
150
- if (value == null) return true;
151
- if (typeof value === 'string') return value.length === 0;
152
- if (Array.isArray(value)) return value.length === 0;
153
- if (typeof value === 'object') {
154
- return Object.values(value).every(isDeepEmpty);
155
- }
156
- return false;
157
- }
158
-
159
149
  const classPrefix = 'add-creatives-section';
160
150
  const CREATIVES_CONTAINER = 'creativesContainer';
161
151
 
162
- const SlideBoxWrapper = styled.div`
163
- .cap-slide-box-v2-container{
164
- .slidebox-header, .slidebox-content-container{
165
- margin-bottom: ${({ slideBoxWrapperMargin }) => `${slideBoxWrapperMargin}`};
166
- padding: 0 rem;
167
- &.has-footer{
168
- overflow-x: hidden;
169
- }
170
- }
171
- .slidebox-footer{
172
- /* Only apply margin-bottom to footer when ErrorInfoNote is shown in footer (BEE editor) */
173
- /* For HTML Editor, errors are shown in ValidationErrorDisplay (inside content area), so no footer margin needed */
174
- margin-bottom: ${({ shouldApplyFooterMargin }) => (shouldApplyFooterMargin ? `${CAP_SPACE_16}` : '0')};
175
- padding: 0 rem;
176
- &.has-footer{
177
- overflow-x: hidden;
178
- }
179
- }
180
- }
181
- `;
182
152
  export class Creatives extends React.Component {
183
153
  constructor(props) {
184
154
  super(props);
185
155
 
186
- const initialSlidBoxContent = this.getSlideBoxContent({ mode: props.creativesMode, templateData: props.templateData, isFullMode: props.isFullMode });
156
+ const useLocalTemplates = get(
157
+ props,
158
+ 'localTemplatesConfig.useLocalTemplates',
159
+ get(props, 'useLocalTemplates', false),
160
+ );
161
+ const initialSlidBoxContent = this.getSlideBoxContent({
162
+ mode: props.creativesMode,
163
+ templateData: props.templateData,
164
+ isFullMode: props.isFullMode,
165
+ useLocalTemplates,
166
+ });
187
167
 
188
168
  this.state = {
189
169
  isLoadingContent: true,
@@ -230,7 +210,13 @@ export class Creatives extends React.Component {
230
210
  }
231
211
 
232
212
  componentWillUnmount() {
233
- if (get(this.props, 'location.query.type', '') === "embedded") {
213
+ const isEmbedded = get(this.props, 'location.query.type', '') === EMBEDDED;
214
+ const useLocalTemplates = get(
215
+ this.props,
216
+ 'localTemplatesConfig.useLocalTemplates',
217
+ get(this.props, 'useLocalTemplates', false),
218
+ );
219
+ if (isEmbedded && !useLocalTemplates) {
234
220
  this.props.templateActions.resetTemplateStoreData();
235
221
  }
236
222
  this.props.globalActions.clearMetaEntities();
@@ -840,15 +826,69 @@ export class Creatives extends React.Component {
840
826
  smsFallBackContent = {},
841
827
  creativeName = "",
842
828
  channel = constants.RCS,
829
+ rcsCardVarMapped,
843
830
  accountId = "",
844
831
  } = templateData || {};
845
- const cardContent = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
832
+ const { isFullMode: isFullModeForRcsPayload } = this.props;
833
+ const isCarouselRcs = (rcsContent?.cardType || '').toString().toUpperCase() === RCS_CONTENT_TYPE.carousel;
834
+ const firstCardIn = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
835
+ const {
836
+ cardDisplayTitle: _omitDispTitleIn,
837
+ cardDisplayDescription: _omitDispDescIn,
838
+ ...cardContent
839
+ } = firstCardIn;
846
840
  const Status = RCS_STATUSES.approved || '';
841
+ const mergedCardVarMapped = (() => {
842
+ const nestedCardVarMapped = cardContent?.cardVarMapped;
843
+ const rootMirrorCardVarMapped = rcsCardVarMapped;
844
+ const nestedRecord =
845
+ nestedCardVarMapped != null && typeof nestedCardVarMapped === 'object'
846
+ ? nestedCardVarMapped
847
+ : {};
848
+ const rootRecord =
849
+ rootMirrorCardVarMapped != null && typeof rootMirrorCardVarMapped === 'object'
850
+ ? rootMirrorCardVarMapped
851
+ : {};
852
+ const mergedFromRootAndNested = {
853
+ ...pickRcsCardVarMappedEntries(rootRecord),
854
+ ...pickRcsCardVarMappedEntries(nestedRecord),
855
+ };
856
+ return Object.keys(mergedFromRootAndNested).length > 0
857
+ ? mergedFromRootAndNested
858
+ : null;
859
+ })();
860
+ // Campaigns (embedded): do not duplicate `cardVarMapped` as root `rcsCardVarMapped` on send —
861
+ // slot map stays on `versions…cardContent[0].cardVarMapped` only. Library full mode keeps root mirror.
862
+ // Use `=== true` so omitted/undefined `isFullMode` does not behave like library (avoids duplicate on approval payload).
863
+ const includeRootRcsCardVarMapped =
864
+ mergedCardVarMapped && isFullModeForRcsPayload === true;
865
+
866
+ const builtCardContent = isCarouselRcs
867
+ ? (rcsContent.cardContent || []).map((card, idx) => {
868
+ const {
869
+ cardDisplayTitle: _dt,
870
+ cardDisplayDescription: _dd,
871
+ ...restCard
872
+ } = card || {};
873
+ return {
874
+ ...restCard,
875
+ ...(idx === 0 && mergedCardVarMapped ? { cardVarMapped: mergedCardVarMapped } : {}),
876
+ Status,
877
+ };
878
+ })
879
+ : [
880
+ {
881
+ ...cardContent,
882
+ ...(mergedCardVarMapped ? { cardVarMapped: mergedCardVarMapped } : {}),
883
+ Status,
884
+ },
885
+ ];
847
886
 
848
887
  creativesTemplateData = {
849
888
  type: channel,
850
889
  edit: true,
851
890
  name: creativeName,
891
+ ...(includeRootRcsCardVarMapped ? { rcsCardVarMapped: mergedCardVarMapped } : {}),
852
892
  versions: {
853
893
  base: {
854
894
  content: {
@@ -856,12 +896,7 @@ export class Creatives extends React.Component {
856
896
  rcsContent: {
857
897
  ...rcsContent,
858
898
  ...(accountId && !isFullMode && { accountId }),
859
- cardContent: [
860
- {
861
- ...cardContent,
862
- Status,
863
- },
864
- ],
899
+ cardContent: builtCardContent,
865
900
  },
866
901
  smsFallBackContent,
867
902
  },
@@ -1009,7 +1044,10 @@ export class Creatives extends React.Component {
1009
1044
  return newExpandableDetails;
1010
1045
  }
1011
1046
 
1012
- getCreativesData = async (channel, template, templateRecords) => { //from creatives to consumers
1047
+ getCreativesData = async (channelParam, template, templateRecords) => { //from creatives to consumers
1048
+ const channel = String(
1049
+ channelParam || template?.type || get(template, 'value.type') || '',
1050
+ ).toUpperCase();
1013
1051
  let templateData = { channel };
1014
1052
  switch (channel) {
1015
1053
  case constants.SMS:
@@ -1356,28 +1394,104 @@ export class Creatives extends React.Component {
1356
1394
  break;
1357
1395
  case constants.RCS:
1358
1396
  if (template.value) {
1359
- const { name = "", versions = {} } = {
1360
- } = template.value || {};
1361
- const smsFallBackContent = get(versions, 'base.content.RCS.smsFallBackContent', {});
1397
+ const { isFullMode: isFullModeForRcsConsumerPayload } = this.props;
1398
+ const { name = "", versions = {} } = template.value || {};
1399
+ const fromSubmit = get(versions, 'base.content.RCS.smsFallBackContent', {});
1400
+ const fromRecords = {
1401
+ ...(templateRecords?.smsFallBackContent || {}),
1402
+ ...(get(templateRecords, 'versions.base.content.RCS.smsFallBackContent') || {}),
1403
+ };
1404
+ const hasMeaningfulRcsSmsFallback = (smsFallbackPayload) => {
1405
+ if (
1406
+ !smsFallbackPayload
1407
+ || typeof smsFallbackPayload !== 'object'
1408
+ || Object.keys(smsFallbackPayload).length === 0
1409
+ ) {
1410
+ return false;
1411
+ }
1412
+ const fallbackBodyText = String(
1413
+ smsFallbackPayload.smsContent
1414
+ ?? smsFallbackPayload.smsTemplateContent
1415
+ ?? smsFallbackPayload.message
1416
+ ?? smsFallbackPayload.content
1417
+ ?? '',
1418
+ ).trim();
1419
+ const fallbackTemplateName = String(
1420
+ smsFallbackPayload.smsTemplateName ?? smsFallbackPayload.templateName ?? '',
1421
+ ).trim();
1422
+ const rcsSmsFallbackVarMapped =
1423
+ smsFallbackPayload?.[RCS_SMS_FALLBACK_VAR_MAPPED_PROP];
1424
+ const hasVarMappedEntries =
1425
+ rcsSmsFallbackVarMapped != null
1426
+ && typeof rcsSmsFallbackVarMapped === 'object'
1427
+ && Object.keys(rcsSmsFallbackVarMapped).length > 0;
1428
+ return (
1429
+ fallbackBodyText !== ''
1430
+ || fallbackTemplateName !== ''
1431
+ || hasVarMappedEntries
1432
+ );
1433
+ };
1434
+ // If submit has only empty strings, do not let it wipe fallback mirrored on templateRecords (library round-trip).
1435
+ const smsFallBackContent = hasMeaningfulRcsSmsFallback(fromSubmit)
1436
+ ? { ...fromRecords, ...fromSubmit }
1437
+ : { ...fromSubmit, ...fromRecords };
1362
1438
  const {
1363
- cardContent = [],
1439
+ cardContent: cardContentFromSubmit = [],
1364
1440
  contentType = "",
1365
1441
  cardType = "",
1366
1442
  cardSettings = {},
1367
1443
  accountId = "",
1368
1444
  } = get(versions, 'base.content.RCS.rcsContent', {});
1445
+ const rootRcsCardVarMappedFromSubmit = get(template, 'value.rcsCardVarMapped');
1446
+ const firstCardFromSubmit = Array.isArray(cardContentFromSubmit)
1447
+ ? cardContentFromSubmit[0]
1448
+ : null;
1449
+ const cardContent = mapRcsCardContentForConsumerWithResolvedTags(
1450
+ cardContentFromSubmit,
1451
+ rootRcsCardVarMappedFromSubmit,
1452
+ isFullModeForRcsConsumerPayload,
1453
+ );
1369
1454
  const rcsContent = {
1370
1455
  contentType,
1371
1456
  cardType,
1372
1457
  cardSettings,
1373
1458
  cardContent,
1374
1459
  };
1460
+ const cardVarMappedFromFirstRcsCard =
1461
+ firstCardFromSubmit?.cardVarMapped != null
1462
+ && typeof firstCardFromSubmit.cardVarMapped === 'object'
1463
+ ? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
1464
+ : null;
1465
+ const includeRootRcsCardVarMappedOnConsumerPayload =
1466
+ cardVarMappedFromFirstRcsCard
1467
+ && Object.keys(cardVarMappedFromFirstRcsCard).length > 0
1468
+ && isFullModeForRcsConsumerPayload === true;
1375
1469
  templateData = {
1376
1470
  channel,
1377
1471
  creativeName: name,
1378
1472
  rcsContent,
1379
1473
  accountId,
1474
+ ...(includeRootRcsCardVarMappedOnConsumerPayload
1475
+ ? { rcsCardVarMapped: cardVarMappedFromFirstRcsCard }
1476
+ : {}),
1380
1477
  };
1478
+ // Library / campaign consumers round-trip templateData via getTemplateData; include SMS fallback
1479
+ // so reopening the editor restores fallback text and tag mappings.
1480
+ // cap-campaigns-v2 API expects `smsFallBackContent.message` (see normalizeRcsMessageContentForApi).
1481
+ if (hasMeaningfulRcsSmsFallback(smsFallBackContent)) {
1482
+ const smsText =
1483
+ smsFallBackContent.message
1484
+ ?? smsFallBackContent.smsContent
1485
+ ?? smsFallBackContent.smsTemplateContent
1486
+ ?? '';
1487
+ templateData.smsFallBackContent = {
1488
+ ...smsFallBackContent,
1489
+ ...(String(smsText).trim() !== ''
1490
+ ? { message: String(smsText).trim() }
1491
+ : {}),
1492
+ };
1493
+ }
1494
+ normalizeRcsMessageContentForApi(templateData);
1381
1495
  }
1382
1496
  break;
1383
1497
  case constants.ZALO:
@@ -1495,7 +1609,10 @@ export class Creatives extends React.Component {
1495
1609
  return templateData;
1496
1610
  };
1497
1611
 
1498
- getSlideBoxContent({ mode, templateData, isFullMode }) {
1612
+ getSlideBoxContent({ mode, templateData, isFullMode, useLocalTemplates }) {
1613
+ if (useLocalTemplates && mode === 'create' && isEmpty(templateData)) {
1614
+ return 'templates';
1615
+ }
1499
1616
  let creativesMode = isFullMode ? 'createTemplate' : 'templates';// for library mode templates page is initial mode and for full mode createTemplates
1500
1617
  if (mode === 'create' && isFullMode) {
1501
1618
  creativesMode = 'createTemplate';
@@ -1583,24 +1700,110 @@ export class Creatives extends React.Component {
1583
1700
  getFormData = (template) => {
1584
1701
  // Always reset isGetFormData so the child does not re-send form data on every re-render
1585
1702
  // (e.g. when user fixes validation error by typing, we must not auto-close the slidebox)
1586
- this.setState({ isGetFormData: false });
1587
- if (template.validity) {
1588
- this.setState(
1589
- {},
1590
- () => {
1591
- const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1592
- const channel = templateData.type;
1593
- const creativesData = this.getCreativesData(channel, template, templateData);// convers data to consumer understandable format
1594
- creativesData.then((data) => {
1595
- this.logGTMEvent(channel, data);
1596
- this.processCentralCommsMetaId(channel, data);
1703
+ this.setState(
1704
+ (prevState) => {
1705
+ const next = { isGetFormData: false };
1706
+ if (!template.validity) {
1707
+ return next;
1708
+ }
1709
+ const baseTd = prevState.templateData || template;
1710
+ const channel = (
1711
+ baseTd?.type
1712
+ || template?.type
1713
+ || get(template, 'value.type')
1714
+ || ''
1715
+ ).toUpperCase();
1716
+ // Library mode: persist last submitted creatives shape so reopening still hydrates the editor
1717
+ // (parent may not merge getCreativesData back into templateData).
1718
+ if (this.props.isFullMode === false && template.value) {
1719
+ if (channel === constants.RCS) {
1720
+ const smsFallBackFromPayload = get(
1721
+ template.value,
1722
+ 'versions.base.content.RCS.smsFallBackContent',
1723
+ );
1724
+ const rcsCardVarMappedFromPayload = get(
1725
+ template.value,
1726
+ 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped',
1727
+ );
1728
+ next.templateData = {
1729
+ ...baseTd,
1730
+ type: constants.RCS,
1731
+ name: template?.value?.name,
1732
+ versions: template?.value?.versions,
1733
+ ...(smsFallBackFromPayload != null
1734
+ && typeof smsFallBackFromPayload === 'object'
1735
+ && Object.keys(smsFallBackFromPayload).length > 0
1736
+ ? { smsFallBackContent: smsFallBackFromPayload }
1737
+ : {}),
1738
+ ...(rcsCardVarMappedFromPayload != null
1739
+ && typeof rcsCardVarMappedFromPayload === 'object'
1740
+ ? { rcsCardVarMapped: rcsCardVarMappedFromPayload }
1741
+ : {}),
1742
+ };
1743
+ if (template._id) {
1744
+ next.templateData._id = template._id;
1745
+ }
1746
+ } else if (channel === constants.SMS) {
1747
+ const submittedSmsTemplateValue = template?.value;
1748
+ const smsVersions =
1749
+ submittedSmsTemplateValue?.history != null
1750
+ ? submittedSmsTemplateValue
1751
+ : {
1752
+ base: submittedSmsTemplateValue?.base,
1753
+ history: submittedSmsTemplateValue?.base
1754
+ ? [submittedSmsTemplateValue.base]
1755
+ : [],
1756
+ };
1757
+ next.templateData = {
1758
+ ...baseTd,
1759
+ type: constants.SMS,
1760
+ name: baseTd?.name || constants.DEFAULT_SMS_TEMPLATE_NAME,
1761
+ versions: smsVersions,
1762
+ };
1763
+ if (template?._id) {
1764
+ next.templateData._id = template._id;
1765
+ }
1766
+ }
1767
+ }
1768
+ return next;
1769
+ },
1770
+ () => {
1771
+ if (!template.validity) {
1772
+ return;
1773
+ }
1774
+ const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1775
+ const channelForConsumer = String(
1776
+ templateData.type
1777
+ || template.type
1778
+ || get(template, 'value.type')
1779
+ || '',
1780
+ ).toUpperCase();
1781
+ const creativesData = this.getCreativesData(
1782
+ channelForConsumer,
1783
+ template,
1784
+ this.state.templateData || template,
1785
+ );// convers data to consumer understandable format
1786
+ creativesData.then((data) => {
1787
+ this.logGTMEvent(channelForConsumer, data);
1788
+ this.processCentralCommsMetaId(channelForConsumer, data, {
1789
+ closeSlideBoxAfterSubmit: template.closeSlideBoxAfterSubmit,
1597
1790
  });
1598
- },
1599
- );
1600
- }
1791
+ });
1792
+ },
1793
+ );
1601
1794
  }
1602
1795
 
1603
- processCentralCommsMetaId = (channel, creativesData) => {
1796
+ processCentralCommsMetaId = (channel, creativesData, options = {}) => {
1797
+ const { closeSlideBoxAfterSubmit = false } = options;
1798
+ const maybeCloseLibrarySlideBox = () => {
1799
+ if (
1800
+ closeSlideBoxAfterSubmit
1801
+ && this.props.isFullMode === false
1802
+ && typeof this.handleCloseSlideBox === 'function'
1803
+ ) {
1804
+ this.handleCloseSlideBox();
1805
+ }
1806
+ };
1604
1807
  // Create the payload for the centralcommnsmetaId API call
1605
1808
  const { isLoyaltyModule = false, loyaltyMetaData = {} } = this.props;
1606
1809
  const { actionName, setMetaData = () => { } } = loyaltyMetaData;
@@ -1626,6 +1829,7 @@ export class Creatives extends React.Component {
1626
1829
  if (result?.status?.code === 200) {
1627
1830
  setMetaData(result);
1628
1831
  this.props.getCreativesData(creativesData);
1832
+ maybeCloseLibrarySlideBox();
1629
1833
  } else {
1630
1834
  CapNotification.error({ message: <FormattedMessage {...messages.somethingWentWrong} /> });
1631
1835
  }
@@ -1636,6 +1840,7 @@ export class Creatives extends React.Component {
1636
1840
  } else {
1637
1841
  // If not a loyalty module or different action, should work as usual
1638
1842
  this.props.getCreativesData(creativesData);
1843
+ maybeCloseLibrarySlideBox();
1639
1844
  }
1640
1845
  };
1641
1846
 
@@ -1668,7 +1873,9 @@ export class Creatives extends React.Component {
1668
1873
  }
1669
1874
  this.setState((prevState) => ({
1670
1875
  ...prevState,
1671
- templateData: undefined,
1876
+ // Library mode (isFullMode === false): retain last template so reopening still has RCS payload.
1877
+ // Undefined isFullMode defaults to full-mode close behavior (clear templateData).
1878
+ ...(this.props.isFullMode !== false ? { templateData: undefined } : {}),
1672
1879
  showSlideBox: false,
1673
1880
  liquidErrorMessage: { STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] },
1674
1881
  isLiquidValidationError: false,
@@ -1871,21 +2078,17 @@ export class Creatives extends React.Component {
1871
2078
  }
1872
2079
 
1873
2080
  showLiquidErrorInFooter = (errorMessagesFromFormBuilder, currentFormBuilderTab) => {
1874
- const liquidMsgs = get(errorMessagesFromFormBuilder, constants.LIQUID_ERROR_MSG, []);
1875
- const standardMsgs = get(errorMessagesFromFormBuilder, constants.STANDARD_ERROR_MSG, []);
1876
- const hasLiquid = !isDeepEmpty(liquidMsgs);
1877
- const hasStandard = !isDeepEmpty(standardMsgs);
1878
- const isLiquidValidationError = hasLiquid || hasStandard;
1879
- // Don't overwrite existing liquid error with empty only for Mobile Push OLD (FormBuilder/clear calls empty there); SMS/others clear on input change
1880
- const isMobilePush = this.state.currentChannel?.toUpperCase() === constants.MOBILE_PUSH;
1881
- if (!hasLiquid && !hasStandard && this.state.isLiquidValidationError && isMobilePush) {
1882
- return;
1883
- }
1884
- this.setState({
1885
- isLiquidValidationError,
1886
- liquidErrorMessage: errorMessagesFromFormBuilder,
1887
- activeFormBuilderTab: currentFormBuilderTab === 1 ? constants.ANDROID : (currentFormBuilderTab === 2 ? constants.IOS : null), // Update activeFormBuilderTab, default to 1 if undefined
1888
- });
2081
+ const next = computeLiquidFooterUpdateFromFormBuilder(
2082
+ errorMessagesFromFormBuilder,
2083
+ this.state.liquidErrorMessage,
2084
+ currentFormBuilderTab,
2085
+ {
2086
+ previousIsLiquidValidationError: this.state.isLiquidValidationError,
2087
+ currentChannelUpper: this.state.currentChannel?.toUpperCase(),
2088
+ },
2089
+ );
2090
+ if (next == null) return;
2091
+ this.setState(next);
1889
2092
  }
1890
2093
 
1891
2094
  // Callback to update HTML Editor validation state (called from EmailWrapper)
@@ -2008,6 +2211,11 @@ export class Creatives extends React.Component {
2008
2211
  inAppEditorType,
2009
2212
  htmlEditorValidationState,
2010
2213
  } = this.state;
2214
+ const useLocalTemplates = get(
2215
+ this.props,
2216
+ 'localTemplatesConfig.useLocalTemplates',
2217
+ get(this.props, 'useLocalTemplates', false),
2218
+ );
2011
2219
  const {
2012
2220
  isFullMode,
2013
2221
  creativesMode,
@@ -2026,7 +2234,6 @@ export class Creatives extends React.Component {
2026
2234
  smsRegister,
2027
2235
  enableNewChannels,
2028
2236
  eventContextTags,
2029
- waitEventContextTags = {},
2030
2237
  isLoyaltyModule,
2031
2238
  loyaltyMetaData = {},
2032
2239
  } = this.props;
@@ -2060,14 +2267,7 @@ export class Creatives extends React.Component {
2060
2267
  // IMPORTANT: Never show ErrorInfoNote in footer when in HTML Editor mode, even if liquidErrorMessage exists
2061
2268
  const shouldShowErrorInfoNoteInFooter = isHTMLEditorMode ? false : hasBEEEditorErrors;
2062
2269
 
2063
- // Calculate margin for header/content (always apply if there are errors, regardless of editor type)
2064
- const slideBoxWrapperMargin = (get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0 && get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0)
2065
- ? CAP_SPACE_64
2066
- : get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0
2067
- ? CAP_SPACE_56
2068
- : get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0
2069
- ? CAP_SPACE_32
2070
- : 0;
2270
+ const slideBoxWrapperMargin = getSlideBoxWrapperMarginFromLiquidErrors(liquidErrorMessage);
2071
2271
  /* TODO: Instead of passing down same props separately to each component down, write common function to these props and pass it accordingly */
2072
2272
 
2073
2273
  // Compute anonymous user type and channel restrictions
@@ -2096,7 +2296,10 @@ export class Creatives extends React.Component {
2096
2296
  <SlideBoxWrapper
2097
2297
  slideBoxWrapperMargin={slideBoxWrapperMargin}
2098
2298
  shouldApplyFooterMargin={shouldShowErrorInfoNoteInFooter}
2099
- className={classnames(`${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`)}
2299
+ className={classnames(
2300
+ `${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`,
2301
+ useLocalTemplates && slidBoxContent === 'templates' && 'creatives-slidebox--local-sms-templates',
2302
+ )}
2100
2303
  >
2101
2304
  <CapSlideBox
2102
2305
  header={
@@ -2121,12 +2324,13 @@ export class Creatives extends React.Component {
2121
2324
  smsRegister={smsRegister}
2122
2325
  handleClose={this.handleCloseSlideBox}
2123
2326
  moduleType={this.props.messageDetails?.type}
2327
+ useLocalTemplates={useLocalTemplates}
2124
2328
  />
2125
2329
  )}
2126
2330
  content={(
2127
2331
  <SlideBoxContent
2128
2332
  key="creatives-container-slidebox-content"
2129
- onSelectTemplate={this.onSelectTemplate}
2333
+ onSelectTemplate={this.props.onSelectTemplate != null ? this.props.onSelectTemplate : this.onSelectTemplate}
2130
2334
  onCreateComplete={getCreativesData}
2131
2335
  onPreviewTemplate={this.onPreviewTemplate}
2132
2336
  slidBoxContent={slidBoxContent}
@@ -2194,7 +2398,6 @@ export class Creatives extends React.Component {
2194
2398
  creativesMode={creativesMode} // An existing prop that we're using here. Required to ensure correct account details in Edit or Preview in case of Embedded mode.
2195
2399
  hostName={this.props?.hostName || ''}
2196
2400
  eventContextTags={eventContextTags}
2197
- waitEventContextTags={waitEventContextTags}
2198
2401
  isLoyaltyModule={isLoyaltyModule}
2199
2402
  loyaltyMetaData={loyaltyMetaData}
2200
2403
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
@@ -2203,7 +2406,8 @@ export class Creatives extends React.Component {
2203
2406
  isTestAndPreviewMode={(() => this.state.isTestAndPreviewMode)()}
2204
2407
  onHtmlEditorValidationStateChange={this.updateHtmlEditorValidationState}
2205
2408
  onPersonalizationTokensChange={this.handlePersonalizationTokensChange}
2206
- />
2409
+ localTemplatesConfig={pick(this.props.localTemplatesConfig || this.props, constants.LOCAL_TEMPLATE_CONFIG_KEYS)}
2410
+ />
2207
2411
  )}
2208
2412
  footer={this.shouldShowFooter() ? (
2209
2413
  <SlideBoxFooter
@@ -2288,13 +2492,31 @@ Creatives.propTypes = {
2288
2492
  orgUnitId: PropTypes.number,
2289
2493
  hostName: PropTypes.string,
2290
2494
  eventContextTags: PropTypes.array,
2291
- waitEventContextTags: PropTypes.object,
2292
2495
  loyaltyTagFetchingDependencies: PropTypes.object,
2293
2496
  customerType: PropTypes.string,
2294
2497
  intl: PropTypes.shape({
2295
2498
  formatMessage: PropTypes.func,
2296
2499
  }),
2297
2500
  stopValidation: PropTypes.func,
2501
+ // Local template list (e.g. for SMS fallback): when set, TemplatesV2 uses these instead of Redux.
2502
+ // All optional. Pass either localTemplatesConfig (object) or individual props below.
2503
+ localTemplatesConfig: PropTypes.shape({
2504
+ useLocalTemplates: PropTypes.bool,
2505
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2506
+ localTemplatesLoading: PropTypes.bool,
2507
+ localTemplatesFilterContent: PropTypes.node,
2508
+ localTemplatesSentinelContent: PropTypes.node,
2509
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2510
+ localTemplatesUseSkeleton: PropTypes.bool,
2511
+ }),
2512
+ useLocalTemplates: PropTypes.bool,
2513
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2514
+ localTemplatesLoading: PropTypes.bool,
2515
+ localTemplatesFilterContent: PropTypes.node,
2516
+ localTemplatesSentinelContent: PropTypes.node,
2517
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2518
+ localTemplatesUseSkeleton: PropTypes.bool,
2519
+ onSelectTemplate: PropTypes.func,
2298
2520
  };
2299
2521
  const mapStatesToProps = () => createStructuredSelector({
2300
2522
  isLoading: isLoadingSelector(),