What It Does
The profile is the hub for a user’s content, stats, and social connections. Supports viewing, editing, password/email changes, and account deletion.
User Flow — View
- Own profile: Profile tab →
app/(tabs)/profile.tsx
- Other user: Tap any username/avatar →
useProfileNavigation().navigateToProfile(username) pushes app/profile/[username].tsx
- Shows: header title (
@username), ProfileCard (avatar, name, location, bio, Instagram shortcut, followers/posts/recipes stats, edit/follow action), then icon-only content tabs
The header bar changes based on context:
- Own profile:
Plus icon (left) → /create, @username (center), NotificationBell (right)
- Other user:
ChevronLeft icon (left) → router.back(), @username (center), empty (right)
Content Tabs
Three icon-only tabs below the profile card (active tab has 2px bottom border):
| Tab | Icon | Content | Empty State |
|---|
| Posts | ImageIcon | User’s posts | NoPosts |
| Recipes | Film | User’s recipes | NoRecipes |
| Collections | LibraryBig | User’s public collections | ”No collections yet” |
Default tab is Posts. Each tab shows content in a MasonryGrid with variant="imageOnly" cards, sorted by created_at descending. Collections are loaded separately in background via dedicated hooks for faster initial profile load.
Loading State
ProfilePageSkeleton — centered avatar circle (120px), name/username placeholders, 3-column stats row, bio lines, action button, then 3 tab placeholders + MasonryGridSkeleton.
Unauthenticated State
If not logged in, the own-profile tab shows “Sign in to view your profile” with Sign In / Sign Up buttons.
User Flow — Edit
Own profile → “Edit Profile” button on ProfileCard → app/settings.tsx:
Settings Tabs
Two text tabs: Profile and Account.
Profile Tab:
- Profile photo (
ProfileImagePicker, 80px)
- First name, surname, username (with real-time availability check — green checkmark when available, red alert when taken, spinner while checking), location, bio (120 char limit with counter), Instagram handle
- Save Changes button (disabled while updating or username unavailable)
- All changes confirmed via
Alert.alert before submission
- Submit →
supabase.rpc('update_user_profile', { p_user_id, ... }) directly
Account Tab:
- Email display + Change button → inline email change form with confirmation
- Appearance →
ThemeSelector (light/dark/system)
- Change Password → new password + confirm fields
- Sign Out (destructive confirmation)
- Delete Account (destructive confirmation, red text)
Password & Email Changes
- Password change: From
app/settings.tsx, calls Supabase auth updateUser({ password }) directly. Minimum 8 characters, must match confirmation.
- Email change: Calls
updateUser({ email }) → triggers double-confirmation flow (see Login journey for full callback chain). Shows “Confirmation email sent to your new address” toast on success.
Account Deletion
- Mobile calls
supabase.rpc('delete_user_account', { p_user_id }) directly
- Cascading deletion of all user content
- Removes user from Loops.so (via Loops API call)
- Clears session and navigates to login
- Requires
Alert.alert destructive confirmation
Screens
| Screen | Purpose |
|---|
app/(tabs)/profile.tsx | Own profile (Profile tab) |
app/profile/[username].tsx | Other user’s profile |
app/connections/[username].tsx | Combined followers/following view |
app/followers/[username].tsx | Follower list |
app/following/[username].tsx | Following list |
app/settings.tsx | Profile editing + app settings |
Components
| Component | Purpose |
|---|
ProfileCard | Profile header with followers/posts/recipes stats and primary action button |
FollowButton | Follow/unfollow toggle |
ThemeSelector | Dark/light/system theme picker |
ProfileImagePicker | Avatar/cover photo picker |
Context
ProfileProvider (src/context/ProfileContext.tsx) — exposes navigateToProfile(username), a guarded route helper that pushes the dedicated app/profile/[username].tsx screen.
Hooks
| Hook | Purpose |
|---|
useProfileData(username) | Profile data by username (infinite query via get_user_profile_data RPC) |
useProfileDataById(userId) | Profile data by ID (for own profile, via get_user_profile_data_by_id RPC) |
useProfileCollections(username) | Collections loaded separately in background (via get_user_profile_collections RPC) |
useProfileCollectionsById(userId) | Collections by ID (via get_user_profile_collections_by_id RPC) |
useUserProfile(userId) | Basic profile + follow status (direct table query, not RPC) |
useProfileUpdate | Profile edit mutations |
useTabAvatar(userId) | Lightweight avatar-only query for tab bar icon |
useUserByUsername(username) | Basic profile lookup by username |
useSearchUsers(query) | User search for @mentions and tagging |
useUsernameCheck | Debounced username availability check via RPC |
profileTransforms | Data transformation utilities (camelCase RPC → snake_case mobile) |
Data Access Pattern
The mobile app calls Supabase RPC functions directly for profile data — supabase.rpc('get_user_profile_data'), supabase.rpc('update_user_profile'), etc. However, useUserProfile(userId) in useUser.ts does a direct table query (supabase.from('profiles').select(...)) plus a separate follow status check — it does NOT use an RPC. The web API routes exist for the web client (Phase 2).
Web API Routes (Phase 2)
| Method | Route | Purpose |
|---|
GET | /api/users/[id]/profile-data | Get full profile (calls get_user_profile_data or get_user_profile_data_by_id RPC) |
PATCH | /api/users/me | Update own profile (calls update_user_profile RPC) |
GET | /api/users/[id] | Get basic user info |
DELETE | /api/users/[id] | Delete account (calls delete_user_account RPC) |
GET | /api/users/[id]/recipes | User’s recipes |
GET | /api/users/[id]/collections | User’s collections |
GET | /api/users/[id]/favorites | User’s favorites |
GET | /api/users/[id]/saved-items | User’s saved items |
GET | /api/users/[id]/reposts | User’s reposts |
GET | /api/users/search | Search users (for @mentions, user tagging) |
RPC Functions
| Function | Parameters | Returns |
|---|
get_user_profile_data | p_username, p_current_user_id, p_limit (8), p_offset (0) | JSON — single round-trip: profile + recipes + posts + collections + follow status + stats |
get_user_profile_data_by_id | p_user_id, p_current_user_id, p_limit (8), p_offset (0) | JSON — same as above but by UUID |
get_user_profile_collections | p_username, p_limit (8), p_offset (0) | JSON — user’s collections (paginated) |
get_user_profile_collections_by_id | p_user_id, p_limit (8), p_offset (0) | JSON — same by UUID |
update_user_profile | p_user_id, p_username, p_first_name, p_surname, p_location, p_bio, p_instagram_handle, p_avatar_url, p_cover_photo_url (all optional) | SETOF profiles — returns updated profile row |
delete_user_account | p_user_id | void — cascading account deletion |
Database (profiles table)
| Column | Type | Notes |
|---|
id | uuid PK | FK→auth.users.id |
username | text UNIQUE | Required |
first_name | text | Required |
surname | text | Required |
location | text | Optional, default ” |
bio | text | Optional, default ” |
instagram_handle | text | Optional |
avatar_url | text | Optional |
cover_photo_url | text | Optional |
created_at | timestamptz | Default now() |
updated_at | timestamptz | Default now(), auto-updated via trigger |
recipe_count | int | Denormalized, default 0 |
post_count | int | Denormalized, default 0 |
collection_count | int | Denormalized, default 0 |
follower_count | int | Denormalized, default 0 |
following_count | int | Denormalized, default 0 |
RLS Policies
- SELECT:
true (public — all profiles visible)
- INSERT:
auth.uid() = id
- UPDATE:
auth.uid() = id
Tab Bar Avatar
The Profile tab icon shows the user’s avatar (via useTabAvatar hook + ExpoImage) instead of a generic User icon when logged in.