diff --git a/apps/pwa/src/App.tsx b/apps/pwa/src/App.tsx index c11e6ea..719b957 100644 --- a/apps/pwa/src/App.tsx +++ b/apps/pwa/src/App.tsx @@ -44,7 +44,7 @@ * - /api/me query shared so AppNav has user data on all routes. */ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router'; import { useQuery } from '@tanstack/react-query'; import { CalendarShell } from './components/CalendarShell.js'; @@ -59,12 +59,9 @@ import { PushPermissionPrompt } from './components/PushPermissionPrompt.js'; import { PermissionDeniedBanner } from './components/PermissionDeniedBanner.js'; import { SetupBanner } from './components/SetupBanner.js'; import { SettingsSheet } from './components/SettingsSheet.js'; +import { useIsPhone } from './hooks/useIsPhone.js'; import { fetchMe, fetchSetupStatus, fetchAuthMode } from './api/client.js'; -function isPhone(): boolean { - return typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches; -} - /** * OidcRedirect — tiny helper that triggers a top-level navigation to /api/login. * @@ -75,13 +72,18 @@ function isPhone(): boolean { * which browsers cannot follow as a fetch/XHR (T-07-04). */ function OidcRedirect() { - window.location.replace('/api/login'); + // IN-01: navigate from an effect, not during render. React may invoke a + // component body more than once (StrictMode double-invoke, concurrent + // re-renders); side effects belong in useEffect. + useEffect(() => { + window.location.replace('/api/login'); + }, []); return