Skip to content

Commit

Permalink
feat(llm): setup anonymous content cards
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasWerey committed Apr 11, 2024
1 parent b3f3d3c commit 75015c8
Show file tree
Hide file tree
Showing 20 changed files with 2,677 additions and 90 deletions.
6 changes: 6 additions & 0 deletions .changeset/twelve-rocks-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"ledger-live-desktop": patch
"live-mobile": patch
---

Privacy enhancement: users won't be tracked with braze if both analytics and personalization are false. Each one of them will have a random id for braze to still receive CC
6 changes: 3 additions & 3 deletions apps/ledger-live-desktop/src/renderer/actions/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,12 +356,12 @@ export const setHasSeenAnalyticsOptInPrompt = (hasSeenAnalyticsOptInPrompt: bool
payload: hasSeenAnalyticsOptInPrompt,
});

export const setDismissedContentCard = (payload: { id: string; timestamp: number }) => ({
type: "SET_CONTENT_CARDS_DISMISSED",
export const setDismissedContentCards = (payload: { id: string; timestamp: number }) => ({
type: "SET_DISMISSED_CONTENT_CARDS",
payload,
});

export const clearDismissedContentCards = (payload: string[]) => ({
type: "CLEAR_CONTENT_CARDS_DISMISSED",
type: "CLEAR_DISMISSED_CONTENT_CARDS",
payload,
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ import { useDispatch, useSelector } from "react-redux";
import * as braze from "@braze/web-sdk";
import { PortfolioContentCard } from "~/types/dynamicContent";
import { setPortfolioCards } from "~/renderer/actions/dynamicContent";
import {
trackingEnabledSelector,
contentCardsDismissedSelector,
} from "~/renderer/reducers/settings";
import { setDismissedContentCard } from "~/renderer/actions/settings";
import { trackingEnabledSelector } from "~/renderer/reducers/settings";
import { setDismissedContentCards } from "~/renderer/actions/settings";

export const getTransitions = (transition: "slide" | "flip", reverse = false) => {
const mult = reverse ? -1 : 1;
Expand Down Expand Up @@ -61,21 +58,19 @@ export const useDefaultSlides = (): {
const [cachedContentCards, setCachedContentCards] = useState<braze.Card[]>([]);
const portfolioCards = useSelector(portfolioContentCardSelector);
const isTrackedUser = useSelector(trackingEnabledSelector);
const contentCardsDismissed = useSelector(contentCardsDismissedSelector);
const dispatch = useDispatch();

useEffect(() => {
const cards = braze.getCachedContentCards().cards;
setCachedContentCards(cards);
}, [contentCardsDismissed]);
}, []);

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]);
}
Expand All @@ -91,12 +86,11 @@ export const useDefaultSlides = (): {
const slide = portfolioCards[index];
if (slide?.id) {
const currentCard = cachedContentCards.find(card => card.id === slide.id);

if (currentCard) {
isTrackedUser
? braze.logCardDismissal(currentCard)
: currentCard.id &&
dispatch(setDismissedContentCard({ id: currentCard.id, timestamp: Date.now() }));
dispatch(setDismissedContentCards({ id: currentCard.id, timestamp: Date.now() }));
setCachedContentCards(cachedContentCards.filter(n => n.id !== currentCard.id));
dispatch(setPortfolioCards(portfolioCards.filter(n => n.id !== slide.id)));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import styled from "styled-components";
import { useTransition, animated } from "react-spring";
import IconArrowRight from "~/renderer/icons/ArrowRight";
Expand Down Expand Up @@ -205,6 +205,8 @@ const Carousel = ({
});
}, [index, slides.length, changeVisibleSlide]);

const canceled = useMemo(() => slides.length === 1, [slides.length]);

if (!slides.length) {
// No slides or dismissed, no problem
return null;
Expand All @@ -218,15 +220,12 @@ const Carousel = ({
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
{slides.length > 1 ? (
<ProgressBarWrapper>
<TimeBasedProgressBar onComplete={onNext} duration={speed} paused={paused} />
</ProgressBarWrapper>
) : null}
<ProgressBarWrapper>
<TimeBasedProgressBar onComplete={onNext} duration={speed} paused={paused || canceled} />
</ProgressBarWrapper>
<Slides>
{transitions.map(({ item, props, key }) => {
if (!slides?.[item]) return null;

const { Component } = slides[item];
return (
<animated.div key={key} style={{ ...props }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { setActionCards } from "~/renderer/actions/dynamicContent";
import { openURL } from "~/renderer/linking";
import { track } from "../analytics/segment";
import { trackingEnabledSelector } from "../reducers/settings";
import { setDismissedContentCard } from "../actions/settings";
import { setDismissedContentCards } from "../actions/settings";

const useActionCards = () => {
const dispatch = useDispatch();
Expand Down Expand Up @@ -35,7 +35,7 @@ const useActionCards = () => {
isTrackedUser
? braze.logCardDismissal(currentCard)
: currentCard.id &&
dispatch(setDismissedContentCard({ id: currentCard.id, timestamp: Date.now() }));
dispatch(setDismissedContentCards({ id: currentCard.id, timestamp: Date.now() }));
setCachedContentCards(cachedContentCards.filter(n => n.id !== currentCard.id));
dispatch(setActionCards(actionCards.filter(n => n.id !== currentCard.id)));
}
Expand Down
32 changes: 16 additions & 16 deletions apps/ledger-live-desktop/src/renderer/hooks/useBraze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import getUser from "~/helpers/user";
import {
developerModeSelector,
trackingEnabledSelector,
contentCardsDismissedSelector,
dismissedContentCardsSelector,
} from "../reducers/settings";
import { clearDismissedContentCards } from "../actions/settings";
import { getEnv } from "@ledgerhq/live-env";
import { generateAnonymousId, getOldCampaignIds } from "@ledgerhq/live-common/braze/anonymousUsers";
import { getOldCampaignIds, generateAnonymousId } from "@ledgerhq/live-common/braze/anonymousUsers";

const getDesktopCards = (elem: braze.ContentCards) =>
elem.cards.filter(card => card.extras?.platform === Platform.Desktop);
Expand Down Expand Up @@ -86,13 +86,14 @@ export const mapAsNotificationContentCard = (card: ClassicCard): NotificationCon
export async function useBraze() {
const dispatch = useDispatch();
const devMode = useSelector(developerModeSelector);
const contentCardsDissmissed = useSelector(contentCardsDismissedSelector);
const isUntrackedUser = !useSelector(trackingEnabledSelector);
const contentCardsDissmissed = useSelector(dismissedContentCardsSelector);
const isTrackedUser = useSelector(trackingEnabledSelector);

const initBraze = useCallback(async () => {
const user = await getUser();
const brazeConfig = getBrazeConfig();
const isPlaywright = !!getEnv("PLAYWRIGHT_RUN");
dispatch(clearDismissedContentCards(getOldCampaignIds(contentCardsDissmissed)));

braze.initialize(brazeConfig.apiKey, {
baseUrl: brazeConfig.endpoint,
Expand All @@ -107,32 +108,31 @@ export async function useBraze() {
return;
}

if (user && !isUntrackedUser) {
braze.changeUser(user.id);
} else {
braze.changeUser(generateAnonymousId());
}
if (user) braze.changeUser(isTrackedUser ? user.id : generateAnonymousId());

braze.requestPushPermission();

braze.requestContentCardsRefresh();

braze.subscribeToContentCardsUpdates(cards => {
const desktopCards = getDesktopCards(cards);
dispatch(clearDismissedContentCards(getOldCampaignIds(contentCardsDissmissed)));
const dismissedCardIds = Object.keys(contentCardsDissmissed);
const filteredDesktopCards = desktopCards.filter(
card => !dismissedCardIds.includes(String(card.id)),
);

const portfolioCards = filterByPage(desktopCards, LocationContentCard.Portfolio)
.filter(card => !dismissedCardIds.includes(String(card.id)))
const portfolioCards = filterByPage(filteredDesktopCards, LocationContentCard.Portfolio)
.map(card => mapAsPortfolioContentCard(card as ClassicCard))
.sort(compareCards);

const actionCards = filterByPage(desktopCards, LocationContentCard.Action)
.filter(card => !dismissedCardIds.includes(String(card.id)))
const actionCards = filterByPage(filteredDesktopCards, LocationContentCard.Action)
.map(card => mapAsActionContentCard(card as ClassicCard))
.sort(compareCards);

const notificationsCards = filterByPage(desktopCards, LocationContentCard.NotificationCenter)
const notificationsCards = filterByPage(
filteredDesktopCards,
LocationContentCard.NotificationCenter,
)
.map(card => mapAsNotificationContentCard(card as ClassicCard))
.sort(compareCards);

Expand All @@ -143,7 +143,7 @@ export async function useBraze() {

braze.automaticallyShowInAppMessages();
braze.openSession();
}, [dispatch, devMode, isUntrackedUser, contentCardsDissmissed]);
}, [dispatch, devMode, isTrackedUser, contentCardsDissmissed]);

useEffect(() => {
initBraze();
Expand Down
20 changes: 10 additions & 10 deletions apps/ledger-live-desktop/src/renderer/reducers/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export type SettingsState = {
vaultSigner: VaultSigner;
supportedCounterValues: SupportedCountervaluesData[];
hasSeenAnalyticsOptInPrompt: boolean;
contentCardsDismissed: { [key: string]: number };
dismissedContentCards: { [key: string]: number };
};

export const getInitialLanguageAndLocale = (): { language: Language; locale: Locale } => {
Expand Down Expand Up @@ -194,7 +194,7 @@ const INITIAL_STATE: SettingsState = {
// Vault
vaultSigner: { enabled: false, host: "", token: "", workspace: "" },
supportedCounterValues: [],
contentCardsDismissed: {} as Record<string, number>,
dismissedContentCards: {} as Record<string, number>,
};

/* Handlers */
Expand Down Expand Up @@ -243,10 +243,10 @@ type HandlersPayloads = {
SET_VAULT_SIGNER: VaultSigner;
SET_SUPPORTED_COUNTER_VALUES: SupportedCountervaluesData[];
SET_HAS_SEEN_ANALYTICS_OPT_IN_PROMPT: boolean;
SET_CONTENT_CARDS_DISMISSED: {
SET_DISMISSED_CONTENT_CARDS: {
[key: string]: number;
};
CLEAR_CONTENT_CARDS_DISMISSED: never;
CLEAR_DISMISSED_CONTENT_CARDS: never;
};
type SettingsHandlers<PreciseKey = true> = Handlers<SettingsState, HandlersPayloads, PreciseKey>;

Expand Down Expand Up @@ -410,19 +410,19 @@ const handlers: SettingsHandlers = {
...state,
hasSeenAnalyticsOptInPrompt: payload,
}),
SET_CONTENT_CARDS_DISMISSED: (state: SettingsState, { payload }) => ({
SET_DISMISSED_CONTENT_CARDS: (state: SettingsState, { payload }) => ({
...state,
contentCardsDismissed: {
...state.contentCardsDismissed,
dismissedContentCards: {
...state.dismissedContentCards,
[payload.id]: payload.timestamp,
},
}),

CLEAR_CONTENT_CARDS_DISMISSED: (state: SettingsState, { payload }: { payload?: string[] }) => {
CLEAR_DISMISSED_CONTENT_CARDS: (state: SettingsState, { payload }: { payload?: string[] }) => {
const newState = { ...state };
if (payload) {
payload.forEach(id => {
delete newState.contentCardsDismissed[id];
delete newState.dismissedContentCards[id];
});
}
return newState;
Expand Down Expand Up @@ -733,4 +733,4 @@ export const supportedCounterValuesSelector = (state: State) =>
state.settings.supportedCounterValues;
export const hasSeenAnalyticsOptInPromptSelector = (state: State) =>
state.settings.hasSeenAnalyticsOptInPrompt;
export const contentCardsDismissedSelector = (state: State) => state.settings.contentCardsDismissed;
export const dismissedContentCardsSelector = (state: State) => state.settings.dismissedContentCards;
10 changes: 10 additions & 0 deletions apps/ledger-live-mobile/src/actions/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ import {
SettingsSetUserNps,
SettingsSetSupportedCounterValues,
SettingsSetHasSeenAnalyticsOptInPrompt,
SettingsSetDismissedContentCardsPayload,
SettingsClearDismissedContentCardsPayload,
} from "./types";
import { ImageType } from "~/components/CustomImage/types";

Expand Down Expand Up @@ -290,6 +292,14 @@ export const setHasSeenAnalyticsOptInPrompt = createAction<SettingsSetHasSeenAna
SettingsActionTypes.SET_HAS_SEEN_ANALYTICS_OPT_IN_PROMPT,
);

export const setDismissedContentCard = createAction<SettingsSetDismissedContentCardsPayload>(
SettingsActionTypes.SET_DISMISSED_CONTENT_CARD,
);

export const clearDismissedContentCards = createAction<SettingsClearDismissedContentCardsPayload>(
SettingsActionTypes.CLEAR_DISMISSED_CONTENT_CARDS,
);

type PortfolioRangeOption = {
key: PortfolioRange;
value: string;
Expand Down
8 changes: 7 additions & 1 deletion apps/ledger-live-mobile/src/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ export enum SettingsActionTypes {
SET_USER_NPS = "SET_USER_NPS",
SET_SUPPORTED_COUNTER_VALUES = "SET_SUPPORTED_COUNTER_VALUES",
SET_HAS_SEEN_ANALYTICS_OPT_IN_PROMPT = "SET_HAS_SEEN_ANALYTICS_OPT_IN_PROMPT",
SET_DISMISSED_CONTENT_CARD = "SET_DISMISSED_CONTENT_CARD",
CLEAR_DISMISSED_CONTENT_CARDS = "CLEAR_DISMISSED_CONTENT_CARDS",
}

export type SettingsImportPayload = Partial<SettingsState>;
Expand Down Expand Up @@ -393,6 +395,8 @@ export type SettingsSetGeneralTermsVersionAccepted = SettingsState["generalTerms
export type SettingsSetUserNps = number;
export type SettingsSetSupportedCounterValues = SettingsState["supportedCounterValues"];
export type SettingsSetHasSeenAnalyticsOptInPrompt = SettingsState["hasSeenAnalyticsOptInPrompt"];
export type SettingsSetDismissedContentCardsPayload = SettingsState["dismissedContentCards"];
export type SettingsClearDismissedContentCardsPayload = string[];

export type SettingsPayload =
| SettingsImportPayload
Expand Down Expand Up @@ -450,7 +454,9 @@ export type SettingsPayload =
| SettingsSetClosedNetworkBannerPayload
| SettingsSetUserNps
| SettingsSetSupportedCounterValues
| SettingsSetHasSeenAnalyticsOptInPrompt;
| SettingsSetHasSeenAnalyticsOptInPrompt
| SettingsSetDismissedContentCardsPayload
| SettingsClearDismissedContentCardsPayload;

// === WALLET CONNECT ACTIONS ===
export enum WalletConnectActionTypes {
Expand Down
23 changes: 19 additions & 4 deletions apps/ledger-live-mobile/src/dynamicContent/brazeContentCard.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import { useCallback } from "react";
import Braze from "@braze/react-native-sdk";
import { trackingEnabledSelector } from "../reducers/settings";
import { useSelector, useDispatch } from "react-redux";
import { setDismissedContentCard } from "../actions/settings";

export const useBrazeContentCard = () => {
const logDismissCard = useCallback((cardId: string) => Braze.logContentCardDismissed(cardId), []);
const isTrackedUser = useSelector(trackingEnabledSelector);
const dispatch = useDispatch();

const logClickCard = useCallback((cardId: string) => Braze.logContentCardClicked(cardId), []);
const logDismissCard = useCallback(
(cardId: string) =>
isTrackedUser
? Braze.logContentCardDismissed(cardId)
: dispatch(setDismissedContentCard({ [cardId]: Date.now() })),
[isTrackedUser, dispatch],
);

const logClickCard = useCallback(
(cardId: string) => isTrackedUser && Braze.logContentCardClicked(cardId),
[isTrackedUser],
);

const logImpressionCard = useCallback(
(cardId: string) => Braze.logContentCardImpression(cardId),
[],
(cardId: string) => isTrackedUser && Braze.logContentCardImpression(cardId),
[isTrackedUser],
);

const refreshDynamicContent = () => Braze.requestContentCardsRefresh();
Expand Down
6 changes: 4 additions & 2 deletions apps/ledger-live-mobile/src/dynamicContent/useContentCards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { useEffect } from "react";
import { useDynamicContentLogic } from "./useDynamicContentLogic";

const HookDynamicContentCards = () => {
const { refreshDynamicContent, fetchData } = useDynamicContentLogic();
const { refreshDynamicContent, fetchData, clearOldDismissedContentCards } =
useDynamicContentLogic();

useEffect(() => {
clearOldDismissedContentCards();
refreshDynamicContent();
fetchData();
}, [fetchData, refreshDynamicContent]);
}, [fetchData, refreshDynamicContent, clearOldDismissedContentCards]);

return null;
};
Expand Down

0 comments on commit 75015c8

Please sign in to comment.