@capillarytech/creatives-library 8.0.241 → 8.0.242-alpha.0

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 (119) hide show
  1. package/package.json +1 -1
  2. package/sagas/__tests__/assetPolling.test.js +607 -0
  3. package/sagas/assetPolling.js +156 -0
  4. package/services/api.js +16 -0
  5. package/services/tests/api.test.js +124 -0
  6. package/translations/en.json +1 -0
  7. package/utils/assetStatusConstants.js +12 -0
  8. package/utils/asyncAssetUpload.js +161 -0
  9. package/utils/tests/asyncAssetUpload.test.js +292 -0
  10. package/utils/transformerUtils.js +42 -0
  11. package/v2Components/CapImageUpload/constants.js +2 -0
  12. package/v2Components/CapImageUpload/index.js +54 -14
  13. package/v2Components/CapImageUpload/index.scss +4 -1
  14. package/v2Components/CapImageUpload/messages.js +4 -0
  15. package/v2Components/CapImageUrlUpload/constants.js +19 -0
  16. package/v2Components/CapImageUrlUpload/index.js +455 -0
  17. package/v2Components/CapImageUrlUpload/index.scss +35 -0
  18. package/v2Components/CapImageUrlUpload/messages.js +47 -0
  19. package/v2Containers/App/constants.js +5 -0
  20. package/v2Containers/Cap/tests/__snapshots__/index.test.js.snap +1 -0
  21. package/v2Containers/CreativesContainer/SlideBoxContent.js +57 -2
  22. package/v2Containers/CreativesContainer/SlideBoxHeader.js +1 -0
  23. package/v2Containers/CreativesContainer/constants.js +2 -0
  24. package/v2Containers/CreativesContainer/index.js +152 -0
  25. package/v2Containers/CreativesContainer/messages.js +4 -0
  26. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
  27. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
  28. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +25 -0
  29. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +18 -0
  30. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +46 -0
  31. package/v2Containers/SmsTrai/Create/tests/__snapshots__/index.test.js.snap +4 -0
  32. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +8 -0
  33. package/v2Containers/Templates/ChannelTypeIllustration.js +13 -1
  34. package/v2Containers/Templates/_templates.scss +203 -0
  35. package/v2Containers/Templates/actions.js +2 -1
  36. package/v2Containers/Templates/constants.js +1 -0
  37. package/v2Containers/Templates/index.js +273 -30
  38. package/v2Containers/Templates/messages.js +24 -0
  39. package/v2Containers/Templates/reducer.js +2 -0
  40. package/v2Containers/Templates/tests/index.test.js +10 -0
  41. package/v2Containers/TemplatesV2/index.js +3 -2
  42. package/v2Containers/TemplatesV2/messages.js +4 -0
  43. package/v2Containers/WebPush/Create/components/ButtonForm.js +175 -0
  44. package/v2Containers/WebPush/Create/components/ButtonItem.js +101 -0
  45. package/v2Containers/WebPush/Create/components/ButtonList.js +144 -0
  46. package/v2Containers/WebPush/Create/components/_buttons.scss +246 -0
  47. package/v2Containers/WebPush/Create/components/tests/ButtonForm.test.js +554 -0
  48. package/v2Containers/WebPush/Create/components/tests/ButtonItem.test.js +607 -0
  49. package/v2Containers/WebPush/Create/components/tests/ButtonList.test.js +633 -0
  50. package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonForm.test.js.snap +666 -0
  51. package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonItem.test.js.snap +74 -0
  52. package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonList.test.js.snap +80 -0
  53. package/v2Containers/WebPush/Create/index.js +1755 -0
  54. package/v2Containers/WebPush/Create/index.scss +123 -0
  55. package/v2Containers/WebPush/Create/messages.js +199 -0
  56. package/v2Containers/WebPush/Create/preview/DevicePreviewContent.js +241 -0
  57. package/v2Containers/WebPush/Create/preview/NotificationContainer.js +290 -0
  58. package/v2Containers/WebPush/Create/preview/PreviewContent.js +81 -0
  59. package/v2Containers/WebPush/Create/preview/PreviewControls.js +240 -0
  60. package/v2Containers/WebPush/Create/preview/PreviewDisclaimer.js +23 -0
  61. package/v2Containers/WebPush/Create/preview/WebPushPreview.js +144 -0
  62. package/v2Containers/WebPush/Create/preview/assets/Light.svg +53 -0
  63. package/v2Containers/WebPush/Create/preview/assets/Top.svg +5 -0
  64. package/v2Containers/WebPush/Create/preview/assets/chrome-icon.png +0 -0
  65. package/v2Containers/WebPush/Create/preview/assets/edge-icon.png +0 -0
  66. package/v2Containers/WebPush/Create/preview/assets/firefox-icon.svg +106 -0
  67. package/v2Containers/WebPush/Create/preview/assets/iOS.svg +26 -0
  68. package/v2Containers/WebPush/Create/preview/assets/opera-icon.svg +18 -0
  69. package/v2Containers/WebPush/Create/preview/assets/safari-icon.svg +29 -0
  70. package/v2Containers/WebPush/Create/preview/components/AndroidMobileChromeHeader.js +44 -0
  71. package/v2Containers/WebPush/Create/preview/components/AndroidMobileExpanded.js +110 -0
  72. package/v2Containers/WebPush/Create/preview/components/IOSHeader.js +45 -0
  73. package/v2Containers/WebPush/Create/preview/components/NotificationExpandedContent.js +72 -0
  74. package/v2Containers/WebPush/Create/preview/components/NotificationHeader.js +55 -0
  75. package/v2Containers/WebPush/Create/preview/components/WindowsChromeExpanded.js +70 -0
  76. package/v2Containers/WebPush/Create/preview/components/tests/AndroidMobileExpanded.test.js +512 -0
  77. package/v2Containers/WebPush/Create/preview/components/tests/__snapshots__/AndroidMobileExpanded.test.js.snap +77 -0
  78. package/v2Containers/WebPush/Create/preview/config/notificationMappings.js +527 -0
  79. package/v2Containers/WebPush/Create/preview/constants.js +162 -0
  80. package/v2Containers/WebPush/Create/preview/notification-container.scss +104 -0
  81. package/v2Containers/WebPush/Create/preview/preview.scss +409 -0
  82. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-chrome.scss +300 -0
  83. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-edge.scss +12 -0
  84. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-firefox.scss +12 -0
  85. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-opera.scss +12 -0
  86. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-chrome.scss +303 -0
  87. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-edge.scss +11 -0
  88. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-firefox.scss +11 -0
  89. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-opera.scss +11 -0
  90. package/v2Containers/WebPush/Create/preview/styles/_base.scss +188 -0
  91. package/v2Containers/WebPush/Create/preview/styles/_ios.scss +106 -0
  92. package/v2Containers/WebPush/Create/preview/styles/_ipados.scss +107 -0
  93. package/v2Containers/WebPush/Create/preview/styles/_macos-chrome.scss +75 -0
  94. package/v2Containers/WebPush/Create/preview/styles/_windows-chrome.scss +174 -0
  95. package/v2Containers/WebPush/Create/preview/tests/DevicePreviewContent.test.js +909 -0
  96. package/v2Containers/WebPush/Create/preview/tests/NotificationContainer.test.js +1077 -0
  97. package/v2Containers/WebPush/Create/preview/tests/PreviewControls.test.js +723 -0
  98. package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +943 -0
  99. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/DevicePreviewContent.test.js.snap +128 -0
  100. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/NotificationContainer.test.js.snap +121 -0
  101. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/PreviewControls.test.js.snap +144 -0
  102. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/WebPushPreview.test.js.snap +127 -0
  103. package/v2Containers/WebPush/Create/utils/urlValidation.js +116 -0
  104. package/v2Containers/WebPush/Create/utils/urlValidation.test.js +449 -0
  105. package/v2Containers/WebPush/actions.js +60 -0
  106. package/v2Containers/WebPush/constants.js +108 -0
  107. package/v2Containers/WebPush/index.js +2 -0
  108. package/v2Containers/WebPush/reducer.js +104 -0
  109. package/v2Containers/WebPush/sagas.js +119 -0
  110. package/v2Containers/WebPush/selectors.js +65 -0
  111. package/v2Containers/WebPush/tests/reducer.test.js +863 -0
  112. package/v2Containers/WebPush/tests/sagas.test.js +566 -0
  113. package/v2Containers/WebPush/tests/selectors.test.js +960 -0
  114. package/v2Containers/Whatsapp/constants.js +9 -0
  115. package/v2Containers/Whatsapp/reducer.js +34 -5
  116. package/v2Containers/Whatsapp/sagas.js +61 -10
  117. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +132 -0
  118. package/v2Containers/Whatsapp/tests/reducer.test.js +188 -0
  119. package/v2Containers/Whatsapp/tests/saga.test.js +420 -7
