Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add offline support for anonymous targeted user via braze notifications #8603

Merged
merged 1 commit into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(() => {
themooneer marked this conversation as resolved.
Show resolved Hide resolved
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
PortfolioContentCard,
} from "~/types/dynamicContent";
import { Handlers } from "./types";
import { SettingsState, trackingEnabledSelector } from "./settings";
import { State } from ".";

export type DynamicContentState = {
portfolioCards: PortfolioContentCard[];
Expand Down Expand Up @@ -61,8 +63,18 @@ 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;
}) => {
const { settings, dynamicContent } = state;
return dynamicContent.notificationsCards.map(n => ({
...n,
viewed: trackingEnabledSelector(state as State)
? n.viewed
: !!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;
Loading