@capillarytech/creatives-library 8.0.357 → 8.0.359-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 (36) hide show
  1. package/index.html +0 -1
  2. package/package.json +1 -1
  3. package/utils/cdnTransformation.js +3 -75
  4. package/utils/tests/cdnTransformation.test.js +0 -127
  5. package/v2Components/CommonTestAndPreview/UnifiedPreview/PreviewHeader.js +0 -16
  6. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberPreviewContent.js +132 -14
  7. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +163 -54
  8. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +6 -52
  9. package/v2Components/CommonTestAndPreview/constants.js +0 -2
  10. package/v2Components/CommonTestAndPreview/index.js +231 -77
  11. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/PreviewHeader.test.js +0 -163
  12. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/ViberPreviewContent.test.js +364 -0
  13. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +0 -255
  14. package/v2Components/CommonTestAndPreview/tests/constants.test.js +1 -2
  15. package/v2Components/CommonTestAndPreview/tests/index.test.js +0 -194
  16. package/v2Components/FormBuilder/index.js +52 -162
  17. package/v2Components/TestAndPreviewSlidebox/index.js +2 -2
  18. package/v2Containers/App/constants.js +0 -3
  19. package/v2Containers/CreativesContainer/index.js +24 -60
  20. package/v2Containers/Templates/_templates.scss +77 -0
  21. package/v2Containers/Templates/index.js +92 -82
  22. package/v2Containers/Templates/sagas.js +1 -6
  23. package/v2Containers/Templates/tests/sagas.test.js +6 -23
  24. package/v2Containers/Viber/constants.js +19 -0
  25. package/v2Containers/Viber/index.js +714 -47
  26. package/v2Containers/Viber/index.scss +148 -0
  27. package/v2Containers/Viber/messages.js +116 -0
  28. package/v2Containers/Viber/tests/index.test.js +80 -0
  29. package/v2Containers/WebPush/Create/index.js +8 -91
  30. package/v2Containers/WebPush/Create/index.scss +0 -7
  31. package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +0 -169
  32. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WebPushPreviewContent.test.js +0 -522
  33. package/v2Containers/App/tests/constants.test.js +0 -61
  34. package/v2Containers/Templates/tests/webpush.test.js +0 -375
  35. package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +0 -348
  36. package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +0 -325
@@ -72,6 +72,7 @@ import * as ebillActions from '../Ebill/actions';
72
72
  import * as emailActions from '../Email/actions';
73
73
  import * as lineActions from '../Line/Container/actions';
74
74
  import * as viberActions from '../Viber/actions';
75
+ import { VIBER_MEDIA_TYPES } from '../Viber/constants';
75
76
  import * as facebookActions from '../Facebook/actions';
76
77
  import * as whatsappActions from '../Whatsapp/actions';
77
78
  import * as rcsActions from '../Rcs/actions';
@@ -104,9 +105,6 @@ import {
104
105
  VIBER as VIBER_CHANNEL,
105
106
  FACEBOOK as FACEBOOK_CHANNEL,
106
107
  CREATE,
107
- EXTERNAL_URL,
108
- URL,
109
- SITE_URL,
110
108
  } from '../App/constants';
111
109
  import {MAX_WHATSAPP_TEMPLATES, WARNING_WHATSAPP_TEMPLATES , ACCOUNT_MAPPING_ON_CHANNEL, noFilteredWhatsappZaloTemplatesTitle, noFilteredWhatsappZaloTemplatesDesc, noApprovedWhatsappZaloTemplatesTitle, noApprovedWhatsappTemplatesDesc, zaloDescIllustration, noApprovedRcsTemplatesTitle, noApprovedRcsTemplatesDesc, ARCHIVE_STATUS_ACTIVE, ARCHIVE_STATUS_ARCHIVED, ARCHIVE_REFRESH_TYPE_ARCHIVE, ARCHIVE_REFRESH_TYPE_UNARCHIVE} from './constants';
112
110
  import { COPY_OF, EMBEDDED } from '../../constants/unified';
@@ -1374,6 +1372,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1374
1372
  const image = viberContent.image || {};
1375
1373
  const video = viberContent.video || {};
1376
1374
  const button = viberContent.button || {};
