Skip to content

feat: Feature: Profile Settings / Edit Profile - #198

Merged
Benjtalkshow merged 5 commits into
boundlessfi:mainfrom
Oluwatos94:profileSettings
Apr 30, 2026
Merged

feat: Feature: Profile Settings / Edit Profile#198
Benjtalkshow merged 5 commits into
boundlessfi:mainfrom
Oluwatos94:profileSettings

Conversation

@Oluwatos94

@Oluwatos94 Oluwatos94 commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add /settings route protected by server-side auth guard (redirects unauthenticated users to /auth)
  • Profile tab edit display name, avatar URL, bio, and social links (GitHub, Twitter, personal website) with inline
    zod validation and optimistic update with rollback on error
  • Notifications tab per-event-type in-app/email toggles and email digest cadence (off/daily/weekly)
  • Wallet tab shows connected wallet address and balance; disconnect requires AlertDialog confirmation
  • Danger Zone tab delete account with double-confirm (user must type delete my account exactly before button
    enables)
  • Extend hooks/use-user-mutations.ts with bio, github, twitter, website fields on UpdateUserParams and add
    useDeleteAccountMutation

Files Created

  • app/settings/page.tsx — server component with auth guard
  • app/settings/settings-client.tsx — tabbed shell
  • components/settings/profile-tab.tsx
  • components/settings/notifications-tab.tsx
  • components/settings/wallet-tab.tsx
  • components/settings/danger-zone-tab.tsx

Files Modified

  • hooks/use-user-mutations.ts extended profile fields + delete account mutation

Test plan

  • Visit /settings while logged out should redirect to /auth
  • Visit /settings while logged in all four tabs render
  • Profile tab: submit with empty name inline error appears (no toast)
  • Profile tab: submit valid data optimistic update applies, success toast shown
  • Wallet tab: click Disconnect confirmation dialog appears before action fires
  • Danger Zone: confirm button stays disabled until delete my account is typed exactly

closes #183

Summary by CodeRabbit

  • New Features
    • New Settings page titled "Settings" (requires sign-in; unauthenticated users redirected to sign-in) with Profile, Notifications, Wallet, and Danger Zone tabs.
    • Profile: update name, bio, avatar and social/website links (validation, optimistic save).
    • Notifications: configure in-app/email channels and digest cadence; save preferences.
    • Wallet: connect, view address/balance, copy address, and disconnect.
    • Danger Zone: account deletion requires exact confirmation phrase and final confirmation; clears session on success.

@vercel

vercel Bot commented Apr 28, 2026

Copy link
Copy Markdown

@Oluwatos94 is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 71e13b48-1698-4492-9ddf-df7a47323a63

📥 Commits

Reviewing files that changed from the base of the PR and between 03d6973 and f1711ad.

📒 Files selected for processing (2)
  • hooks/use-user-mutations.ts
  • lib/auth-client.ts
 ____________________________________________________________________________________________________________________________________________________
< There are no final decisions. No decision is cast in stone. Instead, consider each as being written in the sand at the beach, and plan for change. >
 ----------------------------------------------------------------------------------------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
📝 Walkthrough

Walkthrough

Adds a guarded server-side /settings page and a client-side tabbed settings UI (Profile, Notifications, Wallet, Danger Zone). Extends User shape and update/delete mutations, adds client components for each tab, and wires optimistic updates, confirmation dialogs, and session/cache handling.

Changes

Cohort / File(s) Summary
Settings Route
app/settings/page.tsx
New server-side page exporting metadata, loads current user, redirects unauthenticated requests to /auth, and renders SettingsClient with the authenticated user.
Settings Client UI
app/settings/settings-client.tsx
New client entrypoint deriving profile defaults from user, rendering back navigation, header, and Tabs delegating to tab components.
Profile Tab
components/settings/profile-tab.tsx
New client form with Zod + react-hook-form, trims/validates inputs, diffs dirty fields, applies optimistic update to authKeys.session() cache, rolls back on error, and uses useUpdateUserMutation.
Notifications Tab
components/settings/notifications-tab.tsx
Client tab that initializes prefs from localStorage, allows toggling in-app/email per event and digest cadence, and saves prefs with pending state and toast feedback.
Wallet Tab
components/settings/wallet-tab.tsx
Client tab integrating useSmartWallet to show connection, address (copy), balance, and an AlertDialog-confirmed disconnect flow.
Danger Zone
components/settings/danger-zone-tab.tsx
Client tab with controlled AlertDialog requiring exact confirmation text to enable destructive "Delete Account"; calls useDeleteAccountMutation and navigates to /auth on success.
User Mutations & Types
hooks/use-user-mutations.ts, lib/server-auth.ts
Expanded User type with bio, github, twitter, website; extended UpdateUserParams; updateUser now throws on API errors; useUpdateUserMutation invalidates authKeys.session() and shows toasts; added deleteAccount/useDeleteAccountMutation which clears query cache on success.

