From a4a7438641de31e85e9ebdb89f5749b061a96bbe Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Thu, 18 Jun 2026 13:40:00 -0400 Subject: [PATCH] fix(17): WR-05 IN-03 IN-01 resize-aware useIsPhone hook; OidcRedirect side-effect in effect --- apps/pwa/src/App.tsx | 16 ++++--- apps/pwa/src/components/CalendarShell.tsx | 7 +-- apps/pwa/src/components/CredentialSheet.tsx | 3 +- apps/pwa/src/components/SettingsSheet.tsx | 11 +++-- apps/pwa/src/hooks/useIsPhone.ts | 50 +++++++++++++++++++++ apps/pwa/src/routes/AdminPage.tsx | 11 +++-- 6 files changed, 73 insertions(+), 25 deletions(-) create mode 100644 apps/pwa/src/hooks/useIsPhone.ts 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