Skip to content

Commit

Permalink
fix: add offline support for anonymous targeted user via braze notifi…
Browse files Browse the repository at this point in the history
…cations
  • Loading branch information
themooneer committed Dec 6, 2024
1 parent 56fcd2a commit 21b1ea0
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/curvy-parents-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ledger-live-desktop": minor
---

Support anonymous user based braze campaign notification management.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,6 +16,7 @@ interface LogContentCardWrapperProps {
}

const PERCENTAGE_OF_CARD_VISIBLE = 0.5;
const OFFLINE_SEEN_DELAY = 2e3;

const LogContentCardWrapper: React.FC<LogContentCardWrapperProps> = ({
id,
Expand All @@ -20,24 +25,42 @@ const LogContentCardWrapper: React.FC<LogContentCardWrapperProps> = ({
}) => {
const ref = useRef<HTMLDivElement>(null);
const isTrackedUser = useSelector(trackingEnabledSelector);
const anonymousUserNotifications = useSelector(anonymousUserNotificationsSelector);
const dispatch = useDispatch();

const currentCard = useMemo(() => {
const cards = braze.getCachedContentCards().cards;
return cards.find(card => card.id === id);
}, [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 },
Expand All @@ -54,7 +77,7 @@ const LogContentCardWrapper: React.FC<LogContentCardWrapperProps> = ({
intersectionObserver.unobserve(currentRef);
}
};
}, [currentCard, isTrackedUser, additionalProps]);
}, [currentCard, isTrackedUser, additionalProps, dispatch, anonymousUserNotifications]);

return (
<Box width="100%" ref={ref}>
Expand Down
1 change: 1 addition & 0 deletions apps/ledger-live-desktop/src/renderer/actions/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
11 changes: 11 additions & 0 deletions apps/ledger-live-desktop/src/renderer/actions/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -454,3 +455,13 @@ export const toggleShouldDisplayMemoTagInfo = (payload: boolean) => {
payload,
};
};

export const updateAnonymousUserNotifications = (payload: {
notifications: Record<string, string | number>;
purgeState?: boolean;
}) => {
return {
type: UPDATE_ANONYMOUS_USER_NOTIFICATIONS,
payload,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 39 additions & 2 deletions apps/ledger-live-desktop/src/renderer/hooks/useBraze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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));

Expand All @@ -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<string, string | number>, 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,
Expand Down Expand Up @@ -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();
Expand Down
14 changes: 12 additions & 2 deletions apps/ledger-live-desktop/src/renderer/reducers/dynamicContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
PortfolioContentCard,
} from "~/types/dynamicContent";
import { Handlers } from "./types";
import { SettingsState } from "./settings";

export type DynamicContentState = {
portfolioCards: PortfolioContentCard[];
Expand Down Expand Up @@ -61,8 +62,17 @@ 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.shareAnalytics || state.settings.sharePersonalizedRecommandations
? n.viewed
: !!state.settings.anonymousUserNotifications[n.id],
}));

// Exporting reducer

Expand Down
16 changes: 16 additions & 0 deletions apps/ledger-live-desktop/src/renderer/reducers/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -134,6 +135,7 @@ export type SettingsState = {
onboardingUseCase: OnboardingUseCase | null;
lastOnboardedDevice: Device | null;
alwaysShowMemoTagInfo: boolean;
anonymousUserNotifications: Record<string, number>;
};

export const getInitialLanguageAndLocale = (): { language: Language; locale: Locale } => {
Expand Down Expand Up @@ -239,6 +241,7 @@ export const INITIAL_STATE: SettingsState = {
onboardingUseCase: null,
lastOnboardedDevice: null,
alwaysShowMemoTagInfo: true,
anonymousUserNotifications: {},
};

/* Handlers */
Expand Down Expand Up @@ -311,6 +314,10 @@ type HandlersPayloads = {
[TOGGLE_MEV]: boolean;
[TOGGLE_MEMOTAG_INFO]: boolean;
[TOGGLE_MARKET_WIDGET]: boolean;
[UPDATE_ANONYMOUS_USER_NOTIFICATIONS]: {
notifications: Record<string, number>;
purgeState?: boolean;
};
};
type SettingsHandlers<PreciseKey = true> = Handlers<SettingsState, HandlersPayloads, PreciseKey>;

Expand Down Expand Up @@ -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<SettingsState, HandlersPayloads[keyof HandlersPayloads]>(
Expand Down Expand Up @@ -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;

0 comments on commit 21b1ea0

Please sign in to comment.