1375
+ const messageType = viberContent.type || '';
1376
+ const cardsRaw = viberContent.cards;
1377
+ const cards = Array.isArray(cardsRaw) ? cardsRaw : [];
1378
+ const isCarousel =
1379
+ messageType === VIBER_MEDIA_TYPES.CAROUSEL || cards.length > 0;
1377
1380
 
1378
1381
  const viberPreview = {
1379
1382
  messageContent: text,
@@ -1391,81 +1394,39 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1391
1394
  };
1392
1395
  }
1393
1396
 
1397
+ if (isCarousel) {
1398
+ viberPreview.type = VIBER_MEDIA_TYPES.CAROUSEL;
1399
+ viberPreview.cards = cards.map((card) => ({
1400
+ text: card?.text || '',
1401
+ mediaUrl: card?.mediaUrl || '',
1402
+ buttons: (card?.buttons || []).map((btn) => ({
1403
+ title: btn?.title || '',
1404
+ action: btn?.action || '',
1405
+ })),
1406
+ }));
1407
+ viberPreview.showCarouselEditorPreview = true;
1408
+ }
1409
+
1394
1410
  // Extract account name
1395
1411
  const accountName = get(template, 'definition.accountName', '');
1396
1412
 
1397
1413
  // Return Viber content object (same as Viber/index.js getTemplateContent)
1398
1414
  return {
1399
1415
  viberPreviewContent: viberPreview,
1400
- accountName: accountName ? [accountName] : [],
1401
- brandName: accountName ? [accountName] : [],
1402
- };
1403
- }
1404
-
1405
- case WEBPUSH: {
1406
- // WebPush content is stored in creatives format (brandIcon, onClickAction, ctas)
1407
- // Must be transformed to campaign/test-message format matching getTemplateContent() in WebPush/Create/index.js
1408
- const webpushContent = get(baseContent, 'content.webpush', {});
1409
- const {
1410
- title = '',
1411
- message = '',
1412
- brandIcon,
1413
- iconImageUrl: existingIconImageUrl,
1414
- onClickAction,
1415
- cta: existingCta,
1416
- image,
1417
- ctas: rawCtas,
1418
- expandableDetails: existingExpandable,
1419
- } = webpushContent;
1420
- const accountId = get(template, 'definition.accountId', null);
1421
- const templateName = template?.name || '';
1422
-
1423
- // iconImageUrl stored as brandIcon in creatives format
1424
- const iconImageUrl = brandIcon || existingIconImageUrl || undefined;
1425
-
1426
- // cta stored as onClickAction in creatives format (type: URL|SITE_URL, url)
1427
- // or already as cta in campaign format (type: EXTERNAL_URL|SITE_URL, actionLink)
1428
- let cta = null;
1429
- if (onClickAction) {
1430
- const ctaType = onClickAction.type === URL ? EXTERNAL_URL : (onClickAction.type || SITE_URL);
1431
- cta = { type: ctaType, actionLink: onClickAction.url || '' };
1432
- } else if (existingCta) {
1433
- cta = { type: existingCta.type || EXTERNAL_URL, actionLink: existingCta.actionLink || '' };
1434
- }
1435
-
1436
- // expandableDetails: image → media[], ctas[] → mapped ctas
1437
- let expandableDetails = null;
1438
- const hasImage = !!image;
1439
- const hasCtas = Array.isArray(rawCtas) && rawCtas.length > 0;
1440
- if (hasImage || hasCtas) {
1441
- expandableDetails = {
1442
- media: hasImage ? [{ url: image, type: IMAGE }] : [],
1443
- ctas: hasCtas ? rawCtas.map((ctaItem) => ({
1444
- type: ctaItem?.type === URL ? EXTERNAL_URL : (ctaItem?.type || EXTERNAL_URL),
1445
- action: ctaItem?.action || '',
1446
- title: ctaItem?.actionText || ctaItem?.title || '',
1447
- actionLink: ctaItem?.actionLink || '',
1448
- })) : [],
1449
- };
1450
- } else if (existingExpandable) {
1451
- expandableDetails = {
1452
- media: existingExpandable?.media || [],
1453
- ctas: existingExpandable?.ctas || [],
1454
- };
1455
- }
1456
-
1457
- return {
1458
- channel: WEBPUSH,
1459
- accountId,
1460
- content: {
1461
- title,
1462
- message,
1463
- ...(iconImageUrl ? { iconImageUrl } : {}),
1464
- ...(cta ? { cta } : {}),
1465
- ...(expandableDetails ? { expandableDetails } : {}),
1466
- },
1467
- messageSubject: templateName || title,
1468
- offers: [],
1416
+ accountName: accountName || '',
1417
+ brandName: accountName || '',
1418
+ messageContent: text,
1419
+ ...(isCarousel && {
1420
+ type: VIBER_MEDIA_TYPES.CAROUSEL,
1421
+ cards: viberPreview.cards,
1422
+ }),
1423
+ ...(!isCarousel && !isEmpty(button) && button.text && (button.url || viberContent.buttonURL) && {
1424
+ button: {
1425
+ text: button.text,
1426
+ url: button.url || viberContent.buttonURL || '',
1427
+ },
1428
+ }),
1429
+ buttonURL: button.url || viberContent.buttonURL || '',
1469
1430
  };
1470
1431
  }
