@capillarytech/creatives-library 8.0.345-alpha.13 → 8.0.345-alpha.15

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 (138) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/api.js +0 -20
  4. package/services/tests/api.test.js +13 -59
  5. package/utils/commonUtils.js +19 -1
  6. package/utils/rcsPayloadUtils.js +92 -0
  7. package/utils/templateVarUtils.js +201 -0
  8. package/utils/tests/templateVarUtils.test.js +204 -0
  9. package/v2Components/CapActionButton/constants.js +7 -0
  10. package/v2Components/CapActionButton/index.js +167 -109
  11. package/v2Components/CapActionButton/index.scss +157 -6
  12. package/v2Components/CapActionButton/messages.js +19 -3
  13. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  14. package/v2Components/CapCustomSkeleton/index.js +1 -1
  15. package/v2Components/CapCustomSkeleton/tests/__snapshots__/index.test.js.snap +12 -12
  16. package/v2Components/CapTagList/index.js +10 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +160 -15
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +341 -76
  28. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  29. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  30. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  31. package/v2Components/CommonTestAndPreview/index.js +676 -186
  32. package/v2Components/CommonTestAndPreview/messages.js +49 -3
  33. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  34. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  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 +34 -13
  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 +132 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  46. package/v2Components/FormBuilder/index.js +8 -10
  47. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  48. package/v2Components/SmsFallback/constants.js +73 -0
  49. package/v2Components/SmsFallback/index.js +955 -0
  50. package/v2Components/SmsFallback/index.scss +265 -0
  51. package/v2Components/SmsFallback/messages.js +78 -0
  52. package/v2Components/SmsFallback/smsFallbackUtils.js +118 -0
  53. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  54. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +277 -0
  58. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  59. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  60. package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
  61. package/v2Components/TemplatePreview/constants.js +2 -0
  62. package/v2Components/TemplatePreview/index.js +143 -28
  63. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  64. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  65. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  66. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  67. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  68. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  69. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  70. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  71. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  72. package/v2Containers/CreativesContainer/SlideBoxFooter.js +11 -4
  73. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  74. package/v2Containers/CreativesContainer/constants.js +9 -0
  75. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  76. package/v2Containers/CreativesContainer/index.js +300 -108
  77. package/v2Containers/CreativesContainer/index.scss +51 -1
  78. package/v2Containers/CreativesContainer/messages.js +0 -4
  79. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  80. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -34
  81. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  82. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  83. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  84. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -18
  85. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  86. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  87. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  88. package/v2Containers/Rcs/constants.js +119 -8
  89. package/v2Containers/Rcs/index.js +2379 -807
  90. package/v2Containers/Rcs/index.js.rej +1336 -0
  91. package/v2Containers/Rcs/index.scss +276 -6
  92. package/v2Containers/Rcs/index.scss.rej +74 -0
  93. package/v2Containers/Rcs/messages.js +38 -3
  94. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  95. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  96. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  97. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
  98. package/v2Containers/Rcs/tests/index.test.js +152 -121
  99. package/v2Containers/Rcs/tests/mockData.js +38 -0
  100. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  101. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  102. package/v2Containers/Rcs/utils.js +478 -11
  103. package/v2Containers/Sms/Create/index.js +100 -40
  104. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  105. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  106. package/v2Containers/SmsTrai/Create/index.js +9 -4
  107. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  108. package/v2Containers/SmsTrai/Edit/index.js +636 -130
  109. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  110. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  111. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  112. package/v2Containers/SmsWrapper/index.js +37 -8
  113. package/v2Containers/TagList/index.js +6 -0
  114. package/v2Containers/Templates/ChannelTypeIllustration.js +6 -23
  115. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  116. package/v2Containers/Templates/_templates.scss +181 -126
  117. package/v2Containers/Templates/actions.js +11 -36
  118. package/v2Containers/Templates/constants.js +2 -23
  119. package/v2Containers/Templates/index.js +142 -333
  120. package/v2Containers/Templates/messages.js +0 -68
  121. package/v2Containers/Templates/reducer.js +0 -68
  122. package/v2Containers/Templates/sagas.js +55 -98
  123. package/v2Containers/Templates/selectors.js +0 -12
  124. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +0 -12
  125. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  126. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1042 -1256
  127. package/v2Containers/Templates/tests/index.test.js +0 -6
  128. package/v2Containers/Templates/tests/reducer.test.js +0 -178
  129. package/v2Containers/Templates/tests/sagas.test.js +200 -436
  130. package/v2Containers/Templates/tests/selector.test.js +0 -32
  131. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  132. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  133. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  134. package/v2Containers/TemplatesV2/index.js +86 -23
  135. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  136. package/v2Containers/Whatsapp/index.js +3 -20
  137. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  138. package/v2Containers/Assets/images/archive_Empty_Illustration.svg +0 -9
