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 ImageIconUser’s posts NoPostsRecipes FilmUser’s recipes NoRecipesCollections LibraryBigUser’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.tsxOwn profile (Profile tab) app/profile/[username].tsxOther user’s profile app/connections/[username].tsxCombined followers/following view app/followers/[username].tsxFollower list app/following/[username].tsxFollowing list app/settings.tsxProfile editing + app settings
Components
Component Purpose ProfileCardProfile header with followers/posts/recipes stats and primary action button FollowButtonFollow/unfollow toggle ThemeSelectorDark/light/system theme picker ProfileImagePickerAvatar/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) useProfileUpdateProfile 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 useUsernameCheckDebounced username availability check via RPC profileTransformsData 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-dataGet full profile (calls get_user_profile_data or get_user_profile_data_by_id RPC) PATCH/api/users/meUpdate 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]/recipesUser’s recipes GET/api/users/[id]/collectionsUser’s collections GET/api/users/[id]/favoritesUser’s favorites GET/api/users/[id]/saved-itemsUser’s saved items GET/api/users/[id]/repostsUser’s reposts GET/api/users/searchSearch users (for @mentions, user tagging)
RPC Functions
Function Parameters Returns get_user_profile_datap_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_idp_user_id, p_current_user_id, p_limit (8), p_offset (0)JSON — same as above but by UUID get_user_profile_collectionsp_username, p_limit (8), p_offset (0)JSON — user’s collections (paginated) get_user_profile_collections_by_idp_user_id, p_limit (8), p_offset (0)JSON — same by UUID update_user_profilep_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_accountp_user_idvoid — cascading account deletion
Database (profiles table)
Column Type Notes iduuid PK FK→auth.users.id usernametext UNIQUE Required first_nametext Required surnametext Required locationtext Optional, default ” biotext Optional, default ” instagram_handletext Optional avatar_urltext Optional cover_photo_urltext Optional created_attimestamptz Default now() updated_attimestamptz Default now(), auto-updated via trigger recipe_countint Denormalized, default 0 post_countint Denormalized, default 0 collection_countint Denormalized, default 0 follower_countint Denormalized, default 0 following_countint 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.
Image Upload Avatar and cover photo
Registration Profile creation on signup