Surface shipped data sources in onboarding and tell the truth

Rewrite the source step to list the live integrations (Apple Health,
WHOOP, Oura, Polar, Nightscout, Fitbit) alongside Withings and manual
entry, each linking to where it is connected; drop the stale Apple
Health coming-soon badge. Add an import affordance for existing history.

Reword the goals and source copy so it no longer promises automatic
dashboard or reminder configuration that does not happen yet. Add a
calibration line to the done screen setting the expectation that some
insights sharpen over the first weeks, and lead the success screen
forward with connect-a-device and log-first-measurement actions.

Generalize the dashboard checklist data-source item so any connected
integration satisfies it, not Withings alone.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 21:26:09 +02:00
1 parent 46f9dfa47f
commit 1ddc8a0c47
11 files changed
+295 -168

No files matched your search

+20 -11
View File
@@ -21,7 +21,7 @@ function inputs(overrides: Partial<Parameters<typeof buildChecklist>[0]> = {}) {
profile: completeProfile,
measurementCount: 0,
medicationCount: 0,
withingsConnected: false,
dataSourceConnected: false,
notificationsConfigured: false,
dismissedIds: new Set<ChecklistItemId>(),
...overrides,
@@ -49,7 +49,7 @@ describe("buildChecklist", () => {
"profile",
"measurement",
"medication",
"withings",
"dataSource",
"notifications",
]);
});
@@ -64,12 +64,21 @@ describe("buildChecklist", () => {
expect(items.find((i) => i.id === "measurement")?.done).toBe(true);
});
it("flags medication and withings independently", () => {
it("flags medication and dataSource independently", () => {
const items = buildChecklist(
inputs({ medicationCount: 2, withingsConnected: true }),
inputs({ medicationCount: 2, dataSourceConnected: true }),
);
expect(items.find((i) => i.id === "medication")?.done).toBe(true);
expect(items.find((i) => i.id === "withings")?.done).toBe(true);
expect(items.find((i) => i.id === "dataSource")?.done).toBe(true);
});
it("dataSource is satisfied by any connected source", () => {
// The predicate is source-agnostic — a single boolean stands in for
// Withings, WHOOP, Oura, Polar, Nightscout, Fitbit or Apple Health.
const off = buildChecklist(inputs({ dataSourceConnected: false }));
expect(off.find((i) => i.id === "dataSource")?.done).toBe(false);
const on = buildChecklist(inputs({ dataSourceConnected: true }));
expect(on.find((i) => i.id === "dataSource")?.done).toBe(true);
});
it("propagates per-item dismissal", () => {
@@ -86,7 +95,7 @@ describe("buildChecklist", () => {
expect(hrefs.profile).toBe("/settings/account");
expect(hrefs.measurement).toBe("/measurements");
expect(hrefs.medication).toBe("/medications");
expect(hrefs.withings).toBe("/settings/integrations");
expect(hrefs.dataSource).toBe("/settings/integrations");
expect(hrefs.notifications).toBe("/settings/notifications");
});
});
@@ -94,10 +103,10 @@ describe("buildChecklist", () => {
describe("visibleChecklist + checklistProgress", () => {
it("hides per-item dismissed rows from the visible list", () => {
const items = buildChecklist(
inputs({ dismissedIds: new Set<ChecklistItemId>(["withings"]) }),
inputs({ dismissedIds: new Set<ChecklistItemId>(["dataSource"]) }),
);
const visible = visibleChecklist(items);
expect(visible.map((i) => i.id)).not.toContain("withings");
expect(visible.map((i) => i.id)).not.toContain("dataSource");
});
it("counts done items inside the visible subset", () => {
@@ -106,7 +115,7 @@ describe("visibleChecklist + checklistProgress", () => {
measurementCount: 3,
medicationCount: 1,
// dismiss the only one not done so percent jumps to 100
dismissedIds: new Set<ChecklistItemId>(["withings", "notifications"]),
dismissedIds: new Set<ChecklistItemId>(["dataSource", "notifications"]),
}),
);
const progress = checklistProgress(items);
@@ -121,7 +130,7 @@ describe("visibleChecklist + checklistProgress", () => {
profile: { heightCm: null, dateOfBirth: null, gender: null },
measurementCount: 0,
medicationCount: 0,
withingsConnected: false,
dataSourceConnected: false,
notificationsConfigured: false,
dismissedIds: new Set(),
});
@@ -185,7 +194,7 @@ describe("shouldShowChecklist", () => {
inputs({
measurementCount: 3,
medicationCount: 1,
withingsConnected: true,
dataSourceConnected: true,
notificationsConfigured: true,
}),
);
+11 -7
View File
@@ -10,7 +10,7 @@ export const CHECKLIST_ITEM_IDS = [
"profile",
"measurement",
"medication",
"withings",
"dataSource",
"notifications",
] as const;
@@ -41,8 +41,12 @@ export interface ChecklistInputs {
measurementCount: number;
/** Number of medications the user has created. */
medicationCount: number;
/** True iff Withings OAuth is connected (status.connected). */
withingsConnected: boolean;
/**
* True iff at least one data source is connected — Withings, WHOOP,
* Oura, Polar, Nightscout, Fitbit, or Apple Health. Any one satisfies
* the "connect a data source" step.
*/
dataSourceConnected: boolean;
/**
* True iff the user has set up at least one notification channel
* (Telegram, ntfy, or Web Push).
@@ -54,7 +58,7 @@ export interface ChecklistInputs {
/**
* Compute the ordered checklist for the dashboard hero. Stable order:
* profile → measurement → medication → withings → notifications. Each
* profile → measurement → medication → dataSource → notifications. Each
* item carries the deep-link the row's CTA should navigate to.
*/
export function buildChecklist(inputs: ChecklistInputs): ChecklistItem[] {
@@ -79,10 +83,10 @@ export function buildChecklist(inputs: ChecklistInputs): ChecklistItem[] {
dismissed: inputs.dismissedIds.has("medication"),
},
{
id: "withings",
done: inputs.withingsConnected,
id: "dataSource",
done: inputs.dataSourceConnected,
href: "/settings/integrations",
dismissed: inputs.dismissedIds.has("withings"),
dismissed: inputs.dismissedIds.has("dataSource"),
},
{
id: "notifications",