Files

92 lines
4.2 KiB
TypeScript

'use client'
// `/`, the route's client half: the web app for a signed-in reader, the landing
// for everyone else. `/dev` is the same door with its own two answers
// (app/dev/_door.tsx): the builder for a member, Dev's page for a stranger.
//
// The export holds the landing, because a crawler and a stranger are the ones
// who read the file. The app is drawn in the browser, and only once the browser
// has said who is here:
//
// before hydration the landing — what the file holds, so nothing mismatches.
// A live token already marked <html> with `APP` and took
// the landing off the page before it was parsed
// (session.ts `BOOT`, the landing's first child).
// IAM still settling a live token is signed in: the SDK's own fast path
// answers the same way, so the app is not held for it.
// IAM settled its answer. A token that refreshed is signed in; one that
// could not is signed out.
//
// SIGNED OUT, `/` IS THE LANDING, drawn at once: it never leaves for hanzo.id
// to ask whether anyone is signed in there. Doing so held every new visitor's
// first paint for a round trip to another site. A person signed in at hanzo.id
// is found by the sign-in they press (/login asks hanzo.id first). A place in
// the app is a path under /chat or /dev, whose own page signs a stranger in to
// it; `/?chat=<id>`, the address a conversation had before, moves to its path
// before anything is drawn (app/page.tsx `MOVED`), so `/` itself holds none.
// /dev's door names its own (`placed`), and a stranger at one of them is sent
// to the sign-in and brought back to it (destination.ts `enter`).
//
// The marks follow the answer before paint, both ways, and leave with the page.
import { useEffect, useLayoutEffect, useRef, useState, type ComponentRef, type ReactNode } from 'react'
import dynamic from 'next/dynamic'
import { useIam } from '@hanzo/iam/react'
import { View } from '@hanzo/ui'
import { useHydrated } from '@hanzo/app/lib/hydrated'
import { enter } from '@hanzo/app/lib/destination'
import { APP, live } from '@hanzo/app/lib/session'
/** The app, fetched only for a signed-in reader (./_web). Until it lands the
* page is the ground under the `APP` mark: no landing, and nothing else. */
const App = dynamic(() => import('./_web'), { ssr: false })
/** `/` is no place in the app: its old addresses move to their paths first (app/page.tsx `MOVED`). */
const nowhere = (): boolean => false
export function Apex({
children,
placed = nowhere,
app,
}: {
children: ReactNode
/** Whether this address is a place in the app, which a stranger signs in to reach. `/` names none. */
placed?: () => boolean
/** What a signed-in reader is drawn. `/`'s is the app in Chat. */
app?: ReactNode
}) {
const hydrated = useHydrated()
const { isLoading, isAuthenticated } = useIam()
const [away] = useState(placed)
const gone = useRef(false)
const landing = useRef<ComponentRef<typeof View>>(null)
const signed = hydrated && (isLoading ? live() : isAuthenticated)
const isDesktop = typeof window !== 'undefined' && Boolean((window as unknown as { __TAURI__?: unknown; __TAURI_INTERNALS__?: unknown }).__TAURI__ || (window as unknown as { __TAURI__?: unknown; __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__)
const leaving = hydrated && !signed && (away || isDesktop)
useLayoutEffect(() => {
if (!hydrated) return
const html = document.documentElement
html.toggleAttribute(APP, signed || leaving)
// A landing still drawn is one the session says to show, whatever BOOT
// guessed before it was parsed: BOOT hides it inline, and clearing that
// hands the box back to its own `display="contents"`.
if (landing.current instanceof HTMLElement) landing.current.style.display = ''
return () => html.removeAttribute(APP)
}, [hydrated, signed, leaving])
useEffect(() => {
if (!leaving || isLoading || gone.current) return
gone.current = true
enter()
}, [leaving, isLoading])
if (signed) return app ?? <App />
if (leaving) return null
return (
<View ref={landing} data-landing="" display="contents">
{children}
</View>
)
}