diff --git a/.changeset/swift-badgers-clear.md b/.changeset/swift-badgers-clear.md new file mode 100644 index 000000000000..214866b4c531 --- /dev/null +++ b/.changeset/swift-badgers-clear.md @@ -0,0 +1,9 @@ +--- +"@shared/feature-flags": patch +"@ledgerhq/live-common": patch +"@features/flow-analytics-consent": patch +"ledger-live-desktop": patch +"live-mobile": patch +--- + +Remove analytics consentValidityDays and the unused live-common consent expiry helpers diff --git a/apps/ledger-live-desktop/src/mvvm/features/AnalyticsConsentDialog/__integrations__/AnalyticsConsentDialog.portfolio.integration.test.tsx b/apps/ledger-live-desktop/src/mvvm/features/AnalyticsConsentDialog/__integrations__/AnalyticsConsentDialog.portfolio.integration.test.tsx index 420622bc1477..5ede28f8ee54 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/AnalyticsConsentDialog/__integrations__/AnalyticsConsentDialog.portfolio.integration.test.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/AnalyticsConsentDialog/__integrations__/AnalyticsConsentDialog.portfolio.integration.test.tsx @@ -658,8 +658,6 @@ describe("AnalyticsConsentDialog on portfolio route", () => { params: { ...FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params, policyVersion: "1.1", - consentValidityDays: - FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params?.consentValidityDays ?? 365, }, }, }, @@ -671,8 +669,6 @@ describe("AnalyticsConsentDialog on portfolio route", () => { params: { ...FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params, policyVersion: "1.1", - consentValidityDays: - FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params?.consentValidityDays ?? 365, }, }, }, diff --git a/apps/ledger-live-desktop/src/mvvm/features/AnalyticsConsentDialog/__tests__/useAnalyticsConsentDialogViewModel.test.ts b/apps/ledger-live-desktop/src/mvvm/features/AnalyticsConsentDialog/__tests__/useAnalyticsConsentDialogViewModel.test.ts index 11ec7fd0ce8f..a5273f27e5a1 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/AnalyticsConsentDialog/__tests__/useAnalyticsConsentDialogViewModel.test.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/AnalyticsConsentDialog/__tests__/useAnalyticsConsentDialogViewModel.test.ts @@ -56,8 +56,6 @@ const featureFlagsWithMinorBump = { params: { ...FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params, policyVersion: "1.1", - consentValidityDays: - FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params?.consentValidityDays ?? 365, }, }, }, @@ -69,8 +67,6 @@ const featureFlagsWithMinorBump = { params: { ...FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params, policyVersion: "1.1", - consentValidityDays: - FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params?.consentValidityDays ?? 365, }, }, }, @@ -179,8 +175,6 @@ describe("useAnalyticsConsentDialogViewModel", () => { params: { ...FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params, policyVersion: "2.0", - consentValidityDays: - FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params?.consentValidityDays ?? 365, }, }, }, @@ -192,8 +186,6 @@ describe("useAnalyticsConsentDialogViewModel", () => { params: { ...FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params, policyVersion: "2.0", - consentValidityDays: - FEATURE_FLAGS_DEFAULTS.analyticsOptIn.params?.consentValidityDays ?? 365, }, }, }, diff --git a/apps/ledger-live-desktop/src/renderer/reducers/settings.test.ts b/apps/ledger-live-desktop/src/renderer/reducers/settings.test.ts index c52bd91a51ae..0d0764590f48 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/settings.test.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/settings.test.ts @@ -682,7 +682,7 @@ describe("trackingEnabledSelector", () => { lastAnalyticsConsentDate: FIXED_NOW.toISOString(), privacyPolicyVersion: "1.0", }, - { params: { policyVersion: "2.0", consentValidityDays: 365 } }, + { params: { policyVersion: "2.0" } }, ), ), ).toBe(false); @@ -698,7 +698,7 @@ describe("trackingEnabledSelector", () => { lastAnalyticsConsentDate: FIXED_NOW.toISOString(), privacyPolicyVersion: "1.0", }, - { params: { policyVersion: "1.1", consentValidityDays: 365 } }, + { params: { policyVersion: "1.1" } }, ), ), ).toBe(true); @@ -714,7 +714,7 @@ describe("trackingEnabledSelector", () => { lastAnalyticsConsentDate: FIXED_NOW.toISOString(), privacyPolicyVersion: "1.0", }, - { params: { policyVersion: "not-a-version", consentValidityDays: 365 } }, + { params: { policyVersion: "not-a-version" } }, ), ), ).toBe(true); diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/AnalyticsConsentOptInDevTool/AnalyticsConsentOptInDevScreen.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/AnalyticsConsentOptInDevTool/AnalyticsConsentOptInDevScreen.tsx index b91369671a6c..82e1bcfd96f0 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/AnalyticsConsentOptInDevTool/AnalyticsConsentOptInDevScreen.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/AnalyticsConsentOptInDevTool/AnalyticsConsentOptInDevScreen.tsx @@ -25,7 +25,6 @@ const COPY = { title: "Analytics opt-in consent — QA", readSection: "Current settings (read-only)", currentPrivacyVersion: (version: string) => `App current privacy policy version: ${version}`, - currentConsentValidityDays: (days: string) => `App current consent validity (days): ${days}`, storedPrivacyVersion: (version: string) => `Stored privacy policy version: ${version}`, consentDate: (value: string) => `Consent date (raw): ${value}`, decision: (value: string) => `Shared decision: ${value}`, @@ -55,13 +54,6 @@ const formatConsentDate = (value: string | null) => { return value; }; -function readConsentValidityDays(feature: { params?: unknown } | null | undefined): string { - const params = feature?.params; - if (!params || typeof params !== "object") return "n/a"; - const days = (params as { consentValidityDays?: unknown }).consentValidityDays; - return days == null ? "n/a" : String(days); -} - export function AnalyticsConsentOptInDevScreen() { const navigate = useNavigate(); const dispatch = useDispatch(); @@ -71,7 +63,6 @@ export function AnalyticsConsentOptInDevScreen() { const analyticsOptInFeature = useFeature("analyticsOptIn"); const { currentPolicyVersion } = resolveAnalyticsOptInParams(analyticsOptInFeature); const decision = getAnalyticsConsentDecision(consentInfo, { currentPolicyVersion }); - const consentValidityDays = readConsentValidityDays(analyticsOptInFeature); const shouldOfferModal = Boolean(analyticsOptInFeature?.enabled) && hasCompletedOnboarding && decision.kind !== "none"; @@ -147,9 +138,6 @@ export function AnalyticsConsentOptInDevScreen() {

{COPY.currentPrivacyVersion(currentPolicyVersion?.normalized ?? "null")}

-

- {COPY.currentConsentValidityDays(consentValidityDays)} -

{COPY.decision(`${decision.kind} (${decision.reason})`)}

diff --git a/apps/ledger-live-mobile/src/mvvm/features/AnalyticsConsentDrawer/__tests__/useAnalyticsConsentDrawerViewModel.test.ts b/apps/ledger-live-mobile/src/mvvm/features/AnalyticsConsentDrawer/__tests__/useAnalyticsConsentDrawerViewModel.test.ts index c526dd13cbea..0aee2a29acc6 100644 --- a/apps/ledger-live-mobile/src/mvvm/features/AnalyticsConsentDrawer/__tests__/useAnalyticsConsentDrawerViewModel.test.ts +++ b/apps/ledger-live-mobile/src/mvvm/features/AnalyticsConsentDrawer/__tests__/useAnalyticsConsentDrawerViewModel.test.ts @@ -9,7 +9,7 @@ import { } from "../hooks/useAnalyticsConsentDrawerViewModel"; import { withConsentDrawerOpeningFresh, withConsentDrawerState } from "./helpers"; -/** Fixed clock so `needsConsentRenewal` / consent ISO strings are deterministic. */ +/** Fixed clock so consent ISO strings are deterministic. */ const FIXED_NOW = new Date("2024-01-15T12:00:00.000Z"); const drawerEventPayload = { diff --git a/features/flow/analytics-consent/README.md b/features/flow/analytics-consent/README.md index f2a563016fd5..67e83d49ed86 100644 --- a/features/flow/analytics-consent/README.md +++ b/features/flow/analytics-consent/README.md @@ -8,9 +8,7 @@ acknowledge a new privacy policy, or stay silent. Implements [ADR: Analytics Consent Renewal](https://ledgerhq.atlassian.net/wiki/spaces/Engagement/pages/7249592355): `analyticsOptIn.params.policyVersion` carries major/minor semantics. -Renewal is version-driven only. A stored consent never expires on a timer, so -`analyticsOptIn.params.consentValidityDays` is not read here — it remains for desktop, which -still runs its own rolling window through `libs/ledger-live-common`. +Renewal is version-driven only. A stored consent never expires on a timer. One verdict drives both the consent drawer and the tracking gate, so optional analytics is disabled exactly while a fresh choice is pending. diff --git a/features/flow/analytics-consent/package.json b/features/flow/analytics-consent/package.json index 4506788119df..dbeefed790b7 100644 --- a/features/flow/analytics-consent/package.json +++ b/features/flow/analytics-consent/package.json @@ -2,7 +2,7 @@ "name": "@features/flow-analytics-consent", "version": "0.1.0", "private": true, - "description": "Analytics consent decision engine: policy version major/minor semantics plus rolling consent expiry", + "description": "Analytics consent decision engine: policy version major/minor semantics", "main": "src/index.ts", "types": "src/index.ts", "exports": { diff --git a/features/flow/analytics-consent/src/utils/resolveAnalyticsOptInParams.test.ts b/features/flow/analytics-consent/src/utils/resolveAnalyticsOptInParams.test.ts index b42230ae9901..e697bddd13eb 100644 --- a/features/flow/analytics-consent/src/utils/resolveAnalyticsOptInParams.test.ts +++ b/features/flow/analytics-consent/src/utils/resolveAnalyticsOptInParams.test.ts @@ -23,7 +23,7 @@ describe("resolveAnalyticsOptInParams", () => { }); }); - it("ignores params the mobile decision no longer reads", () => { + it("ignores unknown params (e.g. legacy consentValidityDays)", () => { expect( resolveAnalyticsOptInParams({ params: { policyVersion: 2, consentValidityDays: 730 } }), ).toEqual({ currentPolicyVersion: { major: 2, minor: 0, normalized: "2.0" } }); diff --git a/libs/ledger-live-common/.unimportedrc.json b/libs/ledger-live-common/.unimportedrc.json index 6669ec234715..e54164b0b6ec 100644 --- a/libs/ledger-live-common/.unimportedrc.json +++ b/libs/ledger-live-common/.unimportedrc.json @@ -7,7 +7,6 @@ "src/analytics/featureFlagHelpers/onboardingCounterfeitWarning.ts", "src/analytics/featureFlagHelpers/wallet40.ts", "src/analytics/fundsReceived.ts", - "src/analyticsConsent/index.ts", "src/apps/filtering.ts", "src/apps/formatting.ts", "src/apps/hw.ts", diff --git a/libs/ledger-live-common/src/analyticsConsent/analyticsConsentUtils.test.ts b/libs/ledger-live-common/src/analyticsConsent/analyticsConsentUtils.test.ts deleted file mode 100644 index 03e0e1f45fcc..000000000000 --- a/libs/ledger-live-common/src/analyticsConsent/analyticsConsentUtils.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { add } from "date-fns"; -import { - needsConsentRenewal, - needsPrivacyPolicyAck, - resolveAnalyticsConsentPhase, -} from "./analyticsConsentUtils"; - -describe("analyticsConsentUtils", () => { - describe("needsPrivacyPolicyAck", () => { - it("returns true when stored version is null", () => { - expect(needsPrivacyPolicyAck(null, 1)).toBe(true); - }); - - it("returns true when stored version is below current", () => { - expect(needsPrivacyPolicyAck(0, 1)).toBe(true); - expect(needsPrivacyPolicyAck(1, 2)).toBe(true); - }); - - it("returns false when stored version matches current", () => { - expect(needsPrivacyPolicyAck(1, 1)).toBe(false); - expect(needsPrivacyPolicyAck(20260531, 20260531)).toBe(false); - }); - - it("supports date-style numeric policy versions", () => { - expect(needsPrivacyPolicyAck(1, 20260531)).toBe(true); - expect(needsPrivacyPolicyAck(20260531, 20261231)).toBe(true); - expect(needsPrivacyPolicyAck(20261231, 20260531)).toBe(false); - }); - }); - - describe("needsConsentRenewal", () => { - const NOW = new Date("2024-06-15T12:00:00.000Z"); - - it("returns true when consent is null or empty", () => { - expect(needsConsentRenewal(null, 365, NOW)).toBe(true); - expect(needsConsentRenewal("", 365, NOW)).toBe(true); - }); - - it("returns false when consent is within the validity window", () => { - const iso = add(NOW, { days: -300 }).toISOString(); - expect(needsConsentRenewal(iso, 365, NOW)).toBe(false); - }); - - it("returns true when consent is past the rolling window", () => { - const iso = add(NOW, { days: -400 }).toISOString(); - expect(needsConsentRenewal(iso, 365, NOW)).toBe(true); - }); - - it("returns false on the deadline instant", () => { - const consent = new Date("2023-06-01T12:00:00.000Z"); - const deadline = add(consent, { days: 365 }); - expect(needsConsentRenewal(consent.toISOString(), 365, deadline)).toBe(false); - }); - - it("returns true immediately after the deadline", () => { - const consent = new Date("2023-06-01T12:00:00.000Z"); - const deadline = add(consent, { days: 365 }); - const justAfter = new Date(deadline.getTime() + 1); - expect(needsConsentRenewal(consent.toISOString(), 365, justAfter)).toBe(true); - }); - - it("respects a shorter configured validity", () => { - const consent = add(NOW, { days: -31 }); - expect(needsConsentRenewal(consent.toISOString(), 30, NOW)).toBe(true); - }); - - it("returns true for invalid ISO strings", () => { - expect(needsConsentRenewal("not-a-date", 365, NOW)).toBe(true); - }); - }); - - describe("resolveAnalyticsConsentPhase", () => { - it("returns current phase when not closed", () => { - expect(resolveAnalyticsConsentPhase("privacy", true, true, false)).toBe("privacy"); - }); - - it("returns consentReconfirm when renewal needed and sharing was on", () => { - expect(resolveAnalyticsConsentPhase("closed", true, false, true)).toBe("consentReconfirm"); - }); - - it("returns consentFresh when renewal needed and sharing was off", () => { - expect(resolveAnalyticsConsentPhase("closed", true, false, false)).toBe("consentFresh"); - }); - - it("returns privacy when only policy update needed", () => { - expect(resolveAnalyticsConsentPhase("closed", false, true, true)).toBe("privacy"); - }); - - it("returns consentFresh when nothing special is needed", () => { - expect(resolveAnalyticsConsentPhase("closed", false, false, true)).toBe("consentFresh"); - }); - }); -}); diff --git a/libs/ledger-live-common/src/analyticsConsent/analyticsConsentUtils.ts b/libs/ledger-live-common/src/analyticsConsent/analyticsConsentUtils.ts deleted file mode 100644 index eea8e40db596..000000000000 --- a/libs/ledger-live-common/src/analyticsConsent/analyticsConsentUtils.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { add, isAfter, isValid, parseISO } from "date-fns"; - -export type AnalyticsConsentPhase = "closed" | "privacy" | "consentFresh" | "consentReconfirm"; - -export function resolveAnalyticsConsentPhase( - currentPhase: AnalyticsConsentPhase, - needsRenewal: boolean, - needsUpdatePrivacy: boolean, - analyticsSharingEnabled: boolean, -): AnalyticsConsentPhase { - if (currentPhase !== "closed") return currentPhase; - if (needsRenewal) return analyticsSharingEnabled ? "consentReconfirm" : "consentFresh"; - if (needsUpdatePrivacy) return "privacy"; - return "consentFresh"; -} - -export function needsPrivacyPolicyAck( - storedVersion: number | null, - currentVersion: number, -): boolean { - if (storedVersion == null) return true; - return storedVersion < currentVersion; -} - -export function needsConsentRenewal( - consentDateIso: string | null, - consentValidityDays: number, - now: Date = new Date(), -): boolean { - if (consentDateIso == null || consentDateIso === "") return true; - const consentDate = parseISO(consentDateIso); - if (!isValid(consentDate)) return true; - const deadline = add(consentDate, { days: consentValidityDays }); - return isAfter(now, deadline); -} diff --git a/libs/ledger-live-common/src/analyticsConsent/analyticsOptInParams.test.ts b/libs/ledger-live-common/src/analyticsConsent/analyticsOptInParams.test.ts deleted file mode 100644 index 4700bea11ce1..000000000000 --- a/libs/ledger-live-common/src/analyticsConsent/analyticsOptInParams.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { resolveAnalyticsOptInParams } from "./analyticsOptInParams"; - -describe("resolveAnalyticsOptInParams", () => { - it("returns defaults when feature or params is missing", () => { - expect(resolveAnalyticsOptInParams(undefined)).toEqual({ - policyVersion: 1, - consentValidityDays: 365, - }); - expect(resolveAnalyticsOptInParams({})).toEqual({ - policyVersion: 1, - consentValidityDays: 365, - }); - }); - - it("coerces string numbers from remote params", () => { - expect( - resolveAnalyticsOptInParams({ - params: { policyVersion: "2", consentValidityDays: "180" }, - }), - ).toEqual({ policyVersion: 2, consentValidityDays: 180 }); - }); - - it("merges partial params with defaults", () => { - expect(resolveAnalyticsOptInParams({ params: { policyVersion: 3 } })).toEqual({ - policyVersion: 3, - consentValidityDays: 365, - }); - }); -}); diff --git a/libs/ledger-live-common/src/analyticsConsent/analyticsOptInParams.ts b/libs/ledger-live-common/src/analyticsConsent/analyticsOptInParams.ts deleted file mode 100644 index 14d8d7df8c11..000000000000 --- a/libs/ledger-live-common/src/analyticsConsent/analyticsOptInParams.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { z } from "zod"; - -const analyticsOptInParamsSchema = z.object({ - policyVersion: z.coerce.number().finite().catch(1), - consentValidityDays: z.coerce.number().finite().int().min(1).catch(365), -}); - -export type AnalyticsOptInParams = z.infer; - -const defaultParams: AnalyticsOptInParams = { policyVersion: 1, consentValidityDays: 365 }; - -/** - * Merges remote/partial `analyticsOptIn` params with defaults; coerces strings and recovers invalid values via `.catch()`. - */ -export function resolveAnalyticsOptInParams( - feature: { params?: unknown } | null | undefined, -): AnalyticsOptInParams { - const raw = feature?.params; - if (!raw || typeof raw !== "object") { - return analyticsOptInParamsSchema.parse(defaultParams); - } - return analyticsOptInParamsSchema.parse({ ...defaultParams, ...raw }); -} diff --git a/libs/ledger-live-common/src/analyticsConsent/index.ts b/libs/ledger-live-common/src/analyticsConsent/index.ts deleted file mode 100644 index 447bd09dd201..000000000000 --- a/libs/ledger-live-common/src/analyticsConsent/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./analyticsConsentUtils"; -export * from "./analyticsOptInParams"; diff --git a/shared/feature-flags/src/flags/team-engagement/analyticsOptIn.ts b/shared/feature-flags/src/flags/team-engagement/analyticsOptIn.ts index e5e21456dc45..d65531317901 100644 --- a/shared/feature-flags/src/flags/team-engagement/analyticsOptIn.ts +++ b/shared/feature-flags/src/flags/team-engagement/analyticsOptIn.ts @@ -3,13 +3,11 @@ import { flagWith } from "../../define"; const analyticsOptInParamsShape = { policyVersion: z.union([z.number(), z.string()]).default(1), - consentValidityDays: z.number().int().positive().default(365), } satisfies z.ZodRawShape; export const analyticsOptIn = flagWith(analyticsOptInParamsShape, { enabled: false, params: { policyVersion: 1, - consentValidityDays: 365, }, });