1471
1432
 
@@ -1481,7 +1442,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1481
1442
  * @returns {Boolean} - True if channel supports Test and Preview
1482
1443
  */
1483
1444
  isTestAndPreviewSupported = () => {
1484
- const supportedChannels = [EMAIL, SMS, INAPP, MOBILE_PUSH, VIBER, ZALO, WEBPUSH];
1445
+ const supportedChannels = [EMAIL, SMS, INAPP, MOBILE_PUSH, VIBER, ZALO];
1485
1446
  return supportedChannels.includes(this.state.channel.toUpperCase());
1486
1447
  }
1487
1448
 
@@ -2041,7 +2002,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2041
2002
  // Show preview icon only for channels that don't support Test and Preview
2042
2003
  (() => {
2043
2004
  // Channels that have Test and Preview integrated
2044
- const testAndPreviewChannels = [EMAIL, SMS, WHATSAPP, RCS, INAPP, MOBILE_PUSH, VIBER, ZALO, WEBPUSH];
2005
+ const testAndPreviewChannels = [EMAIL, SMS, WHATSAPP, RCS, INAPP, MOBILE_PUSH, VIBER, ZALO];
2045
2006
  const isTestAndPreviewSupported = testAndPreviewChannels.includes(currentChannel.toUpperCase());
2046
2007
 
2047
2008
  // Don't show preview icon if channel supports Test and Preview
@@ -2411,10 +2372,52 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2411
2372
  image = {},
2412
2373
  button = {},
2413
2374
  video = {},
2375
+ type = '',
2376
+ cards = [],
2414
2377
  } = {},
2415
2378
  } = {},
2416
2379
  } = template.versions.base;
2380
+ const isViberCarousel = type === VIBER_MEDIA_TYPES.CAROUSEL;
2417
2381
  templateData.content = text;
2382
+ if (isViberCarousel) {
2383
+ const previewCards = Array.isArray(cards) ? cards.slice(0, 1) : [];
2384
+ templateData.className = 'viber-carousel-static';
2385
+ templateData.content = (
2386
+ <CapRow className="viber-carousel-static-content">
2387
+ <CapRow className="viber-carousel-static-message-box">
2388
+ <CapLabel type="label1" className="viber-carousel-static-message">
2389
+ {text}
2390
+ </CapLabel>
2391
+ </CapRow>
2392
+ <CapRow className="viber-carousel-static-cards">
2393
+ {previewCards.map((card, cardIdx) => (
2394
+ <CapRow className="viber-carousel-static-card" key={`viber-static-card-${cardIdx}`}>
2395
+ {card?.mediaUrl ? (
2396
+ <CapImage src={card.mediaUrl} className="viber-carousel-static-image" />
2397
+ ) : (
2398
+ <CapRow className="viber-carousel-static-image-placeholder" />
2399
+ )}
2400
+ <CapLabel type="label1" className="viber-carousel-static-text">
2401
+ {card?.text}
2402
+ </CapLabel>
2403
+ <CapRow className="viber-carousel-static-buttons">
2404
+ {(Array.isArray(card?.buttons) && card.buttons.length ? card.buttons : [{}, {}]).slice(0, 2).map((carouselButton, btnIdx) => (
2405
+ <CapLabel
2406
+ key={`viber-static-btn-${cardIdx}-${btnIdx}`}
2407
+ type="label1"
2408
+ className={`viber-carousel-static-button ${btnIdx === 1 ? 'viber-carousel-static-button-secondary' : ''}`}
2409
+ >
2410
+ {(carouselButton?.title || '').trim()}
2411
+ </CapLabel>
2412
+ ))}
2413
+ </CapRow>
2414
+ </CapRow>
2415
+ ))}
2416
+ </CapRow>
2417
+ </CapRow>
2418
+ );
2419
+ break;
2420
+ }
2418
2421
  if (!isEmpty(image)) {
2419
2422
  const { url = '' } = image;
2420
2423
  templateData.url = url;
@@ -4976,15 +4979,22 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4976
4979
  ? this.props.Templates.selectedWhatsappAccount.name
4977
4980
  : null
4978
4981
  }
