ultisuite-client/components/mobile/native-auth-gate.tsx
R3D347HR4Y d6d18f911b
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
Lots of stuff and mobile app
2026-06-17 00:13:28 +02:00

129 lines
3.2 KiB
TypeScript

"use client"
import { useEffect, useRef, useState, type ReactNode } from "react"
import { usePathname } from "next/navigation"
import { isAuthPublicPath } from "@/lib/auth/public-paths"
import { ensureNativeAccessToken } from "@/lib/auth/native-session"
import { hydrateNativeRuntimeConfig } from "@/lib/runtime-config/native"
import { getRuntimeConfig } from "@/lib/runtime-config"
import { useNativeRuntime } from "@/lib/platform"
import { NativeLogin } from "@/components/mobile/native-login"
import { ServerPicker } from "@/components/mobile/server-picker"
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) => {
setTimeout(() => reject(new Error("timeout")), ms)
}),
])
}
type AuthScreen = "picker" | "login" | "none"
function initialAuthScreen(pathname: string): AuthScreen {
if (isAuthPublicPath(pathname)) return "none"
// SSR-safe: never read localStorage here (hydration mismatch breaks touch on Android).
return "picker"
}
function NativeAuthScreen({ children }: { children: ReactNode }) {
return (
<div className="native-auth-screen fixed inset-0 z-[10000] flex h-dvh flex-col overflow-auto bg-background touch-manipulation">
{children}
</div>
)
}
/**
* On native shells, show server picker / login inline (no client router redirects).
*/
export function NativeAuthGate({ children }: { children: ReactNode }) {
const native = useNativeRuntime()
const pathname = usePathname()
const booted = useRef(false)
const [authScreen, setAuthScreen] = useState<AuthScreen>(() =>
native ? initialAuthScreen(pathname) : "none"
)
const [returnTo, setReturnTo] = useState("/mail/inbox")
useEffect(() => {
if (!native) return
const dest = pathname.startsWith("/") ? pathname : "/mail/inbox"
setReturnTo(dest)
if (isAuthPublicPath(pathname)) {
setAuthScreen("none")
}
}, [native, pathname])
useEffect(() => {
if (!native || booted.current) return
let cancelled = false
async function bootstrap() {
try {
await withTimeout(hydrateNativeRuntimeConfig(), 3_000)
} catch {
/* localStorage fallback */
}
if (cancelled) return
if (isAuthPublicPath(pathname)) {
setAuthScreen("none")
return
}
if (!getRuntimeConfig()) {
setAuthScreen("picker")
return
}
let token: string | null = null
try {
token = await withTimeout(ensureNativeAccessToken(), 8_000)
} catch {
token = null
}
if (cancelled) return
setAuthScreen(token ? "none" : "login")
}
void bootstrap().finally(() => {
if (!cancelled) booted.current = true
})
return () => {
cancelled = true
}
}, [native, pathname])
if (!native) return children
if (isAuthPublicPath(pathname) || authScreen === "none") {
return children
}
if (authScreen === "picker") {
return (
<NativeAuthScreen>
<ServerPicker
onSelected={() => {
setAuthScreen("login")
}}
/>
</NativeAuthScreen>
)
}
return (
<NativeAuthScreen>
<NativeLogin returnTo={returnTo} />
</NativeAuthScreen>
)
}