@capillarytech/creatives-library 8.0.353-alpha.5 → 8.0.353-alpha.6

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 (136) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/tests/api.test.js +35 -20
  4. package/utils/commonUtils.js +19 -1
  5. package/utils/rcsPayloadUtils.js +92 -0
  6. package/utils/templateVarUtils.js +201 -0
  7. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  8. package/utils/tests/templateVarUtils.test.js +204 -0
  9. package/v2Components/CapActionButton/constants.js +7 -0
  10. package/v2Components/CapActionButton/index.js +166 -108
  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/CapTagList/index.js +10 -0
  15. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +72 -49
  16. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +213 -21
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  22. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  23. package/v2Components/CommonTestAndPreview/UnifiedPreview/PreviewHeader.js +0 -17
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +157 -15
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +346 -146
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +138 -48
  27. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  28. package/v2Components/CommonTestAndPreview/constants.js +38 -4
  29. package/v2Components/CommonTestAndPreview/index.js +691 -235
  30. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  31. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  32. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  33. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  34. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  35. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  37. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  38. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  39. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/PreviewHeader.test.js +0 -159
  40. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -256
  42. package/v2Components/CommonTestAndPreview/tests/constants.test.js +1 -2
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -198
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +36 -26
  46. package/v2Components/FormBuilder/index.js +11 -6
  47. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  48. package/v2Components/SmsFallback/constants.js +73 -0
  49. package/v2Components/SmsFallback/index.js +956 -0
  50. package/v2Components/SmsFallback/index.scss +265 -0
  51. package/v2Components/SmsFallback/messages.js +78 -0
  52. package/v2Components/SmsFallback/smsFallbackUtils.js +119 -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 +223 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -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 +38 -23
  61. package/v2Components/TemplatePreview/constants.js +2 -0
  62. package/v2Components/TemplatePreview/index.js +143 -31
  63. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  64. package/v2Components/TestAndPreviewSlidebox/index.js +15 -3
  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/App/constants.js +0 -3
  71. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  72. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  73. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  74. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  75. package/v2Containers/CreativesContainer/constants.js +9 -0
  76. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  77. package/v2Containers/CreativesContainer/index.js +322 -103
  78. package/v2Containers/CreativesContainer/index.scss +51 -1
  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 -15
  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/MobilePush/Create/test/saga.test.js +2 -2
  89. package/v2Containers/Rcs/constants.js +119 -10
  90. package/v2Containers/Rcs/index.js +2445 -813
  91. package/v2Containers/Rcs/index.scss +280 -8
  92. package/v2Containers/Rcs/messages.js +34 -3
  93. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  94. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  95. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  96. package/v2Containers/Rcs/tests/index.test.js +152 -121
  97. package/v2Containers/Rcs/tests/mockData.js +38 -0
  98. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  99. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  100. package/v2Containers/Rcs/utils.js +478 -11
  101. package/v2Containers/Sms/Create/index.js +106 -40
  102. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  103. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  104. package/v2Containers/SmsTrai/Create/index.js +9 -4
  105. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  106. package/v2Containers/SmsTrai/Edit/index.js +640 -130
  107. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  108. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  109. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  110. package/v2Containers/SmsWrapper/index.js +37 -8
  111. package/v2Containers/TagList/index.js +6 -0
  112. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  113. package/v2Containers/Templates/_templates.scss +166 -9
  114. package/v2Containers/Templates/actions.js +11 -0
  115. package/v2Containers/Templates/constants.js +2 -0
  116. package/v2Containers/Templates/index.js +122 -120
  117. package/v2Containers/Templates/sagas.js +56 -12
  118. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  119. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1062 -1017
  120. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  121. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  122. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  123. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  124. package/v2Containers/TemplatesV2/index.js +86 -23
  125. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  126. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  127. package/v2Containers/WebPush/Create/index.js +8 -91
  128. package/v2Containers/WebPush/Create/index.scss +0 -7
  129. package/v2Containers/Whatsapp/index.js +3 -20
  130. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  131. package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +0 -169
  132. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WebPushPreviewContent.test.js +0 -522
  133. package/v2Containers/App/tests/constants.test.js +0 -61
  134. package/v2Containers/Templates/tests/webpush.test.js +0 -375
  135. package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +0 -338
  136. package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +0 -325
