diff --git a/CHANGELOG.md b/CHANGELOG.md index 45d96f795..bd8159d1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ cycles like a period logged by hand, a finished Garmin period also marks its last day, and periods imported earlier are repaired once on the next start. A day you logged yourself is never changed. (#1333) +- **Garmin in the setup questions.** The onboarding question about where + your readings come from now offers Garmin, and can end on connecting it. + (#1333) - **Typing into the smoking history.** "When you stopped" only took text that was already a whole YYYY-MM month, so every keystroke was dropped and only a paste got in. The start year clamped each keystroke into its diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 1d896a3de..eaf513ca2 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -36694,7 +36694,7 @@ components: type: string const: done sources: - maxItems: 10 + maxItems: 11 type: array items: type: string @@ -36705,6 +36705,7 @@ components: - oura - whoop - polar + - garmin - fitbit - strava - nightscout @@ -83103,6 +83104,7 @@ components: - oura - whoop - polar + - garmin - fitbit - strava - nightscout diff --git a/messages/de.json b/messages/de.json index 5abe7c515..addd1369c 100644 --- a/messages/de.json +++ b/messages/de.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/en.json b/messages/en.json index 4160fe8fd..6eecabb83 100644 --- a/messages/en.json +++ b/messages/en.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/es.json b/messages/es.json index aff7c3bca..663d66f6e 100644 --- a/messages/es.json +++ b/messages/es.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/fr.json b/messages/fr.json index 348f3d5a4..50e0eb018 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/it.json b/messages/it.json index ad13867cc..c05731311 100644 --- a/messages/it.json +++ b/messages/it.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/ko.json b/messages/ko.json index 98499067d..10ebcb44e 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/nl.json b/messages/nl.json index 96d427f61..a53d0187c 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/pl.json b/messages/pl.json index d5fb8cd97..c9b05a0c0 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/pt-BR.json b/messages/pt-BR.json index 7436334e1..eb89e8d03 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/messages/ru.json b/messages/ru.json index 7ef35a608..6563de908 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -5277,6 +5277,7 @@ "oura": "Oura", "whoop": "WHOOP", "polar": "Polar", + "garmin": "Garmin", "fitbit": "Fitbit", "strava": "Strava", "nightscout": "Nightscout", diff --git a/src/lib/onboarding/__tests__/first-result-config.test.ts b/src/lib/onboarding/__tests__/first-result-config.test.ts index 5eebd8325..d05ee5f12 100644 --- a/src/lib/onboarding/__tests__/first-result-config.test.ts +++ b/src/lib/onboarding/__tests__/first-result-config.test.ts @@ -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 = { + "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(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(); diff --git a/src/lib/onboarding/first-result-config.ts b/src/lib/onboarding/first-result-config.ts index 304755c59..9c033b761 100644 --- a/src/lib/onboarding/first-result-config.ts +++ b/src/lib/onboarding/first-result-config.ts @@ -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: { diff --git a/src/lib/onboarding/needs.ts b/src/lib/onboarding/needs.ts index 5ab044d73..a319b918b 100644 --- a/src/lib/onboarding/needs.ts +++ b/src/lib/onboarding/needs.ts @@ -99,6 +99,7 @@ export const ONBOARDING_SOURCE_KEYS = [ "oura", "whoop", "polar", + "garmin", "fitbit", "strava", "nightscout", diff --git a/src/lib/onboarding/wizard-steps.ts b/src/lib/onboarding/wizard-steps.ts index d84b8adb4..8f411d264 100644 --- a/src/lib/onboarding/wizard-steps.ts +++ b/src/lib/onboarding/wizard-steps.ts @@ -34,6 +34,7 @@ export const BROWSER_CONNECTABLE_SOURCES = [ "oura", "whoop", "polar", + "garmin", "fitbit", "strava", "nightscout",