Skip to content

fix(expo): align native prebuilt component APIs#8699

Draft
mikepitre wants to merge 14 commits into
mainfrom
mike/expo-native-prebuilt-parity
Draft

fix(expo): align native prebuilt component APIs#8699
mikepitre wants to merge 14 commits into
mainfrom
mike/expo-native-prebuilt-parity

Conversation

@mikepitre
Copy link
Copy Markdown
Contributor

@mikepitre mikepitre commented May 28, 2026

Summary

This PR aligns the Expo native prebuilt component surface with the way the Clerk iOS and Android SDKs are used directly: native views render content, app code owns presentation, and auth state is observed through the provider/hooks instead of component-specific completion callbacks.

Important pieces

  • Make AuthView and UserProfileView app-presented content

    • Removes the old presentAuth / presentUserProfile helper paths, Android presentation activities, the user-profile modal hook, and the stale inline component aliases.
    • Motivation: Expo usage should mirror clerk-ios patterns like presenting AuthView() in a sheet. The prebuilt view should not decide whether it is full screen, sheet, route, or inline content.
  • Wrap the platform-native UserButton

    • Adds native UserButton view managers on iOS and Android and removes the JS avatar/button implementation.
    • Motivation: UserButton should behave like the native SDK button, including presenting the native user profile itself. JS should not reimplement the avatar, tap behavior, or profile presentation.
  • Centralize native-to-JS auth state sync in ClerkProvider

    • Moves auth completion/sign-out synchronization out of AuthView and UserProfileView.
    • Native auth UI now emits internal auth-state events and the provider syncs the native client token, reloads JS resources when needed, and activates/clears the JS session.
    • Motivation: useAuth() already owns auth state for app developers. Component-level callbacks like onComplete duplicate that responsibility and make examples clunkier.
  • Keep JS sign-out and native sign-out in sync

    • NativeSessionSync now avoids clearing a persisted native session during cold start before JS has adopted it, while still propagating a real JS sign-out to native.
    • Motivation: a native AuthView sign-in, native UserButton sign-out, and JS signOut() should all converge on one session state.
  • Trim bridge surface area

    • Removes unused UIKit/Android presentation bridge code and leaves only the native module functions/events needed for provider sync and the native view managers needed to mount prebuilt UI.
    • Motivation: smaller bridge surface, fewer Expo-only presentation semantics, and less code to keep in parity with the native SDKs.

Before / after examples

Auth presentation

Before: Expo exposed imperative native presentation helpers, so presentation was owned by the SDK bridge.

import { ClerkExpoModule } from '@clerk/expo/native-module';

await ClerkExpoModule.presentAuth({
  mode: 'signInOrUp',
  dismissible: true,
});

After: AuthView is content. The app chooses sheet, route, full screen, or inline presentation.

import { AuthView } from '@clerk/expo/native';
import { useState } from 'react';
import { Button, Modal } from 'react-native';

export function SignedOut() {
  const [isPresented, setIsPresented] = useState(false);

  return (
    <>
      <Button
        title='Sign in'
        onPress={() => setIsPresented(true)}
      />
      <Modal
        visible={isPresented}
        presentationStyle='pageSheet'
        onRequestClose={() => setIsPresented(false)}
      >
        <AuthView
          onDismiss={() => setIsPresented(false)}
        />
      </Modal>
    </>
  );
}

Full-screen/root auth

Before: full-screen behavior came from native presentation-specific bridge code.

await ClerkExpoModule.presentAuth({
  mode: 'signInOrUp',
  dismissible: false,
});

After: render AuthView wherever the app wants it to live.

import { AuthView } from '@clerk/expo/native';

export function SignedOut() {
  return <AuthView />;
}

User button

Before: UserButton reimplemented avatar rendering in JS, fetched native user state, and manually called the native profile presenter.

import { UserButton } from '@clerk/expo/native';

export function Header() {
  return <UserButton />;
}

After: UserButton is backed by the platform-native Clerk button. Tapping it opens the native user profile, just like the iOS and Android SDKs.

import { UserButton } from '@clerk/expo/native';

export function SignedIn() {
  return <UserButton />;
}

User profile presentation

Before: profile presentation could be driven through Expo-specific imperative helpers.

import { useUserProfileModal } from '@clerk/expo';

export function Account() {
  const { presentUserProfile } = useUserProfileModal();

  return (
    <Button
      title='Account'
      onPress={() => void presentUserProfile()}
    />
  );
}

After: UserProfileView is content. The app owns the presentation surface.

import { UserProfileView } from '@clerk/expo/native';
import { useState } from 'react';
import { Button, Modal } from 'react-native';

export function Account() {
  const [isPresented, setIsPresented] = useState(false);

  return (
    <>
      <Button
        title='Account'
        onPress={() => setIsPresented(true)}
      />
      <Modal
        visible={isPresented}
        presentationStyle='pageSheet'
        onRequestClose={() => setIsPresented(false)}
      >
        <UserProfileView
          onDismiss={() => setIsPresented(false)}
        />
      </Modal>
    </>
  );
}

Auth completion

Before: AuthView parsed native completion events and synced JS state from inside the component.

<AuthView />

After: auth completion is observed through Clerk auth state. The component stays presentation/content-only.

import { useAuth } from '@clerk/expo';

export function Root() {
  const { isLoaded, isSignedIn } = useAuth();

  if (!isLoaded) {
    return null;
  }

  return isSignedIn ? <SignedIn /> : <SignedOut />;
}

Testing

  • pnpm --filter @clerk/expo format:check
  • pnpm --filter @clerk/expo test
  • Targeted eslint on touched Expo native/provider files. The remaining output is existing React Native internal parser noise plus the existing @clerk/react/internal resolver issue when linting ClerkProvider.tsx.
  • Local clerk-expo-quickstart: npx tsc --noEmit
  • Local clerk-expo-quickstart: npx expo run:ios --device 9A1571DF-CC3D-465F-A2E1-C89EC2388B31

Notes

  • UserButton keeps a tiny 36x36 host style because React Native/Yoga does not infer the intrinsic size of the native host view. The native SDK still owns rendering and presentation.
  • The changeset is minor, not major, because these prebuilt components are still beta and this is expected beta-surface iteration rather than a stable package-level breaking release.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 28, 2026

🦋 Changeset detected

Latest commit: 91dbd71

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
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

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

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jun 1, 2026 7:27pm

Request Review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 28, 2026

Open in StackBlitz

@clerk/astro

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

@clerk/backend

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

@clerk/chrome-extension

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

@clerk/clerk-js

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

@clerk/expo

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

@clerk/expo-passkeys

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

@clerk/express

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

@clerk/fastify

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

@clerk/hono

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

@clerk/localizations

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

@clerk/nextjs

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

@clerk/nuxt

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

@clerk/react

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

@clerk/react-router

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

@clerk/shared

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

@clerk/tanstack-react-start

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

@clerk/testing

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

@clerk/ui

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

@clerk/upgrade

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

@clerk/vue

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

commit: 91dbd71

Comment thread packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt Outdated
Comment thread packages/expo/android/src/main/java/expo/modules/clerk/ClerkUserButtonExpoView.kt Outdated
chriscanin

This comment was marked as outdated.

Comment thread packages/expo/ios/ClerkExpoModule.swift Outdated
Comment thread packages/expo/ios/ClerkExpoModule.swift Outdated
Comment thread packages/expo/ios/ClerkExpoModule.swift Outdated
Comment thread packages/expo/ios/ClerkExpoModule.swift Outdated
@clerk clerk deleted a comment from chriscanin Jun 1, 2026
@clerk clerk deleted a comment from chriscanin Jun 1, 2026
@clerk clerk deleted a comment from chriscanin Jun 1, 2026
@swolfand swolfand closed this Jun 1, 2026
@swolfand swolfand reopened this Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants