Skip to content

Repository files navigation

Portal Native Test

Expo prototype for a native Fluid portal shell — one builder publish, rendered as a real native app.

📊 Proposal deck: native-shell-site.vercel.app — the "why" and the roadmap, in 12 slides.

Status: proof of concept. It runs against the live portal-tenant BFF, but the native-feel layer is built with injected-CSS/JS heuristics that a native_shell=1 render mode in the portal SDK would eventually replace (see the deck and the contract section below).

The app demonstrates the recommended hybrid model:

  • the native Expo app owns top-level chrome, tab navigation, auth/account, messaging, notifications, profile, badges, deep links, and push permissions
  • builder-published screens render as body-only portal canvases through react-native-webview
  • admin nav changes flow through a manifest, so app layout can change without shipping a new binary

Native-feel architecture

Navigation is a root native stack with three route types:

  • tabs — bottom tab navigator built from manifest.bottomTabs. Tabs are pre-mounted (lazy: false) so switching is instant, and the tab bar is a translucent blur bar (iOS) that canvas content scrolls underneath (a matching bottom inset is injected into each page).
  • portal-link — any builder link that isn't a root tab presents as a native page sheet (system card transition, swipe down to dismiss) with a real navigation bar: in-page back, live title sync from the page, native share sheet.
  • more-route — routes listed under More push as real native screens with swipe-back.

Other native-feel details:

  • themed skeleton placeholders while canvases load — the WebView never flashes white
  • pull-to-refresh inside every canvas
  • haptic feedback on tab presses, link presentation, and publish
  • offline/error state with retry, styled with the portal theme
  • native Share sheet for share bridge messages

WebView bridge

The shell injects window.FluidNativeApp = { version, capabilities } and listens for window.ReactNativeWebView.postMessage(JSON.stringify(message)) with these message types:

type payload behavior
navigate { slug } jump to a native tab / manifest route
presentPortalLink { url, title? } present a builder page as a native sheet
openExternalUrl { url } open in the system browser
share { title, url } native share sheet
setTitle { title } update the native header title (sheets)
haptic { style } selection | light | medium | success | warning

Getting started

Pinned to Expo SDK 54 (React Native 0.81) specifically so it runs in the App Store build of Expo Go — no dev build or custom binary required.

On your phone (recommended — this is where the haptics and native sheets are the point):

  1. Install Expo Go from the App Store / Play Store.
  2. Clone and start the bundler:
    npm install
    npx expo start
  3. Scan the QR code with Expo Go (make sure your phone is on the same Wi-Fi as your machine). If the LAN QR won't connect, run npx expo start --tunnel.

On the iOS Simulator (needs Xcode): press i in the Expo CLI, or npm run ios.

Signing in

The portal canvases load p.portal.fluid.app, which needs a session. On a fresh launch with no session you'll get a native login screen hosting the portal's own sign-in; once you're in, the shared cookie makes the manifest fetch succeed and the live tabs + theme swap in. The Sync latest publish row under More force-refreshes nav and theme from the builder — use it to see an admin publish reorder the app without a rebuild.

Live publish contract (wired)

The shell loads its navigation from the real portal-tenant BFF endpoint the web portal renders from:

GET {portal}/api/app/manifest

src/data/live-manifest.ts fetches it using the WebView's shared session cookie and projects manifest.profile.navigation.navigation_items into native tabs using the web shell's exact rules (position sort, top-level filter, first 5 tabs + More overflow, first navigable slug = home/launch tab). The manifest re-syncs on app foreground and via the header sync button, so a builder publish reorders the app without shipping a binary. src/data/mock-native-manifests.ts remains only as offline/no-session sample data, and first render is gated behind a branded splash so sample nav never flashes.

Builder-authored routes should point to a portal runtime mode like:

/home?native_shell=1&chrome=none

In that mode, the portal SDK should render the screen body only (today the shell approximates this with injected CSS). Expo owns the native shell around it.

About

Native Expo shell over Fluid's builder-published portal — POC. Deck: https://native-shell-site.vercel.app

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages