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,9 +21,9 @@
|
||||
* - EU DMA iOS 17.4+ caveat: PWA may open in Safari tabs; install guide addresses this.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Smartphone, X } from 'lucide-react'
|
||||
import { PushPermissionPrompt } from './PushPermissionPrompt.js'
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Smartphone, X } from 'lucide-react';
|
||||
import { PushPermissionPrompt } from './PushPermissionPrompt.js';
|
||||
|
||||
// ── iOS detection ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -38,12 +38,11 @@ import { PushPermissionPrompt } from './PushPermissionPrompt.js'
|
||||
* 3. Check navigator.standalone is NOT true (standalone = already installed).
|
||||
*/
|
||||
export function isIOSSafariNonStandalone(): boolean {
|
||||
const ua = navigator.userAgent
|
||||
const ua = navigator.userAgent;
|
||||
const isIOS =
|
||||
/iPad|iPhone|iPod/.test(ua) &&
|
||||
!(window as unknown as { MSStream?: unknown }).MSStream
|
||||
const isStandalone = (navigator as unknown as { standalone?: boolean }).standalone === true
|
||||
return isIOS && !isStandalone
|
||||
/iPad|iPhone|iPod/.test(ua) && !(window as unknown as { MSStream?: unknown }).MSStream;
|
||||
const isStandalone = (navigator as unknown as { standalone?: boolean }).standalone === true;
|
||||
return isIOS && !isStandalone;
|
||||
}
|
||||
|
||||
// ── Installed state check ──────────────────────────────────────────────────
|
||||
@@ -56,14 +55,14 @@ function isInstalled(): boolean {
|
||||
return (
|
||||
window.matchMedia('(display-mode: standalone)').matches ||
|
||||
(navigator as unknown as { standalone?: boolean }).standalone === true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ── Android beforeinstallprompt hook ─────────────────────────────────────
|
||||
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt(): Promise<void>
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
|
||||
prompt(): Promise<void>;
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,40 +74,40 @@ interface BeforeInstallPromptEvent extends Event {
|
||||
* triggerInstall: function that calls prompt() on the deferred event
|
||||
*/
|
||||
export function useAndroidInstallPrompt() {
|
||||
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null)
|
||||
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
||||
// justInstalled: set to true when the appinstalled event fires, enabling the
|
||||
// post-install push permission prompt trigger (D-08).
|
||||
const [justInstalled, setJustInstalled] = useState(false)
|
||||
const [justInstalled, setJustInstalled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
e.preventDefault()
|
||||
setDeferredPrompt(e as BeforeInstallPromptEvent)
|
||||
}
|
||||
e.preventDefault();
|
||||
setDeferredPrompt(e as BeforeInstallPromptEvent);
|
||||
};
|
||||
const installedHandler = () => {
|
||||
setDeferredPrompt(null)
|
||||
setJustInstalled(true)
|
||||
}
|
||||
setDeferredPrompt(null);
|
||||
setJustInstalled(true);
|
||||
};
|
||||
|
||||
window.addEventListener('beforeinstallprompt', handler)
|
||||
window.addEventListener('appinstalled', installedHandler)
|
||||
window.addEventListener('beforeinstallprompt', handler);
|
||||
window.addEventListener('appinstalled', installedHandler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeinstallprompt', handler)
|
||||
window.removeEventListener('appinstalled', installedHandler)
|
||||
}
|
||||
}, [])
|
||||
window.removeEventListener('beforeinstallprompt', handler);
|
||||
window.removeEventListener('appinstalled', installedHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const triggerInstall = async () => {
|
||||
if (!deferredPrompt) return
|
||||
await deferredPrompt.prompt()
|
||||
const { outcome } = await deferredPrompt.userChoice
|
||||
if (!deferredPrompt) return;
|
||||
await deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
if (outcome === 'accepted') {
|
||||
setDeferredPrompt(null)
|
||||
setDeferredPrompt(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { canInstall: deferredPrompt !== null, triggerInstall, justInstalled }
|
||||
return { canInstall: deferredPrompt !== null, triggerInstall, justInstalled };
|
||||
}
|
||||
|
||||
// ── iOS Walkthrough Sheet ─────────────────────────────────────────────────
|
||||
@@ -119,10 +118,10 @@ const IOS_STEPS = [
|
||||
"Scroll down and tap 'Add to Home Screen'",
|
||||
"Tap 'Add' in the top right",
|
||||
'Open FamilySync from your Home Screen — it opens without the browser bar',
|
||||
]
|
||||
];
|
||||
|
||||
interface WalkthroughSheetProps {
|
||||
onClose: () => void
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function WalkthroughSheet({ onClose }: WalkthroughSheetProps) {
|
||||
@@ -142,7 +141,7 @@ function WalkthroughSheet({ onClose }: WalkthroughSheetProps) {
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// Close on backdrop click
|
||||
if (e.target === e.currentTarget) onClose()
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -272,7 +271,7 @@ function WalkthroughSheet({ onClose }: WalkthroughSheetProps) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ── InstallPrompt ─────────────────────────────────────────────────────────
|
||||
@@ -290,44 +289,44 @@ function WalkthroughSheet({ onClose }: WalkthroughSheetProps) {
|
||||
// degrades gracefully (treated as "not dismissed") instead of crashing the component.
|
||||
function readDismissed(): boolean {
|
||||
try {
|
||||
return localStorage.getItem('installPromptDismissed') === '1'
|
||||
return localStorage.getItem('installPromptDismissed') === '1';
|
||||
} catch {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function persistDismissed(): void {
|
||||
try {
|
||||
localStorage.setItem('installPromptDismissed', '1')
|
||||
localStorage.setItem('installPromptDismissed', '1');
|
||||
} catch {
|
||||
// Ignore write failures (private mode / storage disabled)
|
||||
}
|
||||
}
|
||||
|
||||
export function InstallPrompt() {
|
||||
const [dismissed, setDismissed] = useState<boolean>(readDismissed)
|
||||
const [walkthroughOpen, setWalkthroughOpen] = useState(false)
|
||||
const { canInstall, triggerInstall, justInstalled } = useAndroidInstallPrompt()
|
||||
const [dismissed, setDismissed] = useState<boolean>(readDismissed);
|
||||
const [walkthroughOpen, setWalkthroughOpen] = useState(false);
|
||||
const { canInstall, triggerInstall, justInstalled } = useAndroidInstallPrompt();
|
||||
|
||||
// Re-check isInstalled on mount — matchMedia is only available in the browser
|
||||
const [installed, setInstalled] = useState(false)
|
||||
const [installed, setInstalled] = useState(false);
|
||||
useEffect(() => {
|
||||
setInstalled(isInstalled())
|
||||
}, [])
|
||||
setInstalled(isInstalled());
|
||||
}, []);
|
||||
|
||||
// Show post-install permission prompt (D-08) immediately after the appinstalled event
|
||||
// fires — before the page knows it is in standalone mode. This covers Android Chrome
|
||||
// where the page stays loaded after install without a reload.
|
||||
if (justInstalled) {
|
||||
return <PushPermissionPrompt />
|
||||
return <PushPermissionPrompt />;
|
||||
}
|
||||
|
||||
// Nothing to show when already installed (App.tsx mounts PushPermissionPrompt there)
|
||||
if (installed) return null
|
||||
if (installed) return null;
|
||||
|
||||
function dismiss() {
|
||||
persistDismissed()
|
||||
setDismissed(true)
|
||||
persistDismissed();
|
||||
setDismissed(true);
|
||||
}
|
||||
|
||||
// ── iOS banner ──────────────────────────────────────────────────────────
|
||||
@@ -416,7 +415,7 @@ export function InstallPrompt() {
|
||||
|
||||
{walkthroughOpen && <WalkthroughSheet onClose={() => setWalkthroughOpen(false)} />}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ── Android banner ──────────────────────────────────────────────────────
|
||||
@@ -458,7 +457,7 @@ export function InstallPrompt() {
|
||||
{/* Install CTA */}
|
||||
<button
|
||||
onClick={() => {
|
||||
void triggerInstall().then(() => dismiss())
|
||||
void triggerInstall().then(() => dismiss());
|
||||
}}
|
||||
style={{
|
||||
background: 'var(--color-text-primary, #111318)',
|
||||
@@ -499,9 +498,9 @@ export function InstallPrompt() {
|
||||
<X size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Nothing applicable — desktop, already installed, or dismissed
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user