Skip to content

feat: Add edit profile photo and name functionality#1895

Draft
Copilot wants to merge 4 commits intomainfrom
copilot/fix-181429480-611845080-ff4480f5-c4d0-4cb0-8105-05dee4f3db38
Draft

feat: Add edit profile photo and name functionality#1895
Copilot wants to merge 4 commits intomainfrom
copilot/fix-181429480-611845080-ff4480f5-c4d0-4cb0-8105-05dee4f3db38

Conversation

Copy link
Contributor

Copilot AI commented Mar 9, 2026

  • Add avatarUrl column to DB schema + migration
  • Update API UserSchema and handlers to support avatarUrl
  • Update features/profile/types.ts with avatarUrl
  • Create useUpdateProfile hook for saving profile changes
  • Fix name.tsx to load real user data and save via API
  • Update profile/index.tsx: make avatar tappable (image picker) and name row navigable
  • Remove misleading middle name field (no DB backend support)
  • Fix silent failure: check updateProfile return after R2 upload
  • Fix upload spinner: overlay visible even when existing avatar is shown
  • Stabilize canSave baseline with useRef to avoid store sync drift
  • Add 5 MB image size validation before upload
  • Add profile.imageTooLarge translation key
  • Show permission-specific alert with Open Settings when photo library access is denied
  • Trim whitespace in canSave / handleSave; use useMemo to avoid duplication
  • Guard handleSave against double-submit via keyboard Return key
  • Add permissions.* and common.cancel i18n keys
Original prompt

This section details on the original issue you should resolve

<issue_title>Feature Request: Add functionality to edit profile photo and name</issue_title>
<issue_description>Currently, the app lacks the functionality for users to customize their profiles. There is no option to upload or change the account profile photo, nor is there a way to edit the display name.

This is a core feature for user personalization and identity. The interface should include editable fields for the name and a clickable area for the profile to upload a new image.

To reproduce:

  1. Navigate to the profile/edit profile screen.
  2. Tap on the profile image placeholder.
  3. Tap on the name text field.

Expected behavior:

  • A bottom sheet or image picker should appear when tapping the photo
  • A keyboard should appear to edit the name, with a "Save" button

Actual behavior:
Nothing happens when tapping the elements; the UI appears static.</issue_description>

Comments on the Issue (you are @copilot in this section)


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Summary by CodeRabbit

  • New Features
    • Users can now upload and update profile avatars with a 5 MB size limit.
    • Added ability to edit first and last name directly from the profile screen.
    • Improved user feedback with loading indicators and error messages during profile updates.

- Add avatarUrl column to users table with migration
- Update API UserSchema and handlers to support avatarUrl
- Add useUpdateProfile hook for saving profile changes
- Fix name.tsx to use real user data and save via API
- Make profile avatar tappable with image picker and upload
- Add navigation from name row to name edit screen

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Copilot AI changed the title [WIP] Feature Request: Add functionality to edit profile photo and name feat: Add edit profile photo and name functionality Mar 9, 2026
@andrew-bierman
Copy link
Collaborator

@coderabbitai Please review this PR for code quality, best practices, and potential issues.

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me gather the relevant code to perform a thorough review.


🏁 Script executed:

#!/bin/bash
# Find relevant files changed in this PR
echo "=== Finding profile-related files ==="
fd -e tsx -e ts "profile" --exclude node_modules
echo ""
echo "=== Finding useUpdateProfile hook ==="
fd -e ts -e tsx "useUpdateProfile" --exclude node_modules
echo ""
echo "=== Finding migration 0033 ==="
fd "0033" --exclude node_modules
echo ""
echo "=== Finding UserSchema and UpdateUserRequestSchema ==="
rg -rn "UpdateUserRequestSchema\|avatarUrl\|avatar_url" --type ts --type tsx -l 2>/dev/null || rg -rn "UpdateUserRequestSchema\|avatarUrl\|avatar_url" -l --include="*.ts" --include="*.tsx" 2>/dev/null | head -20

Length of output: 418


🏁 Script executed:

#!/bin/bash
echo "=== Finding profile index file ==="
fd "index" --full-path -e tsx | grep -i profile | head -10

echo "=== Finding profile name file ==="
fd "name" --full-path -e tsx | grep -i profile | head -10

echo "=== Finding useImagePicker ==="
fd -e ts -e tsx "useImagePicker" --exclude node_modules | head -5

