Skip to content

Sean/support custom user profile rows#382

Open
seanperez29 wants to merge 25 commits intomainfrom
sean/Support-custom-user-profile-rows
Open

Sean/support custom user profile rows#382
seanperez29 wants to merge 25 commits intomainfrom
sean/Support-custom-user-profile-rows

Conversation

@seanperez29
Copy link
Collaborator

@seanperez29 seanperez29 commented Mar 20, 2026

Summary by CodeRabbit

  • New Features

    • Customizable profile rows: icon types, bundle support, placement rules, and multiple occurrences.
    • Register custom destinations for profile rows and embed profile views into host navigation.
    • New user profile header with avatar, name/username display, and Edit Profile action.
  • Refactor

    • Navigation reworked toward sheet-focused sheet navigation with clearer dismiss/exit behavior.
    • Profile-related views and account flows updated to use the new sheet-based navigation model.

Allow embedding UserProfileView in a parent NavigationStack by passing
an optional navigationPath binding. When provided, destinations push
onto the parent's path instead of an internal stack.

- Add externalPath and externalPathBaseCount to UserProfileNavigation
- Add navigate(to:) and popToRoot() methods for unified navigation
- Extract profileContent to conditionally wrap in NavigationStack
- Track isEmbedded in telemetry payload

# Conflicts:
#	Sources/ClerkKitUI/Components/UserProfile/UserProfileView.swift
Move external navigation path configuration from init to taskOnce
to ensure the binding is properly captured when the view appears.
This fixes an issue where the external path could be stale when
passed through initializer state wrappers.
When deleting the last session in embedded mode, we need to also
pop the UserProfileView entry itself from the parent's navigation
stack, not just the views pushed by the profile flow.
Replace imperative navigation.navigate() calls with a new
UserProfileRouter struct that manages push/pop operations via
closures. This provides a cleaner API for embedded and standalone
navigation scenarios.

- Extract routing logic from UserProfileNavigation into UserProfileRouter
- Track embedded push count via @State to properly pop to root
- Inject router through environment for consistent access
Move navigation path from UserProfileNavigation to local state
within UserProfileView, simplifying the navigation class to only
manage presentation state. Replace custom EnvironmentKey with
@entry macro for cleaner environment value declaration.
Use initialPathCount captured at init to calculate entries to
remove during popToRoot, eliminating the need for onChange tracking
and embeddedPushCount state management.
Clarifies the caller's intent when popping to root after account
deletion. The decision about whether to include the current view
in the pop is now made explicitly at the call site based on
whether the user still exists and if the account switcher will
be presented.
Allow developers to inject custom rows into the root profile screen
with flexible placement options. Custom rows can be positioned at
section start/end or before/after specific built-in rows.
Replace AnyHashable-based custom row and destination routing with typed
Route generics, add UserProfileNavigator for environment-driven
navigation, and keep a built-in router for Clerk-owned child views.
Support custom profile rows and destination routing from
`UserButton` into `UserProfileView`.

Update profile row icon rendering so asset and system icons
share consistent sizing and theming.
The public navigator should only allow pushing custom routes.
Built-in row navigation is now handled internally through
the private navigate(to:) helper method.
The UserProfileNavigator instance was being created on every body
evaluation via the computed property. Inline the calls to navigate()
directly and construct the navigator only when needed for child views.
Use view modifier API for custom items and destinations
This ensures the initial path count is captured when the view actually
appears rather than during init, providing more reliable navigation
state tracking.
@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

Walkthrough

User profile UI refactor: UserButton and UserProfile are now generic (Route/Destination), added custom-row APIs and icon typing, replaced path-based UserProfileNavigation with sheet-focused UserProfileSheetNavigation plus UserProfileNavigator/UserProfileBuiltInRouter, and moved multiple views to the new sheet navigation model.

Changes

Cohort / File(s) Summary
UserButton Core Refactoring
Sources/ClerkKitUI/Components/UserButton/UserButton.swift
Made UserButton generic over Route and Destination; added stored presentationContext, customRows, optional customDestination; added userProfileRows(_:) and userProfileDestination(...) modifiers; legacy initializers constrained to Route == Never, Destination == EmptyView.
UserButton Account Switcher
Sources/ClerkKitUI/Components/UserButton/UserButtonAccountSwitcher.swift
Switched environment dependency to UserProfileSheetNavigation and updated preview to inject it; updated navigation state writes to sheet-based navigation.
Row View & Icon Model
Sources/ClerkKitUI/Components/UserButton/UserProfileRowView.swift, Sources/ClerkKitUI/Components/UserProfile/UserProfileCustomRow.swift
Added UserProfileCustomRow<Route> and related enums (UserProfileRowIcon, placement/section/row types). UserProfileRowView.icon now uses UserProfileRowIcon and optional bundle; rendering logic split for asset vs system icons.
UserProfile Core Refactoring
Sources/ClerkKitUI/Components/UserProfile/UserProfileView.swift
Made UserProfileView<Route, Destination> generic; added optional navigationPath embedding, custom-row insertion/placement, custom destination builder support, and new public modifiers to register rows/destinations; telemetry now includes isEmbedded.
Navigation Architecture
Sources/ClerkKitUI/Components/UserProfile/UserProfileNavigation.swift
Removed previous UserProfileNavigation path model; added UserProfileSheetNavigation (sheet state), public UserProfileNavigator<Route> (push/popToRoot), and internal UserProfileBuiltInRouter plus supporting enums for built-in destinations/dismiss actions.
Sheet Navigation Switches (multiple views)
Sources/ClerkKitUI/Components/UserProfile/...
Several user-profile views (MFA, security, add-MFA, backup codes, delete-account, etc.) now read/write UserProfileSheetNavigation instead of the old navigation model; previews updated to inject UserProfileSheetNavigation.
Account Deletion / Dismissal Flow
Sources/ClerkKitUI/Components/UserProfile/UserProfileDeleteAccountConfirmationView.swift
Post-delete flow now computes whether to present account switcher or dismiss; uses UserProfileBuiltInRouter to dismiss with .exitUserProfile or .popToRoot and conditionally presents account switcher based on session/user state.
Header & Preview Helpers
Sources/ClerkKitUI/Components/UserProfile/UserProfileHeaderView.swift, Sources/ClerkKitUI/Extensions/View+PreviewMocks.swift
Added UserProfileHeaderView component. Preview mocks updated to inject UserProfileSheetNavigation() for preview environment.

Sequence Diagram

sequenceDiagram
    participant User
    participant UserButton
    participant Sheet as ProfileSheet
    participant UserProfileView
    participant Navigator as UserProfileNavigator
    participant BuiltInRouter as UserProfileBuiltInRouter
    participant NavStack as ParentNavigationStack

    User->>UserButton: tap open profile
    UserButton->>Sheet: present sheet (passes customRows/customDestination)
    Sheet->>UserProfileView: show profile (embedded? navigationPath)
    User->>UserProfileView: tap custom row
    UserProfileView->>Navigator: push(route)
    Navigator->>BuiltInRouter: translate route/dismiss intent
    BuiltInRouter->>NavStack: append/remove entries (if embedded)
    NavStack->>UserProfileView: navigationDestination renders Destination
    User->>UserProfileView: trigger sign out / delete
    UserProfileView->>BuiltInRouter: dismiss(.popToRoot/.exitUserProfile)
    BuiltInRouter->>NavStack: adjust path or signal sheet dismissal
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 I hopped through rows and routed trails,

New sheets unfurl where stacks prevailed.
Icons changed and custom rows bloom,
Push, pop, and render — a tidy room.
Cheers from my burrow, code well-trimmed ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Sean/support custom user profile rows' refers to a main feature in the changeset—enabling custom rows in the user profile component. However, it includes the developer's name/branch prefix, which is not part of the primary change description and reduces clarity.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sean/Support-custom-user-profile-rows

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
Sources/ClerkKitUI/Components/UserProfile/BackupCodesView.swift (1)

130-135: ⚠️ Potential issue | 🟡 Minor

Preview missing UserProfileSheetNavigation environment.

This preview doesn't use clerkPreview() and doesn't inject UserProfileSheetNavigation, which will cause a runtime crash when the "Done" button is tapped (lines 65-70 access navigation.presentedAddMfaType).

🔧 Suggested fix
 `#Preview` {
   BackupCodesView(
     backupCodes: ["abc", "def", "ghi", "jkl", "lmn", "opq", "rst", "uvw", "xyz"],
     mfaType: .authenticatorApp
   )
+  .environment(UserProfileSheetNavigation())
 }