4979
- // Pass formData for EMAIL and SMS channels for tag extraction
4982
+ // Pass formData for tag extraction (EMAIL/SMS use nested formData; VIBER uses content object)
4980
4983
  formData={
4981
- this.state.testAndPreviewContent &&
4982
- (this.state.channel?.toUpperCase() === EMAIL ||
4983
- this.state.channel?.toUpperCase() === SMS) &&
4984
- typeof this.state.testAndPreviewContent === 'object' &&
4985
- this.state.testAndPreviewContent?.formData
4986
- ? this.state.testAndPreviewContent.formData
4987
- : null
4984
+ (() => {
4985
+ const previewContent = this.state.testAndPreviewContent;
4986
+ const channelUpper = this.state.channel?.toUpperCase();
4987
+ if (!previewContent || typeof previewContent !== 'object') {
4988
+ return null;
4989
+ }
4990
+ if (channelUpper === EMAIL || channelUpper === SMS) {
4991
+ return previewContent.formData || null;
4992
+ }
4993
+ if (channelUpper === VIBER) {
4994
+ return previewContent;
4995
+ }
4996
+ return null;
4997
+ })()
4988
4998
  }
4989
4999
  />
4990
5000
  )}
@@ -6,7 +6,7 @@ import { CapNotification } from '@capillarytech/cap-ui-library';
6
6
  // import { schema, normalize } from 'normalizr';
7
7
  import * as Api from '../../services/api';
8
8
  import * as types from './constants';
9
- import { saveCdnConfigs, removeAllCdnLocalStorageItems, initCdnConfigFromEnv } from '../../utils/cdnTransformation';
9
+ import { saveCdnConfigs, removeAllCdnLocalStorageItems } from '../../utils/cdnTransformation';
10
10
  import { COPY_OF } from '../../constants/unified';
11
11
  import { ZALO_TEMPLATE_INFO_REQUEST } from '../Zalo/constants';
12
12
  import { getTemplateInfoById } from '../Zalo/saga';
@@ -107,11 +107,6 @@ export function* getOrgLevelCampaignSettings() {
107
107
 
108
108
  export function* getCdnTransformationConfig() {
109
109
  try {
110
- // VAPT CAP-183204: prefer env vars injected via window.APP_ENV — avoids the
111
- // API response that leaked CDN/S3 infrastructure details. Fallback to API
112
- // keeps clusters that haven't received the env vars yet working during rollout.
113
- if (initCdnConfigFromEnv()) return;
114
-
115
110
  const res = yield call(Api.getCdnTransformationConfig);
116
111
  if (res?.success && res?.status?.code === 200) {
117
112
  const cdnConfigs = res?.response;
@@ -54,25 +54,10 @@ jest.mock('@capillarytech/cap-ui-library', () => ({
54
54
  }));
55
55
 
56
56
  describe('getCdnTransformationConfig saga', () => {
57
- afterEach(() => {
58
- delete window.APP_ENV;
59
- });
60
-
61
- it("short-circuits to env config and skips the API call when window.APP_ENV is set", async () => {
62
- const initSpy = jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(true);
63
- const apiSpy = jest.spyOn(api, "getCdnTransformationConfig");
64
-
65
- await expectSaga(getCdnTransformationConfig).run();
66
-
67
- expect(initSpy).toHaveBeenCalled();
68
- expect(apiSpy).not.toHaveBeenCalled();
69
- });
70
-
71
- it("handle valid response from api", async () => {
72
- jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(false);
57
+ it("handle valid response from api", () => {
73
58
  const saveCdnConfigsSpy = jest.spyOn(cdnUtils, "saveCdnConfigs");
74
59
 
75
- await expectSaga(getCdnTransformationConfig)
60
+ expectSaga(getCdnTransformationConfig)
76
61
  .provide([
77
62
  [
78
63
  call(api.getCdnTransformationConfig),
@@ -83,14 +68,13 @@ describe('getCdnTransformationConfig saga', () => {
83
68
  expect(saveCdnConfigsSpy).toHaveBeenCalled();
84
69
  });
85
70
 
86
- it("handle error response from api", async () => {
87
- jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(false);
71
+ it("handle error response from api", () => {
88
72
  const removeAllCdnLocalStorageItemsSpy = jest.spyOn(
89
73
  cdnUtils,
90
74
  "removeAllCdnLocalStorageItems"
91
75
  );
92
76
 
93
- await expectSaga(getCdnTransformationConfig)
77
+ expectSaga(getCdnTransformationConfig)
94
78
  .provide([
95
79
  [
96
80
  call(api.getCdnTransformationConfig),
@@ -101,8 +85,7 @@ describe('getCdnTransformationConfig saga', () => {
101
85
  expect(removeAllCdnLocalStorageItemsSpy).toHaveBeenCalled();
102
86
  });
103
87
 
104
- it("remove localStorage items when an error is thrown", async () => {
105
- jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(false);
88
+ it("remove localStorage items when an error is thrown", () => {
106
89
  const saveCdnConfigsSpy = jest.spyOn(cdnUtils, "saveCdnConfigs").mockImplementation(()=>{
107
90
  throw new Error()
108
91
  });
@@ -111,7 +94,7 @@ describe('getCdnTransformationConfig saga', () => {
111
94
  "removeAllCdnLocalStorageItems"
112
95
  );
113
96
 
114
- await expectSaga(getCdnTransformationConfig)
97
+ expectSaga(getCdnTransformationConfig)
115
98
  .provide([
116
99
  [
117
100
  call(api.getCdnTransformationConfig),
@@ -47,7 +47,22 @@ export const VIBER_MEDIA_TYPES = {
47
47
  TEXT: 'TEXT',
48
48
  IMAGE: 'IMAGE',
49
49
  VIDEO: 'VIDEO',
50
+ CAROUSEL: 'CAROUSEL',
50
51
  };
52
+ export const VIBER_CAROUSEL_MIN_CARDS = 2;
53
+ export const VIBER_CAROUSEL_MAX_CARDS = 5;
54
+ export const VIBER_CAROUSEL_MAX_BUTTONS = 2;
55
+ export const VIBER_CAROUSEL_CARD_TITLE_MIN_LENGTH = 2;
56
+ export const VIBER_CAROUSEL_CARD_TITLE_MAX_LENGTH = 38;
57
+ export const VIBER_CAROUSEL_FIRST_BUTTON_TITLE_MAX_LENGTH = 10;
58
+ export const VIBER_CAROUSEL_SECOND_BUTTON_TITLE_MAX_LENGTH = 12;
59
+ export const VIBER_CAROUSEL_BUTTON_URL_MAX_LENGTH = 1000;
60
+ /** Recommended / validated carousel image height in pixels (passed to image upload). */
61
+ export const VIBER_CAROUSEL_IMG_HEIGHT = 600;
62
+ /** Recommended / validated carousel image width in pixels (passed to image upload). */
63
+ export const VIBER_CAROUSEL_IMG_WIDTH = 696;
64
+ /** Maximum carousel image file size (bytes). 10_000_000 ≈ 10 MB (decimal). */
65
+ export const VIBER_CAROUSEL_IMG_SIZE = 10000000;
51
66
  export const ALLOWED_IMAGE_EXTENSIONS_REGEX_VIBER = /\.(jpe?g|png)$/i;
52
67
  export const ALLOWED_EXTENSIONS_VIDEO_REGEX_VIBER = /\.(mp4|3gp|m4v|mov)$/i;
53
68
  export const NONE = 'NONE';
@@ -69,6 +84,10 @@ export const mediaRadioOptions = [
69
84
  value: VIBER_MEDIA_TYPES.VIDEO,
70
85
  label: <FormattedMessage {...messages.mediaVideo} />,
71
86
  },
87
+ {
88
+ value: VIBER_MEDIA_TYPES.CAROUSEL,
89
+ label: <FormattedMessage {...messages.mediaCarousel} />,
90
+ },
72
91
  ];
73
92
 
74
93
  export const buttonRadioOptions = [