Skip to content
Open
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/calm-desks-renew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ledger-live-desktop": minor
---

Wire desktop analytics consent renewal to shared policyVersion major/minor decision flow
2 changes: 2 additions & 0 deletions apps/ledger-live-desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@datadog/browser-rum": "6.30.1",
"@domain/api-pay-card": "workspace:^",
"@domain/api-currency-token": "workspace:^",
"@domain/entity-analytics-consent": "workspace:*",
"@domain/entity-contact": "workspace:*",
"@domain/entity-currency": "workspace:^",
"@domain/entity-currency-crypto": "workspace:*",
Expand All @@ -76,6 +77,7 @@
"@devtools/bindings": "workspace:*",
"@electron/fuses": "2.0.0",
"@features/flow-card": "workspace:^",
"@features/flow-analytics-consent": "workspace:*",
"@features/flow-contacts": "workspace:^",
"@features/flow-large-screen-upsell": "workspace:^",
"@features/platform-currencies": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AnalyticsConsentDialog } from "../index";
const analyticsOptInOverrides = {
...FEATURE_FLAGS_INITIAL_STATE.overrides,
analyticsOptIn: {
...FEATURE_FLAGS_DEFAULTS.analyticsOptIn,
...(FEATURE_FLAGS_INITIAL_STATE.overrides.analyticsOptIn ?? {}),
enabled: true,
},
Expand Down Expand Up @@ -97,7 +98,7 @@ describe("AnalyticsConsentDialog on portfolio route", () => {
{
button: "analytics_consent_opt_in",
page: ANALYTICS_CONSENT_DIALOG_PAGE,
privacyPolicyVersion: 1,
privacyPolicyVersion: "1.0",
},
true,
);
Expand Down Expand Up @@ -143,7 +144,7 @@ describe("AnalyticsConsentDialog on portfolio route", () => {
{
button: "analytics_consent_opt_out",
page: ANALYTICS_CONSENT_DIALOG_PAGE,
privacyPolicyVersion: 1,
privacyPolicyVersion: "1.0",
},
true,
);
Expand Down Expand Up @@ -242,7 +243,7 @@ describe("AnalyticsConsentDialog on portfolio route", () => {
{
button: "analytics_consent_opt_in",
page: ANALYTICS_CONSENT_DIALOG_PAGE,
privacyPolicyVersion: 1,
privacyPolicyVersion: "1.0",
},
true,
);
Expand Down Expand Up @@ -288,7 +289,7 @@ describe("AnalyticsConsentDialog on portfolio route", () => {
{
button: "analytics_consent_opt_out",
page: ANALYTICS_CONSENT_DIALOG_PAGE,
privacyPolicyVersion: 1,
privacyPolicyVersion: "1.0",
},
true,
);
Expand Down Expand Up @@ -389,7 +390,7 @@ describe("AnalyticsConsentDialog on portfolio route", () => {
{
button: "analytics_consent_opt_in",
page: ANALYTICS_CONSENT_DIALOG_PAGE,
privacyPolicyVersion: 1,
privacyPolicyVersion: "1.0",
},
true,
);
Expand Down Expand Up @@ -435,7 +436,7 @@ describe("AnalyticsConsentDialog on portfolio route", () => {
{
button: "analytics_consent_opt_out",
page: ANALYTICS_CONSENT_DIALOG_PAGE,
privacyPolicyVersion: 1,
privacyPolicyVersion: "1.0",
},
true,
);
Expand Down Expand Up @@ -534,7 +535,7 @@ describe("AnalyticsConsentDialog on portfolio route", () => {
{
button: "analytics_consent_opt_in",
page: ANALYTICS_CONSENT_DIALOG_PAGE,
privacyPolicyVersion: 1,
privacyPolicyVersion: "1.0",
},
true,
);
Expand Down Expand Up @@ -580,7 +581,7 @@ describe("AnalyticsConsentDialog on portfolio route", () => {
{
button: "analytics_consent_opt_out",
page: ANALYTICS_CONSENT_DIALOG_PAGE,
privacyPolicyVersion: 1,
privacyPolicyVersion: "1.0",
},
true,
);
Expand Down Expand Up @@ -648,17 +649,47 @@ describe("AnalyticsConsentDialog on portfolio route", () => {
});

describe("needs privacy policy version update", () => {
const featureFlagsWithMinorBump = {
...featureFlagsWithAnalyticsOptIn,
overrides: {
...analyticsOptInOverrides,
analyticsOptIn: {
...analyticsOptInOverrides.analyticsOptIn,
params: {
...FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params,
policyVersion: "1.1",
consentValidityDays:
FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params?.consentValidityDays ?? 365,
},
},
},
resolved: {
...FEATURE_FLAGS_DEFAULTS,
...analyticsOptInOverrides,
analyticsOptIn: {
...analyticsOptInOverrides.analyticsOptIn,
params: {
...FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params,
policyVersion: "1.1",
consentValidityDays:
FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params?.consentValidityDays ?? 365,
},
},
},
};

it("should show the privacy update sheet, persist the policy version, and close after Got it", async () => {
const consentDate = new Date().toISOString();
const { user, store } = render(<TestRouter />, {
initialRoute: "/",
initialState: {
featureFlags: featureFlagsWithAnalyticsOptIn,
featureFlags: featureFlagsWithMinorBump,
settings: baseSettings({
shareAnalytics: true,
sharePersonalizedRecommandations: true,
analyticsConsentInfo: {
consentDate: new Date().toISOString(),
privacyPolicyVersion: 0,
consentDate,
privacyPolicyVersion: "1.0",
},
}),
},
Expand All @@ -684,15 +715,16 @@ describe("AnalyticsConsentDialog on portfolio route", () => {
{
button: "analytics_consent_privacy_got_it",
page: ANALYTICS_CONSENT_DIALOG_PAGE,
privacyPolicyVersion: 1,
privacyPolicyVersion: "1.1",
},
true,
);

await waitFor(() => {
expect(title).not.toBeInTheDocument();
});
expect(store.getState().settings.analyticsConsentInfo.privacyPolicyVersion).toBe(1);
expect(store.getState().settings.analyticsConsentInfo.consentDate).toBe(consentDate);
expect(store.getState().settings.analyticsConsentInfo.privacyPolicyVersion).toBe("1.1");
expect(store.getState().settings.hasSeenAnalyticsOptInPrompt).toBe(true);
expect(updateIdentify).toHaveBeenCalledWith({ force: true });
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {

const mockUseMatch = jest.fn();

/** Frozen clock; consent offsets align with `needsConsentRenewal` in live-common (`analyticsConsentUtils`). */
const FIXED_NOW = new Date("2024-06-15T12:00:00.000Z");

jest.mock("react-router", () => ({
Expand All @@ -36,6 +35,7 @@ type SettingsOverrides = Partial<SettingsState> & {
const analyticsOptInOverrides = {
...FEATURE_FLAGS_INITIAL_STATE.overrides,
analyticsOptIn: {
...FEATURE_FLAGS_DEFAULTS.analyticsOptIn,
...(FEATURE_FLAGS_INITIAL_STATE.overrides.analyticsOptIn ?? {}),
enabled: true,
},
Expand All @@ -47,6 +47,35 @@ const featureFlagsWithAnalyticsOptIn = {
resolved: { ...FEATURE_FLAGS_DEFAULTS, ...analyticsOptInOverrides },
};

const featureFlagsWithMinorBump = {
...featureFlagsWithAnalyticsOptIn,
overrides: {
...analyticsOptInOverrides,
analyticsOptIn: {
...analyticsOptInOverrides.analyticsOptIn,
params: {
...FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params,
policyVersion: "1.1",
consentValidityDays:
FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params?.consentValidityDays ?? 365,
},
},
},
resolved: {
...FEATURE_FLAGS_DEFAULTS,
...analyticsOptInOverrides,
analyticsOptIn: {
...analyticsOptInOverrides.analyticsOptIn,
params: {
...FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params,
policyVersion: "1.1",
consentValidityDays:
FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params?.consentValidityDays ?? 365,
},
},
},
};

const defaultSettings: SettingsState = {
...INITIAL_STATE,
hasCompletedOnboarding: true,
Expand All @@ -64,10 +93,13 @@ const flushEffects = async () => {
});
};

const renderViewModel = (settings: SettingsOverrides = {}) =>
const renderViewModel = (
settings: SettingsOverrides = {},
featureFlags = featureFlagsWithAnalyticsOptIn,
) =>
renderHook(() => useAnalyticsConsentDialogViewModel(), {
initialState: {
featureFlags: featureFlagsWithAnalyticsOptIn,
featureFlags,
settings: {
...defaultSettings,
...settings,
Expand All @@ -91,12 +123,6 @@ const expectConsentReconfirm = (viewModel: ViewModel) => {

const renderReconfirmViewModel = () => renderViewModel();

const createStaleConsentDate = () => {
const staleConsentDate = new Date(FIXED_NOW);
staleConsentDate.setUTCDate(staleConsentDate.getUTCDate() - 366);
return staleConsentDate.toISOString();
};

describe("useAnalyticsConsentDialogViewModel", () => {
beforeEach(() => {
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
Expand All @@ -123,22 +149,61 @@ describe("useAnalyticsConsentDialogViewModel", () => {
expectConsentReconfirm(result.current);
});

it("keeps modal closed when consent is still within the renewal window", () => {
const consentWithinWindow = new Date(FIXED_NOW);
consentWithinWindow.setUTCDate(consentWithinWindow.getUTCDate() - 300);

it("keeps modal closed when consent is up to date", () => {
const { result } = renderViewModel({
shareAnalytics: false,
sharePersonalizedRecommandations: false,
analyticsConsentInfo: {
consentDate: consentWithinWindow.toISOString(),
consentDate: FIXED_NOW.toISOString(),
privacyPolicyVersion: 1,
},
});

expectClosed(result.current);
});

it("opens consentReconfirm on a major policyVersion bump", async () => {
const { result } = renderViewModel(
{
analyticsConsentInfo: {
consentDate: FIXED_NOW.toISOString(),
privacyPolicyVersion: "1.0",
},
},
{
...featureFlagsWithAnalyticsOptIn,
overrides: {
...analyticsOptInOverrides,
analyticsOptIn: {
...analyticsOptInOverrides.analyticsOptIn,
params: {
...FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params,
policyVersion: "2.0",
consentValidityDays:
FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params?.consentValidityDays ?? 365,
},
},
},
resolved: {
...FEATURE_FLAGS_DEFAULTS,
...analyticsOptInOverrides,
analyticsOptIn: {
...analyticsOptInOverrides.analyticsOptIn,
params: {
...FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params,
policyVersion: "2.0",
consentValidityDays:
FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params?.consentValidityDays ?? 365,
},
},
},
},
);

await flushEffects();
expectConsentReconfirm(result.current);
});

it("dispatches opt-in and closes modal", async () => {
const { result, store } = renderReconfirmViewModel();

Expand All @@ -153,7 +218,7 @@ describe("useAnalyticsConsentDialogViewModel", () => {
expect(settings.shareAnalytics).toBe(true);
expect(settings.sharePersonalizedRecommandations).toBe(true);
expect(settings.analyticsConsentInfo.consentDate).not.toBeNull();
expect(settings.analyticsConsentInfo.privacyPolicyVersion).toBe(1);
expect(settings.analyticsConsentInfo.privacyPolicyVersion).toBe("1.0");
expectClosed(result.current);
});

Expand All @@ -162,7 +227,7 @@ describe("useAnalyticsConsentDialogViewModel", () => {
shareAnalytics: false,
sharePersonalizedRecommandations: false,
analyticsConsentInfo: {
consentDate: createStaleConsentDate(),
consentDate: null,
privacyPolicyVersion: 1,
},
});
Expand All @@ -179,19 +244,23 @@ describe("useAnalyticsConsentDialogViewModel", () => {
{
button: "analytics_consent_opt_out",
page: ANALYTICS_CONSENT_DIALOG_PAGE,
privacyPolicyVersion: 1,
privacyPolicyVersion: "1.0",
},
true,
);
});

it("tracks privacy acknowledgement as mandatory", async () => {
const { result } = renderViewModel({
analyticsConsentInfo: {
consentDate: FIXED_NOW.toISOString(),
privacyPolicyVersion: 0,
it("tracks privacy acknowledgement as mandatory and preserves consentDate", async () => {
const consentDate = FIXED_NOW.toISOString();
const { result, store } = renderViewModel(
{
analyticsConsentInfo: {
consentDate,
privacyPolicyVersion: "1.0",
},
},
});
featureFlagsWithMinorBump,
);

await flushEffects();
expect(result.current.phase).toBe("privacy");
Expand All @@ -205,10 +274,12 @@ describe("useAnalyticsConsentDialogViewModel", () => {
{
button: "analytics_consent_privacy_got_it",
page: ANALYTICS_CONSENT_DIALOG_PAGE,
privacyPolicyVersion: 1,
privacyPolicyVersion: "1.1",
},
true,
);
expect(store.getState().settings.analyticsConsentInfo.consentDate).toBe(consentDate);
expect(store.getState().settings.analyticsConsentInfo.privacyPolicyVersion).toBe("1.1");
});

it("tracks preferences confirmation as mandatory", async () => {
Expand All @@ -230,7 +301,7 @@ describe("useAnalyticsConsentDialogViewModel", () => {
{
button: "analytics_consent_preferences_confirm",
page: ANALYTICS_CONSENT_DIALOG_PAGE,
privacyPolicyVersion: 1,
privacyPolicyVersion: "1.0",
},
true,
);
Expand Down
Loading
Loading