feat(ui): let embedded components keep their own top app bar - #798
Conversation
8f7c82d to
e02de58
Compare
📝 WalkthroughWalkthroughAdds 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. ChangesFramework API contract
Host back navigation
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
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
e02de58 to
ed287f7
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (4)
source/ui/src/main/java/com/clerk/ui/navigation/ClerkEmbeddedNavigation.kt (2)
96-109: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winKeep registered callbacks current across recomposition.
This effect does not restart when
poporpopToRootchanges, so the handler can retain callbacks from an earlier composition. That can route embedded authentication actions through staleAuthStatebehavior. UserememberUpdatedStatefor 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 winUse explicit visibility for the new public Kotlin API.
Both files rely on implicit
publicdeclarations. Add explicit visibility modifiers consistently:
source/ui/src/main/java/com/clerk/ui/navigation/ClerkEmbeddedNavigation.kt#L36-L64: markClerkEmbeddedNavigationandrememberClerkEmbeddedNavigationaspublic.source/api/src/main/kotlin/com/clerk/api/FrameworkIntegrationApi.kt#L18-L18: markFrameworkIntegrationApiaspublic.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 winAssert the no-op behavior.
This test has no assertion, so it would still pass if
pop()orpopToRoot()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 winConsider documenting the add-account chrome exception.
The KDoc doesn't mention that the add-account/auth flow (triggered via
AddAccountwhenonAddAccountisn't supplied) keeps Clerk's own chrome even whenembeddedNavigationis 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
📒 Files selected for processing (6)
source/api/src/main/kotlin/com/clerk/api/FrameworkIntegrationApi.ktsource/ui/src/main/java/com/clerk/ui/auth/AuthView.ktsource/ui/src/main/java/com/clerk/ui/core/appbar/ClerkTopAppbar.ktsource/ui/src/main/java/com/clerk/ui/navigation/ClerkEmbeddedNavigation.ktsource/ui/src/main/java/com/clerk/ui/userprofile/UserProfileView.ktsource/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.
bf9db79 to
8766d5f
Compare
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.
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.
Problem
Hosts that embed
UserProfileVieworAuthViewinside 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:
ClerkTopAppBaris the single choke point every screen renders through, so this is one branch there: at the root, wherehasBackButtonis false, show the host's back button instead of nothing. It reuses the sameBackButtoncomposable as every other screen, so it picks up the same styling.The public surface is one provider composable, gated behind
@FrameworkIntegrationApisince 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/uionly.Testing
:source:ui:testDebugUnitTestpasses,spotlessApplyclean, anddetektreports 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/expolocally 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
ClerkHostBackActionProviderandLocalClerkHostBackActionto propagate an optional host back action via composition local to embedded screens.usesHostBackActionparameter toClerkTopAppBar,ClerkThemedAuthScaffold, andClerkThemedProfileScaffold; when true and no internal back button is shown, the host back action is used instead.resolveBackActionutility in BackActionResolver.kt centralizes precedence logic: internal action wins, then host action, then null.UserProfileView,LocalClerkHostBackActionis explicitly set to null so the host back button does not appear mid-flow.ClerkHostBackActionProvideris gated behind the new@FrameworkIntegrationApiopt-in annotation, restricting its use to Clerk's own framework integrations.Macroscope summarized 48a87d0.
Summary by CodeRabbit
New Features
Bug Fixes