- 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
133 lines
4.5 KiB
TypeScript
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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|