@@ -79,6 +79,7 @@ import * as whatsappActions from '../Whatsapp/actions';
79
79
  import * as rcsActions from '../Rcs/actions';
80
80
  import * as zaloActions from '../Zalo/actions';
81
81
  import * as inAppActions from '../InApp/actions';
82
+ import * as webpushActions from '../WebPush/actions';
82
83
  import * as globalActions from '../Cap/actions';
83
84
  import { makeSelectAuthenticated } from '../Cap/selectors';
84
85
  import { UserIsAuthenticated } from '../../utils/authWrapper';
@@ -135,7 +136,7 @@ import rcsMessages from '../Rcs/messages';
135
136
  import globalMessages from '../../v2Containers/Cap/messages';
136
137
  import { handlePreviewInNewTab } from '../../utils/common';
137
138
 
138
- import { MOBILE_PUSH, WECHAT, SMS, EMAIL, EBILL, LINE, VIBER, FACEBOOK, WHATSAPP, RCS, ZALO, INAPP } from '../CreativesContainer/constants';
139
+ import { MOBILE_PUSH, WECHAT, SMS, EMAIL, EBILL, LINE, VIBER, FACEBOOK, WHATSAPP, RCS, ZALO, INAPP, WEBPUSH } from '../CreativesContainer/constants';
139
140
 
140
141
  import {CREATIVE} from '../Facebook/constants';
141
142
  import videoPlay from '../../assets/videoPlay.svg';
@@ -153,10 +154,13 @@ import { DAEMON } from '@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes'
153
154
  import { Rcs } from '../Rcs';
154
155
  import { makeSelectRcs } from '../Rcs/selectors';
155
156
  import { getRcsStatusType } from '../Rcs/utils';
157
+ import { makeSelectWebPush } from '../WebPush/selectors';
156
158
  import { v2MobilePushSagas } from '../MobilePushNew/sagas';
157
159
  import { AUTO_CAROUSEL, BIG_PICTURE, FILMSTRIP_CAROUSEL, MANUAL_CAROUSEL } from '../MobilePushNew/constants';
158
160
  import CapPageSpinner from '../../v2Components/CapPageSpinner';
161
+ import webPushSagas from '../WebPush/sagas';
159
162
  const withMobilePushNewSaga = injectSaga({ key: 'mobilePushNew', saga: v2MobilePushSagas, mode: DAEMON });
163
+ const withWebPushSaga = injectSaga({ key: 'webPush', saga: webPushSagas, mode: DAEMON });
160
164
 
161
165
  const { timeTracker } = GA;
162
166
  const {CapCustomCardList} = CapCustomCard;
@@ -210,6 +214,7 @@ const EBILL_LOWERCASE = EBILL.toLowerCase();
210
214
  const LINE_LOWERCASE = LINE.toLowerCase();
