@capillarytech/creatives-library 8.0.359-alpha.0 → 8.0.360-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 (34) hide show
  1. package/index.html +1 -0
  2. package/package.json +1 -1
  3. package/utils/cdnTransformation.js +75 -3
  4. package/utils/tests/cdnTransformation.test.js +127 -0
  5. package/v2Components/CommonTestAndPreview/UnifiedPreview/PreviewHeader.js +16 -0
  6. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberCarouselPreviewCards.js +132 -0
  7. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberPreviewContent.js +2 -37
  8. package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +169 -0
  9. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +55 -85
  10. package/v2Components/CommonTestAndPreview/UnifiedPreview/_viberCarouselPreviewCards.scss +127 -0
  11. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +52 -6
  12. package/v2Components/CommonTestAndPreview/constants.js +2 -0
  13. package/v2Components/CommonTestAndPreview/index.js +67 -3
  14. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/PreviewHeader.test.js +163 -0
  15. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WebPushPreviewContent.test.js +522 -0
  16. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +255 -0
  17. package/v2Components/CommonTestAndPreview/tests/constants.test.js +2 -1
  18. package/v2Components/CommonTestAndPreview/tests/index.test.js +194 -0
  19. package/v2Components/FormBuilder/index.js +162 -52
  20. package/v2Components/TestAndPreviewSlidebox/index.js +2 -2
  21. package/v2Containers/App/constants.js +3 -0
  22. package/v2Containers/App/tests/constants.test.js +61 -0
  23. package/v2Containers/CreativesContainer/index.js +60 -24
  24. package/v2Containers/Templates/index.js +72 -2
  25. package/v2Containers/Templates/sagas.js +6 -1
  26. package/v2Containers/Templates/tests/sagas.test.js +23 -6
  27. package/v2Containers/Templates/tests/webpush.test.js +375 -0
  28. package/v2Containers/Viber/index.js +24 -18
  29. package/v2Containers/Viber/index.scss +27 -0
  30. package/v2Containers/Viber/messages.js +4 -4
  31. package/v2Containers/WebPush/Create/index.js +91 -8
  32. package/v2Containers/WebPush/Create/index.scss +7 -0
  33. package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +348 -0
  34. package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +325 -0
package/index.html CHANGED
@@ -23,6 +23,7 @@
23
23
  <!-- End Google Tag Manager -->
24
24
  <script>try{Typekit.load({ async: true });}catch(e){console.log(e)}</script>
25
25
  <title>Capillary - Creatives</title>
26
+ <script>try{window.APP_ENV=__ENV_OBJECT__;}catch(e){window.APP_ENV={};}</script>
26
27
  </head>
27
28
  <body>
28
29
  <noscript>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.359-alpha.0",
4
+ "version": "8.0.360-alpha.0",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -422,14 +422,86 @@ export function removeAllCdnLocalStorageItems() {
422
422
  }
423
423
 
