style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
@@ -21,104 +21,104 @@
|
||||
* Security: T-05-24 — all copy is plain-text JSX children, no dangerouslySetInnerHTML.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { X, Bell, AlertCircle, Loader2 } from 'lucide-react'
|
||||
import { usePushSubscription } from '../hooks/usePushSubscription.js'
|
||||
import { InstructionSheet } from './InstructionSheet.js'
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { X, Bell, AlertCircle, Loader2 } from 'lucide-react';
|
||||
import { usePushSubscription } from '../hooks/usePushSubscription.js';
|
||||
import { InstructionSheet } from './InstructionSheet.js';
|
||||
|
||||
// CR-04: fetch VAPID key (from sessionStorage cache if available) for the
|
||||
// tap-gated subscribe() path. Same logic as PushPermissionPrompt.
|
||||
async function fetchVapidKeyForSettings(): Promise<string | null> {
|
||||
try {
|
||||
const cached = sessionStorage.getItem('vapidPublicKey')
|
||||
if (cached) return cached
|
||||
const res = await fetch('/api/push/vapid-public-key', { credentials: 'include' })
|
||||
if (!res.ok) return null
|
||||
const data = (await res.json()) as { publicKey: string }
|
||||
const cached = sessionStorage.getItem('vapidPublicKey');
|
||||
if (cached) return cached;
|
||||
const res = await fetch('/api/push/vapid-public-key', { credentials: 'include' });
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json()) as { publicKey: string };
|
||||
if (data.publicKey) {
|
||||
sessionStorage.setItem('vapidPublicKey', data.publicKey)
|
||||
sessionStorage.setItem('vapidPublicKey', data.publicKey);
|
||||
}
|
||||
return data.publicKey ?? null
|
||||
return data.publicKey ?? null;
|
||||
} catch {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
interface SettingsSheetProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
|
||||
const { subscribe, permission, isSubscribed, setEnabled } = usePushSubscription()
|
||||
const [isTogglingOn, setIsTogglingOn] = useState(false)
|
||||
const [instructionsOpen, setInstructionsOpen] = useState(false)
|
||||
const { subscribe, permission, isSubscribed, setEnabled } = usePushSubscription();
|
||||
const [isTogglingOn, setIsTogglingOn] = useState(false);
|
||||
const [instructionsOpen, setInstructionsOpen] = useState(false);
|
||||
// CR-04: pre-fetch the VAPID key into state so the toggle tap handler can call
|
||||
// subscribe(registration, vapidKey) without any network await before pushManager.subscribe().
|
||||
const [vapidKey, setVapidKey] = useState<string | null>(null)
|
||||
const [vapidKey, setVapidKey] = useState<string | null>(null);
|
||||
// NEW-CR-01: Pre-resolve the ServiceWorkerRegistration into state so the toggle
|
||||
// tap handler has ZERO awaits between the user gesture and pushManager.subscribe().
|
||||
const [swRegistration, setSwRegistration] = useState<ServiceWorkerRegistration | null>(null)
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const [swRegistration, setSwRegistration] = useState<ServiceWorkerRegistration | null>(null);
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Compute initial toggle on/off state per UI-SPEC toggle initial state rule:
|
||||
// on when notificationsEnabled !== '0' AND permission === 'granted' AND isSubscribed
|
||||
const isOn = permission === 'granted' && isSubscribed
|
||||
const isOn = permission === 'granted' && isSubscribed;
|
||||
|
||||
// Escape key listener (CreateListSheet pattern)
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
if (!isOpen) return;
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
return () => document.removeEventListener('keydown', onKeyDown)
|
||||
}, [isOpen, onClose])
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => document.removeEventListener('keydown', onKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// Focus close button on open (a11y)
|
||||
useEffect(() => {
|
||||
if (isOpen && closeButtonRef.current) {
|
||||
closeButtonRef.current.focus()
|
||||
closeButtonRef.current.focus();
|
||||
}
|
||||
}, [isOpen])
|
||||
}, [isOpen]);
|
||||
|
||||
// CR-04: Pre-fetch the VAPID key while the sheet is open so the OS-dialog path
|
||||
// (permission === 'default') has the key ready before the user taps.
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
if (permission === 'denied') return
|
||||
if (!isOpen) return;
|
||||
if (permission === 'denied') return;
|
||||
void fetchVapidKeyForSettings().then((key) => {
|
||||
if (key) setVapidKey(key)
|
||||
})
|
||||
}, [isOpen, permission])
|
||||
if (key) setVapidKey(key);
|
||||
});
|
||||
}, [isOpen, permission]);
|
||||
|
||||
// NEW-CR-01: Pre-resolve the ServiceWorkerRegistration into state so the toggle
|
||||
// tap handler never needs to await navigator.serviceWorker.ready. Any await
|
||||
// between the tap gesture and pushManager.subscribe() breaks iOS.
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
if (permission === 'denied') return
|
||||
if (!navigator.serviceWorker) return
|
||||
if (!isOpen) return;
|
||||
if (permission === 'denied') return;
|
||||
if (!navigator.serviceWorker) return;
|
||||
void navigator.serviceWorker.ready.then((reg) => {
|
||||
setSwRegistration(reg)
|
||||
})
|
||||
}, [isOpen, permission])
|
||||
setSwRegistration(reg);
|
||||
});
|
||||
}, [isOpen, permission]);
|
||||
|
||||
if (!isOpen) return null
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleToggle = async () => {
|
||||
if (permission === 'denied') return // no-op — show hint below
|
||||
if (permission === 'denied') return; // no-op — show hint below
|
||||
|
||||
if (isOn) {
|
||||
// on → off: unsubscribe
|
||||
await setEnabled(false)
|
||||
await setEnabled(false);
|
||||
} else if (permission === 'granted') {
|
||||
// off → on, permission already granted: silent subscribe
|
||||
setIsTogglingOn(true)
|
||||
setIsTogglingOn(true);
|
||||
try {
|
||||
await setEnabled(true)
|
||||
await setEnabled(true);
|
||||
} finally {
|
||||
setIsTogglingOn(false)
|
||||
setIsTogglingOn(false);
|
||||
}
|
||||
} else {
|
||||
// off → on, permission 'default': needs tap-gated subscribe with OS dialog
|
||||
@@ -126,21 +126,21 @@ export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
|
||||
// NEW-CR-01: Both vapidKey AND swRegistration are pre-resolved into state
|
||||
// (useEffects above). There is ZERO await between the tap and
|
||||
// registration.pushManager.subscribe() — iOS gesture gate satisfied.
|
||||
if (!vapidKey || !swRegistration) return // not ready — useEffects still resolving
|
||||
const resolvedVapidKey = vapidKey
|
||||
const resolvedRegistration = swRegistration
|
||||
setIsTogglingOn(true)
|
||||
if (!vapidKey || !swRegistration) return; // not ready — useEffects still resolving
|
||||
const resolvedVapidKey = vapidKey;
|
||||
const resolvedRegistration = swRegistration;
|
||||
setIsTogglingOn(true);
|
||||
try {
|
||||
await subscribe(resolvedRegistration, resolvedVapidKey)
|
||||
await subscribe(resolvedRegistration, resolvedVapidKey);
|
||||
} catch {
|
||||
// Permission denied by OS or error — permission state will update reactively
|
||||
} finally {
|
||||
setIsTogglingOn(false)
|
||||
setIsTogglingOn(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isDisabled = permission === 'denied'
|
||||
const isDisabled = permission === 'denied';
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -289,7 +289,9 @@ export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
|
||||
role="switch"
|
||||
aria-checked={isOn}
|
||||
aria-label={isOn ? 'FamilySync Notifications, on' : 'FamilySync Notifications, off'}
|
||||
onClick={() => { void handleToggle() }}
|
||||
onClick={() => {
|
||||
void handleToggle();
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
style={{
|
||||
// 44px touch target
|
||||
@@ -391,9 +393,7 @@ export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{instructionsOpen && (
|
||||
<InstructionSheet onClose={() => setInstructionsOpen(false)} />
|
||||
)}
|
||||
{instructionsOpen && <InstructionSheet onClose={() => setInstructionsOpen(false)} />}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user