# @familysync/pwa The React 19 PWA frontend for FamilySync. Delivers a single installable, low-friction interface showing the color-coded family calendar and shared collaborative lists, designed for a mixed Android/Apple household. Part of the [FamilySync monorepo](../../README.md). ## Stack | Layer | Library | Version | | ----------------------------- | --------------------------------- | -------- | | UI framework | React | ^19.0.0 | | Build + dev server | Vite | 8.0.16 | | PWA service worker + manifest | vite-plugin-pwa | ^1.3.0 | | Calendar widget | @schedule-x/calendar | 4.6.0 | | Server state | @tanstack/react-query | 5.101.0 | | UI state | zustand | 5.0.14 | | Routing | react-router | ^7.17.0 | | iCalendar parsing | ical.js | 2.2.1 | | Drag-and-drop (lists) | @dnd-kit/core + @dnd-kit/sortable | ^6 / ^10 | ## Development Run the PWA dev server from the monorepo root: ```bash pnpm --filter @familysync/pwa dev ``` The API backend must also be running for most features. See [GETTING-STARTED.md](../../docs/GETTING-STARTED.md) for full stack bring-up instructions. ## Scripts | Command | What it does | | ----------------------------------------- | ------------------------------------------------------------- | | `pnpm --filter @familysync/pwa dev` | Start Vite dev server (HMR) | | `pnpm --filter @familysync/pwa build` | Type-check then build production bundle (`tsc && vite build`) | | `pnpm --filter @familysync/pwa preview` | Serve the production build locally | | `pnpm --filter @familysync/pwa typecheck` | Run `tsc --noEmit` without emitting files | | `pnpm --filter @familysync/pwa test` | Run Vitest test suite once (`vitest run`) | ## Source layout ``` src/ api/ # Typed fetch wrappers for @familysync/api (client.ts, listsClient.ts) components/ # Shared UI components co-located with their *.test.tsx files hooks/ # Custom React hooks (useListSSE, usePushSubscription, useFocusTrap) lib/ # Pure helpers: calendarConfig, colorUtils, eventDateTime, hydrateEvents, loginRedirect routes/ # React Router route components with co-located tests (ListDetail, ListsIndex) store/ # Zustand stores: calendarStore, listsStore styles/ # Global CSS main.tsx # App entry point — React Query client, router, global error handlers App.tsx # Root component sw.ts # Workbox service worker entry test-setup.ts # Vitest + jest-dom global setup ``` Routes and their co-located components own the feature slice. Library code (pure, side-effect-free) lives in `lib/`. Zustand stores hold UI-only state; server data is exclusively managed by TanStack Query. ## Communication with the API All backend calls go through `src/api/client.ts` and `src/api/listsClient.ts`. Key behavior: - `credentials: 'include'` on every request so the OIDC session cookie is forwarded. - `redirect: 'manual'` — a 401 or opaque redirect (the Authelia 302) is caught and thrown as a typed `SessionExpiredError`. The global `QueryCache` / `MutationCache` error handler in `main.tsx` intercepts this and shows the session-expiry interstitial. - Re-authentication requires a top-level navigation to `/api/login` (handled by `src/lib/loginRedirect.ts`), not a fetch redirect, because browsers block CORS redirects to an external IdP. - Live list updates are delivered via SSE through `src/hooks/useListSSE.ts`; the hook calls `queryClient.invalidateQueries` on each event so TanStack Query re-fetches. In development the Vite proxy routes `/api` requests to the API server on port 3000, keeping cookies same-site. In production both apps are served same-origin via the Pangolin/Newt tunnel. ## Testing Tests are co-located with their source files (`*.test.tsx` / `*.test.ts`) and use React Testing Library + `@testing-library/jest-dom`. The test environment is `jsdom`. ```bash # run once pnpm --filter @familysync/pwa test # watch mode (during development) pnpm --filter @familysync/pwa exec vitest ``` No coverage threshold is configured. Run `pnpm --filter @familysync/pwa typecheck` separately — Vitest uses esbuild and will not surface TypeScript errors. ## PWA install notes - The app must be added to the Home Screen on iOS for push notifications to work (iOS 16.4+ minimum). - Push permission must be requested inside a tap handler; calling `pushManager.subscribe()` on page load is blocked. - Background sync and background push are not supported on iOS; all push messages must display a visible notification. ## Further reading - [Architecture overview](../../docs/ARCHITECTURE.md) - [Getting started](../../docs/GETTING-STARTED.md) - [Development guide](../../docs/DEVELOPMENT.md) - [Testing guide](../../docs/TESTING.md)