@capillarytech/creatives-library 9.0.29 → 9.0.31

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 (147) 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 +261 -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 +774 -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 +322 -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/MobilePush/Create/test/saga.test.js +2 -2
  96. package/v2Containers/Rcs/carouselUtils.js +224 -0
  97. package/v2Containers/Rcs/components/CarouselCard.js +317 -0
  98. package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
  99. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  100. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  101. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  102. package/v2Containers/Rcs/constants.js +132 -16
  103. package/v2Containers/Rcs/index.js +1822 -946
  104. package/v2Containers/Rcs/index.scss +443 -8
  105. package/v2Containers/Rcs/messages.js +45 -22
  106. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +494 -0
  107. package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
  108. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  109. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  110. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  111. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  112. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74440 -39387
  113. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  114. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  115. package/v2Containers/Rcs/tests/index.test.js +275 -40
  116. package/v2Containers/Rcs/tests/mockData.js +38 -0
  117. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +912 -0
  118. package/v2Containers/Rcs/tests/utils.test.js +682 -30
  119. package/v2Containers/Rcs/utils.js +514 -12
  120. package/v2Containers/Sms/Create/index.js +115 -48
  121. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  122. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  123. package/v2Containers/SmsTrai/Create/index.js +9 -4
  124. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  125. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  126. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  127. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  128. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  129. package/v2Containers/SmsWrapper/index.js +37 -8
  130. package/v2Containers/TagList/index.js +5 -0
  131. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  132. package/v2Containers/Templates/_templates.scss +196 -12
  133. package/v2Containers/Templates/actions.js +11 -0
  134. package/v2Containers/Templates/constants.js +2 -0
  135. package/v2Containers/Templates/index.js +131 -59
  136. package/v2Containers/Templates/sagas.js +57 -13
  137. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  138. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  139. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  140. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  141. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  142. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  143. package/v2Containers/TemplatesV2/index.js +88 -25
  144. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  145. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  146. package/v2Containers/Whatsapp/index.js +3 -20
  147. 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,9 @@ 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 { pickRcsCardVarMappedEntries } from '../Rcs/rcsLibraryHydrationUtils';
94
+ import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
94
95
  import { CREATIVE } from '../Facebook/constants';
95
96
  import { LOYALTY } from '../App/constants';
96
97
  import {
@@ -131,6 +132,11 @@ import { capSagaForFetchSchemaForEntity, capSagaLiquidEntity } from '../Cap/saga
131
132
  import { v2TemplateSagaWatchGetDefaultBeeTemplates } from '../Templates/sagas';
132
133
  import { DYNAMIC_URL } from '../../v2Components/CapWhatsappCTA/constants';
133
134
  import ErrorInfoNote from '../../v2Components/ErrorInfoNote';
135
+ import SlideBoxWrapper from './CreativesSlideBoxWrapper';
136
+ import {
137
+ computeLiquidFooterUpdateFromFormBuilder,
138
+ getSlideBoxWrapperMarginFromLiquidErrors,
139
+ } from './embeddedSlideboxUtils';
134
140
 
135
141
  import {
136
142
  transformChannelPayload,
@@ -139,51 +145,24 @@ import {
139
145
  import { MANUAL_CAROUSEL } from '../MobilePushNew/constants';
140
146
  import { BIG_HTML } from '../InApp/constants';
141
147
 
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
148
  const classPrefix = 'add-creatives-section';
160
149
  const CREATIVES_CONTAINER = 'creativesContainer';
161
150
 
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
151
  export class Creatives extends React.Component {
183
152
  constructor(props) {
184
153
  super(props);
185
154
 
186
- const initialSlidBoxContent = this.getSlideBoxContent({ mode: props.creativesMode, templateData: props.templateData, isFullMode: props.isFullMode });
155
+ const useLocalTemplates = get(
156
+ props,
157
+ 'localTemplatesConfig.useLocalTemplates',
158
+ get(props, 'useLocalTemplates', false),
159
+ );
160
+ const initialSlidBoxContent = this.getSlideBoxContent({
161
+ mode: props.creativesMode,
162
+ templateData: props.templateData,
163
+ isFullMode: props.isFullMode,
164
+ useLocalTemplates,
165
+ });
187
166
 
188
167
  this.state = {
189
168
  isLoadingContent: true,
@@ -230,7 +209,13 @@ export class Creatives extends React.Component {
230
209
  }
231
210
 
232
211
  componentWillUnmount() {
233
- if (get(this.props, 'location.query.type', '') === "embedded") {
212
+ const isEmbedded = get(this.props, 'location.query.type', '') === EMBEDDED;
213
+ const useLocalTemplates = get(
214
+ this.props,
215
+ 'localTemplatesConfig.useLocalTemplates',
216
+ get(this.props, 'useLocalTemplates', false),
217
+ );
218
+ if (isEmbedded && !useLocalTemplates) {
234
219
  this.props.templateActions.resetTemplateStoreData();
235
220
  }
236
221
  this.props.globalActions.clearMetaEntities();
@@ -840,15 +825,69 @@ export class Creatives extends React.Component {
840
825
  smsFallBackContent = {},
841
826
  creativeName = "",
842
827
  channel = constants.RCS,
828
+ rcsCardVarMapped,
843
829
  accountId = "",
844
830
  } = templateData || {};
845
- const cardContent = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
831
+ const { isFullMode: isFullModeForRcsPayload } = this.props;
832
+ const isCarouselRcs = (rcsContent?.cardType || '').toString().toUpperCase() === RCS_CONTENT_TYPE.carousel;
833
+ const firstCardIn = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
834
+ const {
835
+ cardDisplayTitle: _omitDispTitleIn,
836
+ cardDisplayDescription: _omitDispDescIn,
837
+ ...cardContent
838
+ } = firstCardIn;
846
839
  const Status = RCS_STATUSES.approved || '';
840
+ const mergedCardVarMapped = (() => {
841
+ const nestedCardVarMapped = cardContent?.cardVarMapped;
842
+ const rootMirrorCardVarMapped = rcsCardVarMapped;
843
+ const nestedRecord =
844
+ nestedCardVarMapped != null && typeof nestedCardVarMapped === 'object'
845
+ ? nestedCardVarMapped
846
+ : {};
847
+ const rootRecord =
848
+ rootMirrorCardVarMapped != null && typeof rootMirrorCardVarMapped === 'object'
849
+ ? rootMirrorCardVarMapped
850
+ : {};
851
+ const mergedFromRootAndNested = {
852
+ ...pickRcsCardVarMappedEntries(rootRecord),
853
+ ...pickRcsCardVarMappedEntries(nestedRecord),
854
+ };
855
+ return Object.keys(mergedFromRootAndNested).length > 0
856
+ ? mergedFromRootAndNested
857
+ : null;
858
+ })();
859
+ // Campaigns (embedded): do not duplicate `cardVarMapped` as root `rcsCardVarMapped` on send —
860
+ // slot map stays on `versions…cardContent[0].cardVarMapped` only. Library full mode keeps root mirror.
861
+ // Use `=== true` so omitted/undefined `isFullMode` does not behave like library (avoids duplicate on approval payload).
862
+ const includeRootRcsCardVarMapped =
863
+ mergedCardVarMapped && isFullModeForRcsPayload === true;
864
+
865
+ const builtCardContent = isCarouselRcs
866
+ ? (rcsContent.cardContent || []).map((card, idx) => {
867
+ const {
868
+ cardDisplayTitle: _dt,
869
+ cardDisplayDescription: _dd,
870
+ ...restCard
871
+ } = card || {};
872
+ return {
873
+ ...restCard,
874
+ ...(idx === 0 && mergedCardVarMapped ? { cardVarMapped: mergedCardVarMapped } : {}),
875
+ Status,
876
+ };
877
+ })
878
+ : [
879
+ {
880
+ ...cardContent,
881
+ ...(mergedCardVarMapped ? { cardVarMapped: mergedCardVarMapped } : {}),
882
+ Status,
883
+ },
884
+ ];
847
885
 
848
886
  creativesTemplateData = {
849
887
  type: channel,
850
888
  edit: true,
851
889
  name: creativeName,
890
+ ...(includeRootRcsCardVarMapped ? { rcsCardVarMapped: mergedCardVarMapped } : {}),
852
891
  versions: {
853
892
  base: {
854
893
  content: {
@@ -856,12 +895,7 @@ export class Creatives extends React.Component {
856
895
  rcsContent: {
857
896
  ...rcsContent,
858
897
  ...(accountId && !isFullMode && { accountId }),
859
- cardContent: [
860
- {
861
- ...cardContent,
862
- Status,
863
- },
864
- ],
898
+ cardContent: builtCardContent,
865
899
  },
866
900
  smsFallBackContent,
867
901
  },
@@ -1009,7 +1043,10 @@ export class Creatives extends React.Component {
1009
1043
  return newExpandableDetails;
1010
1044
  }
1011
1045
 
1012
- getCreativesData = async (channel, template, templateRecords) => { //from creatives to consumers
1046
+ getCreativesData = async (channelParam, template, templateRecords) => { //from creatives to consumers
1047
+ const channel = String(
1048
+ channelParam || template?.type || get(template, 'value.type') || '',
1049
+ ).toUpperCase();
1013
1050
  let templateData = { channel };
1014
1051
  switch (channel) {
1015
1052
  case constants.SMS:
@@ -1356,28 +1393,99 @@ export class Creatives extends React.Component {
1356
1393
  break;
1357
1394
  case constants.RCS:
1358
1395
  if (template.value) {
1359
- const { name = "", versions = {} } = {
1360
- } = template.value || {};
1361
- const smsFallBackContent = get(versions, 'base.content.RCS.smsFallBackContent', {});
1396
+ const { isFullMode: isFullModeForRcsConsumerPayload } = this.props;
1397
+ const { name = "", versions = {} } = template.value || {};
1398
+ const fromSubmit = get(versions, 'base.content.RCS.smsFallBackContent', {});
1399
+ const fromRecords = {
1400
+ ...(templateRecords?.smsFallBackContent || {}),
1401
+ ...(get(templateRecords, 'versions.base.content.RCS.smsFallBackContent') || {}),
1402
+ };
1403
+ const hasMeaningfulRcsSmsFallback = (smsFallbackPayload) => {
1404
+ if (
1405
+ !smsFallbackPayload
1406
+ || typeof smsFallbackPayload !== 'object'
1407
+ || Object.keys(smsFallbackPayload).length === 0
1408
+ ) {
1409
+ return false;
1410
+ }
1411
+ const fallbackBodyText = String(
1412
+ smsFallbackPayload.smsContent
1413
+ ?? smsFallbackPayload.smsTemplateContent
1414
+ ?? smsFallbackPayload.message
1415
+ ?? smsFallbackPayload.content
1416
+ ?? '',
1417
+ ).trim();
1418
+ const fallbackTemplateName = String(
1419
+ smsFallbackPayload.smsTemplateName ?? smsFallbackPayload.templateName ?? '',
1420
+ ).trim();
1421
+ const rcsSmsFallbackVarMapped =
1422
+ smsFallbackPayload?.[RCS_SMS_FALLBACK_VAR_MAPPED_PROP];
1423
+ const hasVarMappedEntries =
1424
+ rcsSmsFallbackVarMapped != null
1425
+ && typeof rcsSmsFallbackVarMapped === 'object'
1426
+ && Object.keys(rcsSmsFallbackVarMapped).length > 0;
1427
+ return (
1428
+ fallbackBodyText !== ''
1429
+ || fallbackTemplateName !== ''
1430
+ || hasVarMappedEntries
1431
+ );
1432
+ };
1433
+ // If submit has only empty strings, do not let it wipe fallback mirrored on templateRecords (library round-trip).
1434
+ const smsFallBackContent = hasMeaningfulRcsSmsFallback(fromSubmit)
1435
+ ? { ...fromRecords, ...fromSubmit }
1436
+ : { ...fromSubmit, ...fromRecords };
1362
1437
  const {
1363
- cardContent = [],
1438
+ cardContent: cardContentFromSubmit = [],
1364
1439
  contentType = "",
1365
1440
  cardType = "",
1366
1441
  cardSettings = {},
1367
1442
  accountId = "",
1368
1443
  } = get(versions, 'base.content.RCS.rcsContent', {});
1444
+ const firstCardFromSubmit = Array.isArray(cardContentFromSubmit)
1445
+ ? cardContentFromSubmit[0]
1446
+ : null;
1447
+ const cardContent = cardContentFromSubmit;
1369
1448
  const rcsContent = {
1370
1449
  contentType,
1371
1450
  cardType,
1372
1451
  cardSettings,
1373
1452
  cardContent,
1374
1453
  };
1454
+ const cardVarMappedFromFirstRcsCard =
1455
+ firstCardFromSubmit?.cardVarMapped != null
1456
+ && typeof firstCardFromSubmit.cardVarMapped === 'object'
1457
+ ? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
1458
+ : null;
1459
+ const includeRootRcsCardVarMappedOnConsumerPayload =
1460
+ cardVarMappedFromFirstRcsCard
1461
+ && Object.keys(cardVarMappedFromFirstRcsCard).length > 0
1462
+ && isFullModeForRcsConsumerPayload === true;
1375
1463
  templateData = {
1376
1464
  channel,
1377
1465
  creativeName: name,
1378
1466
  rcsContent,
1379
1467
  accountId,
1468
+ ...(includeRootRcsCardVarMappedOnConsumerPayload
1469
+ ? { rcsCardVarMapped: cardVarMappedFromFirstRcsCard }
1470
+ : {}),
1380
1471
  };
1472
+ // Library / campaign consumers round-trip templateData via getTemplateData; include SMS fallback
1473
+ // so reopening the editor restores fallback text and tag mappings.
1474
+ // cap-campaigns-v2 API expects `smsFallBackContent.message` (see normalizeRcsMessageContentForApi).
1475
+ if (hasMeaningfulRcsSmsFallback(smsFallBackContent)) {
1476
+ const smsText =
1477
+ smsFallBackContent.message
1478
+ ?? smsFallBackContent.smsContent
1479
+ ?? smsFallBackContent.smsTemplateContent
1480
+ ?? '';
1481
+ templateData.smsFallBackContent = {
1482
+ ...smsFallBackContent,
1483
+ ...(String(smsText).trim() !== ''
1484
+ ? { message: String(smsText).trim() }
1485
+ : {}),
1486
+ };
1487
+ }
1488
+ normalizeRcsMessageContentForApi(templateData);
1381
1489
  }
1382
1490
  break;
1383
1491
  case constants.ZALO:
@@ -1495,7 +1603,10 @@ export class Creatives extends React.Component {
1495
1603
  return templateData;
1496
1604
  };
1497
1605
 
1498
- getSlideBoxContent({ mode, templateData, isFullMode }) {
1606
+ getSlideBoxContent({ mode, templateData, isFullMode, useLocalTemplates }) {
1607
+ if (useLocalTemplates && mode === 'create' && isEmpty(templateData)) {
1608
+ return 'templates';
1609
+ }
1499
1610
  let creativesMode = isFullMode ? 'createTemplate' : 'templates';// for library mode templates page is initial mode and for full mode createTemplates
1500
1611
  if (mode === 'create' && isFullMode) {
1501
1612
  creativesMode = 'createTemplate';
@@ -1583,24 +1694,110 @@ export class Creatives extends React.Component {
1583
1694
  getFormData = (template) => {
1584
1695
  // Always reset isGetFormData so the child does not re-send form data on every re-render
1585
1696
  // (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);
1697
+ this.setState(
1698
+ (prevState) => {
1699
+ const next = { isGetFormData: false };
1700
+ if (!template.validity) {
1701
+ return next;
1702
+ }
1703
+ const baseTd = prevState.templateData || template;
1704
+ const channel = (
1705
+ baseTd?.type
1706
+ || template?.type
1707
+ || get(template, 'value.type')
1708
+ || ''
1709
+ ).toUpperCase();
1710
+ // Library mode: persist last submitted creatives shape so reopening still hydrates the editor
1711
+ // (parent may not merge getCreativesData back into templateData).
1712
+ if (this.props.isFullMode === false && template.value) {
1713
+ if (channel === constants.RCS) {
1714
+ const smsFallBackFromPayload = get(
1715
+ template.value,
1716
+ 'versions.base.content.RCS.smsFallBackContent',
1717
+ );
1718
+ const rcsCardVarMappedFromPayload = get(
1719
+ template.value,
1720
+ 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped',
1721
+ );
1722
+ next.templateData = {
1723
+ ...baseTd,
1724
+ type: constants.RCS,
1725
+ name: template?.value?.name,
1726
+ versions: template?.value?.versions,
1727
+ ...(smsFallBackFromPayload != null
1728
+ && typeof smsFallBackFromPayload === 'object'
1729
+ && Object.keys(smsFallBackFromPayload).length > 0
1730
+ ? { smsFallBackContent: smsFallBackFromPayload }
1731
+ : {}),
1732
+ ...(rcsCardVarMappedFromPayload != null
1733
+ && typeof rcsCardVarMappedFromPayload === 'object'
1734
+ ? { rcsCardVarMapped: rcsCardVarMappedFromPayload }
1735
+ : {}),
1736
+ };
1737
+ if (template._id) {
1738
+ next.templateData._id = template._id;
1739
+ }
1740
+ } else if (channel === constants.SMS) {
1741
+ const submittedSmsTemplateValue = template?.value;
1742
+ const smsVersions =
1743
+ submittedSmsTemplateValue?.history != null
1744
+ ? submittedSmsTemplateValue
1745
+ : {
1746
+ base: submittedSmsTemplateValue?.base,
1747
+ history: submittedSmsTemplateValue?.base
1748
+ ? [submittedSmsTemplateValue.base]
1749
+ : [],
1750
+ };
1751
+ next.templateData = {
1752
+ ...baseTd,
1753
+ type: constants.SMS,
1754
+ name: baseTd?.name || constants.DEFAULT_SMS_TEMPLATE_NAME,
1755
+ versions: smsVersions,
1756
+ };
1757
+ if (template?._id) {
1758
+ next.templateData._id = template._id;
1759
+ }
1760
+ }
1761
+ }
1762
+ return next;
1763
+ },
1764
+ () => {
1765
+ if (!template.validity) {
1766
+ return;
1767
+ }
1768
+ const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1769
+ const channelForConsumer = String(
1770
+ templateData.type
1771
+ || template.type
1772
+ || get(template, 'value.type')
1773
+ || '',
1774
+ ).toUpperCase();
1775
+ const creativesData = this.getCreativesData(
1776
+ channelForConsumer,
1777
+ template,
1778
+ this.state.templateData || template,
1779
+ );// convers data to consumer understandable format
1780
+ creativesData.then((data) => {
1781
+ this.logGTMEvent(channelForConsumer, data);
1782
+ this.processCentralCommsMetaId(channelForConsumer, data, {
1783
+ closeSlideBoxAfterSubmit: template.closeSlideBoxAfterSubmit,
1597
1784
  });
1598
- },
1599
- );
1600
- }
1785
+ });
1786
+ },
1787
+ );
1601
1788
  }
1602
1789
 
1603
- processCentralCommsMetaId = (channel, creativesData) => {
1790
+ processCentralCommsMetaId = (channel, creativesData, options = {}) => {
1791
+ const { closeSlideBoxAfterSubmit = false } = options;
1792
+ const maybeCloseLibrarySlideBox = () => {
1793
+ if (
1794
+ closeSlideBoxAfterSubmit
1795
+ && this.props.isFullMode === false
1796
+ && typeof this.handleCloseSlideBox === 'function'
1797
+ ) {
1798
+ this.handleCloseSlideBox();
1799
+ }
1800
+ };
1604
1801
  // Create the payload for the centralcommnsmetaId API call
1605
1802
  const { isLoyaltyModule = false, loyaltyMetaData = {} } = this.props;
1606
1803
  const { actionName, setMetaData = () => { } } = loyaltyMetaData;
@@ -1626,6 +1823,7 @@ export class Creatives extends React.Component {
1626
1823
  if (result?.status?.code === 200) {
1627
1824
  setMetaData(result);
1628
1825
  this.props.getCreativesData(creativesData);
1826
+ maybeCloseLibrarySlideBox();
1629
1827
  } else {
1630
1828
  CapNotification.error({ message: <FormattedMessage {...messages.somethingWentWrong} /> });
1631
1829
  }
@@ -1636,6 +1834,7 @@ export class Creatives extends React.Component {
1636
1834
  } else {
1637
1835
  // If not a loyalty module or different action, should work as usual
1638
1836
  this.props.getCreativesData(creativesData);
1837
+ maybeCloseLibrarySlideBox();
1639
1838
  }
1640
1839
  };
1641
1840
 
@@ -1668,7 +1867,9 @@ export class Creatives extends React.Component {
1668
1867
  }
1669
1868
  this.setState((prevState) => ({
1670
1869
  ...prevState,
1671
- templateData: undefined,
1870
+ // Library mode (isFullMode === false): retain last template so reopening still has RCS payload.
1871
+ // Undefined isFullMode defaults to full-mode close behavior (clear templateData).
1872
+ ...(this.props.isFullMode !== false ? { templateData: undefined } : {}),
1672
1873
  showSlideBox: false,
1673
1874
  liquidErrorMessage: { STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] },
1674
1875
  isLiquidValidationError: false,
@@ -1871,21 +2072,17 @@ export class Creatives extends React.Component {
1871
2072
  }
1872
2073
 
1873
2074
  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
- });
2075
+ const next = computeLiquidFooterUpdateFromFormBuilder(
2076
+ errorMessagesFromFormBuilder,
2077
+ this.state.liquidErrorMessage,
2078
+ currentFormBuilderTab,
2079
+ {
2080
+ previousIsLiquidValidationError: this.state.isLiquidValidationError,
2081
+ currentChannelUpper: this.state.currentChannel?.toUpperCase(),
2082
+ },
2083
+ );
2084
+ if (next == null) return;
2085
+ this.setState(next);
1889
2086
  }
1890
2087
 
1891
2088
  // Callback to update HTML Editor validation state (called from EmailWrapper)
@@ -2008,6 +2205,11 @@ export class Creatives extends React.Component {
2008
2205
  inAppEditorType,
2009
2206
  htmlEditorValidationState,
2010
2207
  } = this.state;
2208
+ const useLocalTemplates = get(
2209
+ this.props,
2210
+ 'localTemplatesConfig.useLocalTemplates',
2211
+ get(this.props, 'useLocalTemplates', false),
2212
+ );
2011
2213
  const {
2012
2214
  isFullMode,
2013
2215
  creativesMode,
@@ -2026,7 +2228,6 @@ export class Creatives extends React.Component {
2026
2228
  smsRegister,
2027
2229
  enableNewChannels,
2028
2230
  eventContextTags,
2029
- waitEventContextTags = {},
2030
2231
  isLoyaltyModule,
2031
2232
  loyaltyMetaData = {},
2032
2233
  } = this.props;
@@ -2060,14 +2261,7 @@ export class Creatives extends React.Component {
2060
2261
  // IMPORTANT: Never show ErrorInfoNote in footer when in HTML Editor mode, even if liquidErrorMessage exists
2061
2262
  const shouldShowErrorInfoNoteInFooter = isHTMLEditorMode ? false : hasBEEEditorErrors;
2062
2263
 
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;
2264
+ const slideBoxWrapperMargin = getSlideBoxWrapperMarginFromLiquidErrors(liquidErrorMessage);
2071
2265
  /* TODO: Instead of passing down same props separately to each component down, write common function to these props and pass it accordingly */
2072
2266
 
2073
2267
  // Compute anonymous user type and channel restrictions
@@ -2096,7 +2290,10 @@ export class Creatives extends React.Component {
2096
2290
  <SlideBoxWrapper
2097
2291
  slideBoxWrapperMargin={slideBoxWrapperMargin}
2098
2292
  shouldApplyFooterMargin={shouldShowErrorInfoNoteInFooter}
2099
- className={classnames(`${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`)}
2293
+ className={classnames(
2294
+ `${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`,
2295
+ useLocalTemplates && slidBoxContent === 'templates' && 'creatives-slidebox--local-sms-templates',
2296
+ )}
2100
2297
  >
2101
2298
  <CapSlideBox
2102
2299
  header={
@@ -2121,12 +2318,13 @@ export class Creatives extends React.Component {
2121
2318
  smsRegister={smsRegister}
2122
2319
  handleClose={this.handleCloseSlideBox}
2123
2320
  moduleType={this.props.messageDetails?.type}
2321
+ useLocalTemplates={useLocalTemplates}
2124
2322
  />
2125
2323
  )}
2126
2324
  content={(
2127
2325
  <SlideBoxContent
2128
2326
  key="creatives-container-slidebox-content"
2129
- onSelectTemplate={this.onSelectTemplate}
2327
+ onSelectTemplate={this.props.onSelectTemplate != null ? this.props.onSelectTemplate : this.onSelectTemplate}
2130
2328
  onCreateComplete={getCreativesData}
2131
2329
  onPreviewTemplate={this.onPreviewTemplate}
2132
2330
  slidBoxContent={slidBoxContent}
@@ -2194,7 +2392,6 @@ export class Creatives extends React.Component {
2194
2392
  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
2393
  hostName={this.props?.hostName || ''}
2196
2394
  eventContextTags={eventContextTags}
2197
- waitEventContextTags={waitEventContextTags}
2198
2395
  isLoyaltyModule={isLoyaltyModule}
2199
2396
  loyaltyMetaData={loyaltyMetaData}
2200
2397
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
@@ -2203,7 +2400,8 @@ export class Creatives extends React.Component {
2203
2400
  isTestAndPreviewMode={(() => this.state.isTestAndPreviewMode)()}
2204
2401
  onHtmlEditorValidationStateChange={this.updateHtmlEditorValidationState}
2205
2402
  onPersonalizationTokensChange={this.handlePersonalizationTokensChange}
2206
- />
2403
+ localTemplatesConfig={pick(this.props.localTemplatesConfig || this.props, constants.LOCAL_TEMPLATE_CONFIG_KEYS)}
2404
+ />
2207
2405
  )}
2208
2406
  footer={this.shouldShowFooter() ? (
2209
2407
  <SlideBoxFooter
@@ -2288,13 +2486,31 @@ Creatives.propTypes = {
2288
2486
  orgUnitId: PropTypes.number,
2289
2487
  hostName: PropTypes.string,
2290
2488
  eventContextTags: PropTypes.array,
2291
- waitEventContextTags: PropTypes.object,
2292
2489
  loyaltyTagFetchingDependencies: PropTypes.object,
2293
2490
  customerType: PropTypes.string,
2294
2491
  intl: PropTypes.shape({
2295
2492
  formatMessage: PropTypes.func,
2296
2493
  }),
2297
2494
  stopValidation: PropTypes.func,
2495
+ // Local template list (e.g. for SMS fallback): when set, TemplatesV2 uses these instead of Redux.
2496
+ // All optional. Pass either localTemplatesConfig (object) or individual props below.
2497
+ localTemplatesConfig: PropTypes.shape({
2498
+ useLocalTemplates: PropTypes.bool,
2499
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2500
+ localTemplatesLoading: PropTypes.bool,
2501
+ localTemplatesFilterContent: PropTypes.node,
2502
+ localTemplatesSentinelContent: PropTypes.node,
2503
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2504
+ localTemplatesUseSkeleton: PropTypes.bool,
2505
+ }),
2506
+ useLocalTemplates: PropTypes.bool,
2507
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2508
+ localTemplatesLoading: PropTypes.bool,
2509
+ localTemplatesFilterContent: PropTypes.node,
2510
+ localTemplatesSentinelContent: PropTypes.node,
2511
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2512
+ localTemplatesUseSkeleton: PropTypes.bool,
2513
+ onSelectTemplate: PropTypes.func,
2298
2514
  };
2299
2515
  const mapStatesToProps = () => createStructuredSelector({
2300
2516
  isLoading: isLoadingSelector(),