Or use .clerkPreview() if the full preview mock setup is desired.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Components/UserProfile/BackupCodesView.swift` around lines
130 - 135, The preview for BackupCodesView lacks the UserProfileSheetNavigation
environment, causing a crash when the "Done" button (code referencing
navigation.presentedAddMfaType in BackupCodesView) is tapped; fix by injecting a
UserProfileSheetNavigation instance into the Preview—either wrap the Preview
BackupCodesView with .environmentObject(UserProfileSheetNavigation()) or replace
the preview call with .clerkPreview() to provide the full mock environment so
BackupCodesView has the required navigation object.
Sources/ClerkKitUI/Components/UserButton/UserButtonAccountSwitcher.swift (1)

171-175: ⚠️ Potential issue | 🟡 Minor

Preview missing UserProfileSheetNavigation environment.

The view reads @Environment(UserProfileSheetNavigation.self) but the preview does not inject it. This will cause a runtime crash when running the preview.

Proposed fix
 `#Preview` {
   UserButtonAccountSwitcher()
     .clerkPreview()
+    .environment(UserProfileSheetNavigation())
     .environment(\.clerkTheme, .clerk)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Components/UserButton/UserButtonAccountSwitcher.swift`
around lines 171 - 175, The preview for UserButtonAccountSwitcher is missing the
required UserProfileSheetNavigation environment and will crash; update the
`#Preview` block that creates UserButtonAccountSwitcher() to inject a test/mocked
UserProfileSheetNavigation via .environment(UserProfileSheetNavigation.self,
<mockInstance>) so the view's `@Environment`(UserProfileSheetNavigation.self)
resolves; if UserProfileSheetNavigation is a protocol or type you don’t have an
instance for, add a lightweight test/mock implementation (or a no-op wrapper)
and pass that instance into the .environment call.
Sources/ClerkKitUI/Components/UserProfile/UserProfileSecurityView.swift (1)

88-94: ⚠️ Potential issue | 🟡 Minor

Preview missing UserProfileSheetNavigation environment.

The view reads @Environment(UserProfileSheetNavigation.self) but the preview does not inject it, which will crash at runtime.