echo "=== Finding uploadImage util ==="
fd -e ts -e tsx "uploadImage" --exclude node_modules | head -10

echo "=== Finding userStore ==="
fd -e ts -e tsx "userStore" --exclude node_modules | head -10

Length of output: 390


🏁 Script executed:

#!/bin/bash
echo "=== Profile Index ==="
cat apps/expo/app/(app)/(tabs)/profile/index.tsx

echo ""
echo "=== Profile Name ==="
cat apps/expo/app/(app)/(tabs)/profile/name.tsx

echo ""
echo "=== useUpdateProfile Hook ==="
cat apps/expo/features/profile/hooks/useUpdateProfile.ts

echo ""
echo "=== Migration 0033 ==="
cat packages/api/drizzle/0033_add_avatar_url_to_users.sql

Length of output: 157


🏁 Script executed:

#!/bin/bash
echo "=== Profile Index ==="
cat "apps/expo/app/(app)/(tabs)/profile/index.tsx"

echo ""
echo "=== Profile Name ==="
cat "apps/expo/app/(app)/(tabs)/profile/name.tsx"

echo ""
echo "=== useUpdateProfile Hook ==="
cat apps/expo/features/profile/hooks/useUpdateProfile.ts

echo ""
echo "=== Migration 0033 ==="
cat packages/api/drizzle/0033_add_avatar_url_to_users.sql

Length of output: 14882


🏁 Script executed:

#!/bin/bash
echo "=== uploadImage util ==="
cat apps/expo/features/packs/utils/uploadImage.ts

echo ""
echo "=== useImagePicker hook ==="
cat apps/expo/features/packs/hooks/useImagePicker.ts

echo ""
echo "=== buildPackTemplateItemImageUrl ==="
fd "buildPackTemplateItemImageUrl" -e ts -e tsx --exclude node_modules
rg -n "buildPackTemplateItemImageUrl" --include="*.ts" --include="*.tsx" -A5 | head -40

echo ""
echo "=== API user profile route ==="
fd "profile" --include="*.ts" | grep -i "api\|route\|server" | head -10
rg -rn "PUT.*profile\|profile.*PUT\|avatar_url\|avatarUrl" --include="*.ts" -l | head -10

Length of output: 6695


🏁 Script executed:

#!/bin/bash
echo "=== buildPackTemplateItemImageUrl ==="
cat apps/expo/lib/utils/buildPackTemplateItemImageUrl.ts

echo ""
echo "=== API user profile route - find files ==="
rg -rn "avatarUrl\|avatar_url" --type ts -l | head -15

echo ""
echo "=== UserSchema ==="
rg -rn "UserSchema\|UpdateUserRequestSchema" --type ts -l | head -10

Length of output: 509


🏁 Script executed:

#!/bin/bash
echo "=== Find API route files for user/profile ==="
fd "." packages/api --type f -e ts | xargs grep -l "profile\|avatar" 2>/dev/null | head -20

echo ""
echo "=== Find UserSchema files ==="
fd "." packages --type f -e ts | xargs grep -l "UserSchema\|UpdateUser" 2>/dev/null | head -10

echo ""
echo "=== Find user profile route ==="
fd "user" packages/api --type f -e ts | head -20

Length of output: 594


🏁 Script executed:

#!/bin/bash
echo "=== API user profile route ==="
cat packages/api/src/routes/user/index.ts

echo ""
echo "=== User schemas ==="
cat packages/api/src/schemas/users.ts

echo ""
echo "=== DB schema (users table) ==="
grep -A 30 "users\b" packages/api/src/db/schema.ts | head -60

echo ""
echo "=== userStore ==="
fd "store" "apps/expo/features/auth" -e ts | head -5
cat apps/expo/features/auth/store.ts 2>/dev/null || fd "store" apps/expo/features/auth -e ts | head -3 | xargs cat

Length of output: 15013


🏁 Script executed:

#!/bin/bash
echo "=== Find userStore ==="
fd "store" apps/expo/features/auth -e ts 2>/dev/null
find apps/expo/features/auth -name "*.ts" -o -name "*.tsx" 2>/dev/null | head -20

echo ""
echo "=== useUser hook ==="
cat apps/expo/features/auth/hooks/useUser.ts 2>/dev/null || fd "useUser" apps/expo -e ts | xargs cat 2>/dev/null

