Skip to content

feat(ui): let embedded components keep their own top app bar - #798

Merged
mikepitre merged 8 commits into
mainfrom
mike/hosted-navigation
Jul 31, 2026
Merged

feat(ui): let embedded components keep their own top app bar#798
mikepitre merged 8 commits into
mainfrom
mike/hosted-navigation

Conversation

@mikepitre

@mikepitre mikepitre commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Problem

Hosts that embed UserProfileView or AuthView inside their own navigation (the Expo SDK, or a native app with its own top app bar) get a double header, and their back affordances can't reach the components' internal back stacks. Context: Slack thread.

What this adds

An embedded component keeps its own top app bar. Screen titles, back buttons, and transitions stay ours and stay native, because our internal navigation is still driven by our own buttons. The host supplies only the back affordance for our root screen, where we have nothing of our own to go back to:

@OptIn(FrameworkIntegrationApi::class)
ClerkHostBackActionProvider(onHostBack = { navController.popBackStack() }) {
  UserProfileView()
}

ClerkTopAppBar is the single choke point every screen renders through, so this is one branch there: at the root, where hasBackButton is false, show the host's back button instead of nothing. It reuses the same BackButton composable as every other screen, so it picks up the same styling.

The public surface is one provider composable, gated behind @FrameworkIntegrationApi since it exists for Clerk's own framework integrations and may change without notice in minor releases. Components used without it behave exactly as before.

The inline add-account flow keeps its own chrome, as before; hosts that want to own it can still pass onAddAccount.

Modules

source/ui only.

Testing