@@ -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();
@@ -768,15 +751,69 @@ export class Creatives extends React.Component {
768
751
  smsFallBackContent = {},
769
752
  creativeName = "",
770
753
  channel = constants.RCS,
754
+ rcsCardVarMapped,
771
755
  accountId = "",
772
756
  } = templateData || {};
773
- const cardContent = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
757
+ const { isFullMode: isFullModeForRcsPayload } = this.props;
758
+ const isCarouselRcs = (rcsContent?.cardType || '').toString().toLowerCase() === 'carousel';
759
+ const firstCardIn = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
760
+ const {
761
+ cardDisplayTitle: _omitDispTitleIn,
762
+ cardDisplayDescription: _omitDispDescIn,
763
+ ...cardContent
764
+ } = firstCardIn;
774
765
  const Status = RCS_STATUSES.approved || '';
766
+ const mergedCardVarMapped = (() => {
767
+ const nestedCardVarMapped = cardContent?.cardVarMapped;
768
+ const rootMirrorCardVarMapped = rcsCardVarMapped;
769
+ const nestedRecord =
770
+ nestedCardVarMapped != null && typeof nestedCardVarMapped === 'object'
771
+ ? nestedCardVarMapped
772
+ : {};
773
+ const rootRecord =
774
+ rootMirrorCardVarMapped != null && typeof rootMirrorCardVarMapped === 'object'
775
+ ? rootMirrorCardVarMapped
776
+ : {};
777
+ const mergedFromRootAndNested = {
778
+ ...pickRcsCardVarMappedEntries(rootRecord),
779
+ ...pickRcsCardVarMappedEntries(nestedRecord),
780
+ };
781
+ return Object.keys(mergedFromRootAndNested).length > 0
782
+ ? mergedFromRootAndNested
783
+ : null;
784
+ })();
785
+ // Campaigns (embedded): do not duplicate `cardVarMapped` as root `rcsCardVarMapped` on send —
786
+ // slot map stays on `versions…cardContent[0].cardVarMapped` only. Library full mode keeps root mirror.
787
+ // Use `=== true` so omitted/undefined `isFullMode` does not behave like library (avoids duplicate on approval payload).
788
+ const includeRootRcsCardVarMapped =
789
+ mergedCardVarMapped && isFullModeForRcsPayload === true;
790
+
791
+ const builtCardContent = isCarouselRcs
792
+ ? (rcsContent.cardContent || []).map((card, idx) => {
793
+ const {
794
+ cardDisplayTitle: _dt,
795
+ cardDisplayDescription: _dd,
796
+ ...restCard
797
+ } = card || {};
798
+ return {
799
+ ...restCard,
800
+ ...(idx === 0 && mergedCardVarMapped ? { cardVarMapped: mergedCardVarMapped } : {}),
801
+ Status,
802
+ };
803
+ })
804
+ : [
805
+ {
806
+ ...cardContent,
807
+ ...(mergedCardVarMapped ? { cardVarMapped: mergedCardVarMapped } : {}),
808
+ Status,
809
+ },
810
+ ];
775
811
 