211
215
  const ZALO_LOWERCASE = ZALO.toLowerCase();
212
216
  const WECHAT_LOWERCASE = WECHAT.toLowerCase();
217
+ const WEBPUSH_LOWERCASE = WEBPUSH.toLowerCase();
213
218
  const duplicateEnum = {
214
219
  sms: "smsActions",
215
220
  line: "lineActions",
@@ -262,6 +267,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
262
267
  // Add flag to prevent duplicate API calls
263
268
  isProcessingEditResponse: false,
264
269
  };
270
+ // Timeout IDs for cleanup
271
+ this._clearEditTimeout = null;
272
+ this._clearCreateTimeout = null;
265
273
  this.getAllTemplates = this.getAllTemplates.bind(this);
266
274
  this.createTemplate = this.createTemplate.bind(this);
267
275
  this.searchTemplate = this.searchTemplate.bind(this);
@@ -384,10 +392,19 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
384
392
  orgUnitId: -1,
385
393
  });
386
394
  break;
395
+ case WEBPUSH:
396
+ channel = 'Webpush';
397
+ activeMode = ACCOUNT_SELECTION_MODE;
398
+ this.props.actions.getWeCrmAccounts('WebPush');
399
+ break;
387
400
  default:
388
401
  channel = '';
389
402
  }
390
403
  this.setState({ channel, activeMode });
404
+ // Clear templates when entering account selection mode to prevent showing old channel templates
405
+ if (activeMode === ACCOUNT_SELECTION_MODE) {
406
+ this.props.actions.resetTemplate();
407
+ }
391
408
  if (isEmpty(this.props.Templates?.userList)) {
392
409
  this.props.actions.getUserList();
393
410
  }
@@ -485,6 +502,10 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
485
502
  channel = 'Facebook';
486
503
  this.setState({defaultAccount: true});
487
504
  nextProps.actions.getAccountsSettings();
505
+ } else if (nextProps.route.name.toLowerCase() === WEBPUSH_LOWERCASE) {
506
+ this.setState({defaultAccount: true});
507
+ channel = 'Webpush';
508
+ nextProps.actions.getWeCrmAccounts('WebPush');
488
509
  }
489
510
 
490
511
  this.setState({ channel }, () => {
@@ -630,6 +651,50 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
630
651
  }, 1000);
631
652
  }
632
653
 
654
+ // Check for WebPush edit response - prevent duplicate calls
655
+ if (selectedChannel === WEBPUSH_LOWERCASE && nextProps.WebPush?.editResponse && (nextProps.WebPush.editResponse.templateId || nextProps.WebPush.editResponse._id) && !isEqual(nextProps.WebPush.editResponse, this.props.WebPush?.editResponse)) {
656
+ const channelLabel = this.props.intl.formatMessage(messages.webpushHeader);
657
+ const message = `${channelLabel} ${this.props.intl.formatMessage(messages.templateUpdateSuccess)}`;
658
+ CapNotification.success({
659
+ key: 'webpushEditSuccess',
660
+ message
661
+ });
662
+ // Clear previous state before loading newer templates
663
+ this.props.actions.resetTemplate();
664
+ this.getAllTemplates({params, resetPage: true});
665
+ // Delay clearing to allow drawer to close first
666
+ // Clear any existing timeout before setting a new one
667
+ if (this._clearEditTimeout) {
668
+ clearTimeout(this._clearEditTimeout);
669
+ }
670
+ this._clearEditTimeout = setTimeout(() => {
671
+ this.props.webpushActions.clearEditResponse();
672
+ this._clearEditTimeout = null;
673
+ }, 200);
674
+ }
675
+
676
+ // Check for WebPush create response (for new template creation)
677
+ if (selectedChannel === WEBPUSH_LOWERCASE && nextProps.WebPush?.response && (nextProps.WebPush.response?.templateId || nextProps.WebPush.response?._id) && !isEqual(nextProps.WebPush.response, this.props.WebPush?.response)) {
678
+ // Skip showing generic "created" toast when this is a duplicate operation
679
+ if (!nextProps.WebPush.response.meta?.isDuplicate) {
680
+ const channelLabel = this.props.intl.formatMessage(messages.webpushHeader);
681
+ const message = `${channelLabel} ${this.props.intl.formatMessage(messages.templateCreateSuccess)}`;
682
+ CapNotification.success({key: 'webpushCreateSuccess', message});
683
+ // Clear previous state before loading newer templates
684
+ this.props.actions.resetTemplate();
685
+ this.getAllTemplates({params, resetPage: true});
686
+ // Delay clearing to allow drawer to close first
687
+ // Clear any existing timeout before setting a new one
688
+ if (this._clearCreateTimeout) {
689
+ clearTimeout(this._clearCreateTimeout);
690
+ }
691
+ this._clearCreateTimeout = setTimeout(() => {
692
+ this.props.webpushActions.clearCreateResponse();
693
+ this._clearCreateTimeout = null;
694
+ }, 200);
695
+ }
696
+ }
697
+
633
698
 
634
699
 
635
700
  if (nextProps.Create && this.props.Create && nextProps.Create.createTemplateError && !isEqual(nextProps.Create.createTemplateError, this.props.Create.createTemplateError)) {
@@ -637,7 +702,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
637
702
  if ((this.state.channel || '').toLowerCase() !== "sms") {
638
703
  CapNotification.error({key: 'somethingWrong', message});
639
704
  }
640
- const { smsActions, mobilepushActions, ebillActions, lineActions, viberActions, facebookActions, whatsappActions, inAppActions, rcsActions } = this.props;
705
+ const { smsActions, mobilepushActions, ebillActions, lineActions, viberActions, facebookActions, whatsappActions, inAppActions, rcsActions, webpushActions } = this.props;
641
706
  switch (selectedChannel) {
642
707
  case SMS_LOWERCASE:
643
708
  smsActions.clearCreateResponse();
@@ -666,6 +731,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
666
731
  case inAppActions:
667
732
  inAppActions.clearCreateResponse();
668
733
  break;
734
+ case WEBPUSH_LOWERCASE:
735
+ webpushActions.clearCreateResponse();
736
+ break;
669
737
  default:
670
738
  break;
671
739
  }
@@ -678,6 +746,10 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
678
746
  nextProps.Templates.deleteResponse) {
679
747
  const message = `${this.state.channel} ${this.props.intl.formatMessage(messages['Template deleted successfully'])}`;
680
748
  CapNotification.success({key: 'deleteSucess', message});
749
+ // Clear previous state before loading newer templates for web push channel
750
+ if (selectedChannel === WEBPUSH_LOWERCASE) {
751
+ this.props.actions.resetTemplate();
752
+ }
681
753
  this.getAllTemplates({params, resetPage: true});
682
754
  }
