// Route A web entry: mounts the phone's h/[hostId] route tree on react-native-web.
// Dark: built by `build:mobile-web:app` into out/mobile-web-app, shipped by nothing until C1.
import { useEffect, type PropsWithChildren } from 'react'
import { createRoot } from 'react-dom/client'
import { ExpoRoot } from 'expo-router'
import type { BridgeRpcClient } from '../src/mobile-web-shell/bridge/bridge-rpc-client'
import {
bootstrapShellPage,
createShellPageClient,
stampPageMountState,
type PageMountTarget
} from '../src/mobile-web-shell/bridge/page-bootstrap'
import { publishPageStorage } from '../src/mobile-web-shell/bridge/page-async-storage'
import { PageFaultBoundary } from '../src/mobile-web-shell/bridge/page-fault-boundary'
import { publishPageHostProfile } from '../src/mobile-web-shell/bridge/page-host-profile'
import { publishExternalLinkOpener } from '../src/platform/external-link.web'
import { publishHapticsNotifier } from '../src/platform/haptics.web'
// Named with its extension: this entry is the web build's and the provider it needs is the web
// sibling's, which takes the page's client. The screens below still import `./client-context`
// and reach the same module, because the builder resolves both specifiers to the same file.
import { RpcClientProvider } from '../src/transport/client-context.web'
// Body replaced at build time: esbuild has no require.context, so the builder synthesizes one.
import routeContext from './route-manifest'
// The route tree starts at app/h, below the native root layout that owns the provider, so the
// page supplies it here through ExpoRoot's own wrapper rather than mounting the native shell.
// No suspense boundary: expo-router wraps every screen in its own, which is what catches the
// route chunks the manifest defers. A chunk that never arrives is a rejection rather than a wait,
// and that is the boundary below's, not suspense's.
// A factory because the client is not in scope until `init` lands, and ExpoRoot takes a component.
function createRootProviders(client: BridgeRpcClient, target: PageMountTarget) {
return function RootProviders({ children }: PropsWithChildren) {
// Effects run child-first, so 'mounted' lands only after the router tree below this wrapper
// has committed. The tree is rendered once, with a ready client, so there is one such commit.
useEffect(() => {
stampPageMountState(target, 'mounted')
}, [])
return {children}
}
}
/**
* The whole page for a shell that opened it and then named no screen.
*
* Built as elements rather than markup, and outside React: the route tree is exactly what cannot
* be mounted here, and a panel that needed it would be a second way to fail. The copy names the
* one thing that fixes it, because nothing on this device will.
*/
function renderShellTooOldPanel(container: HTMLElement): void {
const panel = document.createElement('div')
panel.setAttribute('role', 'alert')
panel.style.cssText =
'font:16px/1.5 system-ui,-apple-system,sans-serif;color:#e6e6e6;background:#141414;' +
'min-height:100vh;display:flex;flex-direction:column;align-items:center;' +
'justify-content:center;gap:8px;padding:24px;text-align:center'
const title = document.createElement('div')
title.style.cssText = 'font-weight:600'
title.textContent = 'Update Orca to open this workspace'
const body = document.createElement('div')
body.style.cssText = 'color:#9a9a9a;font-size:14px'
body.textContent = 'This version of the app cannot open the workspace it downloaded.'
panel.append(title, body)
container.replaceChildren(panel)
}
const container = document.getElementById('root')
if (!container) {
throw new Error('[orca-mobile-web-app] #root missing')
}
const target = document.documentElement
stampPageMountState(target, 'started')
bootstrapShellPage({
target,
client: createShellPageClient(),
replaceUrl: (href) => {
history.replaceState(null, '', href)
},
mount: (client, session) => {
// Before the first render, because both are read from effects that run on it: the host store is
// a plain async function with no provider above it, and the first list paints its pins.
publishPageHostProfile(session.host)
// Same reason, and the same shape: the seam is a plain function in render trees the provider
// does not wrap, so the client's notify is published rather than read from context.
publishExternalLinkOpener((url) => client.notifyExternalLink(url))
// The same shape again, and for the same reason: every haptic on this page is played from a
// plain function inside a row's press handler, which no provider wraps.
publishHapticsNotifier((kind) => client.notifyHaptics(kind))
// Scoped to the host `init` named: with none, no key is writable, which is the right answer
// for a shell too old to say whose list this is.
publishPageStorage(
session.storage,
(key, value) => client.notifyStorageWrite(key, value),
session.host?.id ?? ''
)
createRoot(container).render(
// Above `ExpoRoot`, not inside its wrapper: a route this bundle cannot resolve or import
// throws where the router renders it, and a boundary below the router never sees that.
{
client.notifyPageFault(error)
}}
>
)
},
refuseUnroutedShell: () => {
renderShellTooOldPanel(container)
}
})