fix(fitbit): show the retired card only to an account that had Fitbit

After the Web API turndown every account still saw a Fitbit card that
can no longer connect. The status entry now publishes
retirement.offered: false once the API is gone for an account with no
Fitbit connection, no Fitbit app of its own and no Fitbit data, and the
connections panel leaves the card out. Accounts that had Fitbit keep it
with the retirement notice.
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 15:06:40 +02:00
1 parent bf3830203f
commit b53434daa0
8 files changed
+172 -40

No files matched your search

@@ -181,3 +181,38 @@ describe("ConnectionsPanel — every envelope entry surfaces", () => {
expect(render()).not.toContain('data-testid="integration-fallback-row"');
});
});
describe("ConnectionsPanel — the Fitbit card after the turndown", () => {
const withFitbit = (retirement: Record<string, unknown>) => ({
threshold: 3,
integrations: fullEnvelope().integrations.map((e) =>
e.integration === "fitbit"
? { ...e, connected: false, configured: false, retirement }
: e,
),
});
it("shows no Fitbit card when the server does not offer it", () => {
envelope = withFitbit({
endsOn: "2026-10-30",
ended: true,
successor: "google-health",
offered: false,
});
const html = render();
expect(html).not.toContain('id="fitbit"');
expect(html).not.toContain('data-testid="fitbit-retirement-notice"');
});
it("keeps the card and its notice for an account that had Fitbit", () => {
envelope = withFitbit({
endsOn: "2026-10-30",
ended: true,
successor: "google-health",
offered: true,
});
const html = render();
expect(html).toContain('id="fitbit"');
expect(html).toContain('data-testid="fitbit-retirement-notice"');
});
});
@@ -337,40 +337,44 @@ export function ConnectionsPanel({
callbackUrl={callbackUrls.whoop}
/>
</div>
<div id="fitbit" className="scroll-mt-28">
<FitbitRetirementNotice retirement={fitbitStatus?.retirement} />
<OAuthIntegrationCard
config={OAUTH_INTEGRATIONS.fitbit}
viewModel={fitbitStatus}
callbackUrl={callbackUrls.fitbit}
syncDisabled={fitbitRetired}
connectDisabled={fitbitRetired}
titleAccessory={
<>
<TagChip>{t("settings.fitbitTag")}</TagChip>
{fitbitStatus?.retirement ? (
<Badge
variant="outline"
data-testid="fitbit-retirement-badge"
className="border-warning/50 text-warning"
>
{fitbitRetired
? t("settings.fitbitRetirement.badgeEnded")
: t("settings.fitbitRetirement.badgeEnding")}
</Badge>
) : (
<Badge
variant="outline"
data-testid="fitbit-experimental-badge"
className="border-warning/50 text-warning"
>
{t("settings.fitbitExperimentalBadge")}
</Badge>
)}
</>
}
/>
</div>
{/* After the turndown the card is offered only to an account that had
Fitbit; the server decides (`retirement.offered`). */}
{fitbitStatus?.retirement?.offered !== false && (
<div id="fitbit" className="scroll-mt-28">
<FitbitRetirementNotice retirement={fitbitStatus?.retirement} />
<OAuthIntegrationCard
config={OAUTH_INTEGRATIONS.fitbit}
viewModel={fitbitStatus}
callbackUrl={callbackUrls.fitbit}
syncDisabled={fitbitRetired}
connectDisabled={fitbitRetired}
titleAccessory={
<>
<TagChip>{t("settings.fitbitTag")}</TagChip>
{fitbitStatus?.retirement ? (
<Badge
variant="outline"
data-testid="fitbit-retirement-badge"
className="border-warning/50 text-warning"
>
{fitbitRetired
? t("settings.fitbitRetirement.badgeEnded")
: t("settings.fitbitRetirement.badgeEnding")}
</Badge>
) : (
<Badge
variant="outline"
data-testid="fitbit-experimental-badge"
className="border-warning/50 text-warning"
>
{t("settings.fitbitExperimentalBadge")}
</Badge>
)}
</>
}
/>
</div>
)}
<div id="google-health" className="scroll-mt-28">
<GoogleHealthCard
viewModel={pickStatus(integrationStatus, "google-health")}
@@ -14,7 +14,7 @@ import type { GoogleHealthBackfillEndedEntry } from "@/lib/google-health/backfil
import type { ConnectionHealth } from "@/lib/integrations/connection-health";
import type { MetricFreshnessEntry } from "@/lib/integrations/sync-verdict";
import { queryKeys } from "@/lib/query-keys";
import type { FitbitRetirement } from "@/lib/fitbit/retirement";
import type { PublishedFitbitRetirement } from "@/lib/fitbit/retirement";
export type IntegrationKey =
| "withings"
@@ -73,7 +73,7 @@ export interface IntegrationStatusViewModel {
/** Per-metric last-seen timestamps with the server-computed stale flag. */
metricFreshness?: MetricFreshnessEntry[];
/** Fitbit: the end of the legacy Web API, resolved on the server. */
retirement?: FitbitRetirement;
retirement?: PublishedFitbitRetirement;
}
export interface IntegrationStatusEnvelope {