683
755
 
@@ -816,12 +888,23 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
816
888
  window.removeEventListener("message", this.handleFrameTasks);
817
889
  this.props.actions.resetTemplateStoreData();
818
890
  this.props.globalActions.clearMetaEntities();
891
+ // Clear any pending timeouts to prevent memory leaks
892
+ if (this._clearEditTimeout) {
893
+ clearTimeout(this._clearEditTimeout);
894
+ this._clearEditTimeout = null;
895
+ }
896
+ if (this._clearCreateTimeout) {
897
+ clearTimeout(this._clearCreateTimeout);
898
+ this._clearCreateTimeout = null;
899
+ }
819
900
  // this.props.actions.resetAccount();
820
901
  // this.setState({defaultAccount: false});
821
902
  }
822
903
 
823
- onAccountSelect(e) {
904
+ onAccountSelect(e) {
824
905
  const value = e.target.value;
906
+ // Clear templates immediately when account is selected to prevent showing old channel templates
907
+ this.props.actions.resetTemplate();
825
908
  this.setState({selectedAccount: value}, () => {
826
909
  const params = {};
827
910
  if (this.state.channel.toLowerCase() !== ZALO_LOWERCASE) {
@@ -849,7 +932,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
849
932
  } else {
850
933
  this.setState({ selectedAccountError: false });
851
934
  }
852
- } else if ([LINE.toLowerCase(), RCS_LOWERCASE, ZALO_LOWERCASE, WHATSAPP_LOWERCASE].includes(selectedChannel)) {
935
+ } else if ([LINE.toLowerCase(), RCS_LOWERCASE, ZALO_LOWERCASE, WHATSAPP_LOWERCASE, WEBPUSH_LOWERCASE].includes(selectedChannel)) {
853
936
  const setAcc = this.props?.Templates?.weCrmAccounts?.find((item) => item?.name === this.state.selectedAccount);
854
937
  const { domainProperties = [] } = this.props?.Templates?.senderDetails || {};
855
938
  let hostName = '';
@@ -876,6 +959,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
876
959
  params.accountId = sourceAccountIdentifier;
877
960
  params.host = hostName;
878
961
  }
962
+ if (selectedChannel === WEBPUSH_LOWERCASE && setAcc) {
963
+ params.accountId = setAcc.accountId || setAcc.id;
964
+ }
879
965
  if (selectedChannel === RCS_LOWERCASE && hostName) {
880
966
  const { configs: { accessToken = "" } = {} } = setAcc || {};
881
967
  params.accountId = sourceAccountIdentifier;
@@ -917,32 +1003,44 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
917
1003
  let creativesParams = {mode: ''};
918
1004
  if (!isEmpty(routeParams)) {
919
1005
  const { pathname } = routeParams;
920
- if (pathname.includes('create')) {
921
- creativesParams.mode = 'create';
922
- } else if (pathname.includes('edit') || pathname.includes('overview')) {
923
- creativesParams.mode = 'edit';
924
- if (pathname.includes('richmedia')) {
925
- creativesParams._id = pathname.split('/')[4];
926
- creativesParams.definition = {
927
- msgcontent: "RICH_MEDIA_WECHAT",
928
- };
929
- } else {
930
- creativesParams._id = pathname.split('/')[3];
931
- creativesParams.modeType = pathname.split('/')[4];
932
- creativesParams.account = pathname.split('/')[5];
933
- if (this.state.channel.toLowerCase() === WHATSAPP_LOWERCASE) {
934
- const whatsappSelectedTemplateData = this.selectTemplate(creativesParams._id) || {};
935
- const { name = '', versions = {} } = whatsappSelectedTemplateData;
936
- creativesParams.whatsappTemplateName = name;
937
- creativesParams.whatsappTemplateCategory = get(versions, `base.content.whatsapp.category`, '');
938
- creativesParams.whatsappTemplateLanguageCode = get(versions, `base.content.whatsapp.languages[0].language`, '');
939
- } else if (this.state.channel.toLocaleLowerCase() === ZALO_LOWERCASE) {
940
- const zaloSelectedTemplateData = this.selectTemplate(parseInt(creativesParams._id, 10)) || {};
941
- const { name = '' } = zaloSelectedTemplateData;
942
- creativesParams.name = name
1006
+ if (pathname) {
1007
+ if (pathname.includes('create')) {
1008
+ creativesParams.mode = 'create';
1009
+ } else if (pathname.includes('edit') || pathname.includes('overview')) {
1010
+ creativesParams.mode = 'edit';
1011
+ if (pathname.includes('richmedia')) {
1012
+ creativesParams._id = pathname.split('/')[4];
1013
+ creativesParams.definition = {
1014
+ msgcontent: "RICH_MEDIA_WECHAT",
1015
+ };
1016
+ } else {
1017
+ creativesParams._id = pathname.split('/')[3];
1018
+ creativesParams.modeType = pathname.split('/')[4];
1019
+ creativesParams.account = pathname.split('/')[5];
1020
+ if (this.state.channel.toLowerCase() === WHATSAPP_LOWERCASE) {
1021
+ const whatsappSelectedTemplateData = this.selectTemplate(creativesParams._id) || {};
1022
+ const { name = '', versions = {} } = whatsappSelectedTemplateData;
1023
+ creativesParams.whatsappTemplateName = name;
1024
+ creativesParams.whatsappTemplateCategory = get(versions, `base.content.whatsapp.category`, '');
1025
+ creativesParams.whatsappTemplateLanguageCode = get(versions, `base.content.whatsapp.languages[0].language`, '');
1026
+ } else if (this.state.channel.toLocaleLowerCase() === ZALO_LOWERCASE) {
1027
+ const zaloSelectedTemplateData = this.selectTemplate(parseInt(creativesParams._id, 10)) || {};
1028
+ const { name = '' } = zaloSelectedTemplateData;
1029
+ creativesParams.name = name
1030
+ } else if (this.state.channel.toLowerCase() === WEBPUSH_LOWERCASE) {
1031
+ // For WebPush, include the full template data for edit mode
1032
+ const webpushSelectedTemplateData = this.selectTemplate(creativesParams._id) || {};
1033
+ if (webpushSelectedTemplateData && !isEmpty(webpushSelectedTemplateData)) {
1034
+ // Merge the full template data into creativesParams
1035
+ creativesParams = {
1036
+ ...creativesParams,
1037
+ ...webpushSelectedTemplateData,
1038
+ };
1039
+ }
1040
+ }
943
1041
  }
1042
+ creativesParams.type = this.state.channel.toUpperCase();
944
1043
  }
945
- creativesParams.type = this.state.channel.toUpperCase();
946
1044
  }
947
1045
  }
948
1046
  if (this.state.previewOpen) {
@@ -976,6 +1074,13 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
976
1074
  queryParams.host = params?.host || this.state?.hostName;
977
1075
  };
978
1076
 
1077
+ setWebpushQueryParams = (queryParams, params) => {
1078
+ const selectedAccount = this.props.Templates?.selectedWebPushAccount;
1079
+ queryParams.accountId = params?.accountId
1080
+ || selectedAccount?.accountId
1081
+ || selectedAccount?.id;
1082
+ };
1083
+
979
1084
  getAllTemplates = ({params, getNextPage, resetPage}, resetTemplates) => {
980
1085
  let queryParams = params || {};
981
1086
  let page = this.state.page;
@@ -1007,6 +1112,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1007
1112
  if (this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE) {
1008
1113
  this.setWhatsappQueryParams(queryParams, params);
1009
1114
  }
1115
+ if (this.state?.channel?.toLowerCase() === WEBPUSH_LOWERCASE) {
1116
+ this.setWebpushQueryParams(queryParams, params);
1117
+ }
1010
1118
  if (this.state?.channel?.toLowerCase() === RCS_LOWERCASE && !isEmpty(this.props.Templates?.selectedRcsAccount)) {
1011
1119
  const { sourceAccountIdentifier = '', configs: { accessToken = '' } = {}, hostName = '' } = this.props.Templates.selectedRcsAccount;
1012
1120
  if (sourceAccountIdentifier) {
@@ -1071,6 +1179,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1071
1179
  if (this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE) {
1072
1180
  this.setWhatsappQueryParams(queryParams, params);
1073
1181
  }
1182
+ if (this.state?.channel?.toLowerCase() === WEBPUSH_LOWERCASE) {
1183
+ this.setWebpushQueryParams(queryParams, params);
1184
+ }
1074
1185
  if (this.state?.channel?.toLowerCase() === RCS_LOWERCASE && !isEmpty(this.props.Templates?.selectedRcsAccount)) {
1075
1186
  const { sourceAccountIdentifier = '', configs: { accessToken = '' } = {}, hostName = '' } = this.props.Templates.selectedRcsAccount;
1076
1187
  if (sourceAccountIdentifier) {
@@ -1747,6 +1858,70 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1747
1858
  templateData.content = <CapLabel type="label19" className="zalo-listing-content desc">{template.name}</CapLabel>;
1748
1859
  break;
1749
1860
  }
1861
+ case WEBPUSH: {
1862
+ const webpushContent = get(template, 'versions.base.content.webpush', {});
1863
+ const notificationTitle = webpushContent?.title || '';
1864
+ const notificationMessage = webpushContent?.message || '';
1865
+ const brandIcon = webpushContent?.brandIcon || '';
1866
+ const mediaImage = webpushContent?.image || '';
1867
+ const ctaButtons = Array.isArray(webpushContent?.ctas)
1868
+ ? webpushContent.ctas.filter(
1869
+ (cta) => typeof cta?.actionText === 'string' && cta.actionText.trim()
1870
+ )
1871
+ : [];
1872
+ const visibleCtas = ctaButtons.slice(0, 2);
1873
+ templateData.content = (
1874
+ <div className="sms-template-content webpush-template-content">
1875
+ <div className="webpush-template-card">
1876
+ <div className="webpush-template-meta">
1877
+ {brandIcon ? (
1878
+ <img
1879
+ src={brandIcon}
1880
+ alt="Brand Icon"
1881
+ className="webpush-brand-icon"
1882
+ />
1883
+ ) : (
1884
+ <span className="webpush-brand-icon-placeholder" />
1885
+ )}
1886
+ <div className="webpush-template-text">
1887
+ <div className="webpush-template-header">
1888
+ <div className="webpush-template-title">
1889
+ {notificationTitle}
1890
+ </div>
1891
+ <div className="webpush-template-time">2:29 PM</div>
1892
+ </div>
1893
+ {notificationMessage ? (
1894
+ <div className="webpush-template-message">
1895
+ {notificationMessage}
1896
+ </div>
1897
+ ) : null}
1898
+ {mediaImage ? (
1899
+ <img
1900
+ src={mediaImage}
1901
+ alt="Media"
1902
+ className="webpush-media-image"
1903
+ />
1904
+ ) : null}
1905
+ </div>
1906
+ </div>
1907
+ {visibleCtas.length ? (
1908
+ <div
1909
+ className={`webpush-template-cta-section${
1910
+ visibleCtas.length === 1 ? ' single-cta' : ''
1911
+ }`}
1912
+ >
1913
+ {visibleCtas.map((cta, index) => (
1914
+ <div className="webpush-template-cta-item" key={`webpush-cta-${index}`}>
1915
+ <span className="webpush-template-cta-text">{cta.actionText}</span>
1916
+ </div>
1917
+ ))}
1918
+ </div>
1919
+ ) : null}
1920
+ </div>
1921
+ </div>
1922
+ );
1923
+ break;
1924
+ }
1750
1925
  default:
1751
1926
  templateData.content = "";
1752
1927
  }
@@ -1856,6 +2031,11 @@ return (<div>
1856
2031
  <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel}/>
1857
2032
  </div>
1858
2033
  }
2034
+ {showIllustrationForChannel(WEBPUSH_LOWERCASE) &&
2035
+ <div style={this.isFullMode() ? { height: "calc(100vh - 20.3125rem)", overflow: 'auto' } : {}}>
2036
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2037
+ </div>
2038
+ }
1859
2039
  {<CapCustomSkeleton loader={isInitialLoading && (isLoading || getAllTemplatesInProgress)} />}
1860
2040
  {<CapPageSpinner spinning={!isInitialLoading && (isLoading || getAllTemplatesInProgress)} />}
1861
2041
  </div>
@@ -2249,6 +2429,10 @@ return (<div>
2249
2429
  params.host = this.state?.hostName;
2250
2430
  }
2251
2431
  this.delay(() => {
2432
+ // Clear previous state before loading newer templates for web push channel
2433
+ if (this.state.channel.toLowerCase() === WEBPUSH_LOWERCASE) {
2434
+ this.props.actions.resetTemplate();
2435
+ }
2252
2436
  this.getAllTemplates({params, resetPage: true});
2253
2437
  }, 500);
2254
2438
  });
@@ -2412,6 +2596,32 @@ return (<div>
2412
2596
  this.getAllTemplates({params: {}}, true);
2413
2597
  this.props.inAppActions.clearCreateResponse();
2414
2598
  });
2599
+ } else if (this.state.channel.toLowerCase() === "webpush") {
2600
+ const params = {
2601
+ name: this.state.searchText,
2602
+ sortBy: this.state.sortBy,
2603
+ };
2604
+
2605
+ // Get the selected WebPush account
2606
+ const selectedAccount = this.props.Templates?.selectedWebPushAccount;
2607
+
2608
+ // Set the accountId in the duplicate object
2609
+ if (duplicateObj.definition) {
2610
+ duplicateObj.definition.accountId = selectedAccount?.accountId || selectedAccount?.id;
2611
+ }
2612
+
2613
+ const channelLabel = this.props.intl.formatMessage(messages.webpushHeader);
2614
+ this.props.webpushActions.createTemplate(duplicateObj, (response) => {
2615
+ if (response && (response.templateId || response._id)) {
2616
+ // Clear response immediately to prevent componentWillReceiveProps from showing "created" notification
2617
+ this.props.webpushActions.clearCreateResponse();
2618
+ // Clear previous state before loading newer templates
2619
+ this.props.actions.resetTemplate();
2620
+ const message = `${channelLabel} ${this.props.intl.formatMessage(messages.templateDuplicateSuccess)}`;
2621
+ CapNotification.success({key: 'duplicateSuccess', message});
2622
+ this.getAllTemplates({params, resetPage: true});
2623
+ }
2624
+ }, { isDuplicate: true });
2415
2625
  } else if (this.state.channel.toLowerCase() === "ebill") {
2416
2626
  this.props.ebillActions.createTemplate(duplicateObj);
2417
2627
  } else if (this.state.channel.toLowerCase() === "email") {
@@ -2499,6 +2709,10 @@ return (<div>
2499
2709
  pathName = `/ebill/edit/${id}`;
2500
2710
  break;
2501
2711
  }
2712
+ case WEBPUSH_LOWERCASE: {
2713
+ pathName = `/webpush/edit/${id}`;
2714
+ break;
2715
+ }
2502
2716
  case 'line': {
2503
2717
  const { messages } = template.versions.base.content;
2504
2718
  const type = messages && messages[0] && messages[0].type;
@@ -2544,6 +2758,10 @@ return (<div>
2544
2758
  pathName = `/inapp/edit/${id}/`;
2545
2759
  break;
2546
2760
  }
2761
+ case WEBPUSH: {
2762
+ pathName = `/webpush/edit/${id}`;
2763
+ break;
2764
+ }
2547
2765
  default:
2548
2766
  break;
2549
2767
  }
@@ -2891,6 +3109,14 @@ return (<div>
2891
3109
  fetchingWeCrmAccounts,
2892
3110
  sendingFile,
2893
3111
  } = Templates;
3112
+
3113
+ // Show loading when in account selection or account change mode
3114
+ const isAccountSelectionMode = this.state.activeMode === ACCOUNT_SELECTION_MODE ||
3115
+ this.state.activeMode === ACCOUNT_CHANGE_MODE;
3116
+ if (isAccountSelectionMode) {
3117
+ return true;
3118
+ }
3119
+
2894
3120
  const lineLoader = this.checkLoader('line');
2895
3121
  const smsLoader = this.checkLoader('sms');
2896
3122
  const viberLoader = this.checkLoader(VIBER_CHANNEL);
@@ -2913,6 +3139,10 @@ return (<div>
2913
3139
  fetchingWeCrmAccounts ) &&
2914
3140
  this.state.channel.toLowerCase() === ZALO_LOWERCASE;
2915
3141
  const mobilePushLoader = (((getAllTemplatesInProgress) || (fetchingWeCrmAccounts)) && this.state.channel.toLowerCase() === 'mobilepush');
3142
+ const webpushLoader = (
3143
+ (getAllTemplatesInProgress && this.state.channel.toLowerCase() === WEBPUSH_LOWERCASE) ||
3144
+ (fetchingWeCrmAccounts && this.state.channel.toLowerCase() === WEBPUSH_LOWERCASE)
3145
+ );
2916
3146
  const inAppLoader = (((this.state.selectedAccount !== '' && getAllTemplatesInProgress) || (fetchingWeCrmAccounts)) && this.state.channel.toLowerCase() === INAPP_LOWERCASE);
2917
3147
  const emailLoader = (
2918
3148
  (getAllTemplatesInProgress ||
@@ -2927,6 +3157,7 @@ return (<div>
2927
3157
  (emailLoader !== undefined ? emailLoader : false) ||
2928
3158
  (ebillLoader !== undefined ? ebillLoader : false) ||
2929
3159
  (mobilePushLoader !== undefined ? mobilePushLoader : false) ||
3160
+ (webpushLoader !== undefined ? webpushLoader : false) ||
2930
3161
  (lineLoader !== undefined ? lineLoader : false) ||
2931
3162
  (facebookLoader !== undefined ? facebookLoader : false) ||
2932
3163
  (viberLoader !== undefined ? viberLoader : false) ||
@@ -2997,9 +3228,9 @@ return (<div>
2997
3228
  const isMobilePushChannel = channel === MOBILE_PUSH;
2998
3229
  const isInAppChannel = channel === INAPP;
2999
3230
  const isFacebookChannel = channel === FACEBOOK;
3000
- if ([WECHAT, MOBILE_PUSH, INAPP, LINE, ZALO, WHATSAPP, RCS].includes(channel) && !isEmpty(weCrmAccounts) && !isFacebookChannel) {
3231
+ if ([WECHAT, MOBILE_PUSH, INAPP, LINE, ZALO, WHATSAPP, WEBPUSH, RCS].includes(channel) && !isEmpty(weCrmAccounts) && !isFacebookChannel) {
3001
3232
  forEach(weCrmAccounts, (account) => {
3002
- if ((isWechatChannel && account.configs && account.configs.is_wecrm_enabled) || [MOBILE_PUSH, INAPP, LINE, ZALO, WHATSAPP, RCS].includes(channel)) {
3233
+ if ((isWechatChannel && account.configs && account.configs.is_wecrm_enabled) || [MOBILE_PUSH, INAPP, LINE, ZALO, WHATSAPP, WEBPUSH, RCS].includes(channel)) {
3003
3234
  if (query.type === 'embedded' && (!query.module || (query.module && query.module !== 'library'))) {
3004
3235
  if (query.source_account_id && account.sourceAccountIdentifier === query.source_account_id) {
3005
3236
  accountOptions.push(
@@ -3078,6 +3309,11 @@ return (<div>
3078
3309
  noAccountHeader = messages.noAccountsPresentZalo;
3079
3310
  break;
3080
3311
  }
3312
+ case WEBPUSH: {
3313
+ accountHeader = formatMessage(messages.webpushAccount);
3314
+ noAccountHeader = messages.noAccountsPresentWebpush;
3315
+ break;
3316
+ }
3081
3317
  case RCS: {
3082
3318
  accountHeader = formatMessage(messages.rcsAccount);
3083
3319
  noAccountHeader = messages.noAccountsPresentRcs;
@@ -3134,10 +3370,12 @@ return (<div>
3134
3370
  };
3135
3371
 
3136
3372
  setAccountSelectionMode = () => {
3373
+ this.props.actions.resetTemplate();
3137
3374
  this.setState({activeMode: ACCOUNT_SELECTION_MODE});
3138
3375
  }
3139
3376
 
3140
3377
  setAccountChangeMode = () => {
3378
+ this.props.actions.resetTemplate();
3141
3379
  this.setState({activeMode: ACCOUNT_CHANGE_MODE});
3142
3380
  }
3143
3381
 
@@ -3713,6 +3951,8 @@ Templates.propTypes = {
3713
3951
  rcsActions: PropTypes.object,
3714
3952
  zaloActions: PropTypes.object,
3715
3953
  inAppActions: PropTypes.object,
3954
+ webpushActions: PropTypes.object,
3955
+ WebPush: PropTypes.object,
3716
3956
  smsRegister: PropTypes.any,
3717
3957
  isDltFromRcs: PropTypes.bool,
3718
3958
  };
@@ -3732,6 +3972,7 @@ const mapStateToProps = createStructuredSelector({
3732
3972
  Rcs: makeSelectRcs(),
3733
3973
  Zalo: makeSelectZalo(),
3734
3974
  InApp: makeSelectInApp(),
3975
+ WebPush: makeSelectWebPush(),
3735
3976
  });
3736
3977
 
3737
3978
  function mapDispatchToProps(dispatch) {
@@ -3751,6 +3992,7 @@ function mapDispatchToProps(dispatch) {
3751
3992
  whatsappActions: bindActionCreators(whatsappActions, dispatch),
3752
3993
  rcsActions: bindActionCreators(rcsActions, dispatch),
3753
3994
  zaloActions: bindActionCreators(zaloActions, dispatch),
3995
+ webpushActions: bindActionCreators(webpushActions, dispatch),
3754
3996
  };
3755
3997
  }
3756
3998
 
@@ -3764,6 +4006,7 @@ export default compose(
3764
4006
  UserIsAuthenticated,
3765
4007
  withSaga,
3766
4008
  withMobilePushNewSaga,
4009
+ withWebPushSaga,
3767
4010
  withReducer,
3768
4011
  withConnect,
3769
4012
  )(injectIntl(Templates));
@@ -46,6 +46,10 @@ export default defineMessages({
46
46
  id: `${scope}.mobilepushHeader`,
47
47
  defaultMessage: `Mobile Push`,
48
48
  },
49
+ "webpushHeader": {
50
+ id: `${scope}.webpushHeader`,
51
+ defaultMessage: `Web Push`,
52
+ },
49
53
  "ebillHeader": {
50
54
  id: `${scope}.ebillHeader`,
51
55
  defaultMessage: `Ebill`,
@@ -342,6 +346,10 @@ export default defineMessages({
342
346
  id: `${scope}.newWhatsappTemplate`,
343
347
  defaultMessage: 'Add new Whatsapp {template}',
344
348
  },
349
+ "newWebPushTemplate": {
350
+ id: `${scope}.newWebPushTemplate`,
351
+ defaultMessage: 'Add new Web push {template}',
352
+ },
345
353
  "newInAppMessageTemplate": {
346
354
  id: `${scope}.newInAppMessageTemplate`,
347
355
  defaultMessage: 'Add new In app message {template}',
@@ -378,6 +386,14 @@ export default defineMessages({
378
386
  id: `${scope}.whatsappDescIllustration`,
379
387
  defaultMessage: 'These templates can be reused when creating a\nnew message content.',
380
388
  },
389
+ "webPushTitleIllustration": {
390
+ id: `${scope}.webPushTitleIllustration`,
391
+ defaultMessage: 'Add a new Web push creative {template}',
392
+ },
393
+ "webPushDescIllustration": {
394
+ id: `${scope}.webPushDescIllustration`,
395
+ defaultMessage: 'These templates can be reused when creating a new message content.',
396
+ },
381
397
  "whatsappAccountNotConfiguredTitle": {
382
398
  id: `${scope}.whatsappAccountNotConfiguredTitle`,
383
399
  defaultMessage: 'Whatsapp account is not configured',
@@ -462,6 +478,10 @@ export default defineMessages({
462
478
  id: `${scope}.zaloAccount`,
463
479
  defaultMessage: 'Zalo account',
464
480
  },
481
+ "webpushAccount": {
482
+ id: `${scope}.webpushAccount`,
483
+ defaultMessage: 'Web push account',
484
+ },
465
485
  "rcsAccount": {
466
486
  id: `${scope}.rcsAccount`,
467
487
  defaultMessage: 'RCS account',
@@ -498,6 +518,10 @@ export default defineMessages({
498
518
  id: `${scope}.noAccountsPresentZalo`,
499
519
  defaultMessage: "Zalo accounts are not setup for your brand",
500
520
  },
521
+ "noAccountsPresentWebpush": {
522
+ id: `${scope}.noAccountsPresentWebpush`,
523
+ defaultMessage: "Web push accounts are not setup for your brand",
524
+ },
501
525
  "noAccountsPresentRcs": {
502
526
  id: `${scope}.noAccountsPresentRcs`,
503
527
  defaultMessage: "RCS accounts are not setup for your brand",
@@ -124,6 +124,8 @@ function templatesReducer(state = initialState, action) {
124
124
  return state
125
125
  .set('selectedFacebookAccount', fromJS(action.faceBookAccount))
126
126
  .set('templates', []);
127
+ case types.SET_WEBPUSH_ACCOUNT:
128
+ return state.set('selectedWebPushAccount', fromJS(action.account));
127
129
  case types.RESET_ACCOUNT:
128
130
  return state
129
131
  .remove('selectedWeChatAccount')
@@ -22,6 +22,7 @@ describe('Test Templates container', () => {
22
22
  const getAllTemplates = jest.fn();
23
23
  const getUserList = jest.fn();
24
24
  const getSenderDetails = jest.fn();
25
+ const resetTemplate = jest.fn();
25
26
  let renderedComponent;
26
27
 
27
28
  beforeEach(() => {
@@ -54,6 +55,7 @@ describe('Test Templates container', () => {
54
55
  getAllTemplates,
55
56
  getUserList,
56
57
  getSenderDetails,
58
+ resetTemplate,
57
59
  }}
58
60
  location={{
59
61
  pathname: `/${channel}`,
@@ -79,6 +81,8 @@ describe('Test Templates container', () => {
79
81
  channel: 'WHATSAPP',
80
82
  orgUnitId: -1,
81
83
  });
84
+ // resetTemplate should be called when entering account selection mode
85
+ expect(resetTemplate).toHaveBeenCalled();
82
86
  });
83
87
 
84
88
  it('Should render temlates when whatsapp templates are passed', () => {
@@ -103,6 +107,8 @@ describe('Test Templates container', () => {
103
107
  Templates: {},
104
108
  });
105
109
  expect(renderedComponent).toMatchSnapshot();
110
+ // SMS doesn't enter account selection mode, so resetTemplate shouldn't be called on mount
111
+ expect(resetTemplate).not.toHaveBeenCalled();
106
112
  });
107
113
 
108
114
  it('Should render temlates when whatsapp templates are passed in full mode', () => {
@@ -123,6 +129,8 @@ describe('Test Templates container', () => {
123
129
  it('Should render correct component for zalo channel', () => {
124
130
  RenderFunctionFor('zalo');
125
131
  expect(renderedComponent).toMatchSnapshot();
132
+ // resetTemplate should be called when entering account selection mode
133
+ expect(resetTemplate).toHaveBeenCalled();
126
134
  });
127
135
  it('Should render temlates when zalo templates are passed', () => {
128
136
  RenderFunctionFor('zalo');
@@ -201,6 +209,8 @@ describe('Test Templates container', () => {
201
209
  channel: 'RCS',
202
210
  orgUnitId: -1,
203
211
  });
212
+ // resetTemplate should be called when entering account selection mode
213
+ expect(resetTemplate).toHaveBeenCalled();
204
214
  });
205
215
 
206
216
  it('Should render templates when RCS templates are passed', () => {