diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/Slide.tsx b/apps/ledger-live-desktop/src/renderer/components/Carousel/Slide.tsx deleted file mode 100644 index 02c5a370477f..000000000000 --- a/apps/ledger-live-desktop/src/renderer/components/Carousel/Slide.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import React, { useCallback, useEffect, useRef } from "react"; -import styled from "styled-components"; -import { useSpring, animated } from "react-spring"; -import { openURL } from "~/renderer/linking"; -import Box from "~/renderer/components/Box"; -import Text from "~/renderer/components/Text"; -import { Wrapper, Label, IllustrationWrapper } from "~/renderer/components/Carousel"; -import { useHistory } from "react-router-dom"; -import Image from "../Image"; -import { track } from "~/renderer/analytics/segment"; -import { ContentCard } from "~/types/dynamicContent"; - -const Layer = styled(animated.div)<{ - image: string; - width: number; - height: number; -}>` - // prettier-ignore - background-image: url('${p => p.image}'); - background-size: contain; - background-position: center center; - background-repeat: no-repeat; - will-change: transform; - position: absolute; - width: ${p => p.width}px; - height: ${p => p.height}px; - transform-origin: top left; -`; - -const EllipsedText = styled(Text)` - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -`; - -type Props = ContentCard; - -const Slide = ({ id, url, path, title, description, image, imgs, onClickOnSlide }: Props) => { - const history = useHistory(); - const [{ xy }, set] = useSpring<{ - xy: number[]; - config: { mass: number; tension: number; friction: number }; - }>(() => ({ - xy: [-120, -30], - config: { mass: 10, tension: 550, friction: 140 }, - })); - const getTransform = (offsetX: number, effectX: number, offsetY: number, effectY: number) => ({ - transform: xy.interpolate( - // @ts-expect-error react-spring types are broken - (x, y) => `translate3d(${x / effectX + offsetX}px,${y / effectY + offsetY}px, 0)`, - ), - }); - - const ref = useRef(null); - - // React to the user mouse movement inside the banner for parallax effect - const onMouseMove = (e: React.MouseEvent) => { - if (!ref.current) return; - const rect = ref.current.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - set({ xy: [x - rect.width / 2, y - rect.height / 2] }); - }; - - const onMouseLeave = () => set({ xy: [0, 0] }); - - const onClick = useCallback(() => { - if (onClickOnSlide) { - onClickOnSlide(id); - } - if (path) { - history.push({ pathname: path, state: { source: "banner" } }); - return; - } - if (url) { - openURL(url); - } - track("contentcard_clicked", { - contentcard: title, - link: path || url, - campaign: id, - page: "Portfolio", - }); - }, [history, id, path, title, url, onClickOnSlide]); - - // After initial slide-in animation, set the offset to zero - useEffect(() => { - setTimeout(() => { - set({ xy: [0, 0] }); - }, 400); - }, [set]); - - return ( - - - - - {description} - - - {imgs && ( - - {imgs.map(({ source, transform, size }, i) => ( - - ))} - - )} - {image && ( - - - - )} - - ); -}; - -export default Slide; diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/TimeBasedProgressBar.tsx b/apps/ledger-live-desktop/src/renderer/components/Carousel/TimeBasedProgressBar.tsx deleted file mode 100644 index ad4d45c99d97..000000000000 --- a/apps/ledger-live-desktop/src/renderer/components/Carousel/TimeBasedProgressBar.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { useSpring, animated } from "react-spring"; - -const easing = (t: number) => t; // linear easing function - -type Props = { - duration: number; - onComplete: () => void; - paused?: boolean; -}; - -const TimeBasedProgressBar = ({ duration, onComplete, paused }: Props) => { - const [outOfFocusPaused, setOutOfFocusPaused] = useState(false); - const progress = useSpring({ - from: { value: 0 }, - to: { value: 1 }, - config: { duration, easing }, - delay: duration, - pause: paused || outOfFocusPaused, - onRest: () => { - if (!outOfFocusPaused) { - onComplete(); - } - }, - }); - - const onWindowFocus = useCallback(() => { - setOutOfFocusPaused(false); - }, []); - - const onWindowBlur = useCallback(() => { - setOutOfFocusPaused(true); - }, []); - - useEffect(() => { - window.addEventListener("focus", onWindowFocus); - window.addEventListener("blur", onWindowBlur); - return () => { - window.removeEventListener("focus", onWindowFocus); - window.removeEventListener("blur", onWindowBlur); - }; - }, [onWindowFocus, onWindowBlur]); - - return ( -
- {!paused && !outOfFocusPaused && ( - `scaleX(${v})`), - transformOrigin: "left center", - }} - /> - )} -
- ); -}; - -export default TimeBasedProgressBar; diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/bg.png b/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/bg.png deleted file mode 100644 index 9b623051cd00..000000000000 Binary files a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/bg.png and /dev/null differ diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/cart.png b/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/cart.png deleted file mode 100644 index f0107ba95b98..000000000000 Binary files a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/cart.png and /dev/null differ diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/coin.png b/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/coin.png deleted file mode 100644 index c16bf58cbb28..000000000000 Binary files a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/coin.png and /dev/null differ diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/coin2.png b/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/coin2.png deleted file mode 100644 index 9e821404ad22..000000000000 Binary files a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/coin2.png and /dev/null differ diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/coin3.png b/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/coin3.png deleted file mode 100644 index 817143d8408a..000000000000 Binary files a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/BuyCrypto/images/coin3.png and /dev/null differ diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/bg.png b/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/bg.png deleted file mode 100644 index 956fae269f1c..000000000000 Binary files a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/bg.png and /dev/null differ diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/coin1.png b/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/coin1.png deleted file mode 100644 index e14d1a4b56c0..000000000000 Binary files a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/coin1.png and /dev/null differ diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/coin2.png b/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/coin2.png deleted file mode 100644 index 92a78a671e18..000000000000 Binary files a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/coin2.png and /dev/null differ diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/loop.png b/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/loop.png deleted file mode 100644 index 435ce8bef2f2..000000000000 Binary files a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/loop.png and /dev/null differ diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/smallcoin1.png b/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/smallcoin1.png deleted file mode 100644 index 36c519984691..000000000000 Binary files a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/smallcoin1.png and /dev/null differ diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/smallcoin2.png b/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/smallcoin2.png deleted file mode 100644 index 00b84c6b8850..000000000000 Binary files a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/smallcoin2.png and /dev/null differ diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/smallcoin3.png b/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/smallcoin3.png deleted file mode 100644 index 7004064a20e0..000000000000 Binary files a/apps/ledger-live-desktop/src/renderer/components/Carousel/banners/Swap/images/smallcoin3.png and /dev/null differ diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/helpers.tsx b/apps/ledger-live-desktop/src/renderer/components/Carousel/helpers.tsx deleted file mode 100644 index 1f88a25cad59..000000000000 --- a/apps/ledger-live-desktop/src/renderer/components/Carousel/helpers.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import map from "lodash/map"; -import Slide from "./Slide"; -import { portfolioContentCardSelector } from "~/renderer/reducers/dynamicContent"; -import { useDispatch, useSelector } from "react-redux"; -import * as braze from "@braze/web-sdk"; -import { ContentCard } from "~/types/dynamicContent"; -import { setPortfolioCards } from "~/renderer/actions/dynamicContent"; - -export const getTransitions = (transition: "slide" | "flip", reverse = false) => { - const mult = reverse ? -1 : 1; - return { - flip: { - from: { - opacity: 1, - transform: `rotateX(${180 * mult}deg)`, - }, - enter: { - opacity: 1, - transform: "rotateX(0deg)", - }, - leave: { - opacity: 1, - transform: `rotateX(${-180 * mult}deg)`, - }, - config: { mass: 20, tension: 200, friction: 100 }, - }, - slide: { - from: { - opacity: 1, - transform: `translate3d(${100 * mult}%,0,0)`, - }, - enter: { - opacity: 1, - transform: "translate3d(0%,0,0)", - }, - leave: { - opacity: 1, - transform: `translate3d((${-100 * mult}%,0,0)`, - }, - initial: null, - }, - }[transition]; -}; - -type SlideRes = { - id: string; - Component: React.ComponentType<{}>; -}; - -export const useDefaultSlides = (): { - slides: SlideRes[]; - logSlideImpression: (index: number) => void; - dismissCard: (index: number) => void; -} => { - const [cachedContentCards, setCachedContentCards] = useState([]); - const portfolioCards = useSelector(portfolioContentCardSelector); - const dispatch = useDispatch(); - - useEffect(() => { - const cards = braze.getCachedContentCards().cards; - setCachedContentCards(cards); - }, []); - - const logSlideImpression = useCallback( - (index: number) => { - if (portfolioCards && portfolioCards.length > index) { - const slide = portfolioCards[index]; - if (slide?.id) { - const currentCard = cachedContentCards.find(card => card.id === slide.id); - - if (currentCard) { - braze.logContentCardImpressions([currentCard]); - } - } - } - }, - [portfolioCards, cachedContentCards], - ); - - const dismissCard = useCallback( - (index: number) => { - if (portfolioCards && portfolioCards.length > index) { - const slide = portfolioCards[index]; - if (slide?.id) { - const currentCard = cachedContentCards.find(card => card.id === slide.id); - - if (currentCard) { - braze.logCardDismissal(currentCard); - setCachedContentCards(cachedContentCards.filter(n => n.id !== currentCard.id)); - dispatch(setPortfolioCards(portfolioCards.filter(n => n.id !== slide.id))); - } - } - } - }, - [portfolioCards, cachedContentCards, dispatch], - ); - - const logSlideClick = useCallback( - (cardId: string) => { - const currentCard = cachedContentCards.find(card => card.id === cardId); - - if (currentCard) { - // For some reason braze won't log the click event if the card url is empty - // Setting it as the card id just to have a dummy non empty value - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - currentCard.url = currentCard.id; - braze.logContentCardClick(currentCard); - } - }, - [cachedContentCards], - ); - const slides = useMemo( - () => - map( - portfolioCards, - (slide): SlideRes => ({ - id: slide.id, - // eslint-disable-next-line react/display-name - Component: () => , - }), - ), - [portfolioCards, logSlideClick], - ); - - return { - slides, - logSlideImpression, - dismissCard, - }; -}; diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/index.tsx b/apps/ledger-live-desktop/src/renderer/components/Carousel/index.tsx deleted file mode 100644 index bf460fc3fd3a..000000000000 --- a/apps/ledger-live-desktop/src/renderer/components/Carousel/index.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import styled from "styled-components"; -import { useTransition, animated } from "react-spring"; -import IconArrowRight from "~/renderer/icons/ArrowRight"; -import { Card } from "~/renderer/components/Box"; -import Text from "~/renderer/components/Text"; -import TimeBasedProgressBar from "~/renderer/components/Carousel/TimeBasedProgressBar"; -import IconCross from "~/renderer/icons/Cross"; -import { getTransitions, useDefaultSlides } from "~/renderer/components/Carousel/helpers"; -import { track } from "~/renderer/analytics/segment"; - -const CarouselWrapper = styled(Card)` - position: relative; - height: 100px; - margin: 20px 0; -`; - -const Close = styled.div` - position: absolute; - color: ${p => p.theme.colors.palette.text.shade30}; - top: 16px; - right: 16px; - cursor: pointer; - &:hover { - color: ${p => p.theme.colors.palette.text.shade100}; - } -`; - -const Previous = styled.div` - position: absolute; - color: ${p => p.theme.colors.palette.text.shade30}; - bottom: 16px; - right: 42px; - cursor: pointer; - transform: rotate(180deg); - &:hover { - color: ${p => p.theme.colors.palette.text.shade100}; - } -`; - -const Next = styled.div` - position: absolute; - color: ${p => p.theme.colors.palette.text.shade30}; - bottom: 11px; - right: 16px; - cursor: pointer; - &:hover { - color: ${p => p.theme.colors.palette.text.shade100}; - } -`; - -// NB left here because it handles the transitions -const ProgressBarWrapper = styled.div` - position: absolute; - bottom: 0; - z-index: 100; - width: 100%; - display: none; -`; - -const Bullets = styled.div<{ index: number }>` - position: absolute; - bottom: 16px; - left: 0; - right: 0; - display: flex; - justify-content: center; - & > div { - cursor: pointer; - & > span { - display: block; - border-radius: 6px; - height: 6px; - width: 6px; - background: ${p => p.theme.colors.palette.text.shade40}; - } - padding: 15px 5px; - margin-bottom: -15px; - &:nth-child(${p => p.index + 1}) > span { - background: ${p => p.theme.colors.palette.text.shade80}; - } - } -`; - -const Slides = styled.div` - width: 100%; - height: 100px; - border-radius: 4px; - perspective: 1000px; - overflow: hidden; - background: ${p => p.theme.colors.palette.background.paper}; - - & > div { - transform-origin: center right; - backface-visibility: hidden; - transform-style: preserve-3d; - position: absolute; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - color: white; - font-weight: 800; - font-size: 8em; - will-change: transform, opacity; - } -`; - -export const Label = styled(Text)` - color: ${p => p.theme.colors.palette.text.shade100}; - margin-bottom: 8px; - text-transform: uppercase; - letter-spacing: 0.1em; - - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; -`; - -export const IllustrationWrapper = styled.div` - width: 257px; - height: 100%; - pointer-events: none; - position: relative; - right: 0; - align-self: flex-end; -`; - -export const Wrapper = styled.div` - width: 100%; - height: 100px; - overflow: hidden; - display: flex; - flex-direction: row; - cursor: pointer; -`; - -const Carousel = ({ - withArrows = true, - controls = true, - speed = 6000, - type = "slide", -}: { - withArrows?: boolean; - controls?: boolean; - speed?: number; - type?: "slide" | "flip"; -}) => { - const { slides, logSlideImpression, dismissCard } = useDefaultSlides(); - const [index, setIndex] = useState(0); - const [paused, setPaused] = useState(false); - const [reverse, setReverse] = useState(false); - const transitions = useTransition(index, p => p, getTransitions(type, reverse)); - - useEffect(() => { - logSlideImpression(0); - }, [logSlideImpression]); - - const changeVisibleSlide = useCallback( - (index: number) => { - setIndex(index); - logSlideImpression(index); - }, - [logSlideImpression], - ); - - const onChooseSlide = useCallback( - (newIndex: number) => { - setReverse(index > newIndex); - changeVisibleSlide(newIndex); - }, - [index, changeVisibleSlide], - ); - - const onDismiss = useCallback(() => { - track("contentcard_dismissed", { - card: slides[index].id, - page: "Portfolio", - }); - dismissCard(index); - changeVisibleSlide((index + 1) % slides.length); - }, [index, slides, dismissCard, changeVisibleSlide]); - - const onNext = useCallback(() => { - setReverse(false); - changeVisibleSlide((index + 1) % slides.length); - track("contentcards_slide", { - button: "next", - page: "Portfolio", - }); - }, [index, slides.length, changeVisibleSlide]); - - const onPrev = useCallback(() => { - setReverse(true); - changeVisibleSlide(!index ? slides.length - 1 : index - 1); - track("contentcards_slide", { - button: "previous", - page: "Portfolio", - }); - }, [index, slides.length, changeVisibleSlide]); - - if (!slides.length) { - // No slides or dismissed, no problem - return null; - } - - const showControls = controls && slides.length > 1; - - return ( - setPaused(true)} - onMouseLeave={() => setPaused(false)} - > - {slides.length > 1 ? ( - - - - ) : null} - - {transitions.map(({ item, props, key }) => { - if (!slides?.[item]) return null; - - const { Component } = slides[item]; - return ( - - - - ); - })} - - - - - {showControls ? ( - <> - {withArrows ? ( - <> - - - - - - - - ) : ( - - {slides.map((_, i) => ( -
onChooseSlide(i)}> - -
- ))} -
- )} - - ) : null} -
- ); -}; - -export default Carousel; diff --git a/apps/ledger-live-desktop/src/renderer/components/ContentCards/ActionCard/components.tsx b/apps/ledger-live-desktop/src/renderer/components/ContentCards/ActionCard/components.tsx new file mode 100644 index 000000000000..7a5022ccd7e1 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/components/ContentCards/ActionCard/components.tsx @@ -0,0 +1,48 @@ +import styled from "styled-components"; + +const CardContainer = styled.div` + height: 64px; + padding: 0px 16px 0px 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +`; + +const Header = styled.img` + height: 36px; + width: 36px; + border-radius: 1000px; +`; + +const Body = styled.div` + height: 40px; + flex-grow: 1; + flex-basis: 0; +`; + +const Title = styled.div` + color: white; + font-weight: 600; + font-size: 14px; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + +const Description = styled.div` + font-weight: 500; + font-size: 13px; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + +const Actions = styled.div` + display: flex; + gap: 16px; +`; + +export { CardContainer, Header, Body, Title, Description, Actions }; diff --git a/apps/ledger-live-desktop/src/renderer/components/ContentCards/ActionCard/index.tsx b/apps/ledger-live-desktop/src/renderer/components/ContentCards/ActionCard/index.tsx new file mode 100644 index 000000000000..56df08c924db --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/components/ContentCards/ActionCard/index.tsx @@ -0,0 +1,56 @@ +import React, { useEffect } from "react"; +import ButtonV3 from "~/renderer/components/ButtonV3"; +import { Actions, Body, CardContainer, Header, Description, Title } from "./components"; +import { Link } from "@ledgerhq/react-ui"; +import { useInView } from "react-intersection-observer"; + +type Props = { + img?: string; + + title: string; + description: string; + + actions: { + primary: { + label?: string; + action: Function; + }; + dismiss: { + label?: string; + action: Function; + }; + }; + + onView?: Function; +}; + +const ActionCard = ({ img, title, description, actions, onView }: Props) => { + const { ref, inView } = useInView({ threshold: 0.5, triggerOnce: true }); + + useEffect(() => { + if (inView) onView?.(); + }, [onView, inView]); + + return ( + + {img &&
} + + {title} + {description} + + + actions.dismiss.action()}> + {actions.dismiss.label} + + + {actions.primary.label && ( + actions.primary.action()}> + {actions.primary.label} + + )} + + + ); +}; + +export default ActionCard; diff --git a/apps/ledger-live-desktop/src/renderer/components/Page.tsx b/apps/ledger-live-desktop/src/renderer/components/Page.tsx index cb289d2ccf6c..6c8bbd053435 100644 --- a/apps/ledger-live-desktop/src/renderer/components/Page.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/Page.tsx @@ -3,6 +3,8 @@ import { useLocation } from "react-router-dom"; import styled, { DefaultTheme, ThemedStyledProps } from "styled-components"; import AngleUp from "~/renderer/icons/AngleUp"; import TopBar from "~/renderer/components/TopBar"; +import PortfolioContentCards from "~/renderer/screens/dashboard/PortfolioContentCards"; +import { ABTestingVariants } from "@ledgerhq/types-live"; type Props = { children: React.ReactNode; @@ -151,6 +153,8 @@ const Page = ({ children }: Props) => { > + {/* Only on dashboard page */} + {pathname === "/" && } ); }; diff --git a/apps/ledger-live-desktop/src/renderer/components/RecoverBanner/RecoverBanner.tsx b/apps/ledger-live-desktop/src/renderer/components/RecoverBanner/RecoverBanner.tsx index abbc78fbdae5..24d999c51860 100644 --- a/apps/ledger-live-desktop/src/renderer/components/RecoverBanner/RecoverBanner.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/RecoverBanner/RecoverBanner.tsx @@ -1,13 +1,18 @@ -import { Flex, ProgressLoader, Text } from "@ledgerhq/react-ui"; +import { Flex, Link, ProgressLoader, Text } from "@ledgerhq/react-ui"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useHistory } from "react-router-dom"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; import { getStoreValue, setStoreValue } from "~/renderer/store"; import { useCustomPath } from "@ledgerhq/live-common/hooks/recoverFeatureFlag"; import { useTranslation } from "react-i18next"; -import Button from "~/renderer/components/ButtonV3"; -import { useTheme } from "styled-components"; +import styled from "styled-components"; import { RecoverBannerType } from "./types"; +import { Card } from "~/renderer/components/Box"; +import ButtonV3 from "~/renderer/components/ButtonV3"; + +const BannerContainer = styled(Card)` + background-color: ${p => p.theme.colors.opacityPurple.c10}; +`; export default function RecoverBanner() { const [storageData, setStorageData] = useState(); @@ -16,7 +21,6 @@ export default function RecoverBanner() { const { t } = useTranslation(); const history = useHistory(); - const { colors } = useTheme(); const recoverServices = useFeature("protectServicesDesktop"); const recoverBannerIsEnabled = recoverServices?.params?.bannerSubscriptionNotification; @@ -83,11 +87,10 @@ export default function RecoverBanner() { if (!recoverBannerIsEnabled || !recoverBannerSelected || !displayBannerData) return null; return ( - + - - + - + ); } diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useBraze.ts b/apps/ledger-live-desktop/src/renderer/hooks/useBraze.ts index 12229badc265..be5a819ccd1f 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/useBraze.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/useBraze.ts @@ -13,6 +13,7 @@ import { useDispatch, useSelector } from "react-redux"; import { setNotificationsCards, setPortfolioCards } from "../actions/dynamicContent"; import getUser from "~/helpers/user"; import { developerModeSelector } from "../reducers/settings"; +import { getEnv } from "@ledgerhq/live-env"; const getDesktopCards = (elem: braze.ContentCards) => elem.cards.filter(card => card.extras?.platform === Platform.Desktop); @@ -27,38 +28,37 @@ export const compareCards = (a: LedgerContentCard, b: LedgerContentCard) => { if (!a.order && b.order) { return 1; } - if ((!a.order && !b.order) || a.order === b.order) { - return b.createdAt.getTime() - a.createdAt.getTime(); + if (a.created && b.created && ((!a.order && !b.order) || a.order === b.order)) { + return b.created.getTime() - a.created.getTime(); } return (a.order || 0) - (b.order || 0); }; -export const mapAsPortfolioContentCard = (card: ClassicCard) => - ({ - id: card.id, - title: card.extras?.title, - description: card.extras?.description, - location: LocationContentCard.Portfolio, - image: card.extras?.image, - url: card.extras?.url, - path: card.extras?.path, - createdAt: card.created, - order: parseInt(card.extras?.order) ? parseInt(card.extras?.order) : undefined, - }) as PortfolioContentCard; - -export const mapAsNotificationContentCard = (card: ClassicCard) => - ({ - id: card.id, - title: card.extras?.title, - description: card.extras?.description, - location: LocationContentCard.NotificationCenter, - url: card.extras?.url, - path: card.extras?.path, - cta: card.extras?.cta, - createdAt: card.created, - viewed: card.viewed, - order: parseInt(card.extras?.order) ? parseInt(card.extras?.order) : undefined, - }) as NotificationContentCard; +export const mapAsPortfolioContentCard = (card: ClassicCard): PortfolioContentCard => ({ + id: String(card.id), + title: card.extras?.title, + description: card.extras?.description, + location: LocationContentCard.Portfolio, + image: card.extras?.image, + link: card.extras?.link, + created: card.created as Date, + cta: card.extras?.cta, + dismissCta: card.extras?.dismissCta, + order: parseInt(card.extras?.order) ? parseInt(card.extras?.order) : undefined, +}); + +export const mapAsNotificationContentCard = (card: ClassicCard): NotificationContentCard => ({ + id: String(card.id), + title: card.extras?.title, + description: card.extras?.description, + location: LocationContentCard.NotificationCenter, + url: card.extras?.url, + path: card.extras?.path, + cta: card.extras?.cta, + created: card.created as Date, + viewed: card.viewed, + order: parseInt(card.extras?.order) ? parseInt(card.extras?.order) : undefined, +}); export async function useBraze() { const dispatch = useDispatch(); @@ -67,6 +67,7 @@ export async function useBraze() { const initBraze = useCallback(async () => { const user = await getUser(); const brazeConfig = getBrazeConfig(); + const isPlaywright = !!getEnv("PLAYWRIGHT_RUN"); braze.initialize(brazeConfig.apiKey, { baseUrl: brazeConfig.endpoint, @@ -76,6 +77,11 @@ export async function useBraze() { sessionTimeoutInSeconds: devMode ? 1 : 1800, }); + // If it's playwright, we don't want to fetch content cards + if (isPlaywright) { + return; + } + if (user) { braze.changeUser(user.id); } diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useNotifications.ts b/apps/ledger-live-desktop/src/renderer/hooks/useNotifications.ts index 6414f0bd6b59..4b5c921d655d 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/useNotifications.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/useNotifications.ts @@ -1,12 +1,7 @@ import * as braze from "@braze/web-sdk"; import { useCallback, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { - LocationContentCard, - NotificationContentCard, - Platform, - ContentCard, -} from "~/types/dynamicContent"; +import { LocationContentCard, NotificationContentCard, Platform } from "~/types/dynamicContent"; import { notificationsContentCardSelector } from "~/renderer/reducers/dynamicContent"; import { setNotificationsCards } from "~/renderer/actions/dynamicContent"; import { track } from "../analytics/segment"; @@ -41,7 +36,7 @@ export function useNotifications() { const notifsByDay: Record = notifs.reduce( (sum: Record, notif: NotificationContentCard) => { // group by publication date - const k = startOfDayTime(notif.createdAt); + const k = startOfDayTime(notif.created); return { ...sum, [`${k}`]: [...(sum[k] || []), notif] }; }, @@ -85,7 +80,7 @@ export function useNotifications() { ); const onClickNotif = useCallback( - (card: ContentCard) => { + (card: NotificationContentCard) => { const currentCard = cachedNotifications.find(c => c.id === card.id); if (currentCard) { diff --git a/apps/ledger-live-desktop/src/renderer/hooks/usePortfolioCards.tsx b/apps/ledger-live-desktop/src/renderer/hooks/usePortfolioCards.tsx new file mode 100644 index 000000000000..e2bb2adcf081 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/hooks/usePortfolioCards.tsx @@ -0,0 +1,61 @@ +import React, { useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import ActionCard from "~/renderer/components/ContentCards/ActionCard"; +import { portfolioContentCardSelector } from "~/renderer/reducers/dynamicContent"; +import * as braze from "@braze/web-sdk"; +import { setPortfolioCards } from "~/renderer/actions/dynamicContent"; + +const usePortfolioCards = () => { + const dispatch = useDispatch(); + const [cachedContentCards, setCachedContentCards] = useState(braze.getCachedContentCards().cards); + const portfolioCards = useSelector(portfolioContentCardSelector); + + const findCard = (cardId: string) => cachedContentCards.find(card => card.id === cardId); + + const onImpression = (cardId: string) => { + const currentCard = findCard(cardId); + currentCard && braze.logContentCardImpressions([currentCard]); + }; + + const onDismiss = (cardId: string) => { + const currentCard = findCard(cardId); + + if (currentCard) { + braze.logCardDismissal(currentCard); + setCachedContentCards(cachedContentCards.filter(n => n.id !== currentCard.id)); + dispatch(setPortfolioCards(portfolioCards.filter(n => n.id !== currentCard.id))); + } + }; + + const onClick = (cardId: string) => { + const currentCard = findCard(cardId); + + if (currentCard) { + braze.logContentCardClick(currentCard); + } + }; + + const slides = portfolioCards.map(slide => ( + onClick(slide.id), + }, + dismiss: { + label: slide.dismissCta, + action: () => onDismiss(slide.id), + }, + }} + onView={() => onImpression(slide.id)} + /> + )); + + return slides; +}; + +export default usePortfolioCards; diff --git a/apps/ledger-live-desktop/src/renderer/screens/dashboard/PortfolioContentCards.tsx b/apps/ledger-live-desktop/src/renderer/screens/dashboard/PortfolioContentCards.tsx new file mode 100644 index 000000000000..acf94d55775f --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/screens/dashboard/PortfolioContentCards.tsx @@ -0,0 +1,74 @@ +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { Carousel } from "@ledgerhq/react-ui"; +import { ABTestingVariants } from "@ledgerhq/types-live"; +import React, { PropsWithChildren } from "react"; +import { useSelector } from "react-redux"; +import styled from "styled-components"; +import { useRefreshAccountsOrderingEffect } from "~/renderer/actions/general"; +import { Card } from "~/renderer/components/Box"; +import usePortfolioCards from "~/renderer/hooks/usePortfolioCards"; +import { accountsSelector } from "~/renderer/reducers/accounts"; +import { hasInstalledAppsSelector } from "~/renderer/reducers/settings"; + +const PortfolioVariantA = styled(Card)` + background-color: ${p => p.theme.colors.opacityPurple.c10}; +`; + +const PortfolioVariantBContainer = styled.div` + position: relative; + margin-left: 24px; + margin-right: 24px; +`; + +const PortfolioVariantBWrapper = styled.div` + position: absolute; + width: 100%; + bottom: 30px; + zindex: 10000; + backdrop-filter: blur(15px); + border-radius: 8px; + background-color: ${p => p.theme.colors.opacityPurple.c10}; +`; + +const PortfolioVariantB = ({ children }: PropsWithChildren) => ( + + {children} + +); + +const PortfolioContentCards = ({ variant }: { variant: ABTestingVariants }) => { + const slides = usePortfolioCards(); + const lldPortfolioCarousel = useFeature("lldPortfolioCarousel"); + const totalAccounts = useSelector(accountsSelector).length; + const hasInstalledApps = useSelector(hasInstalledAppsSelector); + + const showCarousel = lldPortfolioCarousel?.enabled && hasInstalledApps && totalAccounts >= 0; + useRefreshAccountsOrderingEffect({ + onMount: true, + }); + + if (!showCarousel || slides.length === 0) return null; + if ( + lldPortfolioCarousel?.params?.variant === ABTestingVariants.variantA && + variant === ABTestingVariants.variantA + ) + return ( + + {slides} + + ); + + if ( + lldPortfolioCarousel?.params?.variant === ABTestingVariants.variantB && + variant === ABTestingVariants.variantB + ) + return ( + + {slides} + + ); + + return null; +}; + +export default PortfolioContentCards; diff --git a/apps/ledger-live-desktop/src/renderer/screens/dashboard/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/dashboard/index.tsx index f8abd8b1d639..23be6f400c89 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/dashboard/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/dashboard/index.tsx @@ -15,7 +15,6 @@ import { useTranslation } from "react-i18next"; import styled from "styled-components"; import TrackPage from "~/renderer/analytics/TrackPage"; import OperationsList from "~/renderer/components/OperationsList"; -import Carousel from "~/renderer/components/Carousel"; import AssetDistribution from "./AssetDistribution"; import ClearCacheBanner from "~/renderer/components/ClearCacheBanner"; import RecoverBanner from "~/renderer/components/RecoverBanner/RecoverBanner"; @@ -25,11 +24,11 @@ import { useSelector } from "react-redux"; import uniq from "lodash/uniq"; import EmptyStateInstalledApps from "~/renderer/screens/dashboard/EmptyStateInstalledApps"; import EmptyStateAccounts from "~/renderer/screens/dashboard/EmptyStateAccounts"; -import { useRefreshAccountsOrderingEffect } from "~/renderer/actions/general"; import CurrencyDownStatusAlert from "~/renderer/components/CurrencyDownStatusAlert"; import PostOnboardingHubBanner from "~/renderer/components/PostOnboardingHub/PostOnboardingHubBanner"; import FeaturedButtons from "~/renderer/screens/dashboard/FeaturedButtons"; -import { AccountLike, Operation } from "@ledgerhq/types-live"; +import { ABTestingVariants, AccountLike, Operation } from "@ledgerhq/types-live"; +import PortfolioContentCards from "~/renderer/screens/dashboard/PortfolioContentCards"; // This forces only one visible top banner at a time export const TopBannerContainer = styled.div` @@ -56,10 +55,6 @@ export default function DashboardPage() { ); const isPostOnboardingBannerVisible = usePostOnboardingEntryPointVisibleOnWallet(); - const showCarousel = hasInstalledApps && totalAccounts >= 0; - useRefreshAccountsOrderingEffect({ - onMount: true, - }); const [shouldFilterTokenOpsZeroAmount] = useFilterTokenOperationsZeroAmount(); const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); const filterOperations = useCallback( @@ -75,14 +70,17 @@ export default function DashboardPage() { }, [hiddenNftCollections, shouldFilterTokenOpsZeroAmount], ); + return ( <> - {showCarousel ? : null} - + + + + {isPostOnboardingBannerVisible && } void; order?: number; + created: Date; +}; + +export type PortfolioContentCard = ContentCard & { + image?: string; + cta?: string; + link?: string; + dismissCta?: string; }; -export type PortfolioContentCard = ContentCard; export type NotificationContentCard = ContentCard & { cta: string; viewed: boolean; + url?: string; + path?: string; }; diff --git a/apps/ledger-live-mobile/src/components/RootNavigator/AnalyticsOptInPromptNavigator.tsx b/apps/ledger-live-mobile/src/components/RootNavigator/AnalyticsOptInPromptNavigator.tsx index 69e1b1eae8bc..66995c324211 100644 --- a/apps/ledger-live-mobile/src/components/RootNavigator/AnalyticsOptInPromptNavigator.tsx +++ b/apps/ledger-live-mobile/src/components/RootNavigator/AnalyticsOptInPromptNavigator.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from "react"; import { createStackNavigator } from "@react-navigation/stack"; import { useTheme } from "styled-components/native"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { LlmAnalyticsOptInPromptVariants } from "@ledgerhq/types-live"; +import { ABTestingVariants } from "@ledgerhq/types-live"; import { ScreenName } from "~/const"; import { getStackNavigatorConfig } from "~/navigation/navigatorConfig"; import AnalyticsOptInPromptMainA from "~/screens/AnalyticsOptInPrompt/variantA/Main"; @@ -12,11 +12,11 @@ import AnalyticsOptInPromptDetailsB from "~/screens/AnalyticsOptInPrompt/variant import { AnalyticsOptInPromptNavigatorParamList } from "./types/AnalyticsOptInPromptNavigator"; const screensByVariant = { - [LlmAnalyticsOptInPromptVariants.variantA]: { + [ABTestingVariants.variantA]: { main: AnalyticsOptInPromptMainA, details: AnalyticsOptInPromptDetailsA, }, - [LlmAnalyticsOptInPromptVariants.variantB]: { + [ABTestingVariants.variantB]: { main: AnalyticsOptInPromptMainB, details: AnalyticsOptInPromptDetailsB, }, @@ -28,9 +28,9 @@ export default function AnalyticsOptInPromptNavigator() { const llmAnalyticsOptInPromptFeature = useFeature("llmAnalyticsOptInPrompt"); const activeVariant = - llmAnalyticsOptInPromptFeature?.params?.variant === LlmAnalyticsOptInPromptVariants.variantB - ? LlmAnalyticsOptInPromptVariants.variantB - : LlmAnalyticsOptInPromptVariants.variantA; + llmAnalyticsOptInPromptFeature?.params?.variant === ABTestingVariants.variantB + ? ABTestingVariants.variantB + : ABTestingVariants.variantA; return ( diff --git a/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts b/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts index b23a4e26b7ae..950f96926795 100644 --- a/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts +++ b/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts @@ -3,7 +3,7 @@ import { Feature, Features, FeatureMap, - LlmAnalyticsOptInPromptVariants, + ABTestingVariants, } from "@ledgerhq/types-live"; import { reduce } from "lodash"; import { formatToFirebaseFeatureId } from "./firebaseFeatureFlags"; @@ -444,7 +444,14 @@ export const DEFAULT_FEATURES: Features = { llmAnalyticsOptInPrompt: { enabled: false, params: { - variant: LlmAnalyticsOptInPromptVariants.variantA, + variant: ABTestingVariants.variantA, + }, + }, + + lldPortfolioCarousel: { + enabled: false, + params: { + variant: ABTestingVariants.variantA, }, }, diff --git a/libs/ledgerjs/packages/types-live/src/ABTesting.ts b/libs/ledgerjs/packages/types-live/src/ABTesting.ts new file mode 100644 index 000000000000..19f811a385dd --- /dev/null +++ b/libs/ledgerjs/packages/types-live/src/ABTesting.ts @@ -0,0 +1,4 @@ +export enum ABTestingVariants { + variantA = "A", + variantB = "B", +} diff --git a/libs/ledgerjs/packages/types-live/src/analyticsOptInPrompt.ts b/libs/ledgerjs/packages/types-live/src/analyticsOptInPrompt.ts deleted file mode 100644 index 560864eb51f4..000000000000 --- a/libs/ledgerjs/packages/types-live/src/analyticsOptInPrompt.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum LlmAnalyticsOptInPromptVariants { - variantA = "A", - variantB = "B", -} diff --git a/libs/ledgerjs/packages/types-live/src/feature.ts b/libs/ledgerjs/packages/types-live/src/feature.ts index a103c9d7929d..6422b9e0f779 100644 --- a/libs/ledgerjs/packages/types-live/src/feature.ts +++ b/libs/ledgerjs/packages/types-live/src/feature.ts @@ -1,4 +1,4 @@ -import { LlmAnalyticsOptInPromptVariants } from "./analyticsOptInPrompt"; +import { ABTestingVariants } from "./ABTesting"; import { CexDepositEntryPointsLocationsDesktop, CexDepositEntryPointsLocationsMobile, @@ -188,6 +188,7 @@ export type Features = CurrencyFeatures & { llmAnalyticsOptInPrompt: Feature_LlmAnalyticsOptInPrompt; myLedgerDisplayAppDeveloperName: Feature_MyLedgerDisplayAppDeveloperName; nftsFromSimplehash: Feature_NftsFromSimpleHash; + lldPortfolioCarousel: Feature_LldPortfolioCarousel; }; /** @@ -485,7 +486,11 @@ export type Feature_FetchAdditionalCoins = Feature<{ }>; export type Feature_LlmAnalyticsOptInPrompt = Feature<{ - variant: LlmAnalyticsOptInPromptVariants; + variant: ABTestingVariants; +}>; + +export type Feature_LldPortfolioCarousel = Feature<{ + variant: ABTestingVariants; }>; export type Feature_LlmNewFirmwareUpdateUx = DefaultFeature; diff --git a/libs/ledgerjs/packages/types-live/src/index.ts b/libs/ledgerjs/packages/types-live/src/index.ts index d4978f663abd..1b8bed605328 100644 --- a/libs/ledgerjs/packages/types-live/src/index.ts +++ b/libs/ledgerjs/packages/types-live/src/index.ts @@ -16,4 +16,4 @@ export * from "./chainwatch"; export * from "./messages"; export * from "./cexDeposit"; export * from "./storyly"; -export * from "./analyticsOptInPrompt"; +export * from "./ABTesting"; diff --git a/libs/ui/packages/react/src/components/layout/Carousel/Footer/variantContentCard.tsx b/libs/ui/packages/react/src/components/layout/Carousel/Footer/variantContentCard.tsx index a809b5c1674b..879384a7e1ca 100644 --- a/libs/ui/packages/react/src/components/layout/Carousel/Footer/variantContentCard.tsx +++ b/libs/ui/packages/react/src/components/layout/Carousel/Footer/variantContentCard.tsx @@ -25,6 +25,8 @@ const FooterArrowContainer = styled.div` `; const FooterContentCard = ({ children, emblaApi, currentIndex, variant }: SubProps) => { + if (children.length === 1) return null; + return (