Skip to main content

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

Profile Header

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):
TabIconContentEmpty State
PostsImageIconUser’s postsNoPosts
RecipesFilmUser’s recipesNoRecipes
CollectionsLibraryBigUser’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 ProfileCardapp/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

ScreenPurpose
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

ComponentPurpose
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

HookPurpose
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)

MethodRoutePurpose
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

FunctionParametersReturns
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)

ColumnTypeNotes
iduuid PKFK→auth.users.id
usernametext UNIQUERequired
first_nametextRequired
surnametextRequired
locationtextOptional, default ”
biotextOptional, default ”
instagram_handletextOptional
avatar_urltextOptional
cover_photo_urltextOptional
created_attimestamptzDefault now()
updated_attimestamptzDefault now(), auto-updated via trigger
recipe_countintDenormalized, default 0
post_countintDenormalized, default 0
collection_countintDenormalized, default 0
follower_countintDenormalized, default 0
following_countintDenormalized, 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.