/** * SetupBanner — self-service credential onboarding banner (D-07). * * Shown ONLY when meQuery.data?.user.needsProviderSetup === true. * NO dismiss/X button — the ONLY way this clears is a successful credential save * which invalidates ['me'] → /api/me refetches → needsProviderSetup becomes false * → this component unmounts on the next render. * * Success-only dismissal contract: * CredentialSheet.onSuccess → invalidateQueries(['me']) → meQuery.data.user.needsProviderSetup=false * → SetupBanner returns null → banner disappears. * There is NO other code path that hides this banner. * * UI-SPEC §Surface 4: * - role="status" + aria-live="polite" (screen readers announce on load) * - KeyRound icon (size 20, var(--color-member-0)) * - Heading "Set up your calendar" (15px/600/primary) * - Body copy (13px/400/secondary) * - "Set up now" CTA (accent-filled, minHeight 44px) → opens CredentialSheet in self-service mode * - Card style: var(--color-surface-dim), 1px border, var(--space-2) radius, var(--space-4) padding * * Security: T-05-24 — all copy is plain-text JSX children, no dangerouslySetInnerHTML. */ import { useState, useRef } from 'react'; import { KeyRound } from 'lucide-react'; import { useQuery } from '@tanstack/react-query'; import { fetchMe } from '../api/client.js'; import { CredentialSheet } from './CredentialSheet.js'; export function SetupBanner() { const [sheetOpen, setSheetOpen] = useState(false); // Use HTMLButtonElement for the ref (assignable to the CredentialSheet's HTMLElement trigger) const ctaRef = useRef(null); // staleTime 0 (gap 6): this banner gates on needsProviderSetup, which is only // correct if ['me'] is fresh on entry to the authenticated shell after the // wizard claim (see App.tsx for the full mechanism note). With a 5-minute // staleTime a pre-claim cache entry kept needsProviderSetup=true and the banner // showed even though the operator already configured the calendar in the wizard. // The success-only dismissal contract is unchanged: this banner still clears // ONLY when needsProviderSetup becomes false (no dismiss/X button). const meQuery = useQuery({ queryKey: ['me'], queryFn: fetchMe, retry: false, staleTime: 0, }); // Only show when needsProviderSetup is explicitly true // (undefined / false = no banner) if (meQuery.data?.user.needsProviderSetup !== true) return null; const memberName = meQuery.data.user.displayName ?? 'Member'; return ( <>
{/* CredentialSheet in self-service mode. On success: invalidates ['me'] → needsProviderSetup=false → this component unmounts. */} setSheetOpen(false)} mode="self-service" memberName={memberName} triggerRef={ctaRef} /> ); }