diff --git a/messages/en.json b/messages/en.json index 2a1315079..9efa8c1b5 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3687,7 +3687,7 @@ }, "integrations": { "title": "Connect your devices", - "body": "Open Settings to link Withings (auto-sync weight, BP and pulse), moodLog, or your Insights provider. Everything stays optional and encrypted." + "body": "Open Settings to link Withings (auto-sync weight, BP and pulse), moodLog, or your Insights provider. Everything stays optional and encrypted — and under Settings → Modules you can turn whole areas like sleep, mood or achievements on or off to keep HealthLog focused on what you track." }, "achievements": { "title": "Earn streaks and badges", diff --git a/src/components/onboarding/__tests__/tour-launcher-gate.test.ts b/src/components/onboarding/__tests__/tour-launcher-gate.test.ts index b282a4e40..320fea350 100644 --- a/src/components/onboarding/__tests__/tour-launcher-gate.test.ts +++ b/src/components/onboarding/__tests__/tour-launcher-gate.test.ts @@ -21,6 +21,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { setTourForceLaunch, shouldAutoLaunchTour, + tourIncludesAchievements, TOUR_AUTOLAUNCH_DELAY_MS, tourForceLaunchKey, } from "../tour-launcher"; @@ -132,6 +133,27 @@ describe("shouldAutoLaunchTour() — v1.4.47 W5 chain-gate", () => { }); }); +describe("tourIncludesAchievements() — v1.18.0 B5 module gate", () => { + it("includes the achievements stop by default (no module map)", () => { + expect(tourIncludesAchievements(undefined)).toBe(true); + expect(tourIncludesAchievements({})).toBe(true); + }); + + it("includes the achievements stop when the module is explicitly on", () => { + expect(tourIncludesAchievements({ achievements: true })).toBe(true); + }); + + it("drops the achievements stop when the module is explicitly off", () => { + expect(tourIncludesAchievements({ achievements: false })).toBe(false); + }); + + it("ignores other disabled modules — only the achievements key matters", () => { + expect( + tourIncludesAchievements({ sleep: false, mood: false }), + ).toBe(true); + }); +}); + describe("setTourForceLaunch() — v1.4.47 W5 manual replay bypass", () => { // (c) of the W5 acceptance: the manual "launch tour" trigger still // works. The launcher's render-phase gate is exercised by diff --git a/src/components/onboarding/tour-launcher.tsx b/src/components/onboarding/tour-launcher.tsx index 73ade6e28..3095542df 100644 --- a/src/components/onboarding/tour-launcher.tsx +++ b/src/components/onboarding/tour-launcher.tsx @@ -101,6 +101,19 @@ export function shouldAutoLaunchTour(args: { return args.nowMs - completedMs >= TOUR_AUTOLAUNCH_DELAY_MS; } +/** + * v1.18.0 B5 — whether the achievements tour stop should be included. + * Reads the per-user module map with the standard client default-on + * convention (`modules?.achievements !== false`): only an explicit + * `false` drops the stop. Pure + exported so the rule is unit-pinned + * without rendering the launcher. + */ +export function tourIncludesAchievements( + modules: Partial> | undefined, +): boolean { + return modules?.achievements !== false; +} + // v1.4.15 H4 — sessionStorage keys are now scoped by user id so an // admin impersonating a second user does not inherit the first user's // "tour dismissed for this session" state. The previous global keys @@ -348,8 +361,15 @@ export function TourLauncher({ ready }: TourLauncherProps) { if (showTour !== true) return null; + // v1.18.0 B5 — drop the achievements tour stop when the account has the + // achievements module turned off. Reads the per-user module map the same + // way every other client gate does (`modules?. !== false`, + // default-on). + const includeAchievements = tourIncludesAchievements(user?.modules); + return ( { // Optimistic: hide the overlay immediately, persist in the // background. The flag is idempotent so re-fires are safe;