feat(06-05): global session-expiry interstitial via QueryCache/MutationCache onError
- Add sessionExpired boolean + setSessionExpired action to Zustand calendarStore - main.tsx: construct QueryClient with QueryCache+MutationCache onError (v5 pattern, not defaultOptions.onError) - onGlobalError: checks instanceof SessionExpiredError, calls setSessionExpired(true) via store.getState() - CalendarShell: read sessionExpired from store; render AuthSplash(state=redirecting, 'Session expired', 'Signing you back in…') - CalendarShell: useEffect fires clearLoginRedirect + maybeRedirectToLogin after 1.5s when sessionExpired (one-shot guard re-armed) - Context7 /tanstack/query confirmed v5 QueryCache/MutationCache constructor + onError signature
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
* success + events → ScheduleXCalendar
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useState, useEffect, useMemo, useRef } from 'react'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { ScheduleXCalendar, useCalendarApp } from '@schedule-x/react'
|
||||
import {
|
||||
@@ -83,8 +83,11 @@ export function CalendarShell({ onOpenSettings }: { onOpenSettings?: () => void
|
||||
const selectedView = useCalendarStore((s) => s.selectedView)
|
||||
const setEventForm = useCalendarStore((s) => s.setEventForm)
|
||||
const eventFormOpen = useCalendarStore((s) => s.eventFormOpen)
|
||||
const sessionExpired = useCalendarStore((s) => s.sessionExpired)
|
||||
const { start, end } = calendarRange
|
||||
const queryClient = useQueryClient()
|
||||
// Ref to ensure the session-expiry redirect timer fires only once per expiry
|
||||
const sessionExpiredRedirectFired = useRef(false)
|
||||
|
||||
// Fetch current user to build per-member color config
|
||||
const meQuery = useQuery({
|
||||
@@ -208,6 +211,26 @@ export function CalendarShell({ onOpenSettings }: { onOpenSettings?: () => void
|
||||
}
|
||||
}, [meQuery.isSuccess])
|
||||
|
||||
// Session-expiry redirect (D-11) — fires when a mid-use 401 is detected by the
|
||||
// global QueryCache/MutationCache handler and arms the Zustand sessionExpired flag.
|
||||
// Re-arms the one-shot guard (clearLoginRedirect) so maybeRedirectToLogin fires
|
||||
// afresh, then schedules the top-level navigation after ~1.5s (UI-SPEC §Surface 2
|
||||
// "≤2s before redirect, no dismiss button").
|
||||
useEffect(() => {
|
||||
if (!sessionExpired) return
|
||||
if (sessionExpiredRedirectFired.current) return
|
||||
sessionExpiredRedirectFired.current = true
|
||||
|
||||
clearLoginRedirect()
|
||||
const timer = setTimeout(() => {
|
||||
maybeRedirectToLogin()
|
||||
}, 1500)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [sessionExpired])
|
||||
|
||||
// ── Render helpers ────────────────────────────────────────────────────────
|
||||
|
||||
// Determine which content to show in the calendar area.
|
||||
@@ -238,6 +261,20 @@ export function CalendarShell({ onOpenSettings }: { onOpenSettings?: () => void
|
||||
return <AuthSplash state="redirecting" />
|
||||
}
|
||||
|
||||
// ── Session-expiry interstitial (D-11) ─────────────────────────────────────
|
||||
// When a mid-use query or mutation returns 401, the global QueryCache/MutationCache
|
||||
// handler sets sessionExpired=true. Show the "Session expired" interstitial while the
|
||||
// 1.5s timer fires (wired above in the sessionExpired useEffect).
|
||||
if (sessionExpired) {
|
||||
return (
|
||||
<AuthSplash
|
||||
state="redirecting"
|
||||
heading="Session expired"
|
||||
body="Signing you back in…"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Calendar content ───────────────────────────────────────────────────────
|
||||
|
||||
// The content panel (right of sidebar on desktop, full-width on phone).
|
||||
|
||||
Reference in New Issue
Block a user