:source:ui:testDebugUnitTest passes, spotlessApply clean, and detekt reports the same 6 findings as an untouched tree (all in files this PR doesn't modify), verified by running it against both.

Not yet exercised on an emulator — the iOS half is verified on device, and this mirrors it, but someone should watch the top app bar render the host back button before merge.

Companion PRs: clerk-ios#519, and the consuming side clerk/javascript#9121, which packages this branch into @clerk/expo locally for development and bumps its pinned version once this releases.

🤖 Generated with Claude Code

Note

Allow embedded UI components to show a host-provided back button in their top app bar

  • Introduces ClerkHostBackActionProvider and LocalClerkHostBackAction to propagate an optional host back action via composition local to embedded screens.
  • Adds usesHostBackAction parameter to ClerkTopAppBar, ClerkThemedAuthScaffold, and ClerkThemedProfileScaffold; when true and no internal back button is shown, the host back action is used instead.
  • A new resolveBackAction utility in BackActionResolver.kt centralizes precedence logic: internal action wins, then host action, then null.
  • During the add-account flow in UserProfileView, LocalClerkHostBackAction is explicitly set to null so the host back button does not appear mid-flow.
  • ClerkHostBackActionProvider is gated behind the new @FrameworkIntegrationApi opt-in annotation, restricting its use to Clerk's own framework integrations.

Macroscope summarized 48a87d0.

Summary by CodeRabbit

  • New Features

    • Added host-controlled back navigation for embedded Clerk components.
    • Top app bars now display and handle a back button when host navigation is available.
    • Added an opt-in annotation for framework-integration APIs.
  • Bug Fixes

    • Prevented the add-account flow from triggering the host’s back navigation unexpectedly.

@mikepitre
mikepitre requested a review from swolfand July 10, 2026 03:05
@mikepitre
mikepitre force-pushed the mike/hosted-navigation branch from 8f7c82d to e02de58 Compare July 30, 2026 02:46
@mikepitre mikepitre changed the title feat(ui): add hosted navigation for embedding in host-owned chrome feat(ui): add embedded navigation for embedding in host-owned chrome Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a framework API opt-in annotation and introduces host-controlled back navigation for Clerk UI components, including top-app-bar handling and suppression during the add-account authentication flow.

Changes

Framework API contract

Layer / File(s) Summary
Framework integration opt-in annotation
source/api/src/main/kotlin/com/clerk/api/FrameworkIntegrationApi.kt
Defines the error-level, binary-retained FrameworkIntegrationApi annotation for classes, constructors, and functions.

Host back navigation

Layer / File(s) Summary
Host back-action provider
source/ui/src/main/java/com/clerk/ui/navigation/ClerkHostBackAction.kt
Provides a host back callback through a public composable provider and an internal composition local.
Top-app-bar back handling
source/ui/src/main/java/com/clerk/ui/core/appbar/ClerkTopAppbar.kt
Derives back-button visibility and selects the component callback or host callback before passing them to both top-bar layouts.
Profile authentication back override
source/ui/src/main/java/com/clerk/ui/userprofile/UserProfileView.kt
Supplies a null host back action while rendering the add-account authentication flow.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Host
  participant ClerkHostBackActionProvider
  participant ClerkTopAppBar
  participant UserProfileView
  participant AuthView
  Host->>ClerkHostBackActionProvider: Provide onHostBack callback
  ClerkHostBackActionProvider->>ClerkTopAppBar: Expose host back action
  ClerkTopAppBar->>Host: Invoke selected back callback
  UserProfileView->>AuthView: Render add-account flow with null host action
Loading

Suggested reviewers: swolfand

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: allowing embedded components to retain their own top app bar.
Description check ✅ Passed The description explains the problem, solution, scope, testing, risks, and related changes, though it omits the template's screenshots section.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mike/hosted-navigation

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.

@mikepitre
mikepitre force-pushed the mike/hosted-navigation branch from e02de58 to ed287f7 Compare July 30, 2026 03:26
Comment thread source/ui/src/main/java/com/clerk/ui/navigation/ClerkEmbeddedNavigation.kt Outdated
@mikepitre
mikepitre marked this pull request as ready for review July 30, 2026 04:12
Comment thread source/ui/src/main/java/com/clerk/ui/navigation/ClerkEmbeddedNavigation.kt Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
source/ui/src/main/java/com/clerk/ui/navigation/ClerkEmbeddedNavigation.kt (2)

96-109: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Keep registered callbacks current across recomposition.

This effect does not restart when pop or popToRoot changes, so the handler can retain callbacks from an earlier composition. That can route embedded authentication actions through stale AuthState behavior. Use rememberUpdatedState for both callbacks before registering the handler.

Proposed fix
+  val currentPop by rememberUpdatedState(pop)
+  val currentPopToRoot by rememberUpdatedState(popToRoot)
+
  DisposableEffect(embeddedNavigation, backStack) {
    val handler: (toRoot: Boolean) -> Unit = { toRoot ->
      if (toRoot) {
-        popToRoot?.invoke()
+        currentPopToRoot?.invoke()
           ?: run {
             while (backStack.size > 1) {
               backStack.removeLastOrNull()
             }
           }
      } else if (backStack.size > 1) {
-        pop?.invoke() ?: backStack.removeLastOrNull()
+        currentPop?.invoke() ?: backStack.removeLastOrNull()
      }
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/ui/src/main/java/com/clerk/ui/navigation/ClerkEmbeddedNavigation.kt`
around lines 96 - 109, Update the DisposableEffect around the handler in
ClerkEmbeddedNavigation to wrap both pop and popToRoot with rememberUpdatedState
before registering embeddedNavigation.popHandler. Invoke the current state
values inside the handler while preserving the existing backStack fallback
behavior.

36-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use explicit visibility for the new public Kotlin API.

Both files rely on implicit public declarations. Add explicit visibility modifiers consistently:

  • source/ui/src/main/java/com/clerk/ui/navigation/ClerkEmbeddedNavigation.kt#L36-L64: mark ClerkEmbeddedNavigation and rememberClerkEmbeddedNavigation as public.
  • source/api/src/main/kotlin/com/clerk/api/FrameworkIntegrationApi.kt#L18-L18: mark FrameworkIntegrationApi as public.

As per coding guidelines, Kotlin files should prefer explicit visibility.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/ui/src/main/java/com/clerk/ui/navigation/ClerkEmbeddedNavigation.kt`
around lines 36 - 64, Make the new public API visibility explicit: in
source/ui/src/main/java/com/clerk/ui/navigation/ClerkEmbeddedNavigation.kt lines
36-64, mark ClerkEmbeddedNavigation and rememberClerkEmbeddedNavigation as
public; in source/api/src/main/kotlin/com/clerk/api/FrameworkIntegrationApi.kt
line 18, mark FrameworkIntegrationApi as public.

Source: Coding guidelines

source/ui/src/test/java/com/clerk/ui/navigation/ClerkEmbeddedNavigationTest.kt (1)

36-42: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the no-op behavior.

This test has no assertion, so it would still pass if pop() or popToRoot() changed navigation state. Seed a non-root depth and verify it remains unchanged after both calls.

Proposed test improvement
   fun `pop is a no-op without a registered handler`() {
     val embeddedNavigation = ClerkEmbeddedNavigation()
+    embeddedNavigation.depth = 1

     embeddedNavigation.pop()
     embeddedNavigation.popToRoot()

+    assertEquals(1, embeddedNavigation.depth)
+    assertTrue(embeddedNavigation.canGoBack)
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@source/ui/src/test/java/com/clerk/ui/navigation/ClerkEmbeddedNavigationTest.kt`
around lines 36 - 42, Update the test `pop is a no-op without a registered
handler` to initialize `ClerkEmbeddedNavigation` with a non-root navigation
depth, invoke both `pop()` and `popToRoot()`, and assert afterward that the
depth remains unchanged. Ensure the assertions directly verify no navigation
state change without a registered handler.
source/ui/src/main/java/com/clerk/ui/userprofile/UserProfileView.kt (1)

90-93: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider documenting the add-account chrome exception.

The KDoc doesn't mention that the add-account/auth flow (triggered via AddAccount when onAddAccount isn't supplied) keeps Clerk's own chrome even when embeddedNavigation is provided. Callers relying purely on this doc might be surprised the top app bar reappears during that sub-flow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/ui/src/main/java/com/clerk/ui/userprofile/UserProfileView.kt` around
lines 90 - 93, The KDoc for embeddedNavigation should document the
add-account/auth exception: when AddAccount triggers the flow without an
onAddAccount callback, Clerk’s own chrome remains visible even if
embeddedNavigation is provided. Add this clarification to the existing
embeddedNavigation parameter documentation without changing the navigation
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@source/ui/src/main/java/com/clerk/ui/navigation/ClerkEmbeddedNavigation.kt`:
- Around line 96-109: Update the DisposableEffect around the handler in
ClerkEmbeddedNavigation to wrap both pop and popToRoot with rememberUpdatedState
before registering embeddedNavigation.popHandler. Invoke the current state
values inside the handler while preserving the existing backStack fallback
behavior.
- Around line 36-64: Make the new public API visibility explicit: in
source/ui/src/main/java/com/clerk/ui/navigation/ClerkEmbeddedNavigation.kt lines
36-64, mark ClerkEmbeddedNavigation and rememberClerkEmbeddedNavigation as
public; in source/api/src/main/kotlin/com/clerk/api/FrameworkIntegrationApi.kt
line 18, mark FrameworkIntegrationApi as public.

In `@source/ui/src/main/java/com/clerk/ui/userprofile/UserProfileView.kt`:
- Around line 90-93: The KDoc for embeddedNavigation should document the
add-account/auth exception: when AddAccount triggers the flow without an
onAddAccount callback, Clerk’s own chrome remains visible even if
embeddedNavigation is provided. Add this clarification to the existing
embeddedNavigation parameter documentation without changing the navigation
behavior.

In
`@source/ui/src/test/java/com/clerk/ui/navigation/ClerkEmbeddedNavigationTest.kt`:
- Around line 36-42: Update the test `pop is a no-op without a registered
handler` to initialize `ClerkEmbeddedNavigation` with a non-root navigation
depth, invoke both `pop()` and `popToRoot()`, and assert afterward that the
depth remains unchanged. Ensure the assertions directly verify no navigation
state change without a registered handler.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b46fd72e-ca16-459f-a663-188d23988740

📥 Commits

Reviewing files that changed from the base of the PR and between a4f4847 and bf9db79.

📒 Files selected for processing (6)
  • source/api/src/main/kotlin/com/clerk/api/FrameworkIntegrationApi.kt
  • source/ui/src/main/java/com/clerk/ui/auth/AuthView.kt
  • source/ui/src/main/java/com/clerk/ui/core/appbar/ClerkTopAppbar.kt
  • source/ui/src/main/java/com/clerk/ui/navigation/ClerkEmbeddedNavigation.kt
  • source/ui/src/main/java/com/clerk/ui/userprofile/UserProfileView.kt
  • source/ui/src/test/java/com/clerk/ui/navigation/ClerkEmbeddedNavigationTest.kt

Hosts that embed UserProfileView or AuthView inside their own navigation
chrome (e.g. the Expo SDK, or apps with their own top app bar) can now pass
the new optional embeddedNavigation parameter. When provided, Clerk's top app
bars are hidden and the host observes stack depth and drives pop()/popToRoot()
through the ClerkEmbeddedNavigation handle. Behavior is unchanged when the
parameter is omitted.

Named embedded rather than hosted because hosted already refers to Clerk's
hosted web pages in the mobile SDKs.

Affects the ui module only.

Obtaining a ClerkEmbeddedNavigation requires opting in to the new
FrameworkIntegrationApi marker (com.clerk.api), the Android counterpart of
the iOS SDK's @_spi(FrameworkIntegration) surface. Components accept the
parameter without opt-in; only handle creation is gated.
EmbeddedNavigationEffects now clears the pop handler only when it still owns
it, so a component leaving composition after a successor registered cannot
tear the successor down, and resets depth to 0 on teardown so the handle no
longer reports canGoBack while pop() would be a no-op.

Affects the ui module only.
Embedded pop commands mutated the back stack directly, bypassing
AuthState.navigateBack's suppression of the in-progress attempt resume, so
popping a factor screen bounced straight back to it. EmbeddedNavigationEffects
now accepts pop overrides and AuthView routes commands through AuthState.

Affects the ui module only.
@mikepitre
mikepitre force-pushed the mike/hosted-navigation branch from bf9db79 to 8766d5f Compare July 30, 2026 13:18
Comment thread source/ui/src/main/java/com/clerk/ui/auth/AuthView.kt Outdated
Hosts embedding components in their own navigation had to hide Clerk's top
app bars and drive back navigation themselves through a coordinator handle,
leaving them to rebuild header chrome that never quite matched the platform.

Components now keep their own top app bar when embedded, so screen titles,
back buttons, and transitions stay native. The host supplies only the root
back affordance via ClerkHostBackActionProvider, which the top app bar shows
in place of nothing on the component's root screen. The coordinator, its
depth reporting, and its pop commands are no longer needed.
@mikepitre
mikepitre marked this pull request as draft July 30, 2026 18:05
@mikepitre mikepitre changed the title feat(ui): add embedded navigation for embedding in host-owned chrome feat(ui): let embedded components keep their own top app bar Jul 30, 2026
The flow replaces the profile entirely, so inheriting the host's back action
put a back button on its root screen that popped the host's navigation
instead of closing the flow.
@mikepitre
mikepitre marked this pull request as ready for review July 30, 2026 22:52
Comment thread source/api/src/main/kotlin/com/clerk/api/FrameworkIntegrationApi.kt Outdated
@mikepitre
mikepitre merged commit 1b88ab8 into main Jul 31, 2026
11 checks passed
@mikepitre
mikepitre deleted the mike/hosted-navigation branch July 31, 2026 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants