Files
familysync/apps/pwa/src/components/SetupBanner.tsx
T
Lucas Berger 79fe3e0e04 fix(10-04): prettier format + remove unnecessary type assertions
- Run prettier on all new/modified PWA files (CredentialSheet, SetupBanner, AdminPage, admin.spec.ts)
- Remove unnecessary 'as React.RefObject<HTMLElement | null>' casts flagged by @typescript-eslint/no-unnecessary-type-assertion
- Format pre-existing API files from Plans 02/03 (me.ts, user.test.ts, requireAdmin.test.ts, me.test.ts)
- All 270 API tests + 191 PWA vitest tests pass; lint/typecheck/build clean
2026-06-13 15:22:50 -04:00

133 lines
4.5 KiB
TypeScript

/**
* 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<HTMLButtonElement>(null);
const meQuery = useQuery({
queryKey: ['me'],
queryFn: fetchMe,
retry: false,
staleTime: 5 * 60 * 1000,
});
// 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 (
<>
<div
role="status"
aria-live="polite"
style={{
background: 'var(--color-surface-dim, #F7F7F8)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--space-2, 8px)',
padding: 'var(--space-4, 16px)',
margin: 'var(--space-4, 16px)',
display: 'flex',
alignItems: 'flex-start',
gap: 'var(--space-3, 12px)',
fontFamily: 'var(--font-family-base)',
}}
>
<KeyRound
size={20}
aria-hidden="true"
style={{ color: 'var(--color-member-0, #4A90D9)', flexShrink: 0, marginTop: '2px' }}
/>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: 'var(--text-body-size, 15px)',
fontWeight: 600,
color: 'var(--color-text-primary)',
lineHeight: 1.4,
marginBottom: 'var(--space-1, 4px)',
}}
>
Set up your calendar
</div>
<div
style={{
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 400,
color: 'var(--color-text-secondary)',
lineHeight: 1.4,
marginBottom: 'var(--space-3, 12px)',
}}
>
To sync your Fastmail calendar, you need to add an app password. This takes about a
minute.
</div>
{/* CTA — no X/dismiss: the ONLY exit is a successful credential save */}
<button
ref={ctaRef}
type="button"
onClick={() => setSheetOpen(true)}
style={{
background: 'var(--color-member-0, #4A90D9)',
color: '#ffffff',
border: 'none',
cursor: 'pointer',
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 600,
minHeight: '44px',
minWidth: '44px',
padding: '0 var(--space-4, 16px)',
borderRadius: 'var(--space-1, 4px)',
fontFamily: 'var(--font-family-base)',
}}
>
Set up now
</button>
</div>
</div>
{/* CredentialSheet in self-service mode.
On success: invalidates ['me'] → needsProviderSetup=false → this component unmounts. */}
<CredentialSheet
isOpen={sheetOpen}
onClose={() => setSheetOpen(false)}
mode="self-service"
memberName={memberName}
triggerRef={ctaRef}
/>
</>
);
}