776
812
  creativesTemplateData = {
777
813
  type: channel,
778
814
  edit: true,
779
815
  name: creativeName,
816
+ ...(includeRootRcsCardVarMapped ? { rcsCardVarMapped: mergedCardVarMapped } : {}),
780
817
  versions: {
781
818
  base: {
782
819
  content: {
@@ -784,12 +821,7 @@ export class Creatives extends React.Component {
784
821
  rcsContent: {
785
822
  ...rcsContent,
786
823
  ...(accountId && !isFullMode && { accountId }),
787
- cardContent: [
788
- {
789
- ...cardContent,
790
- Status,
791
- },
792
- ],
824
+ cardContent: builtCardContent,
793
825
  },
794
826
  smsFallBackContent,
795
827
  },
@@ -937,7 +969,10 @@ export class Creatives extends React.Component {
937
969
  return newExpandableDetails;
938
970
  }
939
971
 
940
- getCreativesData = async (channel, template, templateRecords) => { //from creatives to consumers
972
+ getCreativesData = async (channelParam, template, templateRecords) => { //from creatives to consumers
973
+ const channel = String(
974
+ channelParam || template?.type || get(template, 'value.type') || '',
975
+ ).toUpperCase();
941
976
  let templateData = { channel };
942
977
  switch (channel) {
943
978
  case constants.SMS:
@@ -1284,28 +1319,104 @@ export class Creatives extends React.Component {
1284
1319
  break;
1285
1320
  case constants.RCS:
1286
1321
  if (template.value) {
1287
- const { name = "", versions = {} } = {
1288
- } = template.value || {};
1289
- const smsFallBackContent = get(versions, 'base.content.RCS.smsFallBackContent', {});
1322
+ const { isFullMode: isFullModeForRcsConsumerPayload } = this.props;
1323
+ const { name = "", versions = {} } = template.value || {};
1324
+ const fromSubmit = get(versions, 'base.content.RCS.smsFallBackContent', {});
1325
+ const fromRecords = {
1326
+ ...(templateRecords?.smsFallBackContent || {}),
1327
+ ...(get(templateRecords, 'versions.base.content.RCS.smsFallBackContent') || {}),
1328
+ };
1329
+ const hasMeaningfulRcsSmsFallback = (smsFallbackPayload) => {
1330
+ if (
1331
+ !smsFallbackPayload
1332
+ || typeof smsFallbackPayload !== 'object'
1333
+ || Object.keys(smsFallbackPayload).length === 0
1334
+ ) {
1335
+ return false;
1336
+ }
1337
+ const fallbackBodyText = String(
1338
+ smsFallbackPayload.smsContent
1339
+ ?? smsFallbackPayload.smsTemplateContent
1340
+ ?? smsFallbackPayload.message
1341
+ ?? smsFallbackPayload.content
1342
+ ?? '',
1343
+ ).trim();
1344
+ const fallbackTemplateName = String(
1345
+ smsFallbackPayload.smsTemplateName ?? smsFallbackPayload.templateName ?? '',
1346
+ ).trim();
1347
+ const rcsSmsFallbackVarMapped =
1348
+ smsFallbackPayload?.[RCS_SMS_FALLBACK_VAR_MAPPED_PROP];
1349
+ const hasVarMappedEntries =
1350
+ rcsSmsFallbackVarMapped != null
1351
+ && typeof rcsSmsFallbackVarMapped === 'object'
1352
+ && Object.keys(rcsSmsFallbackVarMapped).length > 0;
1353
+ return (
1354
+ fallbackBodyText !== ''
1355
+ || fallbackTemplateName !== ''
1356
+ || hasVarMappedEntries
1357
+ );
1358
+ };
1359
+ // If submit has only empty strings, do not let it wipe fallback mirrored on templateRecords (library round-trip).
1360
+ const smsFallBackContent = hasMeaningfulRcsSmsFallback(fromSubmit)
1361
+ ? { ...fromRecords, ...fromSubmit }
1362
+ : { ...fromSubmit, ...fromRecords };
1290
1363
  const {
1291
- cardContent = [],
1364
+ cardContent: cardContentFromSubmit = [],
1292
1365
  contentType = "",
1293
1366
  cardType = "",
1294
1367
  cardSettings = {},
1295
1368
  accountId = "",
1296
1369
  } = get(versions, 'base.content.RCS.rcsContent', {});
1370
+ const rootRcsCardVarMappedFromSubmit = get(template, 'value.rcsCardVarMapped');
1371
+ const firstCardFromSubmit = Array.isArray(cardContentFromSubmit)
1372
+ ? cardContentFromSubmit[0]
1373
+ : null;
1374
+ const cardContent = mapRcsCardContentForConsumerWithResolvedTags(
1375
+ cardContentFromSubmit,
1376
+ rootRcsCardVarMappedFromSubmit,
1377
+ isFullModeForRcsConsumerPayload,
1378
+ );
1297
1379
  const rcsContent = {
1298
1380
  contentType,
1299
1381
  cardType,
1300
1382
  cardSettings,
1301
1383
  cardContent,
1302
1384
  };
1385
+ const cardVarMappedFromFirstRcsCard =
1386
+ firstCardFromSubmit?.cardVarMapped != null
1387
+ && typeof firstCardFromSubmit.cardVarMapped === 'object'
1388
+ ? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
1389
+ : null;
1390
+ const includeRootRcsCardVarMappedOnConsumerPayload =
1391
+ cardVarMappedFromFirstRcsCard
1392
+ && Object.keys(cardVarMappedFromFirstRcsCard).length > 0
1393
+ && isFullModeForRcsConsumerPayload === true;
1303
1394
  templateData = {
1304
1395
  channel,
1305
1396
  creativeName: name,
1306
1397
  rcsContent,
1307
1398
  accountId,
1399
+ ...(includeRootRcsCardVarMappedOnConsumerPayload
1400
+ ? { rcsCardVarMapped: cardVarMappedFromFirstRcsCard }
1401
+ : {}),
1308
1402
  };
1403
+ // Library / campaign consumers round-trip templateData via getTemplateData; include SMS fallback
1404
+ // so reopening the editor restores fallback text and tag mappings.
1405
+ // cap-campaigns-v2 API expects `smsFallBackContent.message` (see normalizeRcsMessageContentForApi).
1406
+ if (hasMeaningfulRcsSmsFallback(smsFallBackContent)) {
1407
+ const smsText =
1408
+ smsFallBackContent.message
1409
+ ?? smsFallBackContent.smsContent
1410
+ ?? smsFallBackContent.smsTemplateContent
1411
+ ?? '';
1412
+ templateData.smsFallBackContent = {
1413
+ ...smsFallBackContent,
1414
+ ...(String(smsText).trim() !== ''
1415
+ ? { message: String(smsText).trim() }
1416
+ : {}),
1417
+ };
1418
+ }
1419
+ normalizeRcsMessageContentForApi(templateData);
1309
1420
  }
1310
1421
  break;
1311
1422
  case constants.ZALO:
@@ -1423,7 +1534,10 @@ export class Creatives extends React.Component {
1423
1534
  return templateData;
1424
1535
  };
1425
1536
 
1426
- getSlideBoxContent({ mode, templateData, isFullMode }) {
1537
+ getSlideBoxContent({ mode, templateData, isFullMode, useLocalTemplates }) {
1538
+ if (useLocalTemplates && mode === 'create' && isEmpty(templateData)) {
1539
+ return 'templates';
1540
+ }
1427
1541
  let creativesMode = isFullMode ? 'createTemplate' : 'templates';// for library mode templates page is initial mode and for full mode createTemplates
1428
1542
  if (mode === 'create' && isFullMode) {
1429
1543
  creativesMode = 'createTemplate';
@@ -1511,24 +1625,110 @@ export class Creatives extends React.Component {
1511
1625
  getFormData = (template) => {
1512
1626
  // Always reset isGetFormData so the child does not re-send form data on every re-render
1513
1627
  // (e.g. when user fixes validation error by typing, we must not auto-close the slidebox)
1514
- this.setState({ isGetFormData: false });
1515
- if (template.validity) {
1516
- this.setState(
1517
- {},
1518
- () => {
1519
- const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1520
- const channel = templateData.type;
1521
- const creativesData = this.getCreativesData(channel, template, templateData);// convers data to consumer understandable format
1522
- creativesData.then((data) => {
1523
- this.logGTMEvent(channel, data);
1524
- this.processCentralCommsMetaId(channel, data);
1628
+ this.setState(
1629
+ (prevState) => {
1630
+ const next = { isGetFormData: false };
1631
+ if (!template.validity) {
1632
+ return next;
1633
+ }
1634
+ const baseTd = prevState.templateData || template;
1635
+ const channel = (
1636
+ baseTd?.type
1637
+ || template?.type
1638
+ || get(template, 'value.type')
1639
+ || ''
1640
+ ).toUpperCase();
1641
+ // Library mode: persist last submitted creatives shape so reopening still hydrates the editor
1642
+ // (parent may not merge getCreativesData back into templateData).
1643
+ if (this.props.isFullMode === false && template.value) {
1644
+ if (channel === constants.RCS) {
1645
+ const smsFallBackFromPayload = get(
1646
+ template.value,
1647
+ 'versions.base.content.RCS.smsFallBackContent',
1648
+ );
1649
+ const rcsCardVarMappedFromPayload = get(
1650
+ template.value,
1651
+ 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped',
1652
+ );
1653
+ next.templateData = {
1654
+ ...baseTd,
1655
+ type: constants.RCS,
1656
+ name: template?.value?.name,
1657
+ versions: template?.value?.versions,
1658
+ ...(smsFallBackFromPayload != null
1659
+ && typeof smsFallBackFromPayload === 'object'
1660
+ && Object.keys(smsFallBackFromPayload).length > 0
1661
+ ? { smsFallBackContent: smsFallBackFromPayload }
1662
+ : {}),
1663
+ ...(rcsCardVarMappedFromPayload != null
1664
+ && typeof rcsCardVarMappedFromPayload === 'object'
1665
+ ? { rcsCardVarMapped: rcsCardVarMappedFromPayload }
1666
+ : {}),
1667
+ };
1668
+ if (template._id) {
1669
+ next.templateData._id = template._id;
1670
+ }
1671
+ } else if (channel === constants.SMS) {
1672
+ const submittedSmsTemplateValue = template?.value;
1673
+ const smsVersions =
1674
+ submittedSmsTemplateValue?.history != null
1675
+ ? submittedSmsTemplateValue
1676
+ : {
1677
+ base: submittedSmsTemplateValue?.base,
1678
+ history: submittedSmsTemplateValue?.base
1679
+ ? [submittedSmsTemplateValue.base]
1680
+ : [],
1681
+ };
1682
+ next.templateData = {
1683
+ ...baseTd,
1684
+ type: constants.SMS,
1685
+ name: baseTd?.name || 'Campaign message SMS content',
1686
+ versions: smsVersions,
1687
+ };
1688
+ if (template?._id) {
1689
+ next.templateData._id = template._id;
1690
+ }
1691
+ }
1692
+ }
1693
+ return next;
1694
+ },
1695
+ () => {
1696
+ if (!template.validity) {
1697
+ return;
1698
+ }
1699
+ const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1700
+ const channelForConsumer = String(
1701
+ templateData.type
1702
+ || template.type
1703
+ || get(template, 'value.type')
1704
+ || '',
1705
+ ).toUpperCase();
1706
+ const creativesData = this.getCreativesData(
1707
+ channelForConsumer,
1708
+ template,
1709
+ this.state.templateData || template,
1710
+ );// convers data to consumer understandable format
1711
+ creativesData.then((data) => {
1712
+ this.logGTMEvent(channelForConsumer, data);
1713
+ this.processCentralCommsMetaId(channelForConsumer, data, {
1714
+ closeSlideBoxAfterSubmit: template.closeSlideBoxAfterSubmit,
1525
1715
  });
1526
- },
1527
- );
1528
- }
1716
+ });
1717
+ },
1718
+ );
1529
1719
  }
1530
1720
 
1531
- processCentralCommsMetaId = (channel, creativesData) => {
1721
+ processCentralCommsMetaId = (channel, creativesData, options = {}) => {
1722
+ const { closeSlideBoxAfterSubmit = false } = options;
1723
+ const maybeCloseLibrarySlideBox = () => {
1724
+ if (
1725
+ closeSlideBoxAfterSubmit
1726
+ && this.props.isFullMode === false
1727
+ && typeof this.handleCloseSlideBox === 'function'
1728
+ ) {
1729
+ this.handleCloseSlideBox();
1730
+ }
1731
+ };
1532
1732
  // Create the payload for the centralcommnsmetaId API call
1533
1733
  const { isLoyaltyModule = false, loyaltyMetaData = {} } = this.props;
1534
1734
  const { actionName, setMetaData = () => { } } = loyaltyMetaData;
@@ -1554,6 +1754,7 @@ export class Creatives extends React.Component {
1554
1754
  if (result?.status?.code === 200) {
1555
1755
  setMetaData(result);
1556
1756
  this.props.getCreativesData(creativesData);
1757
+ maybeCloseLibrarySlideBox();
1557
1758
  } else {
1558
1759
  CapNotification.error({ message: <FormattedMessage {...messages.somethingWentWrong} /> });
1559
1760
  }
@@ -1564,6 +1765,7 @@ export class Creatives extends React.Component {
1564
1765
  } else {
1565
1766
  // If not a loyalty module or different action, should work as usual
1566
1767
  this.props.getCreativesData(creativesData);
1768
+ maybeCloseLibrarySlideBox();
1567
1769
  }
1568
1770
  };
1569
1771
 
@@ -1596,7 +1798,9 @@ export class Creatives extends React.Component {
1596
1798
  }
1597
1799
  this.setState((prevState) => ({
1598
1800
  ...prevState,
1599
- templateData: undefined,
1801
+ // Library mode (isFullMode === false): retain last template so reopening still has RCS payload.
1802
+ // Undefined isFullMode defaults to full-mode close behavior (clear templateData).
1803
+ ...(this.props.isFullMode !== false ? { templateData: undefined } : {}),
1600
1804
  showSlideBox: false,
1601
1805
  liquidErrorMessage: { STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] },
1602
1806
  isLiquidValidationError: false,
@@ -1807,21 +2011,17 @@ export class Creatives extends React.Component {
1807
2011
  }
1808
2012
 
1809
2013
  showLiquidErrorInFooter = (errorMessagesFromFormBuilder, currentFormBuilderTab) => {
1810
- const liquidMsgs = get(errorMessagesFromFormBuilder, constants.LIQUID_ERROR_MSG, []);
1811
- const standardMsgs = get(errorMessagesFromFormBuilder, constants.STANDARD_ERROR_MSG, []);
1812
- const hasLiquid = !isDeepEmpty(liquidMsgs);
1813
- const hasStandard = !isDeepEmpty(standardMsgs);
1814
- const isLiquidValidationError = hasLiquid || hasStandard;
1815
- // Don't overwrite existing liquid error with empty only for Mobile Push OLD (FormBuilder/clear calls empty there); SMS/others clear on input change
1816
- const isMobilePush = this.state.currentChannel?.toUpperCase() === constants.MOBILE_PUSH;
1817
- if (!hasLiquid && !hasStandard && this.state.isLiquidValidationError && isMobilePush) {
1818
- return;
1819
- }
1820
- this.setState({
1821
- isLiquidValidationError,
1822
- liquidErrorMessage: errorMessagesFromFormBuilder,
1823
- activeFormBuilderTab: currentFormBuilderTab === 1 ? constants.ANDROID : (currentFormBuilderTab === 2 ? constants.IOS : null), // Update activeFormBuilderTab, default to 1 if undefined
1824
- });
2014
+ const next = computeLiquidFooterUpdateFromFormBuilder(
2015
+ errorMessagesFromFormBuilder,
2016
+ this.state.liquidErrorMessage,
2017
+ currentFormBuilderTab,
2018
+ {
2019
+ previousIsLiquidValidationError: this.state.isLiquidValidationError,
2020
+ currentChannelUpper: this.state.currentChannel?.toUpperCase(),
2021
+ },
2022
+ );
2023
+ if (next == null) return;
2024
+ this.setState(next);
1825
2025
  }
1826
2026
 
1827
2027
  // Callback to update HTML Editor validation state (called from EmailWrapper)
@@ -1944,6 +2144,11 @@ export class Creatives extends React.Component {
1944
2144
  inAppEditorType,
1945
2145
  htmlEditorValidationState,
1946
2146
  } = this.state;
2147
+ const useLocalTemplates = get(
2148
+ this.props,
2149
+ 'localTemplatesConfig.useLocalTemplates',
2150
+ get(this.props, 'useLocalTemplates', false),
2151
+ );
1947
2152
  const {
1948
2153
  isFullMode,
1949
2154
  creativesMode,
@@ -1962,7 +2167,6 @@ export class Creatives extends React.Component {
1962
2167
  smsRegister,
1963
2168
  enableNewChannels,
1964
2169
  eventContextTags,
1965
- waitEventContextTags = {},
1966
2170
  isLoyaltyModule,
1967
2171
  loyaltyMetaData = {},
1968
2172
  } = this.props;
@@ -1996,14 +2200,7 @@ export class Creatives extends React.Component {
1996
2200
  // IMPORTANT: Never show ErrorInfoNote in footer when in HTML Editor mode, even if liquidErrorMessage exists
1997
2201
  const shouldShowErrorInfoNoteInFooter = isHTMLEditorMode ? false : hasBEEEditorErrors;
1998
2202
 
1999
- // Calculate margin for header/content (always apply if there are errors, regardless of editor type)
2000
- const slideBoxWrapperMargin = (get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0 && get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0)
2001
- ? CAP_SPACE_64
2002
- : get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0
2003
- ? CAP_SPACE_56
2004
- : get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0
2005
- ? CAP_SPACE_32
2006
- : 0;
2203
+ const slideBoxWrapperMargin = getSlideBoxWrapperMarginFromLiquidErrors(liquidErrorMessage);
2007
2204
  /* TODO: Instead of passing down same props separately to each component down, write common function to these props and pass it accordingly */
2008
2205
 
2009
2206
  // Compute anonymous user type and channel restrictions
@@ -2032,7 +2229,10 @@ export class Creatives extends React.Component {
2032
2229
  <SlideBoxWrapper
2033
2230
  slideBoxWrapperMargin={slideBoxWrapperMargin}
2034
2231
  shouldApplyFooterMargin={shouldShowErrorInfoNoteInFooter}
2035
- className={classnames(`${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`)}
2232
+ className={classnames(
2233
+ `${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`,
2234
+ useLocalTemplates && slidBoxContent === 'templates' && 'creatives-slidebox--local-sms-templates',
2235
+ )}
2036
2236
  >
2037
2237
  <CapSlideBox
2038
2238
  header={
@@ -2057,12 +2257,13 @@ export class Creatives extends React.Component {
2057
2257
  smsRegister={smsRegister}
2058
2258
  handleClose={this.handleCloseSlideBox}
2059
2259
  moduleType={this.props.messageDetails?.type}
2260
+ useLocalTemplates={useLocalTemplates}
2060
2261
  />
2061
2262
  )}
2062
2263
  content={(
2063
2264
  <SlideBoxContent
2064
2265
  key="creatives-container-slidebox-content"
2065
- onSelectTemplate={this.onSelectTemplate}
2266
+ onSelectTemplate={this.props.onSelectTemplate != null ? this.props.onSelectTemplate : this.onSelectTemplate}
2066
2267
  onCreateComplete={getCreativesData}
2067
2268
  onPreviewTemplate={this.onPreviewTemplate}
2068
2269
  slidBoxContent={slidBoxContent}
@@ -2130,7 +2331,6 @@ export class Creatives extends React.Component {
2130
2331
  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.
2131
2332
  hostName={this.props?.hostName || ''}
2132
2333
  eventContextTags={eventContextTags}
2133
- waitEventContextTags={waitEventContextTags}
2134
2334
  isLoyaltyModule={isLoyaltyModule}
2135
2335
  loyaltyMetaData={loyaltyMetaData}
2136
2336
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
@@ -2139,7 +2339,8 @@ export class Creatives extends React.Component {
2139
2339
  isTestAndPreviewMode={(() => this.state.isTestAndPreviewMode)()}
2140
2340
  onHtmlEditorValidationStateChange={this.updateHtmlEditorValidationState}
2141
2341
  onPersonalizationTokensChange={this.handlePersonalizationTokensChange}
2142
- />
2342
+ localTemplatesConfig={pick(this.props.localTemplatesConfig || this.props, constants.LOCAL_TEMPLATE_CONFIG_KEYS)}
2343
+ />
2143
2344
  )}
2144
2345
  footer={this.shouldShowFooter() ? (
2145
2346
  <SlideBoxFooter
@@ -2224,13 +2425,31 @@ Creatives.propTypes = {
2224
2425
  orgUnitId: PropTypes.number,
2225
2426
  hostName: PropTypes.string,
2226
2427
  eventContextTags: PropTypes.array,
2227
- waitEventContextTags: PropTypes.object,
2228
2428
  loyaltyTagFetchingDependencies: PropTypes.object,
2229
2429
  customerType: PropTypes.string,
2230
2430
  intl: PropTypes.shape({
2231
2431
  formatMessage: PropTypes.func,
2232
2432
  }),
2233
2433
  stopValidation: PropTypes.func,
2434
+ // Local template list (e.g. for SMS fallback): when set, TemplatesV2 uses these instead of Redux.
2435
+ // All optional. Pass either localTemplatesConfig (object) or individual props below.
2436
+ localTemplatesConfig: PropTypes.shape({
2437
+ useLocalTemplates: PropTypes.bool,
2438
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2439
+ localTemplatesLoading: PropTypes.bool,
2440
+ localTemplatesFilterContent: PropTypes.node,
2441
+ localTemplatesSentinelContent: PropTypes.node,
2442
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2443
+ localTemplatesUseSkeleton: PropTypes.bool,
2444
+ }),
2445
+ useLocalTemplates: PropTypes.bool,
2446
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2447
+ localTemplatesLoading: PropTypes.bool,
2448
+ localTemplatesFilterContent: PropTypes.node,
2449
+ localTemplatesSentinelContent: PropTypes.node,
2450
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2451
+ localTemplatesUseSkeleton: PropTypes.bool,
2452
+ onSelectTemplate: PropTypes.func,
2234
2453
  };
2235
2454
  const mapStatesToProps = () => createStructuredSelector({
2236
2455
  isLoading: isLoadingSelector(),