From ceefbc55719d1bf229fbf02d51835594ad8ef902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 11 Sep 2026 11:38:31 +0200 Subject: [PATCH] fix(onboarding): a receipt on file is not evidence the provider works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The consent branch of the done screen's AI panel painted "insights work for you right now" on the strength of the receipt alone, reading no health at all. A person who had consented — including one who had just tapped the offer — was told a dead provider was serving them. That is the claim-nobody-measured defect closed at the offer end of the same panel, still open at the other. The positive sentence now needs a healthy verdict as well. A consented account on a provider that failed reads the same "not answering" note as anybody else, and on an unmeasured one the panel claims nothing. The panel's four variants are pinned across all three health states in every arm. --- .../onboarding/__tests__/done-screen.test.tsx | 68 +++++++++++++++++++ src/components/onboarding/done-screen.tsx | 18 +++-- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/src/components/onboarding/__tests__/done-screen.test.tsx b/src/components/onboarding/__tests__/done-screen.test.tsx index f3010a24c..c03b2d545 100644 --- a/src/components/onboarding/__tests__/done-screen.test.tsx +++ b/src/components/onboarding/__tests__/done-screen.test.tsx @@ -262,6 +262,74 @@ describe(" shared-provider offer", () => { expect(html).not.toContain('data-slot="onboarding-ai-unavailable"'); }); + /** + * The whole decision table, in one place. + * + * Three arms the screen can be in — the offer is available, the offer is + * withheld for a reason that is not health (the assistant surfaces switched + * off, a managed profile), a receipt is already on file — crossed with the + * three health states. The point of the cross is that HEALTH decides the + * claim in every arm: the consent arm used to promise "insights work for + * you right now" without reading health at all, which told somebody who had + * consented that a dead provider was working. That is the same unmeasured + * claim as the `unknown` case at the other end of the panel. + */ + const TABLE = [ + { arm: "offerable", consent: false, health: "healthy", state: "offer" }, + { + arm: "offerable", + consent: false, + health: "unhealthy", + state: "unavailable", + }, + { arm: "offerable", consent: false, health: "unknown", state: "neutral" }, + { arm: "withheld", consent: false, health: "healthy", state: "neutral" }, + { + arm: "withheld", + consent: false, + health: "unhealthy", + state: "unavailable", + }, + { arm: "withheld", consent: false, health: "unknown", state: "neutral" }, + { arm: "consented", consent: true, health: "healthy", state: "consent" }, + { + arm: "consented", + consent: true, + health: "unhealthy", + state: "unavailable", + }, + { arm: "consented", consent: true, health: "unknown", state: "neutral" }, + ] as const; + + it.each(TABLE)( + "settles on $state when the offer is $arm and the provider is $health", + ({ arm, consent, health, state }) => { + aiProviderState.data = { + managedBy: "server", + serverProviderHealth: health, + // The server only ever offers on `healthy`, and never with a receipt + // on file; the withheld arm is the flags-off / managed-profile case. + serverProviderOffer: arm === "offerable" && health === "healthy", + serverProviderConsent: consent, + }; + const html = render(); + expect(html).toContain(`data-ai-state="${state}"`); + // The positive sentence appears in exactly one of the nine cells. + expect(html.includes('data-slot="onboarding-ai-shared-key"')).toBe( + state === "consent", + ); + expect(html.includes('data-slot="onboarding-ai-unavailable"')).toBe( + state === "unavailable", + ); + expect(html.includes('data-slot="onboarding-ai-offer-grant"')).toBe( + state === "offer", + ); + // The rest of the panel is untouched in every cell. + expect(html).toContain('data-slot="onboarding-ai-keyless"'); + expect(html).toContain('href="/settings/ai"'); + }, + ); + it("grants through the affirmative web intent — the only path that may lift a revocation", async () => { mutations.length = 0; apiFetchRaw.mockReset(); diff --git a/src/components/onboarding/done-screen.tsx b/src/components/onboarding/done-screen.tsx index 6bbdad6ad..af4636436 100644 --- a/src/components/onboarding/done-screen.tsx +++ b/src/components/onboarding/done-screen.tsx @@ -169,12 +169,22 @@ export function DoneScreen({ state }: { state: OnboardingStateDto }) { }); const operatorProvides = aiProvider?.managedBy === "server"; + const sharedHealth = aiProvider?.serverProviderHealth; const consentOnFile = justGranted || aiProvider?.serverProviderConsent === true; const offerShared = operatorProvides && !consentOnFile && aiProvider?.serverProviderOffer === true; + // A receipt on file is a fact about the ACCOUNT; it says nothing about + // whether the provider works. This line used to promise "insights work for + // you right now" on the strength of the receipt alone, so somebody who had + // just consented was told a dead provider was serving them — the same + // unmeasured claim as the offer's, made at the other end of the panel. The + // positive sentence is now earned by health as well, and a consented + // account on a broken provider reads the same honest note as anybody else. + const sharedKeyNote = + operatorProvides && consentOnFile && sharedHealth === "healthy"; // Only a health verdict somebody EARNED paints the "not answering" card — // `unhealthy`, which means the provider was tried and the last thing that // happened was a failure. `unknown` is not that verdict: it is an empty @@ -187,9 +197,9 @@ export function DoneScreen({ state }: { state: OnboardingStateDto }) { // nobody measured. const sharedUnavailable = operatorProvides && - !consentOnFile && + !sharedKeyNote && !offerShared && - aiProvider?.serverProviderHealth === "unhealthy"; + sharedHealth === "unhealthy"; return (
)} - {consentOnFile && operatorProvides ? ( + {sharedKeyNote ? (