echo ""
echo "=== Auth store ==="
find apps/expo/features/auth -name "store*" 2>/dev/null | xargs cat 2>/dev/null
find apps/expo/features/auth -name "index*" 2>/dev/null | xargs cat 2>/dev/null

echo ""
echo "=== Auth feature directory structure ==="
find apps/expo/features/auth -type f | head -30

Length of output: 1481


🏁 Script executed:

#!/bin/bash
echo "=== userStore user.ts ==="
cat apps/expo/features/auth/store/user.ts

echo ""
echo "=== userStore index.ts ==="
cat apps/expo/features/auth/store/index.ts

echo ""
echo "=== useImagePicker - check if fileName can be undefined ==="
cat apps/expo/features/packs/hooks/useImagePicker.ts | grep -A5 "fileName"

echo ""
echo "=== Check for image size validation ==="
grep -rn "maxSize\|fileSize\|sizeLimit\|MAX_SIZE\|image.*size\|size.*image" apps/expo/features/packs/ 2>/dev/null | head -10

echo ""
echo "=== Check if there's a missing 'middle' field handling in useUpdateProfile ==="
grep -n "middle" "apps/expo/app/(app)/(tabs)/profile/name.tsx" 2>/dev/null || cat "apps/expo/app/(app)/(tabs)/profile/name.tsx" | grep -n "middle"

Length of output: 3367


@andrew-bierman: Here's a thorough review of PR #1895.


Code Review: Edit Profile Photo & Name

Overall the implementation is well-structured, but there are a few issues ranging from a functional bug to UX gaps.


🔴 High — Middle name is collected but silently discarded

File: apps/expo/app/(app)/(tabs)/profile/name.tsx

The form has a middle field and renders a full TextField for it, but handleSave only forwards firstName and lastName to the backend:

// form.middle is never sent
async function handleSave() {
  const success = await updateProfile({
    firstName: form.first,
    lastName: form.last,   // ← form.middle dropped here
  });

Additionally, canSave only compares first/last against the originals, so the Save button stays disabled even if the user only edits the middle name. Since the backend UpdateUserRequestSchema and the users table don't have a middleName column, the middle name field either needs to be wired up (backend work) or removed entirely to avoid misleading users.


🔴 High — Silent failure after successful R2 upload

File: apps/expo/app/(app)/(tabs)/profile/index.tsxhandleAvatarPress

const remoteFileName = await uploadImage(image.fileName, image.uri);
if (remoteFileName) {
  await updateProfile({ avatarUrl: remoteFileName }); // return value ignored
}

updateProfile returns false on failure but never throws. If the API call fails after a successful R2 upload, the image is silently orphaned in storage and the user sees no feedback. Suggested fix:

if (remoteFileName) {
  const success = await updateProfile({ avatarUrl: remoteFileName });
  if (!success) {
    Alert.alert(t('errors.somethingWentWrong'), t('errors.tryAgain'));
  }
}

🟡 Medium — Upload spinner is invisible when user already has an avatar

File: apps/expo/app/(app)/(tabs)/profile/index.tsxListHeaderComponent

<AvatarFallback>
  {isUploading ? <ActivityIndicator /> : <Text>{initials}</Text>}
</AvatarFallback>

AvatarFallback is only visible when no image is loaded. When avatarUri is set, AvatarImage is rendered and fully covers the fallback, so the ActivityIndicator is never shown to users who are replacing an existing photo. Consider overlaying the spinner directly on the TouchableOpacity:

<TouchableOpacity onPress={handleAvatarPress} disabled={isUploading}>
  <Avatar alt={...} className="h-24 w-24">
    {avatarUri ? <AvatarImage source={{ uri: avatarUri }} /> : null}
    <AvatarFallback><Text>{initials}</Text></AvatarFallback>
  </Avatar>
  {isUploading && (
    <View className="absolute inset-0 items-center justify-center rounded-full bg-black/40">
      <ActivityIndicator color="white" />
    </View>
  )}
</TouchableOpacity>

🟡 Medium — originalFirst/originalLast are recomputed on every render

File: apps/expo/app/(app)/(tabs)/profile/name.tsx

const originalFirst = user?.firstName || '';
const originalLast  = user?.lastName  || '';

These are plain variables re-derived from the live userStore on every render. If the store syncs in the background while the user is typing, the baseline can shift and canSave can unexpectedly flip. Capture the initial values once with useRef:

const initialFirst = React.useRef(user?.firstName || '').current;
const initialLast  = React.useRef(user?.lastName  || '').current;

const canSave =
  (form.first !== initialFirst || form.last !== initialLast) &&
  !!form.first && !!form.last;

🟡 Medium — No image size validation before upload

Neither useImagePicker nor handleAvatarPress checks the file size. A user could select a very large image (the quality: 0.8 compression helps, but doesn't cap it). Consider adding a size guard:

const image = await pickImage();
if (!image) return;

const info = await FileSystem.getInfoAsync(image.uri, { size: true });
const MAX_BYTES = 5 * 1024 * 1024; // 5 MB
if ((info as any).size > MAX_BYTES) {
  Alert.alert(t('errors.imageTooLarge'));
  return;
}

🟢 Low — No camera option exposed in the avatar picker

useImagePicker already has a takePhoto() method, but handleAvatarPress only calls pickImage(). A simple action sheet (e.g., ActionSheetIOS on iOS / a bottom sheet on Android) letting the user choose "Take Photo" vs. "Choose from Library" would be a straightforward UX improvement.


🟢 Low — Old avatar is never deleted from R2

When a user replaces their avatar, the previous R2 object is never cleaned up. This is a minor storage leak. A background cleanup endpoint (or handling it in the PUT /api/user/profile handler on the server side) would address this.


✅ What looks good

  • The useUpdateProfile hook is clean and returns a stable { updateProfile, isLoading, error } interface.
  • buildPackTemplateItemImageUrl correctly handles both relative R2 keys and absolute URLs.
  • Backend PUT /api/user/profile correctly uses partial updates (if (field !== undefined)) so callers only send what they want to change.
  • The migration (0033) is minimal and correct; the column is nullable which is the right default.
  • handleAvatarPress correctly filters the Permission denied error to avoid showing an alert for a user-initiated cancellation.

@andrew-bierman
Copy link
Collaborator

@copilot CodeRabbit has provided review feedback on this PR. Please review and address the comments.

- Remove misleading middle name field (no DB column for it)
- Fix silent failure: check updateProfile return value after R2 upload
- Fix upload spinner: overlay on TouchableOpacity so it shows over avatar images
- Fix canSave baseline: use useRef to capture initial name values at mount
- Add 5 MB file size validation before avatar upload
- Add profile.imageTooLarge translation key

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot CodeRabbit has provided review feedback on this PR. Please review and address the comments.

Addressed all CodeRabbit feedback in d5d8af0:

  • 🔴 Middle name removed — Removed the middle name TextField entirely since there's no middleName column in the DB schema. It was collecting input that was silently discarded.
  • 🔴 Silent upload failure fixedupdateProfile return value is now checked; an error alert is shown if the API call fails after a successful R2 upload.
  • 🟡 Upload spinner now visible over existing avatars — Moved ActivityIndicator out of AvatarFallback (which is covered by AvatarImage) and into a semi-transparent absolute overlay on the TouchableOpacity, so it's visible regardless of whether the user has an existing photo.
  • 🟡 canSave baseline stabilized — Changed from plain variables (re-derived from live userStore every render) to React.useRef so initial name values are captured once at mount and won't drift due to background store syncs.
  • 🟡 Image size validation added — Added a 5 MB limit check via FileSystem.getInfoAsync before uploading, with a user-facing alert and a new profile.imageTooLarge translation key.

@andrew-bierman
Copy link
Collaborator

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Warning

Rate limit exceeded

@andrew-bierman has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 21 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0d737df5-5c4c-453a-8a9a-4d1431437bcd

📥 Commits

Reviewing files that changed from the base of the PR and between d5d8af0 and 7ee88e9.

📒 Files selected for processing (3)
  • apps/expo/app/(app)/(tabs)/profile/index.tsx
  • apps/expo/app/(app)/(tabs)/profile/name.tsx
  • apps/expo/lib/i18n/locales/en.json
📝 Walkthrough

Walkthrough

This pull request implements user profile customization functionality by enabling avatar uploads and name editing. Changes include a new profile update hook, database schema extension for avatar storage, API route updates to handle avatar persistence, and UI modifications to support image selection, validation, and editing with error handling.

Changes

Cohort / File(s) Summary
Avatar Upload & Profile Update (Frontend)
apps/expo/app/(app)/(tabs)/profile/index.tsx, apps/expo/app/(app)/(tabs)/profile/name.tsx, apps/expo/features/profile/hooks/useUpdateProfile.ts, apps/expo/features/profile/types.ts
Adds avatar upload flow with image picker, 5 MB size validation, and avatar display. Updates name screen to fetch user data, dynamically initialize form fields, persist changes via new useUpdateProfile hook, and validate against initial values. Introduces UpdateProfilePayload type and extends User interface with optional avatarUrl field.
Localization
apps/expo/lib/i18n/locales/en.json
Adds "imageTooLarge" localization key under profile section and converts literal bullet characters to Unicode escapes for consistency.
Database Schema & Migration
packages/api/drizzle/0033_add_avatar_url_to_users.sql, packages/api/drizzle/meta/_journal.json, packages/api/src/db/schema.ts
Adds avatar_url text column to users table and updates schema definition to include avatarUrl field in User type.
API Routes & Schemas
packages/api/src/routes/user/index.ts, packages/api/src/schemas/users.ts
Updates GET and PUT profile endpoints to include avatarUrl in request/response payloads and adds corresponding optional avatarUrl fields to UserSchema and UpdateUserRequestSchema with OpenAPI metadata.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant ProfileUI as Profile Screen
    participant ImagePicker as Image Picker
    participant FileSystem as File System
    participant APIClient as API Client
    participant Server as Backend API
    participant Database as Database
    participant UserStore as User Store

    User->>ProfileUI: Tap avatar
    ProfileUI->>ImagePicker: Open image picker
    ImagePicker-->>ProfileUI: Return selected image
    ProfileUI->>FileSystem: Validate file size
    alt File size > 5 MB
        ProfileUI-->>User: Show "Image too large" error
    else File size valid
        ProfileUI->>APIClient: Upload image (PUT /api/user/profile)
        APIClient->>Server: Send avatarUrl payload
        Server->>Database: Update user avatarUrl
        Database-->>Server: Confirm update
        Server-->>APIClient: Return updated user profile
        APIClient->>UserStore: Update user data
        UserStore-->>ProfileUI: Refresh avatar display
        ProfileUI-->>User: Show updated avatar
    end
Loading
sequenceDiagram
    actor User
    participant NameScreen as Name Screen
    participant UserStore as User Store
    participant APIClient as API Client
    participant Server as Backend API
    participant Database as Database

    NameScreen->>UserStore: Fetch user data (useUser)
    UserStore-->>NameScreen: Return firstName, lastName
    NameScreen->>NameScreen: Initialize form with current values
    User->>NameScreen: Edit name fields
    User->>NameScreen: Tap Save
    NameScreen->>APIClient: Call updateProfile(firstName, lastName)
    APIClient->>Server: PUT /api/user/profile
    Server->>Database: Persist firstName, lastName
    Database-->>Server: Confirm update
    Server-->>APIClient: Return updated user profile
    APIClient->>UserStore: Update user store
    alt Update successful
        NameScreen->>NameScreen: Navigate back
    else Update failed
        NameScreen-->>User: Show error alert
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A cottontail hops with avatar glee,
Profile photos now editable—hooray, hooray!
Names can be changed with a simple keystroke,
Five megabytes max—no image joke! 📸
Personalization blooms in the app today!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% 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: Add edit profile photo and name functionality' directly and concisely summarizes the main objective of the changeset—enabling users to edit both their profile photo and name.
Linked Issues check ✅ Passed The PR fully addresses issue #1818 by implementing avatar upload with image picker, name edit screen with save functionality, 5 MB size validation, and backend persistence via the new useUpdateProfile hook.
Out of Scope Changes check ✅ Passed All changes are directly scoped to profile editing: avatarUrl field addition, API/schema updates, image picker integration, form save logic, and related localization entries. No extraneous modifications detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch copilot/fix-181429480-611845080-ff4480f5-c4d0-4cb0-8105-05dee4f3db38

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.

Actionable comments posted: 3

🧹 Nitpick comments (3)
packages/api/src/schemas/users.ts (1)

73-76: Consider adding URL format validation for avatarUrl.

The schema accepts any string for avatarUrl. If the avatar URLs are expected to be valid URLs (e.g., from R2 storage), consider adding .url() validation to catch malformed inputs early:

-    avatarUrl: z.string().nullable().optional().openapi({
+    avatarUrl: z.string().url().nullable().optional().openapi({

This would provide better input validation at the API boundary.

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

In `@packages/api/src/schemas/users.ts` around lines 73 - 76, The avatarUrl Zod
schema currently allows any string; update the avatarUrl field in the users
schema to validate that the value is a proper URL by chaining the Zod url()
validator (e.g., avatarUrl: z.string().url().nullable().optional()) so malformed
URLs are rejected at the API boundary while preserving nullable/optional
behavior; ensure you apply the .url() call on the same schema symbol used in
this file so OpenAPI example/description remain unchanged.
apps/expo/features/profile/hooks/useUpdateProfile.ts (1)

12-35: Refactor to use React Query's useMutation hook.

As per coding guidelines, Expo feature hooks should use React Query hooks (useQuery, useMutation, useInfiniteQuery) for data fetching. The current manual state management works but misses React Query benefits like retry logic, cache management, and consistent error handling patterns.

♻️ Proposed refactor using useMutation
-import { useState } from 'react';
+import { useMutation } from '@tanstack/react-query';

 export function useUpdateProfile() {
-  const [isLoading, setIsLoading] = useState(false);
-  const [error, setError] = useState<string | null>(null);
-
-  const updateProfile = async (payload: UpdateProfilePayload): Promise<boolean> => {
-    setIsLoading(true);
-    setError(null);
-    try {
-      const response = await axiosInstance.put('/api/user/profile', payload);
-      if (response.data?.user) {
-        userStore.set(response.data.user);
-      }
-      return true;
-    } catch (err) {
-      const { message } = handleApiError(err);
-      setError(message);
-      return false;
-    } finally {
-      setIsLoading(false);
-    }
-  };
-
-  return { updateProfile, isLoading, error };
+  const mutation = useMutation({
+    mutationFn: async (payload: UpdateProfilePayload) => {
+      const response = await axiosInstance.put('/api/user/profile', payload);
+      return response.data;
+    },
+    onSuccess: (data) => {
+      if (data?.user) {
+        userStore.set(data.user);
+      }
+    },
+  });
+
+  const updateProfile = async (payload: UpdateProfilePayload): Promise<boolean> => {
+    try {
+      await mutation.mutateAsync(payload);
+      return true;
+    } catch (err) {
+      return false;
+    }
+  };
+
+  const error = mutation.error ? handleApiError(mutation.error).message : null;
+
+  return { updateProfile, isLoading: mutation.isPending, error };
 }

As per coding guidelines: "Use React Query hooks (useQuery, useMutation, useInfiniteQuery) for data fetching in Expo feature hooks".

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

In `@apps/expo/features/profile/hooks/useUpdateProfile.ts` around lines 12 - 35,
Replace the manual stateful hook useUpdateProfile with a React Query mutation:
convert updateProfile into a useMutation that calls
axiosInstance.put('/api/user/profile', payload), uses onSuccess to update
userStore.set(response.data.user) when present, and uses onError to extract the
message via handleApiError(err) and set it into the mutation's error state (or
into local state if you keep error/isLoading wrappers). Ensure you export the
mutation's mutate/mutateAsync, isLoading, error (or map error.message) instead
of the current updateProfile boolean pattern so callers use the React Query API
and benefit from retries/cache.
packages/api/drizzle/meta/_journal.json (1)

243-249: Migration timestamp is out of chronological order.

The when timestamp for migration 0033 (1741516482000 → ~March 2025) is earlier than the preceding migration 0032 (1760175950793 → ~September 2025). While Drizzle typically orders migrations by the idx and filename rather than the when field, this inconsistency could be confusing for auditing purposes. Consider regenerating the migration with a current timestamp if this causes any tooling issues.

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

In `@packages/api/drizzle/meta/_journal.json` around lines 243 - 249, The
migration entry with idx 33 (tag "0033_add_avatar_url_to_users") has a "when"
timestamp (1741516482000) that is older than the previous migration; update the
"when" value on that JSON object to a current/monotonic timestamp greater than
the previous migration's timestamp (e.g., >1760175950793) or regenerate the
migration so the "when" field reflects a chronological ordering to avoid
confusion in auditing/tooling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/expo/app/`(app)/(tabs)/profile/index.tsx:
- Around line 157-160: The catch block in
apps/expo/app/(app)/(tabs)/profile/index.tsx currently swallows the specific
permission-denied error thrown by useImagePicker.ts; update the catch so when
err instanceof Error && err.message === 'Permission to access media library was
denied' you show a permission-specific Alert (with text from i18n keys if
available) offering a button to open app settings (Linking.openSettings()) and a
cancel action, otherwise fall back to the existing generic Alert for other
errors; keep the existing error-check branch for non-permission errors intact.

In `@apps/expo/app/`(app)/(tabs)/profile/name.tsx:
- Around line 32-40: The save logic treats whitespace-only names as valid;
update canSave and handleSave to use trimmed values: compute trimmedFirst =
form.first.trim() and trimmedLast = form.last.trim(), then change the canSave
check to compare trimmedFirst/trimmedLast against
initialFirst.current/initialLast.current (also trim those or normalize when
stored) and require !!trimmedFirst && !!trimmedLast; when calling updateProfile
in handleSave, pass trimmedFirst and trimmedLast so the API receives trimmed
values. Ensure consistency between the comparison and submission (use the same
trimmed variables).
- Around line 37-47: Guard handleSave to prevent submitting when save is not
allowed or already in progress: at the start of the async function handleSave,
check the same conditions used by the button disabled state (e.g., if (!canSave
|| isLoading) return;), so it returns early when inputs are unchanged/invalid or
while a submission is underway; ensure you reference the existing symbols
handleSave, canSave, isLoading, updateProfile, form and preserve the existing
success/failure flow (router.back and Alert) only when the guard passes.

---

Nitpick comments:
In `@apps/expo/features/profile/hooks/useUpdateProfile.ts`:
- Around line 12-35: Replace the manual stateful hook useUpdateProfile with a
React Query mutation: convert updateProfile into a useMutation that calls
axiosInstance.put('/api/user/profile', payload), uses onSuccess to update
userStore.set(response.data.user) when present, and uses onError to extract the
message via handleApiError(err) and set it into the mutation's error state (or
into local state if you keep error/isLoading wrappers). Ensure you export the
mutation's mutate/mutateAsync, isLoading, error (or map error.message) instead
of the current updateProfile boolean pattern so callers use the React Query API
and benefit from retries/cache.

In `@packages/api/drizzle/meta/_journal.json`:
- Around line 243-249: The migration entry with idx 33 (tag
"0033_add_avatar_url_to_users") has a "when" timestamp (1741516482000) that is
older than the previous migration; update the "when" value on that JSON object
to a current/monotonic timestamp greater than the previous migration's timestamp
(e.g., >1760175950793) or regenerate the migration so the "when" field reflects
a chronological ordering to avoid confusion in auditing/tooling.

In `@packages/api/src/schemas/users.ts`:
- Around line 73-76: The avatarUrl Zod schema currently allows any string;
update the avatarUrl field in the users schema to validate that the value is a
proper URL by chaining the Zod url() validator (e.g., avatarUrl:
z.string().url().nullable().optional()) so malformed URLs are rejected at the
API boundary while preserving nullable/optional behavior; ensure you apply the
.url() call on the same schema symbol used in this file so OpenAPI
example/description remain unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 96d558dc-7834-431e-b1e6-dd4a8c23295b

📥 Commits

Reviewing files that changed from the base of the PR and between 3f94763 and d5d8af0.

📒 Files selected for processing (10)
  • apps/expo/app/(app)/(tabs)/profile/index.tsx
  • apps/expo/app/(app)/(tabs)/profile/name.tsx
  • apps/expo/features/profile/hooks/useUpdateProfile.ts
  • apps/expo/features/profile/types.ts
  • apps/expo/lib/i18n/locales/en.json
  • packages/api/drizzle/0033_add_avatar_url_to_users.sql
  • packages/api/drizzle/meta/_journal.json
  • packages/api/src/db/schema.ts
  • packages/api/src/routes/user/index.ts
  • packages/api/src/schemas/users.ts

@andrew-bierman
Copy link
Collaborator

@copilot Please address the CodeRabbit review feedback on this PR (3 inline comments).

@andrew-bierman
Copy link
Collaborator

@copilot Reminder: please address the CodeRabbit review feedback on this PR.

- Show permission-specific alert with Open Settings button when photo
  library access is denied (instead of silently swallowing the error)
- Trim whitespace in canSave and handleSave so whitespace-only names
  are rejected; use useMemo to avoid duplication and redundant trims
- Guard handleSave with !canSave || isLoading to prevent double-submit
  via keyboard Return key bypassing the disabled button state
- Add permissions and common.cancel i18n keys

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
@andrew-bierman
Copy link
Collaborator

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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 Request: Add functionality to edit profile photo and name

2 participants