Proposed fix
 `#Preview` {
   NavigationStack {
     UserProfileSecurityView()
   }
   .clerkPreview()
+  .environment(UserProfileSheetNavigation())
   .environment(\.clerkTheme, .clerk)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Components/UserProfile/UserProfileSecurityView.swift`
around lines 88 - 94, The preview for UserProfileSecurityView lacks the
UserProfileSheetNavigation environment value that the view reads via
`@Environment`(UserProfileSheetNavigation.self), which will crash at runtime;
update the `#Preview` block that constructs UserProfileSecurityView to inject a
suitable preview/mock UserProfileSheetNavigation instance into the environment
(e.g., provide a simple/default or mock initializer for
UserProfileSheetNavigation) using .environment(UserProfileSheetNavigation.self,
/* mock/default instance */) so the preview supplies the dependency when
rendering.
Sources/ClerkKitUI/Components/UserProfile/UserProfileDeleteAccountConfirmationView.swift (1)

113-117: ⚠️ Potential issue | 🟡 Minor

Preview missing required environment objects.

The view reads @Environment(UserProfileSheetNavigation.self) and @Environment(UserProfileBuiltInRouter.self) but the preview does not inject either, which will crash at runtime.

Proposed fix
 `#Preview` {
   UserProfileDeleteAccountConfirmationView()
     .environment(\.clerkTheme, .clerk)
     .environment(\.locale, .init(identifier: "es"))
+    .clerkPreview()
+    .environment(UserProfileSheetNavigation())
+    .environment(
+      UserProfileBuiltInRouter(
+        push: { _ in },
+        dismissAction: { _ in }
+      )
+    )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Sources/ClerkKitUI/Components/UserProfile/UserProfileDeleteAccountConfirmationView.swift`
around lines 113 - 117, The preview is missing the environment objects required
by UserProfileDeleteAccountConfirmationView and will crash; update the `#Preview`
block to inject mock or default instances for the two environment values read by
the view (UserProfileSheetNavigation and UserProfileBuiltInRouter) by adding
environment(...) calls that provide simple test implementations or stubs (e.g. a
NoOp or preview-only initializer) for UserProfileSheetNavigation and
UserProfileBuiltInRouter so the view can render in Xcode previews.
🧹 Nitpick comments (3)
Sources/ClerkKitUI/Components/UserProfile/UserProfileCustomRow.swift (1)

12-45: Consider adding Sendable conformance to UserProfileCustomRow.

Since all stored properties except route are already Sendable-compatible, and Route is constrained to Hashable, you could add Sendable conformance when Route: Sendable for better thread-safety guarantees in concurrent contexts.

💡 Optional enhancement
-public struct UserProfileCustomRow<Route: Hashable> {
+public struct UserProfileCustomRow<Route: Hashable>: Sendable where Route: Sendable {

Alternatively, use a conditional conformance extension:

extension UserProfileCustomRow: Sendable where Route: Sendable {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Components/UserProfile/UserProfileCustomRow.swift` around
lines 12 - 45, Add Sendable conformance for UserProfileCustomRow by making it
conditionally Sendable when its generic Route is Sendable; update the
declaration or add a conditional conformance extension for UserProfileCustomRow
(e.g., extension UserProfileCustomRow: Sendable where Route: Sendable) so the
struct is Sendable in concurrent contexts while preserving existing behavior and
generic constraints.
Sources/ClerkKitUI/Components/UserButton/UserProfileRowView.swift (1)

8-8: Remove unused import Foundation.

The Foundation import appears unnecessary as no Foundation-specific types are used in this file. SwiftUI already re-exports the Foundation types commonly needed.

🧹 Suggested fix
-import Foundation
 import SwiftUI
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Components/UserButton/UserProfileRowView.swift` at line 8,
Remove the unused import by deleting the lone "import Foundation" statement in
UserProfileRowView.swift (the file containing the UserProfileRowView view);
SwiftUI already provides the needed Foundation types so simply remove the import
Foundation line to eliminate the unused import warning.
Sources/ClerkKitUI/Components/UserProfile/UserProfileView.swift (1)

458-474: Consider logging when customDestination is nil for a custom route.

When view(for:) receives a .custom(route) but customDestination is nil, it silently returns EmptyView(). This could make debugging difficult if a developer forgets to call .userProfileDestination(...).

Proposed diagnostic
     case .custom(let route):
       if let customDestination {
         customDestination(route)
       } else {
+        `#if` DEBUG
+        let _ = print("[UserProfileView] No customDestination provided for route: \(route)")
+        `#endif`
         EmptyView()
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Components/UserProfile/UserProfileView.swift` around lines
458 - 474, The view(for:) switch silently returns EmptyView when handling a
.custom(route) and customDestination is nil; update view(for:) in
UserProfileView to log or surface a diagnostic when customDestination is nil for
a .custom route (e.g., use Logger/print/assertionFailure) so developers see that
UserProfileNavigationDestination<Route>.custom had no handler; ensure the
message includes the route value and a clear hint to call
.userProfileDestination(...), but keep the existing EmptyView fallback for
runtime safety.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@Sources/ClerkKitUI/Components/UserButton/UserButtonAccountSwitcher.swift`:
- Around line 171-175: The preview for UserButtonAccountSwitcher is missing the
required UserProfileSheetNavigation environment and will crash; update the
`#Preview` block that creates UserButtonAccountSwitcher() to inject a test/mocked
UserProfileSheetNavigation via .environment(UserProfileSheetNavigation.self,
<mockInstance>) so the view's `@Environment`(UserProfileSheetNavigation.self)
resolves; if UserProfileSheetNavigation is a protocol or type you don’t have an
instance for, add a lightweight test/mock implementation (or a no-op wrapper)
and pass that instance into the .environment call.

In `@Sources/ClerkKitUI/Components/UserProfile/BackupCodesView.swift`:
- Around line 130-135: The preview for BackupCodesView lacks the
UserProfileSheetNavigation environment, causing a crash when the "Done" button
(code referencing navigation.presentedAddMfaType in BackupCodesView) is tapped;
fix by injecting a UserProfileSheetNavigation instance into the Preview—either
wrap the Preview BackupCodesView with
.environmentObject(UserProfileSheetNavigation()) or replace the preview call
with .clerkPreview() to provide the full mock environment so BackupCodesView has
the required navigation object.

In
`@Sources/ClerkKitUI/Components/UserProfile/UserProfileDeleteAccountConfirmationView.swift`:
- Around line 113-117: The preview is missing the environment objects required
by UserProfileDeleteAccountConfirmationView and will crash; update the `#Preview`
block to inject mock or default instances for the two environment values read by
the view (UserProfileSheetNavigation and UserProfileBuiltInRouter) by adding
environment(...) calls that provide simple test implementations or stubs (e.g. a
NoOp or preview-only initializer) for UserProfileSheetNavigation and
UserProfileBuiltInRouter so the view can render in Xcode previews.

In `@Sources/ClerkKitUI/Components/UserProfile/UserProfileSecurityView.swift`:
- Around line 88-94: The preview for UserProfileSecurityView lacks the
UserProfileSheetNavigation environment value that the view reads via
`@Environment`(UserProfileSheetNavigation.self), which will crash at runtime;
update the `#Preview` block that constructs UserProfileSecurityView to inject a
suitable preview/mock UserProfileSheetNavigation instance into the environment
(e.g., provide a simple/default or mock initializer for
UserProfileSheetNavigation) using .environment(UserProfileSheetNavigation.self,
/* mock/default instance */) so the preview supplies the dependency when
rendering.

---

Nitpick comments:
In `@Sources/ClerkKitUI/Components/UserButton/UserProfileRowView.swift`:
- Line 8: Remove the unused import by deleting the lone "import Foundation"
statement in UserProfileRowView.swift (the file containing the
UserProfileRowView view); SwiftUI already provides the needed Foundation types
so simply remove the import Foundation line to eliminate the unused import
warning.

In `@Sources/ClerkKitUI/Components/UserProfile/UserProfileCustomRow.swift`:
- Around line 12-45: Add Sendable conformance for UserProfileCustomRow by making
it conditionally Sendable when its generic Route is Sendable; update the
declaration or add a conditional conformance extension for UserProfileCustomRow
(e.g., extension UserProfileCustomRow: Sendable where Route: Sendable) so the
struct is Sendable in concurrent contexts while preserving existing behavior and
generic constraints.

In `@Sources/ClerkKitUI/Components/UserProfile/UserProfileView.swift`:
- Around line 458-474: The view(for:) switch silently returns EmptyView when
handling a .custom(route) and customDestination is nil; update view(for:) in
UserProfileView to log or surface a diagnostic when customDestination is nil for
a .custom route (e.g., use Logger/print/assertionFailure) so developers see that
UserProfileNavigationDestination<Route>.custom had no handler; ensure the
message includes the route value and a clear hint to call
.userProfileDestination(...), but keep the existing EmptyView fallback for
runtime safety.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ab1d7e74-bca2-459b-8fc7-c65a3378a3aa

📥 Commits

Reviewing files that changed from the base of the PR and between 92ab31a and 2f28583.

📒 Files selected for processing (14)
  • Sources/ClerkKitUI/Components/UserButton/UserButton.swift
  • Sources/ClerkKitUI/Components/UserButton/UserButtonAccountSwitcher.swift
  • Sources/ClerkKitUI/Components/UserButton/UserProfileRowView.swift
  • Sources/ClerkKitUI/Components/UserProfile/BackupCodesView.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileAddMfaView.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileCustomRow.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileDeleteAccountConfirmationView.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileMfaAddSmsView.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileMfaAddTotpView.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileMfaSection.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileNavigation.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileSecurityView.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileView.swift
  • Sources/ClerkKitUI/Extensions/View+PreviewMocks.swift

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/ClerkKitUI/Components/UserProfile/UserProfileDeleteAccountConfirmationView.swift (1)

94-107: ⚠️ Potential issue | 🟠 Major

Add @MainActor annotation to deleteAccount() method.

After the suspension point at line 98, lines 101–107 update SwiftUI state and environment properties (dismiss(), builtInRouter.dismiss(), navigation.accountSwitcherIsPresented, self.error). These mutations must execute on the main thread, but async methods do not automatically inherit the main actor context after suspension. Explicit @MainActor isolation is required here.

Suggested fix
 extension UserProfileDeleteAccountConfirmationView {
+  `@MainActor`
   func deleteAccount() async {
     guard let user else { return }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Sources/ClerkKitUI/Components/UserProfile/UserProfileDeleteAccountConfirmationView.swift`
around lines 94 - 107, The async method deleteAccount() performs UI/state
mutations after an await (user.delete()) — calls like dismiss(),
builtInRouter.dismiss(...), setting navigation.accountSwitcherIsPresented, and
assigning self.error must run on the main actor; annotate the function with
`@MainActor` to ensure main-thread isolation so those updates execute safely after
suspension.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@Sources/ClerkKitUI/Components/UserProfile/UserProfileDeleteAccountConfirmationView.swift`:
- Around line 94-107: The async method deleteAccount() performs UI/state
mutations after an await (user.delete()) — calls like dismiss(),
builtInRouter.dismiss(...), setting navigation.accountSwitcherIsPresented, and
assigning self.error must run on the main actor; annotate the function with
`@MainActor` to ensure main-thread isolation so those updates execute safely after
suspension.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f010a528-d69e-4296-a7ae-36a08743c42a

📥 Commits

Reviewing files that changed from the base of the PR and between 2f28583 and 6b42b0d.

📒 Files selected for processing (6)
  • Sources/ClerkKitUI/Components/UserButton/UserButtonAccountSwitcher.swift
  • Sources/ClerkKitUI/Components/UserButton/UserProfileRowView.swift
  • Sources/ClerkKitUI/Components/UserProfile/BackupCodesView.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileDeleteAccountConfirmationView.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileSecurityView.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileView.swift
✅ Files skipped from review due to trivial changes (1)
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileSecurityView.swift
🚧 Files skipped from review as they are similar to previous changes (4)
  • Sources/ClerkKitUI/Components/UserProfile/BackupCodesView.swift
  • Sources/ClerkKitUI/Components/UserButton/UserButtonAccountSwitcher.swift
  • Sources/ClerkKitUI/Components/UserButton/UserProfileRowView.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileView.swift

Separate built-in and custom route navigation paths to allow custom
routes to use a distinct navigation destination. Extract header view
and custom row helpers into separate files for better organization.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
Sources/ClerkKitUI/Components/UserProfile/UserProfileHeaderView.swift (2)

19-20: Consider checking for empty fullName consistently with username handling.

Line 49 checks !username.isEmptyTrimmed before displaying the username, but fullName is only checked for nil. If fullName can be an empty or whitespace-only string, the header may render an empty text view.

Proposed fix
-    let fullName = user.fullName
-    let hasFullName = fullName != nil
+    let fullName = user.fullName?.isEmptyTrimmed == false ? user.fullName : nil
+    let hasFullName = fullName != nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Components/UserProfile/UserProfileHeaderView.swift` around
lines 19 - 20, The code only checks fullName for nil via fullName and
hasFullName; change the logic to treat empty or whitespace-only names the same
as username’s check by using the same trimmed-empty check (e.g., replace
hasFullName calculation to use fullName?.isEmptyTrimmed == false) and ensure the
UI branch that displays the full name in UserProfileHeaderView uses that updated
hasFullName (or directly checks fullName?.isEmptyTrimmed) so an empty/whitespace
fullName does not render an empty text view.

62-68: The .simultaneousGesture(TapGesture()) appears unnecessary.

This pattern is typically used to allow gesture passthrough to parent views, but for a standalone button calling a callback, it doesn't seem to serve a purpose. Consider removing it unless there's a specific gesture conflict being addressed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Components/UserProfile/UserProfileHeaderView.swift` around
lines 62 - 68, Remove the unnecessary simultaneous gesture attached to the Edit
profile Button in UserProfileHeaderView: delete the
.simultaneousGesture(TapGesture()) modifier so the Button remains as Button {
onUpdateProfile() } label: { Text("Edit profile", bundle: .module) } with the
existing .buttonStyle(.secondary(config: .init(size: .small))). Ensure the
onUpdateProfile callback remains wired and run the UI/interaction check to
confirm no gesture conflicts exist elsewhere.
Sources/ClerkKitUI/Components/UserProfile/UserProfileCustomRow.swift (1)

12-45: Consider adding Sendable conformance for concurrency safety.

UserProfileCustomRow contains Sendable-conforming types (LocalizedStringKey, UserProfileRowIcon, Bundle?, UserProfileCustomRowPlacement) except for Route. Adding a conditional Sendable conformance would enable safe usage across concurrency boundaries.

Proposed addition
 public struct UserProfileCustomRow<Route: Hashable> {
   // ...
 }
+
+extension UserProfileCustomRow: Sendable where Route: Sendable {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Components/UserProfile/UserProfileCustomRow.swift` around
lines 12 - 45, UserProfileCustomRow should declare a conditional Sendable
conformance so it can be used safely across concurrency boundaries; add a public
extension like "extension UserProfileCustomRow: Sendable where Route: Sendable"
(or equivalent declaration) to the type so the compiler treats the struct as
Sendable only when the generic Route is Sendable, referencing the struct name
UserProfileCustomRow and the generic parameter Route and leaving existing
properties (title, icon, bundle, placement) unchanged.
Sources/ClerkKitUI/Components/UserProfile/UserProfileView.swift (1)

323-339: Consider extracting duplicate environment injection into a helper.

The same environment values (sheetNavigation, codeLimiter, UserProfileNavigator, UserProfileBuiltInRouter) are injected in two places: lines 163-176 and lines 325-338. This duplication could lead to inconsistencies if one is updated without the other.

Proposed helper
private func withNavigationEnvironment<Content: View>(
  `@ViewBuilder` _ content: () -> Content
) -> some View {
  content()
    .environment(sheetNavigation)
    .environment(codeLimiter)
    .environment(
      UserProfileNavigator(
        push: navigateToCustom,
        popToRoot: { dismissAction(.popToRoot) }
      )
    )
    .environment(
      UserProfileBuiltInRouter(
        push: navigateToBuiltIn,
        dismissAction: dismissAction
      )
    )
}

Then use it in both .navigationDestination closures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Components/UserProfile/UserProfileView.swift` around lines
323 - 339, Extract the repeated environment injection into a helper like
withNavigationEnvironment that takes a `@ViewBuilder` content closure and applies
.environment(sheetNavigation), .environment(codeLimiter),
.environment(UserProfileNavigator(push: navigateToCustom, popToRoot: {
dismissAction(.popToRoot) })), and .environment(UserProfileBuiltInRouter(push:
navigateToBuiltIn, dismissAction: dismissAction)); then replace the duplicated
environment chains around view(for:) in both places with a call to
withNavigationEnvironment { view(for: destination) } (and the corresponding call
where the other duplicate exists) so all four environments are applied from one
centralized helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/ClerkKitUI/Components/UserProfile/UserProfileCustomRow.swift`:
- Around line 12-45: UserProfileCustomRow should declare a conditional Sendable
conformance so it can be used safely across concurrency boundaries; add a public
extension like "extension UserProfileCustomRow: Sendable where Route: Sendable"
(or equivalent declaration) to the type so the compiler treats the struct as
Sendable only when the generic Route is Sendable, referencing the struct name
UserProfileCustomRow and the generic parameter Route and leaving existing
properties (title, icon, bundle, placement) unchanged.

In `@Sources/ClerkKitUI/Components/UserProfile/UserProfileHeaderView.swift`:
- Around line 19-20: The code only checks fullName for nil via fullName and
hasFullName; change the logic to treat empty or whitespace-only names the same
as username’s check by using the same trimmed-empty check (e.g., replace
hasFullName calculation to use fullName?.isEmptyTrimmed == false) and ensure the
UI branch that displays the full name in UserProfileHeaderView uses that updated
hasFullName (or directly checks fullName?.isEmptyTrimmed) so an empty/whitespace
fullName does not render an empty text view.
- Around line 62-68: Remove the unnecessary simultaneous gesture attached to the
Edit profile Button in UserProfileHeaderView: delete the
.simultaneousGesture(TapGesture()) modifier so the Button remains as Button {
onUpdateProfile() } label: { Text("Edit profile", bundle: .module) } with the
existing .buttonStyle(.secondary(config: .init(size: .small))). Ensure the
onUpdateProfile callback remains wired and run the UI/interaction check to
confirm no gesture conflicts exist elsewhere.

In `@Sources/ClerkKitUI/Components/UserProfile/UserProfileView.swift`:
- Around line 323-339: Extract the repeated environment injection into a helper
like withNavigationEnvironment that takes a `@ViewBuilder` content closure and
applies .environment(sheetNavigation), .environment(codeLimiter),
.environment(UserProfileNavigator(push: navigateToCustom, popToRoot: {
dismissAction(.popToRoot) })), and .environment(UserProfileBuiltInRouter(push:
navigateToBuiltIn, dismissAction: dismissAction)); then replace the duplicated
environment chains around view(for:) in both places with a call to
withNavigationEnvironment { view(for: destination) } (and the corresponding call
where the other duplicate exists) so all four environments are applied from one
centralized helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8a2c99ac-44a1-4a6f-987e-86766aa2e4b9

📥 Commits

Reviewing files that changed from the base of the PR and between 6b42b0d and 28b88ab.

📒 Files selected for processing (4)
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileCustomRow.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileHeaderView.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileNavigation.swift
  • Sources/ClerkKitUI/Components/UserProfile/UserProfileView.swift

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.

1 participant