Skip to content

feat(expo): add embedded navigation mode to native components#9121

Draft
mikepitre wants to merge 1 commit into
mainfrom
mike/expo-hosted-navigation
Draft

feat(expo): add embedded navigation mode to native components#9121
mikepitre wants to merge 1 commit into
mainfrom
mike/expo-hosted-navigation

Conversation

@mikepitre

Copy link
Copy Markdown
Contributor

Problem

UserProfileView and AuthView render their own navigation header, so pushing them onto an expo-router stack produces a double header, and the route's back button can't reach Clerk's internal screens. Requested by a customer in this thread; internal design discussion here.

What this adds

Three tiers, all optional — existing usage is untouched (hideHeader defaults to false, and without it the native views take the same code paths as today).

1. Drop-in expo-router screens (the recommended path)

New entry point @clerk/expo/native/router:

// app/(app)/account.tsx
import { UserProfileScreen } from '@clerk/expo/native/router';

export default function AccountRoute() {
  return <UserProfileScreen options={{ title: 'Account' }} />;
}

Push it like any route: router.push('/account'). The route header keeps the app's own title; its back button, the iOS back gesture, and Android hardware/predictive back all pop Clerk's internal screens first and the route only after that; sign-out and account deletion pop the route automatically. AuthScreen is the same thing for the auth flow:

// app/sign-in.tsx
import { AuthScreen } from '@clerk/expo/native/router';

export default function SignInRoute() {
  return <AuthScreen options={{ title: 'Sign in' }} />;
}

expo-router is a new optional peer dependency, loaded lazily inside this entry point only — apps not using expo-router never resolve it.

2. Raw embedded mode (any navigator, custom headers)

import { useRef, useState } from 'react';
import { UserProfileView, type UserProfileViewRef } from '@clerk/expo/native';

function HostedProfile() {
  const ref = useRef<UserProfileViewRef>(null);
  const [nav, setNav] = useState({ depth: 0, canGoBack: false });

  return (
    <>
      <MyHeader showBack={nav.canGoBack} onBack={() => ref.current?.goBack()} />
      <UserProfileView ref={ref} hideHeader onNavigationChange={setNav} style={{ flex: 1 }} />
    </>
  );
}

New surface on both UserProfileView and AuthView, shared via one HostedNavigationProps / HostedNavigationRef contract:

  • hideHeader?: boolean — hides Clerk's built-in header, keeps internal navigation working
  • onNavigationChange?: ({ depth, canGoBack }) => void — fires on internal push/pop
  • ref: goBack() / popToRoot()

3. Existing usage — unchanged

<Modal visible={open} presentationStyle="pageSheet" onRequestClose={close}>
  <UserProfileView onDismiss={close} />
</Modal>

Native implementation

  • iOS: with hideHeader, the pod owns the NavigationStack/NavigationPath and passes it through UserProfileView(navigationPath:), deriving depth and pop from its own path. AuthView's stack is internal to clerk-ios, so it's driven through the new @_spi(FrameworkIntegration) hosted-navigation handle. Bar hiding comes from the same SPI environment value.
  • Android: passes the new public hostedNavigation parameter to clerk-android's composables and streams depth via snapshotFlow.

Dependencies / merge order

Blocked on SDK releases containing the hosted-navigation support: clerk-ios#519 and clerk-android#798. The native halves here won't compile until those ship and the version pins are bumped. Draft until then.

Testing

  • 96/96 vitest tests pass (4 new for UserProfileView covering the event unwrap, ref forwarding, and the hideHeader-off path)
  • pnpm lint and full pnpm build (including declarations for the new entry point) clean
  • Still to do once the SDK releases exist: runtime pass in the quickstart app on both platforms

🤖 Generated with Claude Code

Adds hideHeader, onNavigationChange, and goBack()/popToRoot() refs to the
native UserProfileView and AuthView so they can be pushed onto a host
navigation stack without a double header, plus a new @clerk/expo/native/router
entry with prewired expo-router screens (UserProfileScreen, AuthScreen).
expo-router becomes an optional peer dependency.

Native halves require clerk-ios and clerk-android releases with hosted
navigation support before the pods/gradle modules compile.
@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7e14106

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@clerk/expo Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jul 9, 2026 11:06pm
swingset Ready Ready Preview, Comment Jul 9, 2026 11:06pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: c754f2e4-1270-4ea8-b509-f24d6cb7e2f2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9121

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9121

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9121

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9121

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9121

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9121

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9121

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9121

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9121

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9121

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9121

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9121

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9121

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9121

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9121

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9121

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9121

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9121

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9121

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9121

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9121

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9121

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9121

commit: 7e14106

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant