fix(onboarding): offer Garmin as a source

The setup question about where readings come from listed every other
connection on Settings and not Garmin, so the flow could neither record it
nor end on connecting it. Garmin is now a Q4 source and a browser
connectable one: the key is pasted on its card, so nothing can be missing
up front.

A guard now pins that every connection the health read covers is either an
onboarding source or named with the reason it is not.

Refs #1333
This commit is contained in:
Marc-André Bombeck committed 2026-11-04 22:03:08 +01:00
1 parent 148af765c9
commit afdb8163ce
16 files changed
+68 -2

No files matched your search

@@ -8,6 +8,7 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { CONNECTION_KEYS } from "@/lib/integrations/connection-health";
import { ONBOARDING_AREA_KEYS } from "@/lib/modules/registry";
import { NAV_DESTINATIONS } from "@/components/layout/nav-model";
@@ -18,8 +19,22 @@ import {
deliveringSources,
SOURCE_INTEGRATION,
} from "../first-result-config";
import { ONBOARDING_SOURCE_KEYS } from "../needs";
import { BROWSER_CONNECTABLE_SOURCES } from "../wizard-steps";
/**
* Connections the setup flow deliberately does not ask about, each with its
* reason. Every other connection must be a Q4 source; a provider shipped on
* Settings and missing here was absent from the flow without anybody deciding
* it.
*/
const NOT_AN_ONBOARDING_SOURCE: Record<string, string> = {
"google-health":
"the successor transport behind the Fitbit answer, which still links the classic Fitbit card",
dawarich:
"movement context for the environment module, not a source of readings",
};
describe("first-result targets", () => {
it("covers every area with exactly one of a form or a page", () => {
for (const area of ONBOARDING_AREA_KEYS) {
@@ -36,6 +51,33 @@ describe("first-result targets", () => {
}
});
it("asks about every connection Settings offers, or says why not", () => {
const sources = new Set<string>(ONBOARDING_SOURCE_KEYS);
const missing = CONNECTION_KEYS.filter(
(key) => !sources.has(key) && !(key in NOT_AN_ONBOARDING_SOURCE),
);
expect(missing).toEqual([]);
for (const key of Object.keys(NOT_AN_ONBOARDING_SOURCE)) {
expect(CONNECTION_KEYS as readonly string[]).toContain(key);
}
});
it("offers Garmin as a source the browser connects with a pasted key", () => {
expect(BROWSER_CONNECTABLE_SOURCES).toContain("garmin");
expect(SOURCE_INTEGRATION.garmin).toEqual({
anchor: "garmin",
statusKey: "garmin",
credentials: "self-hosted",
});
const view = connectSourceView("garmin", {
connected: false,
configured: false,
syncHealth: { verdict: "disconnected", since: null },
lastSuccessAt: null,
});
expect(view?.slot).toBe("connect");
});
it("names an integration for every source the browser can connect", () => {
for (const source of BROWSER_CONNECTABLE_SOURCES) {
expect(SOURCE_INTEGRATION[source].anchor).toBeTruthy();
+8 -1
View File
@@ -61,7 +61,8 @@ export const AREA_PAGE_HREF: Readonly<
* a dead end.
* - `shared` — an operator env app is the fallback (`available` says whether
* any usable credentials resolve, own or shared).
* - `self-hosted` — a URL the person pastes; nothing can be missing up front.
* - `self-hosted` — a URL or a key the person pastes; nothing can be missing
* up front.
*/
export type SourceCredentialModel = "byo" | "shared" | "self-hosted";
@@ -80,6 +81,12 @@ export const SOURCE_INTEGRATION: Readonly<
oura: { anchor: "oura", statusKey: "oura", credentials: "shared" },
whoop: { anchor: "whoop", statusKey: "whoop", credentials: "byo" },
polar: { anchor: "polar", statusKey: "polar", credentials: "shared" },
// The person's own Personal API key, pasted on the card.
garmin: {
anchor: "garmin",
statusKey: "garmin",
credentials: "self-hosted",
},
fitbit: { anchor: "fitbit", statusKey: "fitbit", credentials: "byo" },
strava: { anchor: "strava", statusKey: "strava", credentials: "shared" },
nightscout: {
+1
View File
@@ -99,6 +99,7 @@ export const ONBOARDING_SOURCE_KEYS = [
"oura",
"whoop",
"polar",
"garmin",
"fitbit",
"strava",
"nightscout",
+1
View File
@@ -34,6 +34,7 @@ export const BROWSER_CONNECTABLE_SOURCES = [
"oura",
"whoop",
"polar",
"garmin",
"fitbit",
"strava",
"nightscout",