Sequence Diagram(s)

sequenceDiagram
  participant Browser as Browser
  participant Server as Server (/settings)
  participant SettingsClient as SettingsClient (client)
  participant ProfileTab as ProfileTab
  participant Hooks as useUpdateUserMutation / useDeleteAccountMutation
  participant AuthAPI as authClient / QueryClient

  Browser->>Server: GET /settings
  Server->>AuthAPI: getCurrentUser()
  AuthAPI-->>Server: validated user
  Server-->>Browser: render SettingsClient(user)

  Browser->>SettingsClient: interact (edit / open danger zone / wallet)
  SettingsClient->>ProfileTab: submit changed values
  ProfileTab->>Hooks: mutateAsync(updateUser)
  Hooks->>AuthAPI: authClient.updateUser(params)
  AuthAPI-->>Hooks: response (success/error)
  Hooks-->>ProfileTab: resolve/reject
  Hooks->>AuthAPI: invalidate authKeys.session() (on success)

  Browser->>SettingsClient: confirm delete account
  SettingsClient->>Hooks: mutateAsync(deleteAccount)
  Hooks->>AuthAPI: authClient.deleteUser()
  AuthAPI-->>Hooks: success
  Hooks->>AuthAPI: queryClient.clear()
  Hooks-->>Browser: navigation to /auth
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through tabs to tidy your fields,
Saved names, links, and secrets in verdant fields.
A cautious nudge for delete, a carrot for save,
Optimistic hops—if it fails, I'll behave.
Hop on, settings set — your burrow's now brave!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Feature: Profile Settings / Edit Profile' clearly summarizes the main change—adding a settings route with profile editing functionality—matching the primary objective and changeset.
Linked Issues check ✅ Passed All coding requirements from issue #183 are met: protected /settings route with auth guard, tabbed UI with Profile/Notifications/Wallet/Danger Zone, profile editing with validation and optimistic updates, wallet disconnect with confirmation, and account deletion with typed confirmation.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the profile settings feature. No extraneous modifications, cleanup, or refactoring outside the linked issue requirements are present.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

@Oluwatos94
Oluwatos94 marked this pull request as draft April 28, 2026 07:43
@drips-wave

drips-wave Bot commented Apr 28, 2026

Copy link
Copy Markdown

@Oluwatos94 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
components/settings/profile-tab.tsx (1)

75-83: Type session cache correctly to prevent shape mistakes in optimistic updates.

The cache key authKeys.session() stores session data with structure { user?: ..., ... }, but the code retrieves and types it as ProfileFormValues (the form shape). This weakens type safety on the optimistic update and rollback paths. The rollback at line 88 silently accepts previous without a type guard, which could mask mismatches at runtime.

Create a SessionCache type matching the actual session structure and apply it consistently:

🔧 Proposed refactor
+type SessionCache = { user?: Partial<ProfileFormValues> } & Record<string, unknown>;
+
-    const previous = queryClient.getQueryData<ProfileFormValues>(
+    const previous = queryClient.getQueryData<SessionCache>(
       authKeys.session(),
     );

-    queryClient.setQueryData(authKeys.session(), (old: unknown) => {
+    queryClient.setQueryData<SessionCache>(authKeys.session(), (old) => {
       if (!old || typeof old !== "object") return old;
-      const session = old as { user?: Partial<ProfileFormValues> };
+      const session = old as SessionCache;
       return { ...session, user: { ...session.user, ...values } };
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/settings/profile-tab.tsx` around lines 75 - 83, Define a
SessionCache type reflecting the actual cache shape (e.g. { user?:
Partial<ProfileFormValues> } plus other session fields) and use it as the
generic for queryClient.getQueryData and queryClient.setQueryData calls that
reference authKeys.session(); change the local variable previous to be typed
(previous: SessionCache | undefined) and add a type guard before using previous
in the rollback so you only pass a correctly shaped value back to
queryClient.setQueryData, keeping ProfileFormValues for form values only and
ensuring all optimistic update and rollback code (the anonymous updater passed
to setQueryData and the rollback path that uses previous) operate on
SessionCache rather than ProfileFormValues.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/settings/settings-client.tsx`:
- Around line 18-25: The profileDefaults initialization (profileDefaults)
currently sets bio, github, twitter, and website to empty strings, which causes
existing server-side values to be overwritten on save; update the flow to load
or preserve real profile data instead: either extend the User type and ensure
getCurrentUser() returns bio/github/twitter/website and use those values in
profileDefaults, or fetch the full user profile before rendering the
SettingsForm and initialize profileDefaults from that payload, or implement
dirty-tracking in the form submission logic (submit only fields that changed) so
unchanged server values aren’t sent as empty strings. Ensure you update
references to profileDefaults and any form submit handler to use the chosen
approach and the User/getCurrentUser() symbols to locate the code.

In `@components/settings/notifications-tab.tsx`:
- Around line 58-63: handleSave currently only sleeps then shows toast.success
without persisting preferences; update it so it does not claim success until a
real save occurs: inside handleSave (referencing the handleSave function,
setIsPending, and toast.success/toast.error) either call the real save API or,
if the API is not yet wired, replace the success toast with a clear
informational/error toast like "Preferences not yet saved — feature coming soon"
and avoid implying persistence; also ensure setIsPending is cleared in a finally
block and any errors use toast.error so UI state and messaging remain correct.
- Around line 84-104: The mapped list uses the shorthand Fragment <>...</> which
cannot accept keys; change the fragment wrapper around the mapped JSX to the
explicit Fragment with a key (e.g., <Fragment key={key}>) so React can properly
reconcile the items in eventKeys.map; locate the map over eventKeys in the
component (references: eventKeys, eventLabels, prefs, toggleChannel, Switch,
Label) and replace the shorthand fragment with an explicit Fragment keyed by the
current key.

In `@hooks/use-user-mutations.ts`:
- Around line 16-23: The updateUser response handling should check for an error
payload before inspecting response.data; inside the try block for
authClient.updateUser, first test response.error and if present throw a new
Error that includes response.error.message/details, then only validate
response.data and throw the generic "Failed to update user profile" if data is
missing. Update the logic around authClient.updateUser/response to prioritize
response.error and surface the real error message.

---

Nitpick comments:
In `@components/settings/profile-tab.tsx`:
- Around line 75-83: Define a SessionCache type reflecting the actual cache
shape (e.g. { user?: Partial<ProfileFormValues> } plus other session fields) and
use it as the generic for queryClient.getQueryData and queryClient.setQueryData
calls that reference authKeys.session(); change the local variable previous to
be typed (previous: SessionCache | undefined) and add a type guard before using
previous in the rollback so you only pass a correctly shaped value back to
queryClient.setQueryData, keeping ProfileFormValues for form values only and
ensuring all optimistic update and rollback code (the anonymous updater passed
to setQueryData and the rollback path that uses previous) operate on
SessionCache rather than ProfileFormValues.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7e63efa0-74ae-46ee-b569-749065aed6fc

📥 Commits

Reviewing files that changed from the base of the PR and between 06bf865 and ca50a5c.

📒 Files selected for processing (7)
  • app/settings/page.tsx
  • app/settings/settings-client.tsx
  • components/settings/danger-zone-tab.tsx
  • components/settings/notifications-tab.tsx
  • components/settings/profile-tab.tsx
  • components/settings/wallet-tab.tsx
  • hooks/use-user-mutations.ts

Comment thread app/settings/settings-client.tsx
Comment thread components/settings/notifications-tab.tsx
Comment thread components/settings/notifications-tab.tsx
Comment thread hooks/use-user-mutations.ts Outdated
@Oluwatos94
Oluwatos94 marked this pull request as ready for review April 28, 2026 13:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
components/settings/notifications-tab.tsx (1)

58-68: ⚠️ Potential issue | 🟠 Major

handleSave still doesn’t persist preferences, so the core save flow is incomplete.

Line 58-Line 68 only updates UI state/toast; preferences are not written anywhere durable. Users lose changes on refresh/navigation.

🔧 Proposed direction (wire actual persistence contract)
-export function NotificationsTab() {
-  const [prefs, setPrefs] = useState<NotificationPrefs>(defaultPrefs);
+export function NotificationsTab({
+  initialPrefs = defaultPrefs,
+  onSave,
+}: {
+  initialPrefs?: NotificationPrefs;
+  onSave: (prefs: NotificationPrefs) => Promise<void>;
+}) {
+  const [prefs, setPrefs] = useState<NotificationPrefs>(initialPrefs);
   const [isPending, setIsPending] = useState(false);

   const handleSave = async () => {
     setIsPending(true);
     try {
-      // TODO: wire to backend notification preferences endpoint
-      toast.info(
-        "Notification preferences saved locally. Backend sync coming soon.",
-      );
+      await onSave(prefs);
+      toast.success("Notification preferences saved.");
+    } catch {
+      toast.error("Failed to save notification preferences.");
     } finally {
       setIsPending(false);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/settings/notifications-tab.tsx` around lines 58 - 68, handleSave
currently only toggles UI state and shows a toast (setIsPending, toast.info) but
never persists the user's notification preferences, so changes are lost; update
handleSave to call the backend persistence contract: gather the current
preferences state (e.g., notificationPrefs or the form state used by this
component), send an authenticated request to the notifications/preferences
endpoint (using fetch/axios via your app's api client), await and check the
response, handle errors (show toast.error and keep state consistent), and only
clear setIsPending and show success toast after a successful save; ensure the
method name handleSave and state setters remain unchanged while wiring the
network call and error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/settings/profile-tab.tsx`:
- Around line 92-97: The form isn't being reset after a successful save: after
awaiting mutateAsync(changedValues) you should clear the form's dirty state by
resetting it to the saved values (either the returned payload from mutateAsync
or the changedValues) instead of leaving fields dirty; update the success path
(after mutateAsync) to call the form reset helper (e.g., reset(...) or
equivalent) so the UI and react-hook-form state match the saved session, leaving
the existing error rollback logic that uses
queryClient.setQueryData(authKeys.session(), previous) unchanged.

In `@hooks/use-user-mutations.ts`:
- Around line 16-18: Replace the forced cast by enabling the type-inference
plugin: add the inferAdditionalFields plugin to the Auth client initialization
so authClient.updateUser correctly infers its parameter type, then remove the
manual cast in the use-user-mutations call to authClient.updateUser (and drop
the workaround UpdateUserParams interface). Ensure your server auth config
declares the extra fields under user.additionalFields with input: true so the
plugin can infer them.

---

Duplicate comments:
In `@components/settings/notifications-tab.tsx`:
- Around line 58-68: handleSave currently only toggles UI state and shows a
toast (setIsPending, toast.info) but never persists the user's notification
preferences, so changes are lost; update handleSave to call the backend
persistence contract: gather the current preferences state (e.g.,
notificationPrefs or the form state used by this component), send an
authenticated request to the notifications/preferences endpoint (using
fetch/axios via your app's api client), await and check the response, handle
errors (show toast.error and keep state consistent), and only clear setIsPending
and show success toast after a successful save; ensure the method name
handleSave and state setters remain unchanged while wiring the network call and
error handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fc691697-9d77-454e-88cc-390ed23ab895

📥 Commits

Reviewing files that changed from the base of the PR and between ca50a5c and 741f4e5.

📒 Files selected for processing (5)
  • app/settings/settings-client.tsx
  • components/settings/notifications-tab.tsx
  • components/settings/profile-tab.tsx
  • hooks/use-user-mutations.ts
  • lib/server-auth.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/settings/settings-client.tsx

Comment thread components/settings/profile-tab.tsx
Comment thread hooks/use-user-mutations.ts Outdated

@Benjtalkshow Benjtalkshow 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.

A few items before merge.

components/settings/notifications-tab.tsx:58-68 is a fake save. handleSave just shows toast.info("...saved locally. Backend sync coming soon.") and there's no actual local persistence either — prefs is component state that resets on remount. This is the same UX trap pattern PRs #178 and #186 had to remove. Either disable the Save button with a [Coming soon] label, or wire it to localStorage or a real endpoint.

profile-tab.tsx:84-88 types the session cache as ProfileFormValues, but the cache actually stores { user?: ... }. Define a SessionCache type and use it on both getQueryData and setQueryData so the optimistic update and rollback stay type-safe.

The form fields use shadcn's FormField / FormItem directly. The project's chosen wrapper is FormFieldWrapper, see application-dialog.tsx. Switch to it for consistency.

use-user-mutations.ts:16-18 casts params as Parameters<typeof authClient.updateUser>[0] to pass bio, github, twitter, website. Please confirm the better-auth schema actually accepts those fields, otherwise they'll be silently dropped on the server.

Minor: lib/server-auth.ts:115 uses id: u.id! after a wider cast. The previous code didn't need that because the Zod-validated shape guaranteed id. Drop the ! and either trust the validated shape or narrow with a runtime check.

Please address all CodeRabbit findings as well.

@Benjtalkshow

Copy link
Copy Markdown
Contributor

Hello @Oluwatos94
Whats the update on this PR?

@Oluwatos94

Copy link
Copy Markdown
Contributor Author

Hello @Oluwatos94 Whats the update on this PR?

Hi @Benjtalkshow, sorry I would fix this later today.

@Benjtalkshow

Copy link
Copy Markdown
Contributor

Hello @Oluwatos94 Whats the update on this PR?

Hi @Benjtalkshow, sorry I would fix this later today.

Please be fast. The wave is ending in few hours. Thanks

@Oluwatos94

Copy link
Copy Markdown
Contributor Author

Hello @Oluwatos94 Whats the update on this PR?

Hi @Benjtalkshow, sorry I would fix this later today.

Please be fast. The wave is ending in few hours. Thanks

Yeah, but I believe the point still counts even after wave ended right?

@Benjtalkshow

Copy link
Copy Markdown
Contributor

Hello @Oluwatos94 Whats the update on this PR?

Hi @Benjtalkshow, sorry I would fix this later today.

Please be fast. The wave is ending in few hours. Thanks

Yeah, but I believe the point still counts even after wave ended right?

Kindly finlize this PR soon or i will close the PR and unassign you.

@Oluwatos94

Oluwatos94 commented Apr 30, 2026

Copy link
Copy Markdown
Contributor Author

@Benjtalkshow Review fixed, sorry it long.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
components/settings/notifications-tab.tsx (1)

70-75: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Connect save flow to backend before showing persisted-success messaging.

At Line [74], the success toast implies durable persistence, but this path only writes to localStorage. That misses the “persist preferences to backend” objective and will not survive cross-device/session expectations.

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

In `@components/settings/notifications-tab.tsx` around lines 70 - 75, The save
handler handleSave currently writes prefs to localStorage (STORAGE_KEY) and
immediately shows a success toast; instead call the backend persistence API
(e.g., a function like saveNotificationPreferences or updateUserPreferences)
with prefs, await its response and verify success before calling toast.success,
and only then update localStorage if you still want a client-side cache; on
failure log the error (processLogger or console.error) and show toast.error,
ensure setIsPending(false) runs in finally, and keep error handling around the
API call so durable persistence (cross-device) is achieved rather than only
localStorage.
🧹 Nitpick comments (2)
components/settings/profile-tab.tsx (1)

93-95: ⚡ Quick win

Use the existing root-error slot for mutation failures.

Line 181 renders form.formState.errors.root, but the catch path never sets it. Adding a root error gives inline feedback consistent with the form UX.

🩹 Suggested tweak
     } catch {
       queryClient.setQueryData(authKeys.session(), previous);
+      form.setError("root", {
+        type: "server",
+        message: "Failed to save profile changes. Please try again.",
+      });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/settings/profile-tab.tsx` around lines 93 - 95, The catch block
that reverts the optimistic session
(queryClient.setQueryData(authKeys.session(), previous)) should also set a
root-level form error so the UI renders form.formState.errors.root; update the
catch to call the form error setter (e.g., form.setError("root", { type:
"server", message: "<user-facing message or err.message>" })) or equivalent,
including the server error message when available, and keep the existing
queryClient revert call.
lib/server-auth.ts (1)

103-108: ⚡ Quick win

Prefer typing SessionUser over asserting validatedSession.user.

The cast at Line 103 sidesteps compile-time checks. Defining these fields on SessionUser keeps the parser/result contract type-safe and avoids silent drift.

♻️ Suggested refactor
 interface SessionUser {
   id?: string;
   name?: string | null;
   email?: string | null;
   image?: string | null;
+  bio?: string | null;
+  github?: string | null;
+  twitter?: string | null;
+  website?: string | null;
 }
@@
-    const u = validatedSession.user as typeof validatedSession.user & {
-      bio?: string | null;
-      github?: string | null;
-      twitter?: string | null;
-      website?: string | null;
-    };
+    const u = validatedSession.user;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/server-auth.ts` around lines 103 - 108, The code currently force-casts
validatedSession.user to add optional profile fields (the cast around
validatedSession.user), which bypasses compile-time checks; instead extend or
update the SessionUser type/interface to include bio?: string | null, github?:
string | null, twitter?: string | null, website?: string | null and use that
SessionUser type for validatedSession.user (or change the session return type)
so the compiler knows these fields exist without assertion; locate the
SessionUser/type definition and add these optional properties, then remove the
cast around validatedSession.user in this file so the code uses the
strongly-typed SessionUser directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/settings/notifications-tab.tsx`:
- Around line 36-43: The JSON from localStorage (read in loadPrefs using
STORAGE_KEY and cast to NotificationPrefs) must be validated/normalized before
use: instead of directly casting JSON.parse(...) to NotificationPrefs, check
required fields and types or perform a shallow/deep merge with defaultPrefs to
fill missing keys and enforce shapes, then return that validated object; apply
the same validation/merge whenever the component reads/writes prefs (the code
paths that dereference nested keys where NotificationPrefs is used) so
components never access undefined nested properties.

In `@components/settings/profile-tab.tsx`:
- Around line 22-50: The URL validators for image, github, twitter, and website
currently use z.string().url() which allows non-http(s) schemes; update each
field's schema to additionally validate the protocol by adding a refine that,
when the value is a non-empty string, constructs new URL(value) and ensures its
protocol is exactly "http:" or "https:" (return a descriptive error message like
"URL must use http or https"); keep the existing .or(z.literal("")) and
.optional() semantics so empty strings remain allowed and preserve trimming and
max rules for bio.

---

Duplicate comments:
In `@components/settings/notifications-tab.tsx`:
- Around line 70-75: The save handler handleSave currently writes prefs to
localStorage (STORAGE_KEY) and immediately shows a success toast; instead call
the backend persistence API (e.g., a function like saveNotificationPreferences
or updateUserPreferences) with prefs, await its response and verify success
before calling toast.success, and only then update localStorage if you still
want a client-side cache; on failure log the error (processLogger or
console.error) and show toast.error, ensure setIsPending(false) runs in finally,
and keep error handling around the API call so durable persistence
(cross-device) is achieved rather than only localStorage.

---

Nitpick comments:
In `@components/settings/profile-tab.tsx`:
- Around line 93-95: The catch block that reverts the optimistic session
(queryClient.setQueryData(authKeys.session(), previous)) should also set a
root-level form error so the UI renders form.formState.errors.root; update the
catch to call the form error setter (e.g., form.setError("root", { type:
"server", message: "<user-facing message or err.message>" })) or equivalent,
including the server error message when available, and keep the existing
queryClient revert call.

In `@lib/server-auth.ts`:
- Around line 103-108: The code currently force-casts validatedSession.user to
add optional profile fields (the cast around validatedSession.user), which
bypasses compile-time checks; instead extend or update the SessionUser
type/interface to include bio?: string | null, github?: string | null, twitter?:
string | null, website?: string | null and use that SessionUser type for
validatedSession.user (or change the session return type) so the compiler knows
these fields exist without assertion; locate the SessionUser/type definition and
add these optional properties, then remove the cast around validatedSession.user
in this file so the code uses the strongly-typed SessionUser directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 518d8095-8054-4689-8d0c-0ff606ace694

📥 Commits

Reviewing files that changed from the base of the PR and between 741f4e5 and 03d6973.

📒 Files selected for processing (3)
  • components/settings/notifications-tab.tsx
  • components/settings/profile-tab.tsx
  • lib/server-auth.ts

Comment on lines +36 to +43
function loadPrefs(): NotificationPrefs {
if (typeof window === "undefined") return defaultPrefs;
try {
const stored = localStorage.getItem(STORAGE_KEY);
return stored ? (JSON.parse(stored) as NotificationPrefs) : defaultPrefs;
} catch {
return defaultPrefs;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate stored payload shape before using it as NotificationPrefs.

At Line [40], a raw cast from JSON.parse can return partial/invalid objects; then Line [66] dereferences nested keys and can throw at runtime. Add schema/shape validation (or safe merge with defaultPrefs) before setting state.

🔧 Suggested hardening
 function loadPrefs(): NotificationPrefs {
   if (typeof window === "undefined") return defaultPrefs;
   try {
     const stored = localStorage.getItem(STORAGE_KEY);
-    return stored ? (JSON.parse(stored) as NotificationPrefs) : defaultPrefs;
+    if (!stored) return defaultPrefs;
+    const parsed = JSON.parse(stored) as Partial<NotificationPrefs>;
+    return {
+      ...defaultPrefs,
+      newBounty: { ...defaultPrefs.newBounty, ...parsed.newBounty },
+      applicationUpdate: {
+        ...defaultPrefs.applicationUpdate,
+        ...parsed.applicationUpdate,
+      },
+      bountyCompleted: {
+        ...defaultPrefs.bountyCompleted,
+        ...parsed.bountyCompleted,
+      },
+      mentions: { ...defaultPrefs.mentions, ...parsed.mentions },
+      digestCadence:
+        parsed.digestCadence === "off" ||
+        parsed.digestCadence === "daily" ||
+        parsed.digestCadence === "weekly"
+          ? parsed.digestCadence
+          : defaultPrefs.digestCadence,
+    };
   } catch {
     return defaultPrefs;
   }
 }

Also applies to: 64-67

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

In `@components/settings/notifications-tab.tsx` around lines 36 - 43, The JSON
from localStorage (read in loadPrefs using STORAGE_KEY and cast to
NotificationPrefs) must be validated/normalized before use: instead of directly
casting JSON.parse(...) to NotificationPrefs, check required fields and types or
perform a shallow/deep merge with defaultPrefs to fill missing keys and enforce
shapes, then return that validated object; apply the same validation/merge
whenever the component reads/writes prefs (the code paths that dereference
nested keys where NotificationPrefs is used) so components never access
undefined nested properties.

Comment on lines +22 to +50
image: z
.string()
.trim()
.url("Must be a valid URL")
.or(z.literal(""))
.optional(),
bio: z
.string()
.trim()
.max(500, "Bio must be 500 characters or less")
.optional(),
github: z
.string()
.trim()
.url("Must be a valid URL")
.or(z.literal(""))
.optional(),
twitter: z
.string()
.trim()
.url("Must be a valid URL")
.or(z.literal(""))
.optional(),
website: z
.string()
.trim()
.url("Must be a valid URL")
.or(z.literal(""))
.optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict profile link protocols to http/https.

z.string().url() allows syntactically valid URLs, including non-web schemes. For user-supplied profile links, this weakens XSS/phishing posture if these values are later rendered into href/src.

🔒 Suggested hardening
+const optionalHttpUrl = z
+  .string()
+  .trim()
+  .url("Must be a valid URL")
+  .refine((value) => {
+    const protocol = new URL(value).protocol;
+    return protocol === "http:" || protocol === "https:";
+  }, "Must use an http(s) URL")
+  .or(z.literal(""))
+  .optional();
+
 const profileSchema = z.object({
@@
-  image: z
-    .string()
-    .trim()
-    .url("Must be a valid URL")
-    .or(z.literal(""))
-    .optional(),
+  image: optionalHttpUrl,
@@
-  github: z
-    .string()
-    .trim()
-    .url("Must be a valid URL")
-    .or(z.literal(""))
-    .optional(),
+  github: optionalHttpUrl,
@@
-  twitter: z
-    .string()
-    .trim()
-    .url("Must be a valid URL")
-    .or(z.literal(""))
-    .optional(),
+  twitter: optionalHttpUrl,
@@
-  website: z
-    .string()
-    .trim()
-    .url("Must be a valid URL")
-    .or(z.literal(""))
-    .optional(),
+  website: optionalHttpUrl,
 });
In Zod v4, does z.string().url() accept non-http(s) schemes such as javascript: or data:?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/settings/profile-tab.tsx` around lines 22 - 50, The URL validators
for image, github, twitter, and website currently use z.string().url() which
allows non-http(s) schemes; update each field's schema to additionally validate
the protocol by adding a refine that, when the value is a non-empty string,
constructs new URL(value) and ensures its protocol is exactly "http:" or
"https:" (return a descriptive error message like "URL must use http or https");
keep the existing .or(z.literal("")) and .optional() semantics so empty strings
remain allowed and preserve trimming and max rules for bio.

Add the inferAdditionalFields client plugin mirroring the bio, github,
twitter, and website fields configured on the better-auth backend. This
makes authClient.updateUser accept those fields without needing the
Parameters<typeof authClient.updateUser>[0] cast in use-user-mutations.ts.

Note: this assumes the backend's better-auth user config has these
additionalFields registered. If not, the runtime call will silently drop
them regardless of the cast, so the schema must be aligned for the save
to actually persist.
@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
hooks/use-user-mutations.ts (1)

6-13: ⚡ Quick win

Derive UpdateUserParams from the client instead of mirroring it manually.

Now that authClient already infers the extra profile fields, this exported interface is a second source of truth. Using Parameters<typeof authClient.updateUser>[0] would keep the hook aligned with Better Auth’s generated input shape and remove the manual mirroring. (better-auth.com)

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

In `@hooks/use-user-mutations.ts` around lines 6 - 13, Replace the manually
defined UpdateUserParams interface with a derived type from the auth client so
it stays in sync: change usages of the exported UpdateUserParams to use
Parameters<typeof authClient.updateUser>[0] (or create a type alias like type
UpdateUserParams = Parameters<typeof authClient.updateUser>[0]) and export that
instead; update imports/exports in hooks/use-user-mutations.ts and any places
referencing UpdateUserParams to use the derived type so the hook aligns with the
generated authClient shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@hooks/use-user-mutations.ts`:
- Around line 46-52: The deleteAccount helper currently calls
authClient.deleteUser() without performing a re-auth step; update the
deleteAccount function to require and accept a re-auth credential (e.g., a
password) or trigger a fresh-session reauthentication before calling
authClient.deleteUser, wiring that credential into the deleteUser call (or call
the authClient.reauthenticate/refresh method first) and surface errors
appropriately; verify the Better Auth config and ensure deleteAccount
validates/propagates the re-auth result so users on stale sessions will be
prompted for a password or a fresh session prior to invoking deleteUser.

---

Nitpick comments:
In `@hooks/use-user-mutations.ts`:
- Around line 6-13: Replace the manually defined UpdateUserParams interface with
a derived type from the auth client so it stays in sync: change usages of the
exported UpdateUserParams to use Parameters<typeof authClient.updateUser>[0] (or
create a type alias like type UpdateUserParams = Parameters<typeof
authClient.updateUser>[0]) and export that instead; update imports/exports in
hooks/use-user-mutations.ts and any places referencing UpdateUserParams to use
the derived type so the hook aligns with the generated authClient shape.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 71e13b48-1698-4492-9ddf-df7a47323a63

📥 Commits

Reviewing files that changed from the base of the PR and between 03d6973 and f1711ad.

📒 Files selected for processing (2)
  • hooks/use-user-mutations.ts
  • lib/auth-client.ts

Comment on lines +46 to +52
async function deleteAccount() {
const response = await authClient.deleteUser();
if (response.error) {
throw new Error(response.error.message || "Failed to delete account");
}
return response.data;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

deleteUser() needs a re-auth proof, not just the confirmation phrase.

Better Auth only allows client-side deletion with a password or a fresh session when no password is supplied, so this helper can fail for users on a stale session unless the flow asks for re-authentication first. Please verify the auth config and wire in the required password/fresh-session step before relying on this mutation. (better-auth.com)

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

In `@hooks/use-user-mutations.ts` around lines 46 - 52, The deleteAccount helper
currently calls authClient.deleteUser() without performing a re-auth step;
update the deleteAccount function to require and accept a re-auth credential
(e.g., a password) or trigger a fresh-session reauthentication before calling
authClient.deleteUser, wiring that credential into the deleteUser call (or call
the authClient.reauthenticate/refresh method first) and surface errors
appropriately; verify the Better Auth config and ensure deleteAccount
validates/propagates the re-auth result so users on stale sessions will be
prompted for a password or a fresh session prior to invoking deleteUser.

@Benjtalkshow Benjtalkshow 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.

All four items from the previous round are addressed cleanly. Notifications now persist to localStorage with proper SSR-safe loading, the session cache has a real SessionCache type, the form fields use FormFieldWrapper, and the id: u.id! is replaced with a proper null guard. Nice add on the form.reset(values) after save too.

Pushed a small commit (f1711ad) on your branch removing the last unsafe cast in use-user-mutations.ts. Added the inferAdditionalFields client plugin to lib/auth-client.ts mirroring bio, github, twitter, website, so authClient.updateUser is now properly typed for them and no cast is needed.

Merging this in.

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.

Feature: Profile Settings / Edit Profile

2 participants