@@ -1,9 +1,5 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import {
4
- CAP_SPACE_16, CAP_SPACE_32, CAP_SPACE_56, CAP_SPACE_64,
5
- } from '@capillarytech/cap-ui-library/styled/variables';
6
-
7
3
  import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
8
4
  import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
9
5
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
@@ -13,12 +9,11 @@ import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
13
9
  import { injectIntl, FormattedMessage } from 'react-intl';
14
10
  import classnames from 'classnames';
15
11
  import {
16
- isEmpty, get, forEach, cloneDeep, debounce,
12
+ isEmpty, get, forEach, cloneDeep, debounce, pick,
17
13
  } from 'lodash';
18
14
  import { connect } from 'react-redux';
19
15
  import { createStructuredSelector } from 'reselect';
20
16
  import { bindActionCreators, compose } from 'redux';
21
- import styled from 'styled-components';
22
17
  import { GA } from '@capillarytech/cap-ui-utils';
23
18
  import { DAEMON } from '@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes';
24
19
 
@@ -32,6 +27,7 @@ import SlideBoxContent from './SlideBoxContent';
32
27
  import * as constants from './constants';
33
28
  import * as commonUtil from '../../utils/common';
34
29
  import { gtmPush } from '../../utils/gtmTrackers';
30
+ import { normalizeRcsMessageContentForApi } from '../../utils/rcsPayloadUtils';
35
31
  import './index.scss';
36
32
  import * as templateActions from '../Templates/actions';
37
33
  import * as globalActions from '../Cap/actions';
@@ -47,6 +43,9 @@ import {
47
43
  import {EXTERNAL_URL, SITE_URL, WEBPUSH_MEDIA_TYPES} from '../WebPush/constants';
48
44
  import { IMAGE, VIDEO } from '../Facebook/Advertisement/constant';
49
45
  import { RCS_STATUSES } from '../Rcs/constants';
46
+ import { mapRcsCardContentForConsumerWithResolvedTags } from '../Rcs/utils';
47
+ import { pickRcsCardVarMappedEntries } from '../Rcs/rcsLibraryHydrationUtils';
48
+ import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
50
49
  import { CREATIVE } from '../Facebook/constants';
51
50
  import { LOYALTY } from '../App/constants';
52
51
  import {
@@ -61,6 +60,11 @@ import { capSagaForFetchSchemaForEntity, capSagaLiquidEntity } from '../Cap/saga
61
60
  import { v2TemplateSagaWatchGetDefaultBeeTemplates } from '../Templates/sagas';
62
61
  import { DYNAMIC_URL } from '../../v2Components/CapWhatsappCTA/constants';
63
62
  import ErrorInfoNote from '../../v2Components/ErrorInfoNote';
63
+ import SlideBoxWrapper from './CreativesSlideBoxWrapper';
64
+ import {
65
+ computeLiquidFooterUpdateFromFormBuilder,
66
+ getSlideBoxWrapperMarginFromLiquidErrors,
67
+ } from './embeddedSlideboxUtils';
64
68
 
65
69
  import {
66
70
  transformChannelPayload,
@@ -69,51 +73,24 @@ import {
69
73
  import { MANUAL_CAROUSEL } from '../MobilePushNew/constants';
70
74
  import { BIG_HTML } from '../InApp/constants';
71
75
 
72
- /**
73
- * Returns true if value is "deep empty": no errors present.
74
- * - null/undefined: empty
75
- * - string: empty if length === 0
76
- * - array: empty if length === 0
77
- * - plain object (e.g. { android: [], ios: [], generic: [] }): empty only if every value is deep-empty
78
- */
79
- function isDeepEmpty(value) {
80
- if (value == null) return true;
81
- if (typeof value === 'string') return value.length === 0;
82
- if (Array.isArray(value)) return value.length === 0;
83
- if (typeof value === 'object') {
84
- return Object.values(value).every(isDeepEmpty);
85
- }
86
- return false;
87
- }
88
-
89
76
  const classPrefix = 'add-creatives-section';
90
77
  const CREATIVES_CONTAINER = 'creativesContainer';
91
78
 
92
- const SlideBoxWrapper = styled.div`
93
- .cap-slide-box-v2-container{
94
- .slidebox-header, .slidebox-content-container{
95
- margin-bottom: ${({ slideBoxWrapperMargin }) => `${slideBoxWrapperMargin}`};
96
- padding: 0 rem;
97
- &.has-footer{
98
- overflow-x: hidden;
99
- }
100
- }
101
- .slidebox-footer{
102
- /* Only apply margin-bottom to footer when ErrorInfoNote is shown in footer (BEE editor) */
103
- /* For HTML Editor, errors are shown in ValidationErrorDisplay (inside content area), so no footer margin needed */
104
- margin-bottom: ${({ shouldApplyFooterMargin }) => (shouldApplyFooterMargin ? `${CAP_SPACE_16}` : '0')};
105
- padding: 0 rem;
106
- &.has-footer{
107
- overflow-x: hidden;
108
- }
109
- }
110
- }
111
- `;
112
79
  export class Creatives extends React.Component {
113
80
  constructor(props) {
114
81
  super(props);
115
82
 
116
- const initialSlidBoxContent = this.getSlideBoxContent({ mode: props.creativesMode, templateData: props.templateData, isFullMode: props.isFullMode });
83
+ const useLocalTemplates = get(
84
+ props,
85
+ 'localTemplatesConfig.useLocalTemplates',
86
+ get(props, 'useLocalTemplates', false),
87
+ );
88
+ const initialSlidBoxContent = this.getSlideBoxContent({
89
+ mode: props.creativesMode,
90
+ templateData: props.templateData,
91
+ isFullMode: props.isFullMode,
92
+ useLocalTemplates,
93
+ });
117
94
 
118
95
  this.state = {
119
96
  isLoadingContent: true,
@@ -160,7 +137,13 @@ export class Creatives extends React.Component {
160
137
  }
161
138
 
162
139
  componentWillUnmount() {
163
- if (get(this.props, 'location.query.type', '') === "embedded") {
140
+ const isEmbedded = get(this.props, 'location.query.type', '') === "embedded";
141
+ const useLocalTemplates = get(
142
+ this.props,
143
+ 'localTemplatesConfig.useLocalTemplates',
144
+ get(this.props, 'useLocalTemplates', false),
145
+ );
146
+ if (isEmbedded && !useLocalTemplates) {
164
147
  this.props.templateActions.resetTemplateStoreData();
165
148
  }
166
149
  this.props.globalActions.clearMetaEntities();
@@ -260,10 +243,6 @@ export class Creatives extends React.Component {
260
243
  };
261
244
 
262
245
  onEditTemplate = () => {
263
- if (this.props.templateData && this.props.templateData.isArchived) {
264
- CapNotification.error({ message: this.props.intl.formatMessage(messages.cannotEditArchivedTemplate) });
265
- return;
266
- }
267
246
  this.setState({ slidBoxContent: 'editTemplate', showSlideBox: true, templateNameExists: true });
268
247
  };
269
248
 
@@ -766,25 +745,56 @@ export class Creatives extends React.Component {
766
745
  smsFallBackContent = {},
767
746
  creativeName = "",
768
747
  channel = constants.RCS,
769
- accountId = "",
748
+ rcsCardVarMapped,
770
749
  } = templateData || {};
771
- const cardContent = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
750
+ const { isFullMode: isFullModeForRcsPayload } = this.props;
751
+ const firstCardIn = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
752
+ const {
753
+ cardDisplayTitle: _omitDispTitleIn,
754
+ cardDisplayDescription: _omitDispDescIn,
755
+ ...cardContent
756
+ } = firstCardIn;
772
757
  const Status = RCS_STATUSES.approved || '';
758
+ const mergedCardVarMapped = (() => {
759
+ const nestedCardVarMapped = cardContent?.cardVarMapped;
760
+ const rootMirrorCardVarMapped = rcsCardVarMapped;
761
+ const nestedRecord =
762
+ nestedCardVarMapped != null && typeof nestedCardVarMapped === 'object'
763
+ ? nestedCardVarMapped
764
+ : {};
765
+ const rootRecord =
766
+ rootMirrorCardVarMapped != null && typeof rootMirrorCardVarMapped === 'object'
767
+ ? rootMirrorCardVarMapped
768
+ : {};
769
+ const mergedFromRootAndNested = {
770
+ ...pickRcsCardVarMappedEntries(rootRecord),
771
+ ...pickRcsCardVarMappedEntries(nestedRecord),
772
+ };
773
+ return Object.keys(mergedFromRootAndNested).length > 0
774
+ ? mergedFromRootAndNested
775
+ : null;
776
+ })();
777
+ // Campaigns (embedded): do not duplicate `cardVarMapped` as root `rcsCardVarMapped` on send —
778
+ // slot map stays on `versions…cardContent[0].cardVarMapped` only. Library full mode keeps root mirror.
779
+ // Use `=== true` so omitted/undefined `isFullMode` does not behave like library (avoids duplicate on approval payload).
780
+ const includeRootRcsCardVarMapped =
781
+ mergedCardVarMapped && isFullModeForRcsPayload === true;
773
782
 
774
783
  creativesTemplateData = {
775
784
  type: channel,
776
785
  edit: true,
777
786
  name: creativeName,
787
+ ...(includeRootRcsCardVarMapped ? { rcsCardVarMapped: mergedCardVarMapped } : {}),
778
788
  versions: {
779
789
  base: {
780
790
  content: {
781
791
  RCS: {
782
792
  rcsContent: {
783
793
  ...rcsContent,
784
- ...(accountId && !isFullMode && { accountId }),
785
794
  cardContent: [
786
795
  {
787
796
  ...cardContent,
797
+ ...(mergedCardVarMapped ? { cardVarMapped: mergedCardVarMapped } : {}),
788
798
  Status,
789
799
  },
790
800
  ],
@@ -935,7 +945,10 @@ export class Creatives extends React.Component {
935
945
  return newExpandableDetails;
936
946
  }
937
947
 
938
- getCreativesData = async (channel, template, templateRecords) => { //from creatives to consumers
948
+ getCreativesData = async (channelParam, template, templateRecords) => { //from creatives to consumers
949
+ const channel = String(
950
+ channelParam || template?.type || get(template, 'value.type') || '',
951
+ ).toUpperCase();
939
952
  let templateData = { channel };
940
953
  switch (channel) {
941
954
  case constants.SMS:
@@ -1229,7 +1242,7 @@ export class Creatives extends React.Component {
1229
1242
  };
1230
1243
  }
1231
1244
  break;
1232
- case constants.FACEBOOK:
1245
+ case constants.FACEBOOK: {
1233
1246
  if (template.value) {
1234
1247
  const FacebookAd = template?.value?.versions?.base?.content?.FacebookAd;
1235
1248
  const { type } = FacebookAd[0];
@@ -1273,34 +1286,110 @@ export class Creatives extends React.Component {
1273
1286
  selectedMarketingObjective: template.value.selectedMarketingObjective,
1274
1287
  };
1275
1288
  }
1289
+ }
1276
1290
  break;
1277
- case constants.RCS:
1291
+ case constants.RCS: {
1278
1292
  if (template.value) {
1279
- const { name = "", versions = {} } = {
1280
- } = template.value || {};
1281
- const smsFallBackContent = get(versions, 'base.content.RCS.smsFallBackContent', {});
1293
+ const { isFullMode: isFullModeForRcsConsumerPayload } = this.props;
1294
+ const { name = "", versions = {} } = template.value || {};
1295
+ const fromSubmit = get(versions, 'base.content.RCS.smsFallBackContent', {});
1296
+ const fromRecords = {
1297
+ ...(templateRecords?.smsFallBackContent || {}),
1298
+ ...(get(templateRecords, 'versions.base.content.RCS.smsFallBackContent') || {}),
1299
+ };
1300
+ const hasMeaningfulRcsSmsFallback = (smsFallbackPayload) => {
1301
+ if (
1302
+ !smsFallbackPayload
1303
+ || typeof smsFallbackPayload !== 'object'
1304
+ || Object.keys(smsFallbackPayload).length === 0
1305
+ ) {
1306
+ return false;
1307
+ }
1308
+ const fallbackBodyText = String(
1309
+ smsFallbackPayload.smsContent
1310
+ ?? smsFallbackPayload.smsTemplateContent
1311
+ ?? smsFallbackPayload.message
1312
+ ?? smsFallbackPayload.content
1313
+ ?? '',
1314
+ ).trim();
1315
+ const fallbackTemplateName = String(
1316
+ smsFallbackPayload.smsTemplateName ?? smsFallbackPayload.templateName ?? '',
1317
+ ).trim();
1318
+ const rcsSmsFallbackVarMapped =
1319
+ smsFallbackPayload?.[RCS_SMS_FALLBACK_VAR_MAPPED_PROP];
1320
+ const hasVarMappedEntries =
1321
+ rcsSmsFallbackVarMapped != null
1322
+ && typeof rcsSmsFallbackVarMapped === 'object'
1323
+ && Object.keys(rcsSmsFallbackVarMapped).length > 0;
1324
+ return (
1325
+ fallbackBodyText !== ''
1326
+ || fallbackTemplateName !== ''
1327
+ || hasVarMappedEntries
1328
+ );
1329
+ };
1330
+ // If submit has only empty strings, do not let it wipe fallback mirrored on templateRecords (library round-trip).
1331
+ const smsFallBackContent = hasMeaningfulRcsSmsFallback(fromSubmit)
1332
+ ? { ...fromRecords, ...fromSubmit }
1333
+ : { ...fromSubmit, ...fromRecords };
1282
1334
  const {
1283
- cardContent = [],
1335
+ cardContent: cardContentFromSubmit = [],
1284
1336
  contentType = "",
1285
1337
  cardType = "",
1286
1338
  cardSettings = {},
1287
- accountId = "",
1288
1339
  } = get(versions, 'base.content.RCS.rcsContent', {});
1340
+ const rootRcsCardVarMappedFromSubmit = get(template, 'value.rcsCardVarMapped');
1341
+ const firstCardFromSubmit = Array.isArray(cardContentFromSubmit)
1342
+ ? cardContentFromSubmit[0]
1343
+ : null;
1344
+ const cardContent = mapRcsCardContentForConsumerWithResolvedTags(
1345
+ cardContentFromSubmit,
1346
+ rootRcsCardVarMappedFromSubmit,
1347
+ isFullModeForRcsConsumerPayload,
1348
+ );
1289
1349
  const rcsContent = {
1290
1350
  contentType,
1291
1351
  cardType,
1292
1352
  cardSettings,
1293
1353
  cardContent,
1294
1354
  };
1355
+ const cardVarMappedFromFirstRcsCard =
1356
+ firstCardFromSubmit?.cardVarMapped != null
1357
+ && typeof firstCardFromSubmit.cardVarMapped === 'object'
1358
+ ? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
1359
+ : null;
1360
+ const includeRootRcsCardVarMappedOnConsumerPayload =
1361
+ cardVarMappedFromFirstRcsCard
1362
+ && Object.keys(cardVarMappedFromFirstRcsCard).length > 0
1363
+ && isFullModeForRcsConsumerPayload === true;
1295
1364
  templateData = {
1296
1365
  channel,
1297
1366
  creativeName: name,
1298
1367
  rcsContent,
1299
- accountId,
1368
+ ...(includeRootRcsCardVarMappedOnConsumerPayload
1369
+ ? { rcsCardVarMapped: cardVarMappedFromFirstRcsCard }
1370
+ : {}),
1300
1371
  };
1372
+ // Library / campaign consumers round-trip templateData via getTemplateData; include SMS fallback
1373
+ // so reopening the editor restores fallback text and tag mappings.
1374
+ // cap-campaigns-v2 API expects `smsFallBackContent.message` (see normalizeRcsMessageContentForApi).
1375
+ if (hasMeaningfulRcsSmsFallback(smsFallBackContent)) {
1376
+ const smsText =
1377
+ smsFallBackContent.message
1378
+ ?? smsFallBackContent.smsContent
1379
+ ?? smsFallBackContent.smsTemplateContent
1380
+ ?? '';
1381
+ templateData.smsFallBackContent = {
1382
+ ...smsFallBackContent,
1383
+ ...(String(smsText).trim() !== ''
1384
+ ? { message: String(smsText).trim() }
1385
+ : {}),
1386
+ };
1387
+ }
1388
+ normalizeRcsMessageContentForApi(templateData);
1301
1389
  }
1390
+ }
1302
1391
  break;
1303
- case constants.ZALO:
1392
+ case constants.ZALO: {
1304
1393
  if (template.value) {
1305
1394
  templateData = {
1306
1395
  ...template.value,
@@ -1309,6 +1398,7 @@ export class Creatives extends React.Component {
1309
1398
  delete templateData.type;
1310
1399
  }
1311
1400
  }
1401
+ }
1312
1402
  break;
1313
1403
  case constants.WEBPUSH: {
1314
1404
  if (template.value) {
@@ -1415,7 +1505,10 @@ export class Creatives extends React.Component {
1415
1505
  return templateData;
1416
1506
  };
1417
1507
 
1418
- getSlideBoxContent({ mode, templateData, isFullMode }) {
1508
+ getSlideBoxContent({ mode, templateData, isFullMode, useLocalTemplates }) {
1509
+ if (useLocalTemplates && mode === 'create' && isEmpty(templateData)) {
1510
+ return 'templates';
1511
+ }
1419
1512
  let creativesMode = isFullMode ? 'createTemplate' : 'templates';// for library mode templates page is initial mode and for full mode createTemplates
1420
1513
  if (mode === 'create' && isFullMode) {
1421
1514
  creativesMode = 'createTemplate';
@@ -1503,24 +1596,110 @@ export class Creatives extends React.Component {
1503
1596
  getFormData = (template) => {
1504
1597
  // Always reset isGetFormData so the child does not re-send form data on every re-render
1505
1598
  // (e.g. when user fixes validation error by typing, we must not auto-close the slidebox)
1506
- this.setState({ isGetFormData: false });
1507
- if (template.validity) {
1508
- this.setState(
1509
- {},
1510
- () => {
1511
- const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1512
- const channel = templateData.type;
1513
- const creativesData = this.getCreativesData(channel, template, templateData);// convers data to consumer understandable format
1514
- creativesData.then((data) => {
1515
- this.logGTMEvent(channel, data);
1516
- this.processCentralCommsMetaId(channel, data);
1599
+ this.setState(
1600
+ (prevState) => {
1601
+ const next = { isGetFormData: false };
1602
+ if (!template.validity) {
1603
+ return next;
1604
+ }
1605
+ const baseTd = prevState.templateData || template;
1606
+ const channel = (
1607
+ baseTd?.type
1608
+ || template?.type
1609
+ || get(template, 'value.type')
1610
+ || ''
1611
+ ).toUpperCase();
1612
+ // Library mode: persist last submitted creatives shape so reopening still hydrates the editor
1613
+ // (parent may not merge getCreativesData back into templateData).
1614
+ if (this.props.isFullMode === false && template.value) {
1615
+ if (channel === constants.RCS) {
1616
+ const smsFallBackFromPayload = get(
1617
+ template.value,
1618
+ 'versions.base.content.RCS.smsFallBackContent',
1619
+ );
1620
+ const rcsCardVarMappedFromPayload = get(
1621
+ template.value,
1622
+ 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped',
1623
+ );
1624
+ next.templateData = {
1625
+ ...baseTd,
1626
+ type: constants.RCS,
1627
+ name: template?.value?.name,
1628
+ versions: template?.value?.versions,
1629
+ ...(smsFallBackFromPayload != null
1630
+ && typeof smsFallBackFromPayload === 'object'
1631
+ && Object.keys(smsFallBackFromPayload).length > 0
1632
+ ? { smsFallBackContent: smsFallBackFromPayload }
1633
+ : {}),
1634
+ ...(rcsCardVarMappedFromPayload != null
1635
+ && typeof rcsCardVarMappedFromPayload === 'object'
1636
+ ? { rcsCardVarMapped: rcsCardVarMappedFromPayload }
1637
+ : {}),
1638
+ };
1639
+ if (template._id) {
1640
+ next.templateData._id = template._id;
1641
+ }
1642
+ } else if (channel === constants.SMS) {
1643
+ const submittedSmsTemplateValue = template?.value;
1644
+ const smsVersions =
1645
+ submittedSmsTemplateValue?.history != null
1646
+ ? submittedSmsTemplateValue
1647
+ : {
1648
+ base: submittedSmsTemplateValue?.base,
1649
+ history: submittedSmsTemplateValue?.base
1650
+ ? [submittedSmsTemplateValue.base]
1651
+ : [],
1652
+ };
1653
+ next.templateData = {
1654
+ ...baseTd,
1655
+ type: constants.SMS,
1656
+ name: baseTd?.name || 'Campaign message SMS content',
1657
+ versions: smsVersions,
1658
+ };
1659
+ if (template?._id) {
1660
+ next.templateData._id = template._id;
1661
+ }
1662
+ }
1663
+ }
1664
+ return next;
1665
+ },
1666
+ () => {
1667
+ if (!template.validity) {
1668
+ return;
1669
+ }
1670
+ const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1671
+ const channelForConsumer = String(
1672
+ templateData.type
1673
+ || template.type
1674
+ || get(template, 'value.type')
1675
+ || '',
1676
+ ).toUpperCase();
1677
+ const creativesData = this.getCreativesData(
1678
+ channelForConsumer,
1679
+ template,
1680
+ this.state.templateData || template,
1681
+ );// convers data to consumer understandable format
1682
+ creativesData.then((data) => {
1683
+ this.logGTMEvent(channelForConsumer, data);
1684
+ this.processCentralCommsMetaId(channelForConsumer, data, {
1685
+ closeSlideBoxAfterSubmit: template.closeSlideBoxAfterSubmit,
1517
1686
  });
1518
- },
1519
- );
1520
- }
1687
+ });
1688
+ },
1689
+ );
1521
1690
  }
1522
1691
 
1523
- processCentralCommsMetaId = (channel, creativesData) => {
1692
+ processCentralCommsMetaId = (channel, creativesData, options = {}) => {
1693
+ const { closeSlideBoxAfterSubmit = false } = options;
1694
+ const maybeCloseLibrarySlideBox = () => {
1695
+ if (
1696
+ closeSlideBoxAfterSubmit
1697
+ && this.props.isFullMode === false
1698
+ && typeof this.handleCloseSlideBox === 'function'
1699
+ ) {
1700
+ this.handleCloseSlideBox();
1701
+ }
1702
+ };
1524
1703
  // Create the payload for the centralcommnsmetaId API call
1525
1704
  const { isLoyaltyModule = false, loyaltyMetaData = {} } = this.props;
1526
1705
  const { actionName, setMetaData = () => { } } = loyaltyMetaData;
@@ -1546,6 +1725,7 @@ export class Creatives extends React.Component {
1546
1725
  if (result?.status?.code === 200) {
1547
1726
  setMetaData(result);
1548
1727
  this.props.getCreativesData(creativesData);
1728
+ maybeCloseLibrarySlideBox();
1549
1729
  } else {
1550
1730
  CapNotification.error({ message: <FormattedMessage {...messages.somethingWentWrong} /> });
1551
1731
  }
@@ -1556,6 +1736,7 @@ export class Creatives extends React.Component {
1556
1736
  } else {
1557
1737
  // If not a loyalty module or different action, should work as usual
1558
1738
  this.props.getCreativesData(creativesData);
1739
+ maybeCloseLibrarySlideBox();
1559
1740
  }
1560
1741
  };
1561
1742
 
@@ -1588,7 +1769,9 @@ export class Creatives extends React.Component {
1588
1769
  }
1589
1770
  this.setState((prevState) => ({
1590
1771
  ...prevState,
1591
- templateData: undefined,
1772
+ // Library mode (isFullMode === false): retain last template so reopening still has RCS payload.
1773
+ // Undefined isFullMode defaults to full-mode close behavior (clear templateData).
1774
+ ...(this.props.isFullMode !== false ? { templateData: undefined } : {}),
1592
1775
  showSlideBox: false,
1593
1776
  liquidErrorMessage: { STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] },
1594
1777
  isLiquidValidationError: false,
@@ -1799,21 +1982,12 @@ export class Creatives extends React.Component {
1799
1982
  }
1800
1983
 
1801
1984
  showLiquidErrorInFooter = (errorMessagesFromFormBuilder, currentFormBuilderTab) => {
1802
- const liquidMsgs = get(errorMessagesFromFormBuilder, constants.LIQUID_ERROR_MSG, []);
1803
- const standardMsgs = get(errorMessagesFromFormBuilder, constants.STANDARD_ERROR_MSG, []);
1804
- const hasLiquid = !isDeepEmpty(liquidMsgs);
1805
- const hasStandard = !isDeepEmpty(standardMsgs);
1806
- const isLiquidValidationError = hasLiquid || hasStandard;
1807
- // Don't overwrite existing liquid error with empty only for Mobile Push OLD (FormBuilder/clear calls empty there); SMS/others clear on input change
1808
- const isMobilePush = this.state.currentChannel?.toUpperCase() === constants.MOBILE_PUSH;
1809
- if (!hasLiquid && !hasStandard && this.state.isLiquidValidationError && isMobilePush) {
1810
- return;
1811
- }
1812
- this.setState({
1813
- isLiquidValidationError,
1814
- liquidErrorMessage: errorMessagesFromFormBuilder,
1815
- activeFormBuilderTab: currentFormBuilderTab === 1 ? constants.ANDROID : (currentFormBuilderTab === 2 ? constants.IOS : null), // Update activeFormBuilderTab, default to 1 if undefined
1985
+ const next = computeLiquidFooterUpdateFromFormBuilder(errorMessagesFromFormBuilder, currentFormBuilderTab, {
1986
+ previousIsLiquidValidationError: this.state.isLiquidValidationError,
1987
+ currentChannelUpper: this.state.currentChannel?.toUpperCase(),
1816
1988
  });
1989
+ if (next == null) return;
1990
+ this.setState(next);
1817
1991
  }
1818
1992
 
1819
1993
  // Callback to update HTML Editor validation state (called from EmailWrapper)
@@ -1936,6 +2110,11 @@ export class Creatives extends React.Component {
1936
2110
  inAppEditorType,
1937
2111
  htmlEditorValidationState,
1938
2112
  } = this.state;
2113
+ const useLocalTemplates = get(
2114
+ this.props,
2115
+ 'localTemplatesConfig.useLocalTemplates',
2116
+ get(this.props, 'useLocalTemplates', false),
2117
+ );
1939
2118
  const {
1940
2119
  isFullMode,
1941
2120
  creativesMode,
@@ -1954,7 +2133,6 @@ export class Creatives extends React.Component {
1954
2133
  smsRegister,
1955
2134
  enableNewChannels,
1956
2135
  eventContextTags,
1957
- waitEventContextTags = {},
1958
2136
  isLoyaltyModule,
1959
2137
  loyaltyMetaData = {},
1960
2138
  } = this.props;
@@ -1988,14 +2166,7 @@ export class Creatives extends React.Component {
1988
2166
  // IMPORTANT: Never show ErrorInfoNote in footer when in HTML Editor mode, even if liquidErrorMessage exists
1989
2167
  const shouldShowErrorInfoNoteInFooter = isHTMLEditorMode ? false : hasBEEEditorErrors;
1990
2168
 
1991
- // Calculate margin for header/content (always apply if there are errors, regardless of editor type)
1992
- const slideBoxWrapperMargin = (get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0 && get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0)
1993
- ? CAP_SPACE_64
1994
- : get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0
1995
- ? CAP_SPACE_56
1996
- : get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0
1997
- ? CAP_SPACE_32
1998
- : 0;
2169
+ const slideBoxWrapperMargin = getSlideBoxWrapperMarginFromLiquidErrors(liquidErrorMessage);
1999
2170
  /* TODO: Instead of passing down same props separately to each component down, write common function to these props and pass it accordingly */
2000
2171
 
2001
2172
  // Compute anonymous user type and channel restrictions
@@ -2024,7 +2195,10 @@ export class Creatives extends React.Component {
2024
2195
  <SlideBoxWrapper
2025
2196
  slideBoxWrapperMargin={slideBoxWrapperMargin}
2026
2197
  shouldApplyFooterMargin={shouldShowErrorInfoNoteInFooter}
2027
- className={classnames(`${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`)}
2198
+ className={classnames(
2199
+ `${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`,
2200
+ useLocalTemplates && slidBoxContent === 'templates' && 'creatives-slidebox--local-sms-templates',
2201
+ )}
2028
2202
  >
2029
2203
  <CapSlideBox
2030
2204
  header={
@@ -2049,12 +2223,13 @@ export class Creatives extends React.Component {
2049
2223
  smsRegister={smsRegister}
2050
2224
  handleClose={this.handleCloseSlideBox}
2051
2225
  moduleType={this.props.messageDetails?.type}
2226
+ useLocalTemplates={useLocalTemplates}
2052
2227
  />
2053
2228
  )}
2054
2229
  content={(
2055
2230
  <SlideBoxContent
2056
2231
  key="creatives-container-slidebox-content"
2057
- onSelectTemplate={this.onSelectTemplate}
2232
+ onSelectTemplate={this.props.onSelectTemplate != null ? this.props.onSelectTemplate : this.onSelectTemplate}
2058
2233
  onCreateComplete={getCreativesData}
2059
2234
  onPreviewTemplate={this.onPreviewTemplate}
2060
2235
  slidBoxContent={slidBoxContent}
@@ -2122,7 +2297,6 @@ export class Creatives extends React.Component {
2122
2297
  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.
2123
2298
  hostName={this.props?.hostName || ''}
2124
2299
  eventContextTags={eventContextTags}
2125
- waitEventContextTags={waitEventContextTags}
2126
2300
  isLoyaltyModule={isLoyaltyModule}
2127
2301
  loyaltyMetaData={loyaltyMetaData}
2128
2302
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
@@ -2131,7 +2305,8 @@ export class Creatives extends React.Component {
2131
2305
  isTestAndPreviewMode={(() => this.state.isTestAndPreviewMode)()}
2132
2306
  onHtmlEditorValidationStateChange={this.updateHtmlEditorValidationState}
2133
2307
  onPersonalizationTokensChange={this.handlePersonalizationTokensChange}
2134
- />
2308
+ localTemplatesConfig={pick(this.props.localTemplatesConfig || this.props, constants.LOCAL_TEMPLATE_CONFIG_KEYS)}
2309
+ />
2135
2310
  )}
2136
2311
  footer={this.shouldShowFooter() ? (
2137
2312
  <SlideBoxFooter
@@ -2140,7 +2315,6 @@ export class Creatives extends React.Component {
2140
2315
  onSave={this.saveMessage}
2141
2316
  onDiscard={this.discardMessage}
2142
2317
  onEditTemplate={this.onEditTemplate}
2143
- isTemplateArchived={!!(this.props.templateData && this.props.templateData.isArchived)}
2144
2318
  slidBoxContent={slidBoxContent}
2145
2319
  onCreateNextStep={this.onCreateNextStep}
2146
2320
  currentChannel={currentChannel.toUpperCase()}
@@ -2216,13 +2390,31 @@ Creatives.propTypes = {
2216
2390
  orgUnitId: PropTypes.number,
2217
2391
  hostName: PropTypes.string,
2218
2392
  eventContextTags: PropTypes.array,
2219
- waitEventContextTags: PropTypes.object,
2220
2393
  loyaltyTagFetchingDependencies: PropTypes.object,
2221
2394
  customerType: PropTypes.string,
2222
2395
  intl: PropTypes.shape({
2223
2396
  formatMessage: PropTypes.func,
2224
2397
  }),
2225
2398
  stopValidation: PropTypes.func,
2399
+ // Local template list (e.g. for SMS fallback): when set, TemplatesV2 uses these instead of Redux.
2400
+ // All optional. Pass either localTemplatesConfig (object) or individual props below.
2401
+ localTemplatesConfig: PropTypes.shape({
2402
+ useLocalTemplates: PropTypes.bool,
2403
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2404
+ localTemplatesLoading: PropTypes.bool,
2405
+ localTemplatesFilterContent: PropTypes.node,
2406
+ localTemplatesSentinelContent: PropTypes.node,
2407
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2408
+ localTemplatesUseSkeleton: PropTypes.bool,
2409
+ }),
2410
+ useLocalTemplates: PropTypes.bool,
2411
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2412
+ localTemplatesLoading: PropTypes.bool,
2413
+ localTemplatesFilterContent: PropTypes.node,
2414
+ localTemplatesSentinelContent: PropTypes.node,
2415
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2416
+ localTemplatesUseSkeleton: PropTypes.bool,
2417
+ onSelectTemplate: PropTypes.func,
2226
2418
  };
2227
2419
  const mapStatesToProps = () => createStructuredSelector({
2228
2420
  isLoading: isLoadingSelector(),