diff --git a/.changeset/curvy-parents-push.md b/.changeset/curvy-parents-push.md new file mode 100644 index 000000000000..d10108d03b1b --- /dev/null +++ b/.changeset/curvy-parents-push.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": minor +--- + +Support anonymous user based braze campaign notification management. diff --git a/apps/ledger-live-desktop/src/newArch/features/DynamicContent/components/LogContentCardWrapper.tsx b/apps/ledger-live-desktop/src/newArch/features/DynamicContent/components/LogContentCardWrapper.tsx index 8ae23cfb7e1c..eca0b74c2934 100644 --- a/apps/ledger-live-desktop/src/newArch/features/DynamicContent/components/LogContentCardWrapper.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/DynamicContent/components/LogContentCardWrapper.tsx @@ -1,9 +1,13 @@ 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 { useDispatch, useSelector } from "react-redux"; +import { + anonymousUserNotificationsSelector, + trackingEnabledSelector, +} from "~/renderer/reducers/settings"; import { track } from "~/renderer/analytics/segment"; import { Box } from "@ledgerhq/react-ui"; +import { updateAnonymousUserNotifications } from "~/renderer/actions/settings"; interface LogContentCardWrapperProps { id: string; @@ -12,6 +16,7 @@ interface LogContentCardWrapperProps { } const PERCENTAGE_OF_CARD_VISIBLE = 0.5; +const OFFLINE_SEEN_DELAY = 2e3; const LogContentCardWrapper: React.FC = ({ id, @@ -20,6 +25,8 @@ const LogContentCardWrapper: React.FC = ({ }) => { const ref = useRef(null); const isTrackedUser = useSelector(trackingEnabledSelector); + const anonymousUserNotifications = useSelector(anonymousUserNotificationsSelector); + const dispatch = useDispatch(); const currentCard = useMemo(() => { const cards = braze.getCachedContentCards().cards; @@ -27,17 +34,33 @@ const LogContentCardWrapper: React.FC = ({ }, [id]); useEffect(() => { - if (!currentCard || !isTrackedUser) return; + if (!currentCard) 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, - }); + if (isTrackedUser) { + braze.logContentCardImpressions([currentCard]); + track("contentcard_impression", { + id: currentCard.id, + ...currentCard.extras, + ...additionalProps, + }); + } else if ( + anonymousUserNotifications[currentCard.id as string] !== + currentCard?.expiresAt?.getTime() + ) { + // support new campaign or resumed campaign with the same id + different expiration date targeting anonymous users + setTimeout(() => { + dispatch( + updateAnonymousUserNotifications({ + notifications: { + [currentCard.id as string]: currentCard?.expiresAt?.getTime() as number, + }, + }), + ); + }, OFFLINE_SEEN_DELAY); + } } }, { threshold: PERCENTAGE_OF_CARD_VISIBLE }, @@ -54,7 +77,7 @@ const LogContentCardWrapper: React.FC = ({ intersectionObserver.unobserve(currentRef); } }; - }, [currentCard, isTrackedUser, additionalProps]); + }, [currentCard, isTrackedUser, additionalProps, dispatch, anonymousUserNotifications]); return ( diff --git a/apps/ledger-live-desktop/src/renderer/actions/constants.ts b/apps/ledger-live-desktop/src/renderer/actions/constants.ts index 09cf07655e5a..33fa74b72708 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/constants.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/constants.ts @@ -5,3 +5,4 @@ export const TOGGLE_MEMOTAG_INFO = "settings/toggleShouldDisplayMemoTagInfo"; export const TOGGLE_MEV = "settings/toggleMEV"; export const TOGGLE_MARKET_WIDGET = "settings/toggleMarketWidget"; export const UPDATE_NFT_COLLECTION_STATUS = "settings/updateNftCollectionStatus"; +export const UPDATE_ANONYMOUS_USER_NOTIFICATIONS = "settings/updateAnonymousUserNotifications"; diff --git a/apps/ledger-live-desktop/src/renderer/actions/settings.ts b/apps/ledger-live-desktop/src/renderer/actions/settings.ts index be1df1bef74c..9fc6f8f32913 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/settings.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/settings.ts @@ -29,6 +29,7 @@ import { TOGGLE_MARKET_WIDGET, TOGGLE_MEMOTAG_INFO, TOGGLE_MEV, + UPDATE_ANONYMOUS_USER_NOTIFICATIONS, UPDATE_NFT_COLLECTION_STATUS, } from "./constants"; import { BlockchainsType } from "@ledgerhq/live-nft/supported"; @@ -454,3 +455,13 @@ export const toggleShouldDisplayMemoTagInfo = (payload: boolean) => { payload, }; }; + +export const updateAnonymousUserNotifications = (payload: { + notifications: Record; + purgeState?: boolean; +}) => { + return { + type: UPDATE_ANONYMOUS_USER_NOTIFICATIONS, + payload, + }; +}; 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 4a26d6a32dbc..53e6fa645018 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 @@ -244,7 +244,6 @@ const Separator = styled.div` export function AnnouncementPanel() { const { notificationsCards, groupNotifications, onClickNotif } = useNotifications(); - const groups = useMemo( () => groupNotifications(notificationsCards), // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useBraze.ts b/apps/ledger-live-desktop/src/renderer/hooks/useBraze.ts index eaf92f2d71a1..75ec07d04e0a 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/useBraze.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/useBraze.ts @@ -22,8 +22,13 @@ import { trackingEnabledSelector, dismissedContentCardsSelector, anonymousBrazeIdSelector, + anonymousUserNotificationsSelector, } from "../reducers/settings"; -import { clearDismissedContentCards, setAnonymousBrazeId } from "../actions/settings"; +import { + clearDismissedContentCards, + setAnonymousBrazeId, + updateAnonymousUserNotifications, +} from "../actions/settings"; import { getEnv } from "@ledgerhq/live-env"; import { getOldCampaignIds, generateAnonymousId } from "@ledgerhq/live-common/braze/anonymousUsers"; @@ -90,6 +95,7 @@ export async function useBraze() { const dispatch = useDispatch(); const devMode = useSelector(developerModeSelector); const contentCardsDissmissed = useSelector(dismissedContentCardsSelector); + const anonymousUserNotifications = useSelector(anonymousUserNotificationsSelector); const isTrackedUser = useSelector(trackingEnabledSelector); const anonymousBrazeId = useRef(useSelector(anonymousBrazeIdSelector)); @@ -104,6 +110,30 @@ export async function useBraze() { dispatch(setAnonymousBrazeId(anonymousBrazeId.current)); } + /** + * If the user is opt-out from analytics, we need to purge expired notifications persisted in the store/offline storage + */ + if (!isTrackedUser) { + const expiredAnonymousUserNotifications = getOldCampaignIds(anonymousUserNotifications); + if (expiredAnonymousUserNotifications.length) { + const validAnonymousUserNotificationsOnly = Object.keys(anonymousUserNotifications).reduce( + (validNotifications: Record, key: string) => { + if (!expiredAnonymousUserNotifications.includes(key)) { + validNotifications[key] = anonymousUserNotifications[key]; + } + return validNotifications; + }, + {}, + ); + dispatch( + updateAnonymousUserNotifications({ + notifications: validAnonymousUserNotificationsOnly, + purgeState: true, + }), + ); + } + } + braze.initialize(brazeConfig.apiKey, { baseUrl: brazeConfig.endpoint, allowUserSuppliedJavascript: true, @@ -150,7 +180,14 @@ export async function useBraze() { braze.automaticallyShowInAppMessages(); braze.openSession(); - }, [dispatch, devMode, isTrackedUser, contentCardsDissmissed, anonymousBrazeId]); + }, [ + dispatch, + devMode, + isTrackedUser, + contentCardsDissmissed, + anonymousBrazeId, + anonymousUserNotifications, + ]); useEffect(() => { initBraze(); diff --git a/apps/ledger-live-desktop/src/renderer/reducers/dynamicContent.ts b/apps/ledger-live-desktop/src/renderer/reducers/dynamicContent.ts index 8cdc27dbab2b..88b4364646de 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/dynamicContent.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/dynamicContent.ts @@ -5,6 +5,7 @@ import { PortfolioContentCard, } from "~/types/dynamicContent"; import { Handlers } from "./types"; +import { SettingsState } from "./settings"; export type DynamicContentState = { portfolioCards: PortfolioContentCard[]; @@ -61,8 +62,14 @@ export const portfolioContentCardSelector = (state: { dynamicContent: DynamicCon export const actionContentCardSelector = (state: { dynamicContent: DynamicContentState }) => state.dynamicContent.actionCards; -export const notificationsContentCardSelector = (state: { dynamicContent: DynamicContentState }) => - state.dynamicContent.notificationsCards; +export const notificationsContentCardSelector = (state: { + dynamicContent: DynamicContentState; + settings: SettingsState; +}) => + state.dynamicContent.notificationsCards.map(n => ({ + ...n, + viewed: !!state.settings.anonymousUserNotifications[n.id], + })); // Exporting reducer diff --git a/apps/ledger-live-desktop/src/renderer/reducers/settings.ts b/apps/ledger-live-desktop/src/renderer/reducers/settings.ts index 83817274d815..1e6b16c242a9 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/settings.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/settings.ts @@ -36,6 +36,7 @@ import { TOGGLE_MARKET_WIDGET, TOGGLE_MEV, UPDATE_NFT_COLLECTION_STATUS, + UPDATE_ANONYMOUS_USER_NOTIFICATIONS, } from "../actions/constants"; import { BlockchainsType, SupportedBlockchainsType } from "@ledgerhq/live-nft/supported"; import { NftStatus } from "@ledgerhq/live-nft/types"; @@ -134,6 +135,7 @@ export type SettingsState = { onboardingUseCase: OnboardingUseCase | null; lastOnboardedDevice: Device | null; alwaysShowMemoTagInfo: boolean; + anonymousUserNotifications: Record; }; export const getInitialLanguageAndLocale = (): { language: Language; locale: Locale } => { @@ -239,6 +241,7 @@ export const INITIAL_STATE: SettingsState = { onboardingUseCase: null, lastOnboardedDevice: null, alwaysShowMemoTagInfo: true, + anonymousUserNotifications: {}, }; /* Handlers */ @@ -311,6 +314,10 @@ type HandlersPayloads = { [TOGGLE_MEV]: boolean; [TOGGLE_MEMOTAG_INFO]: boolean; [TOGGLE_MARKET_WIDGET]: boolean; + [UPDATE_ANONYMOUS_USER_NOTIFICATIONS]: { + notifications: Record; + purgeState?: boolean; + }; }; type SettingsHandlers = Handlers; @@ -556,6 +563,13 @@ const handlers: SettingsHandlers = { ...state, alwaysShowMemoTagInfo: payload, }), + [UPDATE_ANONYMOUS_USER_NOTIFICATIONS]: (state: SettingsState, { payload }) => ({ + ...state, + anonymousUserNotifications: { + ...(!payload.purgeState && state.anonymousUserNotifications), + ...payload.notifications, + }, + }), }; export default handleActions( @@ -912,3 +926,5 @@ export const marketPerformanceWidgetSelector = (state: State) => export const alwaysShowMemoTagInfoSelector = (state: State) => state.settings.alwaysShowMemoTagInfo; export const nftCollectionsStatusByNetworkSelector = (state: State) => state.settings.nftCollectionsStatusByNetwork; +export const anonymousUserNotificationsSelector = (state: State) => + state.settings.anonymousUserNotifications;