129 lines
3.2 KiB
TypeScript
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>
|
|
)
|
|
}
|