Skip to content

Commit b0efb94

Browse files
fix: treat dismissal of cards the same ways as notifications'
1 parent 9eb9ec3 commit b0efb94

File tree

6 files changed

+86
-91
lines changed

6 files changed

+86
-91
lines changed

apps/ledger-live-desktop/src/renderer/actions/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// Action types
22

33
/** Settings --------- */
4-
export const PURGE_ANONYMOUS_USER_NOTIFICATIONS = "settings/purgeAnonymousUserNotifications";
4+
export const PURGE_EXPIRED_ANONYMOUS_USER_NOTIFICATIONS =
5+
"settings/purgeExpiredAnonymousUserNotifications";
56
export const RESET_HIDDEN_NFT_COLLECTIONS = "settings/resetHiddenNftCollections";
67
export const TOGGLE_MARKET_WIDGET = "settings/toggleMarketWidget";
78
export const TOGGLE_MEMOTAG_INFO = "settings/toggleShouldDisplayMemoTagInfo";

apps/ledger-live-desktop/src/renderer/actions/settings.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { useRefreshAccountsOrdering } from "~/renderer/actions/general";
2626
import { Language, Locale } from "~/config/languages";
2727
import { Layout } from "LLD/features/Collectibles/types/Layouts";
2828
import {
29-
PURGE_ANONYMOUS_USER_NOTIFICATIONS,
29+
PURGE_EXPIRED_ANONYMOUS_USER_NOTIFICATIONS,
3030
RESET_HIDDEN_NFT_COLLECTIONS,
3131
TOGGLE_MARKET_WIDGET,
3232
TOGGLE_MEMOTAG_INFO,
@@ -391,7 +391,7 @@ export const setDismissedContentCards = (payload: { id: string; timestamp: numbe
391391
payload,
392392
});
393393

394-
export const clearDismissedContentCards = (payload: string[]) => ({
394+
export const clearDismissedContentCards = (payload: { now: Date }) => ({
395395
type: "CLEAR_DISMISSED_CONTENT_CARDS",
396396
payload,
397397
});
@@ -462,16 +462,15 @@ export const toggleShouldDisplayMemoTagInfo = (payload: boolean) => {
462462
};
463463
};
464464

465-
export const purgeAnonymousUserNotifications = (payload: { cutoff: number }) => {
465+
export const purgeExpiredAnonymousUserNotifications = (payload: { now: Date }) => {
466466
return {
467-
type: PURGE_ANONYMOUS_USER_NOTIFICATIONS,
467+
type: PURGE_EXPIRED_ANONYMOUS_USER_NOTIFICATIONS,
468468
payload,
469469
};
470470
};
471471

472472
export const updateAnonymousUserNotifications = (payload: {
473473
notifications: SettingsState["anonymousUserNotifications"];
474-
purgeState?: boolean;
475474
}) => {
476475
return {
477476
type: UPDATE_ANONYMOUS_USER_NOTIFICATIONS,

apps/ledger-live-desktop/src/renderer/hooks/useBraze.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import * as braze from "@braze/web-sdk";
22
import { ClassicCard } from "@braze/web-sdk";
3-
import {
4-
cutoffDate,
5-
generateAnonymousId,
6-
getOldCampaignIds,
7-
} from "@ledgerhq/live-common/braze/anonymousUsers";
3+
import { generateAnonymousId } from "@ledgerhq/live-common/braze/anonymousUsers";
84
import { getEnv } from "@ledgerhq/live-env";
95
import { useCallback, useEffect, useRef } from "react";
106
import { useDispatch, useSelector } from "react-redux";
@@ -26,7 +22,7 @@ import {
2622
} from "../actions/dynamicContent";
2723
import {
2824
clearDismissedContentCards,
29-
purgeAnonymousUserNotifications,
25+
purgeExpiredAnonymousUserNotifications,
3026
setAnonymousBrazeId,
3127
} from "../actions/settings";
3228
import {
@@ -95,6 +91,9 @@ export const mapAsNotificationContentCard = (card: ClassicCard): NotificationCon
9591
viewed: card.viewed,
9692
});
9793

94+
/**
95+
* TODO put this effectful logic into a provider instead
96+
*/
9897
export async function useBraze() {
9998
const dispatch = useDispatch();
10099
const devMode = useSelector(developerModeSelector);
@@ -106,7 +105,6 @@ export async function useBraze() {
106105
const user = await getUser();
107106
const brazeConfig = getBrazeConfig();
108107
const isPlaywright = !!getEnv("PLAYWRIGHT_RUN");
109-
dispatch(clearDismissedContentCards(getOldCampaignIds(contentCardsDissmissed)));
110108

111109
if (!anonymousBrazeId.current) {
112110
anonymousBrazeId.current = generateAnonymousId();
@@ -175,15 +173,16 @@ export async function useBraze() {
175173
initBraze();
176174
}, [initBraze]);
177175

176+
// TODO should there be an interval to periodically purge dismissed cards?
177+
useEffect(() => {
178+
dispatch(clearDismissedContentCards({ now: new Date() }));
179+
}, [dispatch]);
180+
178181
// TODO should there be an interval to periodically purge old notifications?
179182
useEffect(() => {
180183
// If the user is opt-out from analytics, we need to purge expired notifications persisted in the store/offline storage
181184
if (!isTrackedUser) {
182-
dispatch(
183-
purgeAnonymousUserNotifications({
184-
cutoff: cutoffDate(),
185-
}),
186-
);
185+
dispatch(purgeExpiredAnonymousUserNotifications({ now: new Date() }));
187186
}
188187
}, [dispatch, isTrackedUser]);
189188
}

apps/ledger-live-desktop/src/renderer/reducers/settings.test.ts

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
* @jest-environment jsdom
33
*/
44

5+
import { aDeviceInfoBuilder } from "@ledgerhq/live-common/mock/fixtures/aDeviceInfo";
56
import { DeviceModelId } from "@ledgerhq/types-devices";
7+
import { State } from ".";
8+
import { purgeExpiredAnonymousUserNotifications } from "../actions/settings";
69
import reducer, {
710
lastSeenDeviceSelector,
8-
INITIAL_STATE as SETTINGS_INITIAL_STATE,
911
localeSelector,
12+
INITIAL_STATE as SETTINGS_INITIAL_STATE,
13+
SettingsState,
1014
} from "./settings";
11-
import { State } from ".";
12-
import { aDeviceInfoBuilder } from "@ledgerhq/live-common/mock/fixtures/aDeviceInfo";
13-
import { PURGE_ANONYMOUS_USER_NOTIFICATIONS } from "../actions/constants";
15+
import { getBrazeCampaignCutoff } from "@ledgerhq/live-common/lib-es/braze/anonymousUsers";
1416

1517
const invalidDeviceModelIds = ["nanoFTS", undefined, "whatever"];
1618
const validDeviceModelIds: DeviceModelId[] = Object.values(DeviceModelId);
@@ -94,23 +96,22 @@ describe("lastSeenDeviceSelector", () => {
9496

9597
describe("action: purgeAnonymousUserNotifications", () => {
9698
it("should remove notifications older than cutoff but keep newer ones", () => {
97-
const oldTimestamp = Date.now() - 100000; // old enough to purge
98-
const newTimestamp = Date.now() + 100000; // keep
99-
const cutoff = Date.now();
99+
const now = new Date();
100+
const cutoff = getBrazeCampaignCutoff(now);
100101

101-
const state = {
102+
const oldTimestamp = cutoff - 1;
103+
const newTimestamp = cutoff + 1;
104+
105+
const state: SettingsState = {
102106
...SETTINGS_INITIAL_STATE,
103107
anonymousUserNotifications: {
104108
a: oldTimestamp,
105109
b: newTimestamp,
106-
LNSUpsell: oldTimestamp, // should always be kept
110+
LNSUpsell: oldTimestamp,
107111
},
108112
};
109113

110-
const newState = reducer(state, {
111-
type: PURGE_ANONYMOUS_USER_NOTIFICATIONS,
112-
payload: { cutoff },
113-
});
114+
const newState = reducer(state, purgeExpiredAnonymousUserNotifications({ now }));
114115

115116
expect(newState.anonymousUserNotifications).toEqual({
116117
b: newTimestamp,
@@ -119,10 +120,10 @@ describe("action: purgeAnonymousUserNotifications", () => {
119120
});
120121

121122
it("should keep all notifications if none are expired", () => {
122-
const ts = Date.now();
123-
const cutoff = ts - 1000;
123+
const now = new Date();
124+
const ts = now.getTime() - 1000;
124125

125-
const state = {
126+
const state: SettingsState = {
126127
...SETTINGS_INITIAL_STATE,
127128
anonymousUserNotifications: {
128129
a: ts,
@@ -131,47 +132,39 @@ describe("action: purgeAnonymousUserNotifications", () => {
131132
},
132133
};
133134

134-
const newState = reducer(state, {
135-
type: PURGE_ANONYMOUS_USER_NOTIFICATIONS,
136-
payload: { cutoff },
137-
});
135+
const newState = reducer(state, purgeExpiredAnonymousUserNotifications({ now }));
138136

139137
expect(newState).toBe(state);
140138
});
141139

142140
it("should keep LNSUpsell even if expired", () => {
143-
const oldTimestamp = Date.now() - 100000;
144-
const cutoff = Date.now();
141+
const now = new Date();
142+
const cutoff = getBrazeCampaignCutoff(now);
143+
const expired = cutoff - 1;
145144

146-
const state = {
145+
const state: SettingsState = {
147146
...SETTINGS_INITIAL_STATE,
148147
anonymousUserNotifications: {
149-
x: oldTimestamp,
150-
LNSUpsell: oldTimestamp,
148+
x: expired,
149+
LNSUpsell: expired,
151150
},
152151
};
153152

154-
const newState = reducer(state, {
155-
type: PURGE_ANONYMOUS_USER_NOTIFICATIONS,
156-
payload: { cutoff },
157-
});
153+
const newState = reducer(state, purgeExpiredAnonymousUserNotifications({ now }));
158154

159155
expect(newState.anonymousUserNotifications).toEqual({
160-
LNSUpsell: oldTimestamp,
156+
LNSUpsell: expired,
161157
});
162158
});
163159

164160
it("should return original state if anonymousUserNotifications is empty", () => {
165-
const cutoff = Date.now();
166-
const state = {
161+
const now = new Date();
162+
const state: SettingsState = {
167163
...SETTINGS_INITIAL_STATE,
168164
anonymousUserNotifications: {},
169165
};
170166

171-
const newState = reducer(state, {
172-
type: PURGE_ANONYMOUS_USER_NOTIFICATIONS,
173-
payload: { cutoff },
174-
});
167+
const newState = reducer(state, purgeExpiredAnonymousUserNotifications({ now }));
175168

176169
expect(newState).toBe(state);
177170
});

apps/ledger-live-desktop/src/renderer/reducers/settings.ts

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,50 @@
1-
import { handleActions } from "redux-actions";
2-
import { createSelector } from "reselect";
1+
import { DeviceModelId } from "@ledgerhq/devices";
2+
import { getBrazeCampaignCutoff } from "@ledgerhq/live-common/braze/anonymousUsers";
33
import {
44
findCurrencyByTicker,
55
getCryptoCurrencyById,
6-
listSupportedFiats,
76
getFiatCurrencyByTicker,
7+
listSupportedFiats,
88
OFAC_CURRENCIES,
99
} from "@ledgerhq/live-common/currencies/index";
10-
import { DeviceModelId } from "@ledgerhq/devices";
10+
import { getEnv } from "@ledgerhq/live-env";
11+
import { SupportedBlockchain } from "@ledgerhq/live-nft/supported";
12+
import { NftStatus } from "@ledgerhq/live-nft/types";
13+
import { CryptoCurrency, Currency, Unit } from "@ledgerhq/types-cryptoassets";
1114
import {
15+
AccountLike,
1216
DeviceModelInfo,
13-
FeatureId,
1417
Feature,
15-
PortfolioRange,
18+
FeatureId,
1619
FirmwareUpdateContext,
17-
AccountLike,
20+
PortfolioRange,
1821
} from "@ledgerhq/types-live";
19-
import { CryptoCurrency, Currency, Unit } from "@ledgerhq/types-cryptoassets";
20-
import { getEnv } from "@ledgerhq/live-env";
22+
import { Layout, LayoutKey } from "LLD/features/Collectibles/types/Layouts";
23+
import { handleActions } from "redux-actions";
24+
import { createSelector } from "reselect";
2125
import {
26+
DEFAULT_LANGUAGE,
27+
Language,
2228
LanguageIds,
2329
LanguageIdsNotFeatureFlagged,
2430
Languages,
25-
Language,
2631
Locale,
27-
DEFAULT_LANGUAGE,
2832
OFAC_LOCALES,
2933
} from "~/config/languages";
30-
import { State } from ".";
31-
import regionsByKey from "~/renderer/screens/settings/sections/General/regions.json";
3234
import { getAppLocale } from "~/helpers/systemLocale";
33-
import { Handlers } from "./types";
34-
import { Layout, LayoutKey } from "LLD/features/Collectibles/types/Layouts";
35-
import { OnboardingUseCase } from "../components/Onboarding/OnboardingUseCase";
35+
import regionsByKey from "~/renderer/screens/settings/sections/General/regions.json";
36+
import { State } from ".";
3637
import {
37-
TOGGLE_MEMOTAG_INFO,
38+
PURGE_EXPIRED_ANONYMOUS_USER_NOTIFICATIONS,
39+
RESET_HIDDEN_NFT_COLLECTIONS,
3840
TOGGLE_MARKET_WIDGET,
41+
TOGGLE_MEMOTAG_INFO,
3942
TOGGLE_MEV,
40-
UPDATE_NFT_COLLECTION_STATUS,
4143
UPDATE_ANONYMOUS_USER_NOTIFICATIONS,
42-
RESET_HIDDEN_NFT_COLLECTIONS,
43-
PURGE_ANONYMOUS_USER_NOTIFICATIONS,
44+
UPDATE_NFT_COLLECTION_STATUS,
4445
} from "../actions/constants";
45-
import { SupportedBlockchain } from "@ledgerhq/live-nft/supported";
46-
import { NftStatus } from "@ledgerhq/live-nft/types";
46+
import { OnboardingUseCase } from "../components/Onboarding/OnboardingUseCase";
47+
import { Handlers } from "./types";
4748

4849
/* Initial state */
4950

@@ -301,7 +302,7 @@ type HandlersPayloads = {
301302
SET_DISMISSED_CONTENT_CARDS: {
302303
[key: string]: number;
303304
};
304-
CLEAR_DISMISSED_CONTENT_CARDS: never;
305+
CLEAR_DISMISSED_CONTENT_CARDS: { now: Date };
305306
SET_ANONYMOUS_BRAZE_ID: string;
306307
SET_CURRENCY_SETTINGS: { key: string; value: CurrencySettings };
307308

@@ -315,7 +316,7 @@ type HandlersPayloads = {
315316
SET_HAS_REDIRECTED_TO_POST_ONBOARDING: boolean;
316317
SET_LAST_ONBOARDED_DEVICE: Device | null;
317318

318-
[PURGE_ANONYMOUS_USER_NOTIFICATIONS]: { cutoff: number };
319+
[PURGE_EXPIRED_ANONYMOUS_USER_NOTIFICATIONS]: { now: Date };
319320
[TOGGLE_MEV]: boolean;
320321
[TOGGLE_MEMOTAG_INFO]: boolean;
321322
[TOGGLE_MARKET_WIDGET]: boolean;
@@ -516,14 +517,15 @@ const handlers: SettingsHandlers = {
516517
},
517518
}),
518519

519-
CLEAR_DISMISSED_CONTENT_CARDS: (state: SettingsState, { payload }: { payload?: string[] }) => {
520-
const newState = { ...state };
521-
if (payload) {
522-
payload.forEach(id => {
523-
delete newState.dismissedContentCards[id];
524-
});
525-
}
526-
return newState;
520+
CLEAR_DISMISSED_CONTENT_CARDS: (state: SettingsState, { payload: { now } }) => {
521+
const cutoff = getBrazeCampaignCutoff(now);
522+
523+
const prev = state.dismissedContentCards;
524+
const next = Object.fromEntries(Object.entries(prev).filter(([, ts]) => ts >= cutoff));
525+
526+
return Object.keys(next).length === Object.keys(prev).length
527+
? state
528+
: { ...state, dismissedContentCards: next };
527529
},
528530
SET_ANONYMOUS_BRAZE_ID: (state: SettingsState, { payload }) => ({
529531
...state,
@@ -563,9 +565,9 @@ const handlers: SettingsHandlers = {
563565
lastOnboardedDevice: payload,
564566
}),
565567

566-
[PURGE_ANONYMOUS_USER_NOTIFICATIONS]: (state, { payload: { cutoff } }) => {
568+
[PURGE_EXPIRED_ANONYMOUS_USER_NOTIFICATIONS]: (state, { payload: { now } }) => {
567569
const { LNSUpsell, ...rest } = state.anonymousUserNotifications;
568-
570+
const cutoff = getBrazeCampaignCutoff(now);
569571
const next: typeof rest = {
570572
...(LNSUpsell ? { LNSUpsell } : {}),
571573
...Object.fromEntries(Object.entries(rest).filter(([_, ts]) => ts >= cutoff)),
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
const NUMBER_OF_MONTHS = 3;
22
const MILLISECONDS_IN_A_MONTH = 30 * 24 * 60 * 60 * 1000;
33

4-
export const cutoffDate = () => Date.now() - NUMBER_OF_MONTHS * MILLISECONDS_IN_A_MONTH;
4+
export const getBrazeCampaignCutoff = (now: Date) =>
5+
now.getTime() - NUMBER_OF_MONTHS * MILLISECONDS_IN_A_MONTH;
56

67
export const generateAnonymousId = () => {
78
return "anonymous_id_" + (Math.floor(Math.random() * 20) + 1);
89
};
910

1011
export const getOldCampaignIds = (campaigns: Record<string, number>) => {
11-
const timeAgo = cutoffDate();
12+
const cutoff = getBrazeCampaignCutoff(new Date());
1213
return Object.entries(campaigns)
13-
.filter(([_, timestamp]) => timestamp < timeAgo)
14+
.filter(([_, timestamp]) => timestamp < cutoff)
1415
.map(([id, _]) => id);
1516
};

0 commit comments

Comments
 (0)