diff --git a/.changeset/sour-badgers-matter.md b/.changeset/sour-badgers-matter.md new file mode 100644 index 000000000000..d5677b2311d3 --- /dev/null +++ b/.changeset/sour-badgers-matter.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": patch +--- + +Optimize content card log impression based on 50% of the card seen diff --git a/apps/ledger-live-desktop/package.json b/apps/ledger-live-desktop/package.json index f19500b07800..f7c51c5530f9 100644 --- a/apps/ledger-live-desktop/package.json +++ b/apps/ledger-live-desktop/package.json @@ -120,7 +120,6 @@ "react-dom": "18.3.1", "react-easy-crop": "4.7.5", "react-i18next": "13.5.0", - "react-intersection-observer": "8.34.0", "react-is": "17.0.2", "react-key-handler": "1.2.0-beta.3", "react-lottie": "1.2.4", diff --git a/apps/ledger-live-desktop/src/newArch/components/DynamicContent/LogContentCardWrapper.tsx b/apps/ledger-live-desktop/src/newArch/components/DynamicContent/LogContentCardWrapper.tsx new file mode 100644 index 000000000000..dc4fbee42c74 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/components/DynamicContent/LogContentCardWrapper.tsx @@ -0,0 +1,66 @@ +import React, { useRef, useEffect, useMemo } from "react"; +import * as braze from "@braze/web-sdk"; +import { useSelector } from "react-redux"; +import { trackingEnabledSelector } from "~/renderer/reducers/settings"; +import { track } from "~/renderer/analytics/segment"; +import { Flex } from "@ledgerhq/react-ui"; + +interface LogContentCardWrapperProps { + id: string; + children: React.ReactNode; + additionalProps?: object; +} + +const PERCENTAGE_OF_CARD_VISIBLE = 0.5; + +const LogContentCardWrapper: React.FC = ({ + id, + children, + additionalProps, +}) => { + const ref = useRef(null); + const isTrackedUser = useSelector(trackingEnabledSelector); + + const currentCard = useMemo(() => { + const cards = braze.getCachedContentCards().cards; + return cards.find(card => card.id === id); + }, [id]); + + useEffect(() => { + if (!currentCard || !isTrackedUser) return; + + const intersectionObserver = new IntersectionObserver( + ([entry]) => { + if (entry.intersectionRatio > PERCENTAGE_OF_CARD_VISIBLE) { + braze.logContentCardImpressions([currentCard]); + track("contentcard_impression", { + id: currentCard.id, + ...currentCard.extras, + ...additionalProps, + }); + } + }, + { threshold: PERCENTAGE_OF_CARD_VISIBLE }, + ); + + const currentRef = ref.current; + + if (currentRef) { + intersectionObserver.observe(currentRef); + } + + return () => { + if (currentRef) { + intersectionObserver.unobserve(currentRef); + } + }; + }, [currentCard, isTrackedUser, additionalProps]); + + return ( + + {children} + + ); +}; + +export default LogContentCardWrapper; diff --git a/apps/ledger-live-desktop/src/renderer/actions/dynamicContent.ts b/apps/ledger-live-desktop/src/renderer/actions/dynamicContent.ts index f9cf0b97f755..04d844a361f1 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/dynamicContent.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/dynamicContent.ts @@ -1,4 +1,8 @@ -import { ActionContentCard, PortfolioContentCard } from "~/types/dynamicContent"; +import { + ActionContentCard, + PortfolioContentCard, + NotificationContentCard, +} from "~/types/dynamicContent"; export const setPortfolioCards = (payload: PortfolioContentCard[]) => ({ type: "DYNAMIC_CONTENT_SET_PORTFOLIO_CARDS", @@ -10,7 +14,7 @@ export const setActionCards = (payload: ActionContentCard[]) => ({ payload, }); -export const setNotificationsCards = (payload: PortfolioContentCard[]) => ({ +export const setNotificationsCards = (payload: NotificationContentCard[]) => ({ type: "DYNAMIC_CONTENT_SET_NOTIFICATIONS_CARDS", payload, }); diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/helpers.tsx b/apps/ledger-live-desktop/src/renderer/components/Carousel/helpers.tsx index 456fdc240ea8..a3a4540ad8a1 100644 --- a/apps/ledger-live-desktop/src/renderer/components/Carousel/helpers.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/Carousel/helpers.tsx @@ -52,7 +52,6 @@ type SlideRes = { export const useDefaultSlides = (): { slides: SlideRes[]; - logSlideImpression: (index: number) => void; dismissCard: (index: number) => void; } => { const [cachedContentCards, setCachedContentCards] = useState([]); @@ -65,21 +64,6 @@ export const useDefaultSlides = (): { 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) { - isTrackedUser && braze.logContentCardImpressions([currentCard]); - } - } - } - }, - [portfolioCards, cachedContentCards, isTrackedUser], - ); - const dismissCard = useCallback( (index: number) => { if (portfolioCards && portfolioCards.length > index) { @@ -129,7 +113,6 @@ export const useDefaultSlides = (): { 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 index 90ffed8ce072..cb8a458e8123 100644 --- a/apps/ledger-live-desktop/src/renderer/components/Carousel/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/Carousel/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import styled from "styled-components"; import { useTransition, animated } from "react-spring"; import IconArrowRight from "~/renderer/icons/ArrowRight"; @@ -8,6 +8,7 @@ import TimeBasedProgressBar from "~/renderer/components/Carousel/TimeBasedProgre import IconCross from "~/renderer/icons/Cross"; import { getTransitions, useDefaultSlides } from "~/renderer/components/Carousel/helpers"; import { track } from "~/renderer/analytics/segment"; +import LogContentCardWrapper from "LLD/components/DynamicContent/LogContentCardWrapper"; const CarouselWrapper = styled(Card)` position: relative; @@ -149,28 +150,19 @@ const Carousel = ({ speed?: number; type?: "slide" | "flip"; }) => { - const { slides, logSlideImpression, dismissCard } = useDefaultSlides(); + const { slides, 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)); - const [hasLoggedFirstImpression, setHasLoggedFirstImpression] = useState(false); - - useEffect(() => { - if (!hasLoggedFirstImpression) { - setHasLoggedFirstImpression(true); - logSlideImpression(0); - } - }, [hasLoggedFirstImpression, logSlideImpression]); const changeVisibleSlide = useCallback( (newIndex: number) => { if (index !== newIndex) { setIndex(newIndex); - logSlideImpression(newIndex); } }, - [index, logSlideImpression], + [index], ); const onChooseSlide = useCallback( @@ -232,10 +224,12 @@ const Carousel = ({ {transitions.map(({ item, props, key }) => { if (!slides?.[item]) return null; - const { Component } = slides[item]; + const { Component, id } = slides[item]; return ( - + + + ); })} 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 index df36398f55fd..c1e1a0c386c7 100644 --- a/apps/ledger-live-desktop/src/renderer/components/ContentCards/ActionCard/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/ContentCards/ActionCard/index.tsx @@ -1,8 +1,7 @@ -import React, { useEffect } from "react"; +import React 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; @@ -23,19 +22,11 @@ type Props = { dataTestId?: string; }; }; - - onView?: Function; }; -const ActionCard = ({ img, leftContent, title, description, actions, onView }: Props) => { - const { ref, inView } = useInView({ threshold: 0.5, triggerOnce: true }); - - useEffect(() => { - if (inView) onView?.(); - }, [onView, inView]); - +const ActionCard = ({ img, leftContent, title, description, actions }: Props) => { return ( - + {(img &&
) || leftContent} {title} diff --git a/apps/ledger-live-desktop/src/renderer/components/Page.tsx b/apps/ledger-live-desktop/src/renderer/components/Page.tsx index 39bf14b7a7a7..34033a7030aa 100644 --- a/apps/ledger-live-desktop/src/renderer/components/Page.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/Page.tsx @@ -3,7 +3,7 @@ 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/ActionContentCards"; +import ActionContentCards from "~/renderer/screens/dashboard/ActionContentCards"; import { ABTestingVariants } from "@ledgerhq/types-live"; type Props = { @@ -154,7 +154,7 @@ const Page = ({ children }: Props) => { {/* Only on dashboard page */} - {pathname === "/" && } + {pathname === "/" && } ); }; diff --git a/apps/ledger-live-desktop/src/renderer/components/TopBar/NotificationIndicator/AnnouncementPanel.tsx b/apps/ledger-live-desktop/src/renderer/components/TopBar/NotificationIndicator/AnnouncementPanel.tsx index 67da9c0270b8..8984e3fc4307 100644 --- a/apps/ledger-live-desktop/src/renderer/components/TopBar/NotificationIndicator/AnnouncementPanel.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/TopBar/NotificationIndicator/AnnouncementPanel.tsx @@ -1,7 +1,6 @@ -import React, { useCallback, useRef, useMemo } from "react"; +import React, { useCallback, useMemo } from "react"; import styled from "styled-components"; import { Trans } from "react-i18next"; -import { InView } from "react-intersection-observer"; import { useDispatch } from "react-redux"; import InfoCircle from "~/renderer/icons/InfoCircle"; import TriangleWarning from "~/renderer/icons/TriangleWarning"; @@ -16,6 +15,7 @@ import { useNotifications } from "~/renderer/hooks/useNotifications"; import TrackPage from "~/renderer/analytics/TrackPage"; import { urls } from "~/config/urls"; import { useDateFormatted } from "~/renderer/hooks/useDateFormatter"; +import LogContentCardWrapper from "LLD/components/DynamicContent/LogContentCardWrapper"; const DateRowContainer = styled.div` padding: 4px 16px; @@ -243,27 +243,7 @@ const Separator = styled.div` `; export function AnnouncementPanel() { - const { notificationsCards, logNotificationImpression, groupNotifications, onClickNotif } = - useNotifications(); - - const timeoutByUUID = useRef>({}); - const handleInViewNotif = useCallback( - (visible: boolean, uuid: keyof typeof timeoutByUUID.current) => { - const timeouts = timeoutByUUID.current; - - if (notificationsCards.find(n => !n.viewed && n.id === uuid) && visible && !timeouts[uuid]) { - timeouts[uuid] = setTimeout(() => { - logNotificationImpression(uuid); - delete timeouts[uuid]; - }, 2000); - } - if (!visible && timeouts[uuid]) { - clearTimeout(timeouts[uuid]); - delete timeouts[uuid]; - } - }, - [logNotificationImpression, notificationsCards], - ); + const { notificationsCards, groupNotifications, onClickNotif } = useNotifications(); const groups = useMemo( () => groupNotifications(notificationsCards), @@ -305,12 +285,8 @@ export function AnnouncementPanel() { {group.day ? : null} {group.data.map(({ title, description, path, url, viewed, id, cta }, index) => ( - - handleInViewNotif(visible, id)} - onClick={() => onClickNotif(group.data[index])} - > +
onClickNotif(group.data[index])}> +
- + {index < group.data.length - 1 ? : null} - +
))}
))} diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useActionCards.tsx b/apps/ledger-live-desktop/src/renderer/hooks/useActionCards.tsx index 3a2bb7f7347c..b0bff067e655 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/useActionCards.tsx +++ b/apps/ledger-live-desktop/src/renderer/hooks/useActionCards.tsx @@ -1,6 +1,5 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import ActionCard from "~/renderer/components/ContentCards/ActionCard"; import { actionContentCardSelector } from "~/renderer/reducers/dynamicContent"; import * as braze from "@braze/web-sdk"; import { setActionCards } from "~/renderer/actions/dynamicContent"; @@ -23,11 +22,6 @@ const useActionCards = () => { const findCard = (cardId: string) => cachedContentCards.find(card => card.id === cardId); const findActionCard = (cardId: string) => actionCards.find(card => card.id === cardId); - const onImpression = (cardId: string) => { - const currentCard = findCard(cardId); - isTrackedUser && currentCard && braze.logContentCardImpressions([currentCard]); - }; - const onDismiss = (cardId: string) => { const currentCard = findCard(cardId); const actionCard = findActionCard(cardId); @@ -79,27 +73,7 @@ const useActionCards = () => { } }; - const slides = actionCards.map(slide => ( - onClick(slide.id, slide.link), - }, - dismiss: { - label: slide.secondaryCta, - action: () => onDismiss(slide.id), - }, - }} - onView={() => onImpression(slide.id)} - /> - )); - - return slides; + return { onClick, onDismiss, actionCards }; }; export default useActionCards; diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useNotifications.ts b/apps/ledger-live-desktop/src/renderer/hooks/useNotifications.ts index dbfb31685c19..70ae4d92e41d 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/useNotifications.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/useNotifications.ts @@ -3,7 +3,6 @@ import { useCallback, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { LocationContentCard, NotificationContentCard, Platform } from "~/types/dynamicContent"; import { notificationsContentCardSelector } from "~/renderer/reducers/dynamicContent"; -import { setNotificationsCards } from "~/renderer/actions/dynamicContent"; import { track } from "../analytics/segment"; import { trackingEnabledSelector } from "../reducers/settings"; @@ -22,7 +21,7 @@ export function useNotifications() { card.extras?.location === LocationContentCard.NotificationCenter, ); setCachedNotifications(cards); - }, []); + }, [dispatch, notificationsCards]); function startOfDayTime(date: Date): number { const startOfDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); @@ -62,25 +61,6 @@ export function useNotifications() { })); }; - const logNotificationImpression = useCallback( - (cardId: string) => { - const currentCard = cachedNotifications.find(card => card.id === cardId); - - isTrackedUser && braze.logContentCardImpressions(currentCard ? [currentCard] : []); - - const cards = (notificationsCards ?? []).map(n => { - if (n.id === cardId) { - return { ...n, viewed: true }; - } else { - return n; - } - }); - - dispatch(setNotificationsCards(cards)); - }, - [notificationsCards, cachedNotifications, dispatch, isTrackedUser], - ); - const onClickNotif = useCallback( (card: NotificationContentCard) => { const currentCard = cachedNotifications.find(c => c.id === card.id); @@ -110,7 +90,6 @@ export function useNotifications() { braze, cachedNotifications, notificationsCards, - logNotificationImpression, onClickNotif, }; } diff --git a/apps/ledger-live-desktop/src/renderer/screens/dashboard/ActionContentCards.tsx b/apps/ledger-live-desktop/src/renderer/screens/dashboard/ActionContentCards.tsx index 7946cbfe0445..577342fed6bb 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/dashboard/ActionContentCards.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/dashboard/ActionContentCards.tsx @@ -1,11 +1,13 @@ 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 React, { PropsWithChildren, useMemo } from "react"; import styled from "styled-components"; import { useRefreshAccountsOrderingEffect } from "~/renderer/actions/general"; import { Card } from "~/renderer/components/Box"; import useActionCards from "~/renderer/hooks/useActionCards"; +import ActionCard from "~/renderer/components/ContentCards/ActionCard"; +import LogContentCardWrapper from "LLD/components/DynamicContent/LogContentCardWrapper"; const ActionVariantA = styled(Card)` background-color: ${p => p.theme.colors.opacityPurple.c10}; @@ -34,8 +36,29 @@ const ActionVariantB = ({ children }: PropsWithChildren) => ( ); const ActionContentCards = ({ variant }: { variant: ABTestingVariants }) => { - const slides = useActionCards(); + const { actionCards, onClick, onDismiss } = useActionCards(); const lldActionCarousel = useFeature("lldActionCarousel"); + const additionalProps = useMemo(() => ({ variant }), [variant]); + + const slides = actionCards.map(slide => ( + + onClick(slide.id, slide.link), + }, + dismiss: { + label: slide.secondaryCta, + action: () => onDismiss(slide.id), + }, + }} + /> + + )); useRefreshAccountsOrderingEffect({ onMount: true, 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 05dfe5313123..742dcce71512 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/dashboard/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/dashboard/index.tsx @@ -84,7 +84,8 @@ export default function DashboardPage() { const { enabled: marketPerformanceEnabled, variant: marketPerformanceVariant } = useMarketPerformanceFeatureFlag(); - const isActionCardsCampainRunning = useActionCards().length > 0; + const { actionCards } = useActionCards(); + const isActionCardsCampainRunning = actionCards.length > 0; const { isFeatureFlagsAnalyticsPrefDisplayed, analyticsOptInPromptProps } = useDisplayOnPortfolioAnalytics(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c927fee7bab..ac972d39dda7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -496,9 +496,6 @@ importers: react-i18next: specifier: 13.5.0 version: 13.5.0(i18next@23.10.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-intersection-observer: - specifier: 8.34.0 - version: 8.34.0(react@18.3.1) react-is: specifier: 17.0.2 version: 17.0.2 @@ -25839,11 +25836,6 @@ packages: peerDependencies: react: ^16.8.4 - react-intersection-observer@8.34.0: - resolution: {integrity: sha512-TYKh52Zc0Uptp5/b4N91XydfSGKubEhgZRtcg1rhTKABXijc4Sdr1uTp5lJ8TN27jwUsdXxjHXtHa0kPj704sw==} - peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0|| ^18.0.0 - react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -58352,10 +58344,6 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 - react-intersection-observer@8.34.0(react@18.3.1): - dependencies: - react: 18.3.1 - react-is@16.13.1: {} react-is@17.0.2: {}