Skip to main content

Overview

The Friend System allows users to connect with other ParkIn community members, view their profiles, share parking experiences, and track social achievements together. The system uses Firebase’s friend_requests collection to manage relationships and provides real-time updates.

Key Features

Three Main Tabs

The Community screen (app/AmigosScreen.tsx) organizes social features into three tabs:

Friends

View your current friends list with profile access

Requests

Manage incoming friend requests with accept/reject

Search

Discover new users and see personalized suggestions

Adding Friends

Search by Username

Users can find others by searching their username:
// From hooks/useAmigos.ts
const searchUsers = (text: string) => {
  const q = query(
    collection(db, "users"), 
    where("role", "==", "client"),
    where("username", ">=", text),
    where("username", "<=", text + '\uf8ff'),
    limit(5)
  );
};
Search queries include a 500ms debounce to prevent excessive Firebase calls while typing

QR Code Scanning

For in-person connections, users can scan a friend’s QR code:
  1. Open Scanner: Tap the scan icon in the Community header
  2. Camera Permission: App requests camera access if not granted
  3. Scan QR: Point camera at friend’s profile QR code
  4. Auto Navigate: Instantly redirects to their public profile
// From app/AmigosScreen.tsx:26
const handleScan = (scannedId: string) => {
  router.push({ 
    pathname: "/PerfilPublicoScreen", 
    params: { userId: scannedId } 
  });
};
The scanner validates QR codes using the parkinmx: prefix format:
// From components/ScannerModal.tsx:27
if (data.startsWith('parkinmx:')) {
  const userId = data.split(':')[1];
  onScanned(userId);
}

Friend Suggestions

When not searching, users see suggested connections:
  • Shows up to 10 client users
  • Filters out existing friends
  • Displays current relationship status (Friends/Pending/Follow)
const renderSuggestion = ({ item }: any) => {
  const isFriend = friends.some(f => f.id === item.id);
  const isPending = sentRequests.includes(item.id);
  
  // Shows "Amigos", "Pendiente", or "Seguir" button
};

Managing Requests

Sending Requests

When sending a friend request:
const sendFriendRequest = async (targetUserId: string) => {
  await addDoc(collection(db, "friend_requests"), {
    fromId: currentUid,
    toId: targetUserId,
    status: "pending",
    createdAt: new Date()
  });
};

Responding to Requests

Recipients can accept or reject:
1

View Request

Incoming requests appear in the Requests tab with sender info
2

Accept or Reject

Tap checkmark to accept or X to reject
3

Update Status

Accept changes status to “accepted”, reject deletes the request
4

Refresh Lists

Friend list automatically updates after action
const respondRequest = async (requestId: string, accept: boolean) => {
  const ref = doc(db, "friend_requests", requestId);
  if (accept) {
    await updateDoc(ref, { status: "accepted" });
    Alert.alert("¡Nuevo Amigo!", "Ahora están conectados.");
  } else {
    await deleteDoc(ref);
  }
};

Viewing Profiles

Public Profile Screen

Tapping any user opens their public profile (app/PerfilPublicoScreen.tsx) showing:

Profile Info

Avatar, username, display name, and level title

Level & XP

Current level, total XP, and progress bar

Achievements

Unlocked badges and locked goals

Statistics

Reservations, clean records, cards, and friend count

Profile Actions

The profile shows different actions based on relationship:
  • None: “Agregar a Amigos” button
  • Pending: “Solicitud Pendiente” (disabled)
  • Accepted: “Amigos” with checkmark
  • Self: No action button (viewing own profile)
// From app/PerfilPublicoScreen.tsx:90
const fetchFriendData = async () => {
  if (userId === currentUid) {
    setFriendStatus('self');
  } else {
    // Check for existing friend_requests
    const q1 = query(collection(db, "friend_requests"), 
      where("fromId", "==", currentUid), 
      where("toId", "==", userId));
    // Determines status: none, pending, or accepted
  }
};

Removing Friends

Delete Confirmation

Removing a friend requires confirmation:
const confirmDelete = (friendId: string, name: string) => {
  Alert.alert(
    "Eliminar Amigo",
    `¿Estás seguro de eliminar a ${name}?`,
    [
      { text: "Cancelar", style: "cancel" },
      { 
        text: "Eliminar", 
        style: "destructive", 
        onPress: () => removeFriend(friendId) 
      }
    ]
  );
};

Bi-directional Cleanup

Deletion checks both directions to ensure complete removal:
const removeFriend = async (friendId: string) => {
  // Query both directions
  const q1 = query(collection(db, "friend_requests"), 
    where("fromId", "==", currentUid), 
    where("toId", "==", friendId));
  const q2 = query(collection(db, "friend_requests"), 
    where("fromId", "==", friendId), 
    where("toId", "==", currentUid));
  
  // Delete all matching documents
  const [snap1, snap2] = await Promise.all([getDocs(q1), getDocs(q2)]);
  snap1.forEach(async d => await deleteDoc(d.ref));
  snap2.forEach(async d => await deleteDoc(d.ref));
};

Social Achievements

Friends contribute to gamification:

XP Rewards

// From app/PerfilPublicoScreen.tsx:58
totalXP += currentStats.friendsCount * 50; // 50 XP per friend

Primer Amigo

Unlocked with 1+ friends

Alma de la Fiesta

Unlocked with 10+ friends
const achievementsList = [
  { 
    id: 6, 
    title: "Primer Amigo", 
    icon: "people", 
    color: "#2196F3", 
    unlocked: stats.friendsCount >= 1 
  },
  { 
    id: 7, 
    title: "Alma de la Fiesta", 
    icon: "globe", 
    color: "#9C27B0", 
    unlocked: stats.friendsCount >= 10 
  }
];

Smart Avatar System

All profile pictures use the SmartAvatar component for consistent display:
<SmartAvatar 
  uri={item.photoURL} 
  userId={item.id} 
  size={45} // Optional, defaults to 50
/>
This component handles:
  • Photo URL loading
  • Fallback avatars
  • Consistent sizing across the app

Data Management

useAmigos Hook

The hooks/useAmigos.ts hook centralizes all friend operations:
return {
  friends,           // Array of accepted friends
  requests,          // Array of pending incoming requests
  suggestions,       // Array of suggested users (filtered)
  searchResults,     // Array of search results
  loading,           // Boolean loading state
  sentRequests,      // Array of outgoing request IDs
  searchUsers,       // Function to search
  sendFriendRequest, // Function to send request
  respondRequest,    // Function to accept/reject
  removeFriend,      // Function to remove friend
  refreshData        // Function to reload all data
};

Refresh Data

Users can manually refresh using the refresh icon:
const refreshData = () => {
  fetchFriends();
  fetchRequests();
};
The search query requires a composite index in Firebase. If users experience errors, check the Firebase console for the index creation link.

UI Components

Friend Card

Each friend displays:
  • Avatar (tappable to view profile)
  • Username (@username)
  • Display name
  • Delete button (trash icon)

Request Card

Each request shows:
  • Sender’s avatar
  • Username and “Solicitud de amistad” text
  • Reject button (red X)
  • Accept button (green checkmark)

Search/Suggestion Card

Each result displays:
  • Avatar (tappable to view profile)
  • Username and display name
  • Status indicator or follow button
All cards are tappable to navigate to the user’s public profile for more details

Build docs developers (and LLMs) love