Skip to content

fix(expo): inline AuthView OAuth + Android sign-out state cleanup#8260

Open
chriscanin wants to merge 6 commits intomainfrom
chris/fix-inline-authview-sso
Open

fix(expo): inline AuthView OAuth + Android sign-out state cleanup#8260
chriscanin wants to merge 6 commits intomainfrom
chris/fix-inline-authview-sso

Conversation

@chriscanin
Copy link
Copy Markdown
Member

@chriscanin chriscanin commented Apr 7, 2026

Description

Two related fixes for the inline <AuthView> and <UserProfileView> components in @clerk/expo.

iOS, OAuth from forgot password screen

The inline AuthView was embedded as a child UIHostingController inside React Native's view hierarchy. This disrupted ASWebAuthenticationSession callbacks during OAuth flows started from the forgot password screen, causing the SSO sign-in to silently fail.

ClerkAuthNativeView now presents its UIHostingController via UIViewController.present() instead of embedding it as a child view. The visual appearance is unchanged (fullscreen, no animation) but the OAuth callback chain now completes correctly.

Also includes:

  • presentWhenReady() uses UIViewController.transitionCoordinator to wait for any in-flight modal dismissal (for example, UserProfileView sign out) instead of a fixed delay, fixing a visible white flash on initial mount.
  • viewDidDisappear checks the new session before returning a cancelled result so successful auth still propagates.

Android, sign-out state cleanup

Three related fixes to prevent the AuthView from getting stuck on the "Get help / Email support" screen after sign out:

  1. Detect new sign-ins by session ID change instead of "null to value". ClerkAuthExpoView.initialSessionId is captured at construction, but the view can be instantiated before signOut has finished clearing local state, causing it to capture a stale session ID. Switching to ID inequality lets subsequent sign-ins fire the auth completed event correctly.

  2. Per view ViewModelStore for ClerkAuthExpoView. The clerk-android AuthView's navigation ViewModel was scoped to the MainActivity, so its navigation state (for example, "Get help" destination) persisted across mount and unmount cycles within the same activity. Each ClerkAuthExpoView instance now provides its own ViewModelStoreOwner so the AuthView gets a fresh ViewModel scope per mount.

  3. Refresh client from server after sign out. Clerk.auth.signOut() only clears the active session, not the in-progress Clerk.client.signIn. After sign out (whether via the JS bridge signOut(), the inline UserProfileView, or the modal UserProfileActivity), call Client.getSkippingClientId() to fetch a brand new client. The skipping variant is required because Client.get() echoes back the same client_id header, returning the same client with the stale signIn still attached.

Files changed

  • packages/expo/ios/ClerkExpoModule.swift, ClerkAuthNativeView modal presentation and presentWhenReady
  • packages/expo/ios/ClerkViewFactory.swift, viewDidDisappear session detection
  • packages/expo/android/.../ClerkAuthExpoView.kt, session ID change detection and per view ViewModelStore
  • packages/expo/android/.../ClerkExpoModule.kt, Client.getSkippingClientId() after signOut()
  • packages/expo/android/.../ClerkUserProfileActivity.kt, Client.getSkippingClientId() after UserProfile sign out
  • packages/expo/android/.../ClerkUserProfileExpoView.kt, Client.getSkippingClientId() after inline UserProfile sign out

How to test

iOS:

  1. Build and run the NativeComponentQuickstart app on the iOS simulator.
  2. Sign in with Google from the main sign in screen, expect a successful sign in.
  3. Sign out, then sign in with Google from the forgot password screen, expect a successful sign in (this was the original bug).
  4. Sign out via the authenticated screen button, expect a clean return to the sign in screen and the ability to sign in again.
  5. Sign in, open UserProfileView, sign out from there, expect a clean return to the sign in screen.
  6. Verify modal presentAuth() and presentUserProfile() flows still work.

Android:

  1. Build and run the NativeComponentQuickstart app on an Android emulator.
  2. Sign in with Google from the main sign in screen (uses Credential Manager).
  3. Sign in with Google from the forgot password screen (uses OAuth redirect via Custom Tabs).
  4. Sign out via the authenticated screen button, then sign back in.
  5. Sign in, open UserProfileView, sign out from there, then sign back in. Verify the user is not stuck on the "Get help" screen.
  6. Repeat sign in and sign out cycles to confirm no state leakage.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

…password

The inline AuthView embedded as a child UIHostingController in React Native's
view hierarchy disrupts ASWebAuthenticationSession callbacks during OAuth flows.
SSO from the forgot-password screen would silently fail because the OAuth
callback couldn't properly update Clerk.shared.client in the embedded context.

This changes ClerkAuthNativeView to present the AuthView as a full-screen modal
(matching the working presentAuth() behavior) instead of embedding it inline.
Also adds retry logic for modal presentation after sign-out to handle cases
where a previous modal (e.g., UserProfileView) is still dismissing.
@chriscanin chriscanin self-assigned this Apr 7, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 7, 2026

🦋 Changeset detected

Latest commit: 8a0597e

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 Patch

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

@chriscanin chriscanin added the expo label Apr 7, 2026
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 7, 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 Apr 8, 2026 1:07am

Request Review

Replace the unconditional 0.3s asyncAfter delay with presentWhenReady(),
which presents the auth modal synchronously when possible. The fixed delay
caused a visible white flash on initial mount because ClerkAuthNativeView
is an empty UIView while waiting to present.

When a previous modal (e.g., UserProfileView sign-out) is still dismissing,
use UIViewController.transitionCoordinator to wait for the animation to
complete instead of polling. Falls back to a one-frame DispatchQueue.main.async
retry only when no coordinator is available yet.
@chriscanin chriscanin marked this pull request as ready for review April 7, 2026 19:54
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro

Run ID: 70c30312-e42f-42f6-88ff-7c8821d48b46

📥 Commits

Reviewing files that changed from the base of the PR and between 077b011 and 8a0597e.

📒 Files selected for processing (1)
  • .changeset/fix-inline-authview-sso-oauth.md

📝 Walkthrough

Walkthrough

iOS: inline auth view now presents the auth UI controller modally instead of embedding a UIHostingController; creation uses a completion-based factory, presentation/dismissal wait for safe presentation, event emission consolidated with de-duplication and cancelled-result suppression, and removeFromSuperview() dismisses the modal. ClerkAuthWrapperViewController reports signIn when the session id changed on disappearance, otherwise reports cancelled. Android: each auth view uses its own ViewModelStoreOwner cleared on detach; auth completion gating prevents duplicate emits. Shared: sign-out flows attempt Client.getSkippingClientId() after sign-out (errors caught and logged). A patch changeset was added.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main changes: fixing OAuth issues in inline AuthView on iOS and sign-out state cleanup on Android.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the iOS OAuth/modal presentation fix and Android sign-out state cleanup across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 7, 2026

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@8260

@clerk/astro

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

@clerk/backend

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

@clerk/chrome-extension

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

@clerk/clerk-js

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

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@8260

@clerk/expo

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

@clerk/expo-passkeys

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

@clerk/express

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

@clerk/fastify

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

@clerk/hono

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

@clerk/localizations

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

@clerk/nextjs

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

@clerk/nuxt

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

@clerk/react

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

@clerk/react-router

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

@clerk/shared

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

@clerk/tanstack-react-start

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

@clerk/testing

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

@clerk/ui

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

@clerk/upgrade

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

@clerk/vue

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

commit: 8a0597e

@chriscanin
Copy link
Copy Markdown
Member Author

!snapshot

@github-actions

This comment has been minimized.

Three related Android fixes for the inline AuthView:

1. Detect new sign-ins by session ID change (not null-to-value).
   ClerkAuthExpoView's initialSessionId is captured at construction, but the
   view can be instantiated before signOut has finished clearing local state,
   causing it to capture a stale session ID. Switching to ID inequality lets
   subsequent sign-ins fire the auth-completed event correctly.

2. Per-view ViewModelStore for ClerkAuthExpoView.
   The clerk-android AuthView's navigation ViewModel was scoped to the
   MainActivity, so its navigation state (e.g. "Get help" destination)
   persisted across mount/unmount cycles within the same activity. Each
   ClerkAuthExpoView instance now provides its own ViewModelStoreOwner so
   the AuthView gets a fresh ViewModel scope per mount.

3. Refresh client from server after sign-out.
   Clerk.auth.signOut() only clears the active session, not the in-progress
   Clerk.client.signIn. After sign-out (whether via the JS bridge signOut(),
   the inline UserProfile view, or the modal UserProfile activity), call
   Client.getSkippingClientId() to fetch a brand-new client. The skipping
   variant is required because Client.get() echoes back the same client_id
   header, returning the same client with the stale signIn still attached.
@chriscanin chriscanin changed the title fix(expo): present inline AuthView as modal to fix OAuth from forgot-… fix(expo): inline AuthView OAuth + Android sign-out state cleanup Apr 8, 2026
@chriscanin
Copy link
Copy Markdown
Member Author

!snapshot

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

Hey @chriscanin - the snapshot version command generated the following package versions:

Package Version
@clerk/agent-toolkit 0.3.12-snapshot.v20260408154640
@clerk/astro 3.0.12-snapshot.v20260408154640
@clerk/backend 3.2.8-snapshot.v20260408154640
@clerk/chrome-extension 3.1.9-snapshot.v20260408154640
@clerk/clerk-js 6.5.1-snapshot.v20260408154640
@clerk/dev-cli 0.1.1-snapshot.v20260408154640
@clerk/expo 3.1.9-snapshot.v20260408154640
@clerk/expo-passkeys 1.0.10-snapshot.v20260408154640
@clerk/express 2.1.0-snapshot.v20260408154640
@clerk/fastify 3.1.10-snapshot.v20260408154640
@clerk/hono 0.1.10-snapshot.v20260408154640
@clerk/localizations 4.3.3-snapshot.v20260408154640
@clerk/msw 0.0.10-snapshot.v20260408154640
@clerk/nextjs 7.0.12-snapshot.v20260408154640
@clerk/nuxt 2.1.2-snapshot.v20260408154640
@clerk/react 6.2.1-snapshot.v20260408154640
@clerk/react-router 3.0.12-snapshot.v20260408154640
@clerk/shared 4.5.1-snapshot.v20260408154640
@clerk/tanstack-react-start 1.0.12-snapshot.v20260408154640
@clerk/testing 2.0.12-snapshot.v20260408154640
@clerk/ui 1.4.1-snapshot.v20260408154640
@clerk/upgrade 2.0.3-snapshot.v20260408154640
@clerk/vue 2.0.11-snapshot.v20260408154640

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/agent-toolkit

npm i @clerk/agent-toolkit@0.3.12-snapshot.v20260408154640 --save-exact

@clerk/astro

npm i @clerk/astro@3.0.12-snapshot.v20260408154640 --save-exact

@clerk/backend

npm i @clerk/backend@3.2.8-snapshot.v20260408154640 --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@3.1.9-snapshot.v20260408154640 --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@6.5.1-snapshot.v20260408154640 --save-exact

@clerk/dev-cli

npm i @clerk/dev-cli@0.1.1-snapshot.v20260408154640 --save-exact

@clerk/expo

npm i @clerk/expo@3.1.9-snapshot.v20260408154640 --save-exact

@clerk/expo-passkeys

npm i @clerk/expo-passkeys@1.0.10-snapshot.v20260408154640 --save-exact

@clerk/express

npm i @clerk/express@2.1.0-snapshot.v20260408154640 --save-exact

@clerk/fastify

npm i @clerk/fastify@3.1.10-snapshot.v20260408154640 --save-exact

@clerk/hono

npm i @clerk/hono@0.1.10-snapshot.v20260408154640 --save-exact

@clerk/localizations

npm i @clerk/localizations@4.3.3-snapshot.v20260408154640 --save-exact

@clerk/msw

npm i @clerk/msw@0.0.10-snapshot.v20260408154640 --save-exact

@clerk/nextjs

npm i @clerk/nextjs@7.0.12-snapshot.v20260408154640 --save-exact

@clerk/nuxt

npm i @clerk/nuxt@2.1.2-snapshot.v20260408154640 --save-exact

@clerk/react

npm i @clerk/react@6.2.1-snapshot.v20260408154640 --save-exact

@clerk/react-router

npm i @clerk/react-router@3.0.12-snapshot.v20260408154640 --save-exact

@clerk/shared

npm i @clerk/shared@4.5.1-snapshot.v20260408154640 --save-exact

@clerk/tanstack-react-start

npm i @clerk/tanstack-react-start@1.0.12-snapshot.v20260408154640 --save-exact

@clerk/testing

npm i @clerk/testing@2.0.12-snapshot.v20260408154640 --save-exact

@clerk/ui

npm i @clerk/ui@1.4.1-snapshot.v20260408154640 --save-exact

@clerk/upgrade

npm i @clerk/upgrade@2.0.3-snapshot.v20260408154640 --save-exact

@clerk/vue

npm i @clerk/vue@2.0.11-snapshot.v20260408154640 --save-exact

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