424
424
  /**
425
- *
426
- * @param {*} configsResponse
425
+ * Safe JSON.parse — returns fallback if value is empty or malformed.
426
+ * Used to parse the JSON-string env vars (qualityCfg, mapping, s3CdnMap) injected via window.APP_ENV.
427
+ */
428
+ function safeJsonParse(value, fallback) {
429
+ if (!value) return fallback;
430
+ try {
431
+ return JSON.parse(value);
432
+ } catch (e) {
433
+ return fallback;
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Reads CDN config from window.APP_ENV (injected at container startup by entrypoint.sh)
439
+ * and populates localStorage via saveCdnConfigs — same shape as the legacy API response.
440
+ *
441
+ * Returns true if env config was found and applied, false otherwise (saga falls back to API).
442
+ *
443
+ * In deployed containers, window.APP_ENV is populated → no API call. In local dev
444
+ * (`npm start`), window.APP_ENV is empty → returns false → saga falls back to the API
445
+ * (legacy path, retained for the rollout window).
446
+ *
447
+ * Closes the VAPT info-disclosure surface (CAP-183204) by eliminating the
448
+ * /getCdnTransformationConfig API response in production.
449
+ */
450
+ export function initCdnConfigFromEnv() {
451
+ try {
452
+ const env = (typeof window !== 'undefined' && window.APP_ENV) || {};
453
+
454
+ // All four fields are required to construct a CDN URL. If any is missing,
455
+ // partial config would silently make every getCdnUrl call fall through to the
456
+ // raw S3 URL. Return false so the saga falls back to the API (which may still
457
+ // have the values during the cluster-by-cluster rollout window).
458
+ if (!env.CDN_HOSTNAME
459
+ || !env.CDN_IMG_TRANSFORMATION_URL_SUFFIX
460
+ || !env.CREATIVE_ASSETS_BUCKET_PATH
461
+ || !env.S3_CDN_MAP) {
462
+ return false;
463
+ }
464
+
465
+ // Parse and validate S3_CDN_MAP before proceeding; malformed or empty map
466
+ // means CDN lookups would silently fail, so fall back to the API instead.
467
+ const parsedS3CdnMap = safeJsonParse(env.S3_CDN_MAP, null);
468
+ if (
469
+ !parsedS3CdnMap
470
+ || typeof parsedS3CdnMap !== 'object'
471
+ || Array.isArray(parsedS3CdnMap)
472
+ || !Object.keys(parsedS3CdnMap).length
473
+ ) {
474
+ return false;
475
+ }
476
+
477
+ saveCdnConfigs({
478
+ hostname: env.CDN_HOSTNAME,
479
+ transformationUrlSuffix: env.CDN_IMG_TRANSFORMATION_URL_SUFFIX,
480
+ bucketPath: env.CREATIVE_ASSETS_BUCKET_PATH,
481
+ qualityCfg: safeJsonParse(env.CDN_IMG_QUALITY_CFG, {}),
482
+ overrideEmailQuality: env.OVERRIDE_IMAGE_QUALITY === 'true',
483
+ overrideEmailQualityMapping: safeJsonParse(env.IMAGE_QUALITY_MAPPING, []),
484
+ s3CdnMap: parsedS3CdnMap,
485
+ });
486
+ return true;
487
+ } catch (e) {
488
+ Bugsnag.leaveBreadcrumb('initCdnConfigFromEnv failed to apply window.APP_ENV');
489
+ Bugsnag.notify(e, (event) => {
490
+ event.severity = 'error';
491
+ });
492
+ return false;
493
+ }
494
+ }
495
+
496
+ /**
497
+ *
498
+ * @param {*} configsResponse
427
499
  * This util function saves the getCdnConfigs response into the local storage.
428
500
  * The following response items are mapped into the following keys in local storage:
429
501
  * hostname -> CREATIVES_CDN_BASE_URL
430
502
  * qualityCfg ->CREATIVES_CDN_QUALITY_CONFIG
431
503
  * transformationUrlSuffix -> CREATIVES_CDN_TRANSFORMATION_URL_SUFFIX
432
- *
504
+ *
433
505
  * 1. If configsReponse is empty. All above mentioned keys are deleted from localstorage.
434
506
  * 2. If any one of the above keys is missing. The respective keys are deleted from localstorage.
435
507
  * 3. Else it is saved into the localstorage.
@@ -546,6 +546,133 @@ describe("cdnTransformationTests", () => {
546
546
  });
547
547
  });
548
548
 
549
+ describe("initCdnConfigFromEnv()", () => {
550
+ afterEach(() => {
551
+ delete window.APP_ENV;
552
+ });
553
+
554
+ it("returns false when window.APP_ENV is undefined", () => {
555
+ delete window.APP_ENV;
556
+ expect(cdnUtils.initCdnConfigFromEnv()).toBe(false);
557
+ });
558
+
559
+ const completeEnv = {
560
+ CDN_HOSTNAME: "https://storage.crm.n.content-cdn.io",
561
+ CDN_IMG_TRANSFORMATION_URL_SUFFIX: "cdn-cgi/image",
562
+ CREATIVE_ASSETS_BUCKET_PATH: "intouch_creative_assets",
563
+ OVERRIDE_IMAGE_QUALITY: "false",
564
+ CDN_IMG_QUALITY_CFG: "{}",
565
+ IMAGE_QUALITY_MAPPING: "[]",
566
+ S3_CDN_MAP: '{"host.s3.amazonaws.com":{"cdn_host":"cdn.example.com","bucket_path":"x"}}',
567
+ };
568
+
569
+ it.each([
570
+ ["CDN_HOSTNAME"],
571
+ ["CDN_IMG_TRANSFORMATION_URL_SUFFIX"],
572
+ ["CREATIVE_ASSETS_BUCKET_PATH"],
573
+ ["S3_CDN_MAP"],
574
+ ])("returns false when required field %s is missing", (missingKey) => {
575
+ window.APP_ENV = { ...completeEnv };
576
+ delete window.APP_ENV[missingKey];
577
+ expect(cdnUtils.initCdnConfigFromEnv()).toBe(false);
578
+ });
579
+
580
+ it.each([
581
+ ["CDN_HOSTNAME"],
582
+ ["CDN_IMG_TRANSFORMATION_URL_SUFFIX"],
583
+ ["CREATIVE_ASSETS_BUCKET_PATH"],
584
+ ["S3_CDN_MAP"],
585
+ ])("returns false when required field %s is empty string", (emptyKey) => {
586
+ window.APP_ENV = { ...completeEnv, [emptyKey]: "" };
587
+ expect(cdnUtils.initCdnConfigFromEnv()).toBe(false);
588
+ });
589
+
590
+ it("populates localStorage and returns true for a complete env", () => {
591
+ window.APP_ENV = {
592
+ CDN_HOSTNAME: "https://storage.crm.n.content-cdn.io",
593
+ CDN_IMG_TRANSFORMATION_URL_SUFFIX: "cdn-cgi/image",
594
+ CREATIVE_ASSETS_BUCKET_PATH: "intouch_creative_assets",
595
+ OVERRIDE_IMAGE_QUALITY: "true",
596
+ CDN_IMG_QUALITY_CFG: '{"EMAIL":65,"DEFAULT":70}',
597
+ IMAGE_QUALITY_MAPPING: '[[30,90],[80,85]]',
598
+ S3_CDN_MAP: '{"host.s3.amazonaws.com":{"cdn_host":"cdn.example.com","bucket_path":"x"}}',
599
+ };
600
+
601
+ expect(cdnUtils.initCdnConfigFromEnv()).toBe(true);
602
+ expect(localStorage.getItem("CREATIVES_CDN_BASE_URL")).toBe("https://storage.crm.n.content-cdn.io");
603
+ expect(localStorage.getItem("CREATIVES_CDN_TRANSFORMATION_URL_SUFFIX")).toBe("cdn-cgi/image");
604
+ expect(localStorage.getItem("CREATIVES_S3_BUCKET_PATH")).toBe("intouch_creative_assets");
605
+ expect(localStorage.getItem("CREATIVES_CDN_QUALITY_CONFIG")).toBe('{"EMAIL":65,"DEFAULT":70}');
606
+ expect(localStorage.getItem("CREATIVES_CDN_OVERRIDE_DEFAULT_EMAIL_QUALITY")).toBe("true");
607
+ expect(localStorage.getItem("CREATIVES_CDN_OVERRIDE_DEFAULT_EMAIL_QUALITY_MAPPING")).toBe("[[30,90],[80,85]]");
608
+ expect(JSON.parse(localStorage.getItem("S3_CDN_MAP"))).toEqual({
609
+ "host.s3.amazonaws.com": { cdn_host: "cdn.example.com", bucket_path: "x" },
610
+ });
611
+ });
612
+
613
+ it("uses fallback values for malformed optional JSON env vars (qualityCfg, mapping)", () => {
614
+ window.APP_ENV = {
615
+ CDN_HOSTNAME: "https://storage.crm.n.content-cdn.io",
616
+ CDN_IMG_TRANSFORMATION_URL_SUFFIX: "cdn-cgi/image",
617
+ CREATIVE_ASSETS_BUCKET_PATH: "intouch_creative_assets",
618
+ OVERRIDE_IMAGE_QUALITY: "false",
619
+ CDN_IMG_QUALITY_CFG: "not-valid-json{{",
620
+ IMAGE_QUALITY_MAPPING: "also-broken",
621
+ S3_CDN_MAP: '{"host.s3.amazonaws.com":{"cdn_host":"cdn.example.com","bucket_path":"x"}}',
622
+ };
623
+
624
+ expect(cdnUtils.initCdnConfigFromEnv()).toBe(true);
625
+ expect(localStorage.getItem("CREATIVES_CDN_BASE_URL")).toBe("https://storage.crm.n.content-cdn.io");
626
+ });
627
+
628
+ it("returns false when S3_CDN_MAP is malformed JSON", () => {
629
+ window.APP_ENV = { ...completeEnv, S3_CDN_MAP: "}{" };
630
+ expect(cdnUtils.initCdnConfigFromEnv()).toBe(false);
631
+ });
632
+
633
+ it.each([
634
+ ["empty object", "{}"],
635
+ ["array", '["a","b"]'],
636
+ ["null", "null"],
637
+ ["number", "42"],
638
+ ["string", '"foo"'],
639
+ ])("returns false when S3_CDN_MAP parses to %s (not a non-empty object)", (_label, value) => {
640
+ window.APP_ENV = { ...completeEnv, S3_CDN_MAP: value };
641
+ expect(cdnUtils.initCdnConfigFromEnv()).toBe(false);
642
+ });
643
+
644
+ it("treats OVERRIDE_IMAGE_QUALITY values other than the string \"true\" as false", () => {
645
+ window.APP_ENV = {
646
+ CDN_HOSTNAME: "https://storage.crm.n.content-cdn.io",
647
+ CDN_IMG_TRANSFORMATION_URL_SUFFIX: "cdn-cgi/image",
648
+ CREATIVE_ASSETS_BUCKET_PATH: "intouch_creative_assets",
649
+ OVERRIDE_IMAGE_QUALITY: "no",
650
+ CDN_IMG_QUALITY_CFG: "{}",
651
+ IMAGE_QUALITY_MAPPING: "[]",
652
+ S3_CDN_MAP: '{"host.s3.amazonaws.com":{"cdn_host":"cdn.example.com","bucket_path":"x"}}',
653
+ };
654
+
655
+ cdnUtils.initCdnConfigFromEnv();
656
+ expect(localStorage.getItem("CREATIVES_CDN_OVERRIDE_DEFAULT_EMAIL_QUALITY")).toBe(undefined);
657
+ });
658
+
659
+ it("returns false and notifies Bugsnag when window.APP_ENV access throws", () => {
660
+ // Force the `window.APP_ENV` read inside initCdnConfigFromEnv to throw,
661
+ // which exercises the function's outer catch block.
662
+ Object.defineProperty(window, "APP_ENV", {
663
+ get() { throw new Error("APP_ENV access boom"); },
664
+ configurable: true,
665
+ });
666
+ const breadcrumbSpy = jest.spyOn(Bugsnag, "leaveBreadcrumb");
667
+
668
+ expect(cdnUtils.initCdnConfigFromEnv()).toBe(false);
669
+ expect(breadcrumbSpy).toHaveBeenCalledWith(
670
+ "initCdnConfigFromEnv failed to apply window.APP_ENV"
671
+ );
672
+ expect(bugsnagSpy).toHaveBeenCalled();
673
+ });
674
+ });
675
+
549
676
  describe("getEmailImageOverrideQuality()",()=>{
550
677
  it("Should return quality of last element in array if file size is greater than max provided in array",()=>{
551
678
  localStorage.setItem("CREATIVES_CDN_OVERRIDE_DEFAULT_EMAIL_QUALITY", "true");
@@ -23,6 +23,7 @@ const PreviewHeader = ({
23
23
  showDeviceToggle,
24
24
  onDeviceChange,
25
25
  channel,
26
+ setIsFullscreenOpen,
26
27
  }) => {
27
28
  // Determine if this is SMS, WhatsApp, RCS, InApp, MobilePush, or Viber channel (uses Android/iOS) or other channels (uses Desktop/Mobile)
28
29
  const isSmsChannel = channel === CHANNELS.SMS;
@@ -31,8 +32,13 @@ const PreviewHeader = ({
31
32
  const isInAppChannel = channel === CHANNELS.INAPP;
32
33
  const isMobilePushChannel = channel === CHANNELS.MOBILEPUSH;
33
34
  const isViberChannel = channel === CHANNELS.VIBER;
35
+ const isWebPushChannel = channel === CHANNELS.WEBPUSH;
34
36
  const isAndroidIosToggle = isSmsChannel || isWhatsappChannel || isRcsChannel || isInAppChannel || isMobilePushChannel || isViberChannel;
35
37
 
38
+ const handleOpenFullscreen = () => {
39
+ setIsFullscreenOpen(true);
40
+ };
41
+
36
42
  return (
37
43
  <CapRow className="preview-chrome">
38
44
  <div className="preview-header">
@@ -80,6 +86,12 @@ const PreviewHeader = ({
80
86
  )}
81
87
  </CapRow>
82
88
  )}
89
+ {isWebPushChannel && (
90
+ <CapIcon
91
+ type="expander"
92
+ onClick={() => handleOpenFullscreen()}
93
+ />
94
+ )}
83
95
  </div>
84
96
  </CapRow>
85
97
  );
@@ -95,6 +107,8 @@ PreviewHeader.propTypes = {
95
107
  showDeviceToggle: PropTypes.bool,
96
108
  onDeviceChange: PropTypes.func,
97
109
  channel: PropTypes.string,
110
+ isFullscreenOpen: PropTypes.bool,
111
+ setIsFullscreenOpen: PropTypes.func,
98
112
  };
99
113
 
100
114
  PreviewHeader.defaultProps = {
@@ -103,6 +117,8 @@ PreviewHeader.defaultProps = {
103
117
  showDeviceToggle: false,
104
118
  onDeviceChange: () => {},
105
119
  channel: null,
120
+ isFullscreenOpen: false,
121
+ setIsFullscreenOpen: () => {},
106
122
  };
107
123
 
108
124
  export default PreviewHeader;
@@ -0,0 +1,132 @@
1
+ import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
4
+ import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
5
+ import CapImage from '@capillarytech/cap-ui-library/CapImage';
6
+
7
+ const getTrimmedText = (value = '') => (value ?? '').trim();
8
+ const hasTrimmedText = (value = '') => Boolean(getTrimmedText(value));
9
+
10
+ const getCarouselButtonsWithTitles = (card) => (
11
+ (card?.buttons ?? []).filter((button) => hasTrimmedText(button?.title)).slice(0, 2)
12
+ );
13
+
14
+ const getCarouselButtonSlotCount = (cards = []) => {
15
+ const buttonCounts = cards.map((card) => getCarouselButtonsWithTitles(card).length);
16
+ return Math.min(2, Math.max(0, ...buttonCounts, 0));
17
+ };
18
+
19
+ const ViberCarouselPreviewCards = ({ cards = [] }) => {
20
+ const previewCards = Array.isArray(cards) && cards.length ? cards : [{}];
21
+ const carouselButtonSlotCount = useMemo(
22
+ () => getCarouselButtonSlotCount(previewCards),
23
+ [previewCards],
24
+ );
25
+ const carouselTitleRefs = useRef([]);
26
+ const [carouselTitleLineCount, setCarouselTitleLineCount] = useState(1);
27
+
28
+ carouselTitleRefs.current = previewCards.map(() => null);
29
+
30
+ useLayoutEffect(() => {
31
+ let maxLines = 1;
32
+ carouselTitleRefs.current.forEach((node) => {
33
+ if (!node) {
34
+ return;
35
+ }
36
+ const { lineHeight } = window.getComputedStyle(node);
37
+ const parsedLineHeight = parseFloat(lineHeight) || 18;
38
+ const lines = Math.min(2, Math.max(1, Math.round(node.scrollHeight / parsedLineHeight)));
39
+ if (lines > maxLines) {
40
+ maxLines = lines;
41
+ }
42
+ });
43
+ setCarouselTitleLineCount((prev) => (prev === maxLines ? prev : maxLines));
44
+ }, [previewCards]);
45
+
46
+ return (
47
+ <CapRow className="viber-carousel-preview-scroll">
48
+ {previewCards.map((card, index) => {
49
+ const cardButtons = getCarouselButtonsWithTitles(card);
50
+ const titleLineClass = `viber-carousel-preview-text-wrap--${carouselTitleLineCount}-line`;
51
+
52
+ return (
53
+ <CapRow className="viber-carousel-preview-card" key={`viber-carousel-preview-card-${index}`}>
54
+ {hasTrimmedText(card?.mediaUrl) ? (
55
+ <CapImage
56
+ src={card?.mediaUrl}
57
+ className="viber-carousel-preview-image"
58
+ alt="Viber carousel card"
59
+ />
60
+ ) : (
61
+ <CapRow className="viber-carousel-preview-image-placeholder" />
62
+ )}
63
+ <CapRow className="viber-carousel-preview-card-body">
64
+ <CapRow className={`viber-carousel-preview-text-wrap ${titleLineClass}`}>
65
+ {hasTrimmedText(card?.text) ? (
66
+ <div
67
+ className="viber-carousel-preview-text-inner"
68
+ ref={(node) => {
69
+ carouselTitleRefs.current[index] = node;
70
+ }}
71
+ >
72
+ <CapLabel type="label15" className="viber-carousel-preview-text">
73
+ {card?.text}
74
+ </CapLabel>
75
+ </div>
76
+ ) : (
77
+ <CapLabel type="label15" className="viber-carousel-preview-text-placeholder" />
78
+ )}
79
+ </CapRow>
80
+ {carouselButtonSlotCount > 0 && (
81
+ <CapRow className="viber-carousel-preview-buttons">
82
+ {Array.from({ length: carouselButtonSlotCount }).map((_, btnIndex) => {
83
+ const cardButton = cardButtons[btnIndex];
84
+ const trimmedCardButtonTitle = cardButton
85
+ ? getTrimmedText(cardButton?.title)
86
+ : '';
87
+
88
+ if (!trimmedCardButtonTitle) {
89
+ return (
90
+ <CapRow
91
+ className="viber-carousel-preview-button viber-carousel-preview-button-placeholder"
92
+ key={`viber-carousel-preview-btn-placeholder-${index}-${btnIndex}`}
93
+ aria-hidden="true"
94
+ />
95
+ );
96
+ }
97
+
98
+ return (
99
+ <CapLabel
100
+ className={`viber-carousel-preview-button ${btnIndex === 1 ? 'viber-carousel-preview-button-secondary' : ''}`}
101
+ key={`viber-carousel-preview-btn-${index}-${btnIndex}-${trimmedCardButtonTitle}`}
102
+ >
103
+ {trimmedCardButtonTitle}
104
+ </CapLabel>
105
+ );
106
+ })}
107
+ </CapRow>
108
+ )}
109
+ </CapRow>
110
+ </CapRow>
111
+ );
112
+ })}
113
+ </CapRow>
114
+ );
115
+ };
116
+
117
+ ViberCarouselPreviewCards.propTypes = {
118
+ cards: PropTypes.arrayOf(PropTypes.shape({
119
+ text: PropTypes.string,
120
+ mediaUrl: PropTypes.string,
121
+ buttons: PropTypes.arrayOf(PropTypes.shape({
122
+ title: PropTypes.string,
123
+ action: PropTypes.string,
124
+ })),
125
+ })),
126
+ };
127
+
128
+ ViberCarouselPreviewCards.defaultProps = {
129
+ cards: [],
130
+ };
131
+
132
+ export default ViberCarouselPreviewCards;
@@ -24,6 +24,7 @@ import {
24
24
  } from '../constants';
25
25
  import messages from '../messages';
26
26
  import videoPlay from '../../../assets/videoPlay.svg';
27
+ import ViberCarouselPreviewCards from './ViberCarouselPreviewCards';
27
28
 
28
29
  // Import device mockup images (same as SMS)
29
30
  const smsMobileAndroid = require('../../../assets/Android.png');
@@ -244,43 +245,7 @@ const ViberPreviewContent = ({
244
245
  </CapRow>
245
246
 
246
247
  <CapRow className="viber-carousel-cards-pop">
247
- <CapRow className="viber-carousel-preview-scroll">
248
- {previewCarouselCards?.map((card, index) => (
249
- <CapRow className="viber-carousel-preview-card" key={`viber-carousel-preview-card-${index}`}>
250
- {hasTrimmedText(card?.mediaUrl) ? (
251
- <CapImage
252
- src={card?.mediaUrl}
253
- className="viber-carousel-preview-image"
254
- alt="Viber carousel card"
255
- />
256
- ) : (
257
- <CapRow className="viber-carousel-preview-image-placeholder" />
258
- )}
259
- <CapRow className="viber-carousel-preview-card-body">
260
- {hasTrimmedText(card?.text) ? (
261
- <CapLabel type="label15" className="viber-carousel-preview-text">
262
- {card?.text}
263
- </CapLabel>
264
- ) : (
265
- <CapLabel type="label15" className="viber-carousel-preview-text-placeholder" />
266
- )}
267
- {(card?.buttons?.filter((cardButton) => hasTrimmedText(cardButton?.title)) ?? [])
268
- .slice(0, 2)
269
- .map((cardButton, btnIndex) => {
270
- const trimmedCardButtonTitle = getTrimmedText(cardButton?.title);
271
- return (
272
- <CapLabel
273
- className={`viber-carousel-preview-button ${btnIndex === 1 ? 'viber-carousel-preview-button-secondary' : ''}`}
274
- key={`viber-carousel-preview-btn-${index}-${btnIndex}-${trimmedCardButtonTitle}`}
275
- >
276
- {trimmedCardButtonTitle}
277
- </CapLabel>
278
- );
279
- })}
280
- </CapRow>
281
- </CapRow>
282
- ))}
283
- </CapRow>
248
+ <ViberCarouselPreviewCards cards={previewCarouselCards} />
284
249
  </CapRow>
285
250
  </>
286
251
  )}
@@ -0,0 +1,169 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
5
+ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
6
+ import CapModal from '@capillarytech/cap-ui-library/CapModal';
7
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
8
+ import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
9
+ import PreviewControls from '../../../v2Containers/WebPush/Create/preview/PreviewControls';
10
+ import PreviewContent from '../../../v2Containers/WebPush/Create/preview/PreviewContent';
11
+ import DevicePreviewContent from '../../../v2Containers/WebPush/Create/preview/DevicePreviewContent';
12
+ import {
13
+ OS_OPTIONS,
14
+ DEFAULT_OS,
15
+ DEFAULT_BROWSER,
16
+ } from '../../../v2Containers/WebPush/Create/preview/constants';
17
+ import { getBrowserOptionsForOS } from '../../../v2Containers/WebPush/Create/preview/config/notificationMappings';
18
+ import messages from '../messages';
19
+
20
+ const WebPushPreviewContent = ({
21
+ notificationTitle,
22
+ notificationBody,
23
+ imageSrc,
24
+ brandIconSrc,
25
+ buttons,
26
+ url,
27
+ isUpdating,
28
+ error,
29
+ isFullscreenOpen,
30
+ setIsFullscreenOpen,
31
+ selectedCustomer,
32
+ }) => {
33
+ const [selectedOS, setSelectedOS] = useState(DEFAULT_OS);
34
+ const [selectedBrowser, setSelectedBrowser] = useState(DEFAULT_BROWSER);
35
+
36
+ const browserOptionsForOS = getBrowserOptionsForOS(selectedOS);
37
+
38
+ // Coerce browser when OS change removes current browser from available options
39
+ useEffect(() => {
40
+ const isValid = browserOptionsForOS.some((opt) => opt.value === selectedBrowser);
41
+ if (!isValid && browserOptionsForOS.length) {
42
+ setSelectedBrowser(browserOptionsForOS[0].value);
43
+ }
44
+ }, [browserOptionsForOS, selectedBrowser]);
45
+
46
+ const handleOSChange = (value) => {
47
+ setSelectedOS(value);
48
+ };
49
+
50
+ const handleBrowserChange = (value) => {
51
+ setSelectedBrowser(value);
52
+ };
53
+
54
+ const handleCloseFullscreen = () => {
55
+ setIsFullscreenOpen(false);
56
+ };
57
+
58
+ if (isUpdating) {
59
+ return null;
60
+ }
61
+
62
+ if (error) {
63
+ return null;
64
+ }
65
+
66
+ return (
67
+ <CapRow className="webpush-test-preview-container webpush-preview-container">
68
+ <CapRow className="webpush-preview-panel">
69
+ <PreviewControls
70
+ selectedOS={selectedOS}
71
+ selectedBrowser={selectedBrowser}
72
+ osOptions={OS_OPTIONS}
73
+ browserOptions={browserOptionsForOS}
74
+ onOSChange={handleOSChange}
75
+ onBrowserChange={handleBrowserChange}
76
+ layoutMode="newRow"
77
+ showStateDropdown={false}
78
+ />
79
+
80
+ <PreviewContent
81
+ notificationTitle={notificationTitle}
82
+ notificationBody={notificationBody}
83
+ url={url}
84
+ selectedOS={selectedOS}
85
+ selectedBrowser={selectedBrowser}
86
+ notificationState="Collapsed"
87
+ imageSrc={imageSrc}
88
+ brandIconSrc={brandIconSrc}
89
+ buttons={buttons}
90
+ />
91
+ </CapRow>
92
+
93
+ <CapModal
94
+ visible={isFullscreenOpen}
95
+ onCancel={handleCloseFullscreen}
96
+ width="90%"
97
+ centered
98
+ closable={false}
99
+ footer={null}
100
+ title={null}
101
+ maskClosable
102
+ className="webpush-fullscreen-modal webpush-preview-container"
103
+ >
104
+ <CapRow className="webpush-preview-header">
105
+ <div className="webpush-heading-container">
106
+ <CapRow type="flex" className="preview-for">
107
+ <CapLabel type="label16">
108
+ <FormattedMessage {...messages.previewFor} />
109
+ </CapLabel>
110
+ <CapLabel type="label2">
111
+ {selectedCustomer ? selectedCustomer.name : <FormattedMessage {...messages.defaultPreview} />}
112
+ </CapLabel>
113
+ </CapRow>
114
+ <CapIcon
115
+ type="collapse2"
116
+ onClick={() => handleCloseFullscreen()}
117
+ className="webpush-fullscreen-close-icon"
118
+ />
119
+ </div>
120
+ </CapRow>
121
+ <CapDivider className="webpush-fullscreen-divider" />
122
+ <DevicePreviewContent
123
+ notificationTitle={notificationTitle}
124
+ notificationBody={notificationBody}
125
+ url={url}
126
+ imageSrc={imageSrc}
127
+ brandIconSrc={brandIconSrc}
128
+ buttons={buttons}
129
+ />
130
+ </CapModal>
131
+ </CapRow>
132
+ );
133
+ };
134
+
135
+ WebPushPreviewContent.propTypes = {
136
+ notificationTitle: PropTypes.string,
137
+ notificationBody: PropTypes.string,
138
+ imageSrc: PropTypes.string,
139
+ brandIconSrc: PropTypes.string,
140
+ buttons: PropTypes.arrayOf(
141
+ PropTypes.shape({
142
+ text: PropTypes.string,
143
+ url: PropTypes.string,
144
+ type: PropTypes.string,
145
+ })
146
+ ),
147
+ url: PropTypes.string,
148
+ isUpdating: PropTypes.bool,
149
+ error: PropTypes.string,
150
+ isFullscreenOpen: PropTypes.bool,
151
+ setIsFullscreenOpen: PropTypes.func,
152
+ selectedCustomer: PropTypes.object,
153
+ };
154
+
155
+ WebPushPreviewContent.defaultProps = {
156
+ notificationTitle: '',
157
+ notificationBody: '',
158
+ imageSrc: '',
159
+ brandIconSrc: '',
160
+ buttons: [],
161
+ url: '',
162
+ isUpdating: false,
163
+ error: null,
164
+ isFullscreenOpen: false,
165
+ setIsFullscreenOpen: () => {},
166
+ selectedCustomer: null,
167
+ };
168
+
169
+ export default WebPushPreviewContent;