# Use Friend In-Game

In this How-to, we will implement a mutual Friends system in-game using LootLocker. You will learn how to send friend requests, accept or decline them, list your friends and pending requests, and handle blocking relationships. This creates opt-in, two-way connections between players.

## Prerequisites

* [A LootLocker account and a created game](https://lootlocker.com/sign-up)
* Multiple player profiles (e.g. create via separate sessions)
* [An active Game Session](https://docs.lootlocker.com/players/authentication)
* (Optional) Some player ULIDs collected from the [Player Manager](https://console.lootlocker.com/players)
* (Optional) Familiarity with [managing relationships in the Web Console](https://docs.lootlocker.com/players/friends-and-followers/how-to/manage-relationships-web-console)

## Friends vs Followers

Friends are a two-way relationship requiring mutual consent. Player A sends a friend request to Player B, and they only become friends once B accepts. This powers you to build:

* Private messaging systems
* Co-op gameplay invitations
* Trusted player groups for guilds or parties
* Mutual achievement sharing and challenges

If you need one-way relationships that don't require approval, use Followers instead (see: [Use Followers in Game](https://docs.lootlocker.com/players/friends-and-followers/how-to/use-followers-in-game)).

## Core Concepts

* **Friends list**: Players who have mutually accepted each other's friend requests.
* **Incoming requests**: Friend requests sent to the current player by others.
* **Outgoing requests**: Friend requests the current player has sent but not yet accepted/declined.
* **Blocked players**: Players who cannot send friend requests or interact with the current player.
* **Offset pagination**: Lists use page numbers and per-page counts to paginate the data.

## Typical UX Flow

1. Open Social UI → fetch friend counts for header
2. Browse friends list, incoming requests, or outgoing requests in tabs
3. Send friend request from player profile → appears in target's incoming list
4. Accept/decline incoming requests → moves to friends list or disappears
5. Cancel outgoing requests if changed mind
6. Block/unblock players to manage harassment

## List Friends and Requests

The foundation of any friends system is showing existing relationships and pending requests.

{% tabs %}
{% tab title="Unity" %}

```csharp
// Listing Friends, Incoming/Outgoing Requests with Offset Pagination
// Unity Friends API uses Page (0-indexed) and PerPage to paginate the data.
// Assumptions:
// - UI layer methods: UIFriendsList.SetLoading(bool), UIFriendsList.ReplaceItems(array), etc.
// - Error handling via LogFailure(context, response)

using System.Collections.Generic;
using LootLocker.Requests;

public class FriendsListManager
{
    private readonly List<LootLockerAcceptedFriend> _friends = new();
    private readonly List<LootLockerFriend> _incomingRequests = new();
    private readonly List<LootLockerFriend> _outgoingRequests = new();
    private readonly List<LootLockerBlockedPlayer> _blockedPlayers = new();

    // Page tracking (0-indexed)
    private int _friendsCurrentPage = 0;
    private int _incomingCurrentPage = 0;
    private int _outgoingCurrentPage = 0;
    private int _blockedCurrentPage = 0;

    private const int PageSize = 20;

    // Friends List
    public void LoadFriendsPage()
    {
        _friends.Clear();
        _friendsCurrentPage = 0;
        UIFriendsList.SetLoading(true);
        
        LootLockerSDKManager.ListFriendsPaginated(PageSize, _friendsCurrentPage, (resp) =>
        {
            UIFriendsList.SetLoading(false);
            if (!resp.success)
            {
                LogFailure("Friends list", resp);
                return;
            }

            if (resp.friends != null)
            {
                _friends.AddRange(resp.friends);
                UIFriendsList.ReplaceItems(resp.friends);
            }

            bool hasMore = resp.friends != null && resp.pagination.total > _friends.Length;
            UIFriendsList.SetHasMorePages(hasMore);
        });
    }

    public void LoadMoreFriends()
    {
        _friendsCurrentPage++;
        UIFriendsList.SetLoading(true);
        
        LootLockerSDKManager.ListFriendsPaginated(PageSize, _friendsCurrentPage, (resp) =>
        {
            UIFriendsList.SetLoading(false);
            if (!resp.success)
            {
                LogFailure("Friends list (more)", resp);
                _friendsCurrentPage--; // Rollback page increment
                return;
            }

            if (resp.friends != null && resp.friends.Length > 0)
            {
                _friends.AddRange(resp.friends);
                UIFriendsList.AppendItems(resp.friends);
            }

            bool hasMore = resp.friends != null && resp.pagination.total > _friends.Length;
            UIFriendsList.SetHasMorePages(hasMore);
        });
    }

    // Incoming Friend Requests
    public void LoadIncomingRequestsFirstPage()
    {
        _incomingRequests.Clear();
        _incomingCurrentPage = 0;
        UIIncomingRequestsList.SetLoading(true);
        
        LootLockerSDKManager.ListIncomingFriendRequestsPaginated(PageSize, _incomingCurrentPage, (resp) =>
        {
            UIIncomingRequestsList.SetLoading(false);
            if (!resp.success)
            {
                LogFailure("Incoming requests", resp);
                return;
            }

            if (resp.incoming != null)
            {
                _incomingRequests.AddRange(resp.incoming);
                UIIncomingRequestsList.ReplaceItems(resp.incoming);
            }

            bool hasMore = resp.incoming != null && resp.pagination.total > _incomingRequests.Length;
            UIIncomingRequestsList.SetHasMorePages(hasMore);
        });
    }

    public void LoadMoreIncomingRequests()
    {
        _incomingCurrentPage++;
        UIIncomingRequestsList.SetLoading(true);
        
        LootLockerSDKManager.ListIncomingFriendRequestsPaginated(PageSize, _incomingCurrentPage, (resp) =>
        {
            UIIncomingRequestsList.SetLoading(false);
            if (!resp.success)
            {
                LogFailure("Incoming requests (more)", resp);
                _incomingCurrentPage--; // Rollback page increment
                return;
            }

            if (resp.incoming != null && resp.incoming.Length > 0)
            {
                _incomingRequests.AddRange(resp.incoming);
                UIIncomingRequestsList.AppendItems(resp.incoming);
            }

            bool hasMore = resp.incoming != null && resp.pagination.total > _incomingRequests.Length;
            UIIncomingRequestsList.SetHasMorePages(hasMore);
        });
    }

    // Outgoing Friend Requests
    public void LoadOutgoingRequestsFirstPage()
    {
        _outgoingRequests.Clear();
        _outgoingCurrentPage = 0;
        UIOutgoingRequestsList.SetLoading(true);
        
        LootLockerSDKManager.ListOutGoingFriendRequestsPaginated(PageSize, _outgoingCurrentPage, (resp) =>
        {
            UIOutgoingRequestsList.SetLoading(false);
            if (!resp.success)
            {
                LogFailure("Outgoing requests", resp);
                return;
            }

            if (resp.outgoing != null)
            {
                _outgoingRequests.AddRange(resp.outgoing);
                UIOutgoingRequestsList.ReplaceItems(resp.outgoing);
            }

            bool hasMore = resp.outgoing != null && resp.pagination.total > _outgoingRequests.Length;
            UIOutgoingRequestsList.SetHasMorePages(hasMore);
        });
    }

    public void LoadMoreOutgoingRequests()
    {
        _outgoingCurrentPage++;
        UIOutgoingRequestsList.SetLoading(true);
        
        LootLockerSDKManager.ListOutGoingFriendRequestsPaginated(PageSize, _outgoingCurrentPage, (resp) =>
        {
            UIOutgoingRequestsList.SetLoading(false);
            if (!resp.success)
            {
                LogFailure("Outgoing requests (more)", resp);
                _outgoingCurrentPage--; // Rollback page increment
                return;
            }

            if (resp.outgoing != null && resp.outgoing.Length > 0)
            {
                _outgoingRequests.AddRange(resp.outgoing);
                UIOutgoingRequestsList.AppendItems(resp.outgoing);
            }

            bool hasMore = resp.outgoing != null && resp.pagination.total > _outgoingRequests.Length;
            UIOutgoingRequestsList.SetHasMorePages(hasMore);
        });
    }

    // Blocked Players
    public void LoadBlockedPlayersFirstPage()
    {
        _blockedPlayers.Clear();
        _blockedCurrentPage = 0;
        UIBlockedPlayersList.SetLoading(true);
        
        LootLockerSDKManager.ListBlockedPlayersPaginated(PageSize, _blockedCurrentPage, (resp) =>
        {
            UIBlockedPlayersList.SetLoading(false);
            if (!resp.success)
            {
                LogFailure("Blocked players", resp);
                return;
            }

            if (resp.blocked != null)
            {
                _blockedPlayers.AddRange(resp.blocked);
                UIBlockedPlayersList.ReplaceItems(resp.blocked);
            }

            bool hasMore = resp.blocked != null && resp.pagination.total > _blockedPlayers.Length;
            UIBlockedPlayersList.SetHasMorePages(hasMore);
        });
    }

    public void LoadMoreBlockedPlayers()
    {
        _blockedCurrentPage++;
        UIBlockedPlayersList.SetLoading(true);
        
        LootLockerSDKManager.ListBlockedPlayersPaginated(PageSize, _blockedCurrentPage, (resp) =>
        {
            UIBlockedPlayersList.SetLoading(false);
            if (!resp.success)
            {
                LogFailure("Blocked players (more)", resp);
                _blockedCurrentPage--; // Rollback page increment
                return;
            }

            if (resp.blocked != null && resp.blocked.Length > 0)
            {
                _blockedPlayers.AddRange(resp.blocked);
                UIBlockedPlayersList.AppendItems(resp.blocked);
            }

            bool hasMore = resp.blocked != null && resp.pagination.total > _blockedPlayers.Length;
            UIBlockedPlayersList.SetHasMorePages(hasMore);
        });
    }

    private void LogFailure(string context, LootLockerResponse resp)
    {
        UnityEngine.Debug.LogWarning($"[FriendsListManager] {context} failed: {resp?.errorData?.message}");
    }
}
```

{% endtab %}

{% tab title="Unreal C++" %}

```cpp
// Friends List Management - Unreal C++
// Shows listing friends, incoming/outgoing requests, and blocked players using offset pagination.
// Assumptions:
// - UI layer methods for loading states and data display

#include "LootLockerManager.h"
#include "LootLockerSDK/LLFriends.h"

class FFriendsListManager
{
public:
    TArray<FLootLockerAcceptedFriend> Friends;
    TArray<FLootLockerFriend> IncomingRequests;
    TArray<FLootLockerFriend> OutgoingRequests;
    TArray<FLootLockerBlockedPlayer> BlockedPlayers;

    // Page tracking (0-indexed)
    int32 FriendsCurrentPage = 0;
    int32 IncomingCurrentPage = 0;
    int32 OutgoingCurrentPage = 0;
    int32 BlockedCurrentPage = 0;

    static constexpr int32 PageSize = 20;

    // Friends List
    void LoadFriendsFirstPage()
    {
        Friends.Empty();
        FriendsCurrentPage = 0;
        UFriendsListWidget::SetLoading(true);
        
        ULootLockerSDKManager::ListFriendsPaginated(PageSize, FriendsCurrentPage,
            FLootLockerListFriendsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnFriendsLoaded));
    }

    void LoadMoreFriends()
    {
        FriendsCurrentPage++;
        UFriendsListWidget::SetLoading(true);
        
        ULootLockerSDKManager::ListFriendsPaginated(PageSize, FriendsCurrentPage,
            FLootLockerListFriendsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnMoreFriendsLoaded));
    }

    // Incoming Requests
    void LoadIncomingRequestsFirstPage()
    {
        IncomingRequests.Empty();
        IncomingCurrentPage = 0;
        UIncomingRequestsWidget::SetLoading(true);
        
        ULootLockerSDKManager::ListIncomingFriendRequestsPaginated(PageSize, IncomingCurrentPage,
            FLootLockerListIncomingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnIncomingRequestsLoaded));
    }

    void LoadMoreIncomingRequests()
    {
        IncomingCurrentPage++;
        UIncomingRequestsWidget::SetLoading(true);
        
        ULootLockerSDKManager::ListIncomingFriendRequestsPaginated(PageSize, IncomingCurrentPage,
            FLootLockerListIncomingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnMoreIncomingRequestsLoaded));
    }

    // Outgoing Requests
    void LoadOutgoingRequestsFirstPage()
    {
        OutgoingRequests.Empty();
        OutgoingCurrentPage = 0;
        UOutgoingRequestsWidget::SetLoading(true);
        
        ULootLockerSDKManager::ListOutgoingFriendRequestsPaginated(PageSize, OutgoingCurrentPage,
            FLootLockerListOutgoingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnOutgoingRequestsLoaded));
    }

    void LoadMoreOutgoingRequests()
    {
        OutgoingCurrentPage++;
        UOutgoingRequestsWidget::SetLoading(true);
        
        ULootLockerSDKManager::ListOutgoingFriendRequestsPaginated(PageSize, OutgoingCurrentPage,
            FLootLockerListOutgoingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnMoreOutgoingRequestsLoaded));
    }

    // Blocked Players
    void LoadBlockedPlayersFirstPage()
    {
        BlockedPlayers.Empty();
        BlockedCurrentPage = 0;
        UBlockedPlayersWidget::SetLoading(true);
        
        ULootLockerSDKManager::ListBlockedPlayersPaginated(PageSize, BlockedCurrentPage,
            FLootLockerListBlockedPlayersResponseDelegate::CreateRaw(this, &FFriendsListManager::OnBlockedPlayersLoaded));
    }

    void LoadMoreBlockedPlayers()
    {
        BlockedCurrentPage++;
        UBlockedPlayersWidget::SetLoading(true);
        
        ULootLockerSDKManager::ListBlockedPlayersPaginated(PageSize, BlockedCurrentPage,
            FLootLockerListBlockedPlayersResponseDelegate::CreateRaw(this, &FFriendsListManager::OnMoreBlockedPlayersLoaded));
    }

private:
    void OnFriendsLoaded(const FLootLockerListFriendsResponse& Response)
    {
        UFriendsListWidget::SetLoading(false);
        if (!Response.success)
        {
            UE_LOG(LogTemp, Warning, TEXT("Friends list failed: %s"), *Response.Error);
            return;
        }

        Friends = Response.Friends;
        UFriendsListWidget::ReplaceItems(Friends);
        
        bool bHasMore = Friends.Num < Response.Pagination.Total;
        UFriendsListWidget::SetHasMorePages(bHasMore);
    }

    void OnMoreFriendsLoaded(const FLootLockerListFriendsResponse& Response)
    {
        UFriendsListWidget::SetLoading(false);
        if (!Response.success)
        {
            UE_LOG(LogTemp, Warning, TEXT("More friends failed: %s"), *Response.Error);
            FriendsCurrentPage--; // Rollback
            return;
        }

        if (Response.Friends.Num() > 0)
        {
            Friends.Append(Response.Friends);
            UFriendsListWidget::AppendItems(Response.Friends);
        }

        bool bHasMore = Friends.Num < Response.Pagination.Total;
        UFriendsListWidget::SetHasMorePages(bHasMore);
    }

    void OnIncomingRequestsLoaded(const FLootLockerListIncomingFriendRequestsResponse& Response)
    {
        UIncomingRequestsWidget::SetLoading(false);
        if (!Response.success)
        {
            UE_LOG(LogTemp, Warning, TEXT("Incoming requests failed: %s"), *Response.Error);
            return;
        }

        IncomingRequests = Response.Incoming;
        UIncomingRequestsWidget::ReplaceItems(IncomingRequests);
        
        bool bHasMore = IncomingRequests.Num() < Response.Pagination.Total;
        UIncomingRequestsWidget::SetHasMorePages(bHasMore);
    }

    void OnMoreIncomingRequestsLoaded(const FLootLockerListIncomingFriendRequestsResponse& Response)
    {
        UIncomingRequestsWidget::SetLoading(false);
        if (!Response.success)
        {
            UE_LOG(LogTemp, Warning, TEXT("More incoming requests failed: %s"), *Response.Error);
            IncomingCurrentPage--; // Rollback
            return;
        }

        if (Response.Incoming.Num() > 0)
        {
            IncomingRequests.Append(Response.Incoming);
            UIncomingRequestsWidget::AppendItems(Response.Incoming);
        }

        bool bHasMore = IncomingRequests.Num() < Response.Pagination.Total;
        UIncomingRequestsWidget::SetHasMorePages(bHasMore);
    }

    void OnOutgoingRequestsLoaded(const FLootLockerListOutgoingFriendRequestsResponse& Response)
    {
        UOutgoingRequestsWidget::SetLoading(false);
        if (!Response.success)
        {
            UE_LOG(LogTemp, Warning, TEXT("Outgoing requests failed: %s"), *Response.Error);
            return;
        }

        OutgoingRequests = Response.Outgoing;
        UOutgoingRequestsWidget::ReplaceItems(OutgoingRequests);
        
        bool bHasMore = OutgoingRequests.Num() < Response.Pagination.Total;
        UOutgoingRequestsWidget::SetHasMorePages(bHasMore);
    }

    void OnMoreOutgoingRequestsLoaded(const FLootLockerListOutgoingFriendRequestsResponse& Response)
    {
        UOutgoingRequestsWidget::SetLoading(false);
        if (!Response.success)
        {
            UE_LOG(LogTemp, Warning, TEXT("More outgoing requests failed: %s"), *Response.Error);
            OutgoingCurrentPage--; // Rollback
            return;
        }

        if (Response.Outgoing.Num() > 0)
        {
            OutgoingRequests.Append(Response.Outgoing);
            UOutgoingRequestsWidget::AppendItems(Response.Outgoing);
        }

        bool bHasMore = OutgoingRequests.Num() < Response.Pagination.Total;
        UOutgoingRequestsWidget::SetHasMorePages(bHasMore);
    }

    void OnBlockedPlayersLoaded(const FLootLockerListBlockedPlayersResponse& Response)
    {
        UBlockedPlayersWidget::SetLoading(false);
        if (!Response.success)
        {
            UE_LOG(LogTemp, Warning, TEXT("Blocked players failed: %s"), *Response.Error);
            return;
        }

        BlockedPlayers = Response.Blocked;
        UBlockedPlayersWidget::ReplaceItems(BlockedPlayers);
        
        bool bHasMore = BlockedPlayers.Num() < Response.Pagination.Total;
        UBlockedPlayersWidget::SetHasMorePages(bHasMore);
    }

    void OnMoreBlockedPlayersLoaded(const FLootLockerListBlockedPlayersResponse& Response)
    {
        UBlockedPlayersWidget::SetLoading(false);
        if (!Response.success)
        {
            UE_LOG(LogTemp, Warning, TEXT("More blocked players failed: %s"), *Response.Error);
            BlockedCurrentPage--; // Rollback
            return;
        }

        if (Response.Blocked.Num() > 0)
        {
            BlockedPlayers.Append(Response.Blocked);
            UBlockedPlayersWidget::AppendItems(Response.Blocked);
        }

        bool bHasMore = BlockedPlayers.Num() < Response.Pagination.Total;
        UBlockedPlayersWidget::SetHasMorePages(bHasMore);
    }
};
```

{% endtab %}

{% tab title="Unreal Blueprints" %}
Coming soon...
{% endtab %}

{% tab title="REST" %}
Coming soon...
{% endtab %}
{% endtabs %}

### Offset Pagination Strategy

* Use page numbers (0-indexed) and `PerPage` count to paginate the data
* Track current page for each list type separately
* Consider a page "complete" if returned items equal the requested `PerPage`
* Reset to page 0 when refreshing lists

{% hint style="info" %}
Unlike cursor-based pagination, offset pagination lets you jump to specific pages without knowing the content on them
{% endhint %}

## Send and Cancel Friend Requests

Allow players to initiate and manage outgoing friend requests.

{% tabs %}
{% tab title="Unity" %}

```csharp
// Send and Cancel Friend Requests with Optimistic UI
// Assumptions:
// - _outgoingRequests list is maintained locally
// - UI shows button states: Send Request / Pending / Friends
// - ShowToast for user feedback

public class FriendRequestController
{
    private readonly List<LootLockerFriend> _outgoingRequests;
    private readonly HashSet<string> _friendIds; // Track current friends for button state

    public FriendRequestController(List<LootLockerFriend> outgoingRequests, HashSet<string> friendIds)
    {
        _outgoingRequests = outgoingRequests;
        _friendIds = friendIds;
    }

    public void SendFriendRequest(string targetPlayerULID, string displayName)
    {
        if (string.IsNullOrEmpty(targetPlayerULID)) return;
        
        // Check if already friends or request pending
        if (_friendIds.Contains(targetPlayerULID))
        {
            ShowToast("Already friends");
            return;
        }
        
        if (_outgoingRequests.Any(r => r.player_ulid == targetPlayerULID))
        {
            ShowToast("Request already sent");
            return;
        }

        // Optimistic update
        var tempRequest = new LootLockerFriend 
        { 
            player_ulid = targetPlayerULID, 
            player_name = displayName,
            created_at = DateTime.Now
        };
        _outgoingRequests.Add(tempRequest);
        UIProfileButton.SetState(FriendButtonState.Pending);
        UIOutgoingRequestsList.AddItem(tempRequest);

        LootLockerSDKManager.SendFriendRequest(targetPlayerULID, (resp) =>
        {
            if (!resp.success)
            {
                // Rollback optimistic update
                _outgoingRequests.RemoveAll(r => r.player_ulid == targetPlayerULID);
                UIProfileButton.SetState(FriendButtonState.SendRequest);
                UIOutgoingRequestsList.RemoveItem(targetPlayerULID);
                ShowToast($"Failed to send request: {resp.errorData?.message}");
                return;
            }

            ShowToast($"Friend request sent to {displayName}");
        });
    }

    public void CancelFriendRequest(string targetPlayerULID, string displayName)
    {
        var request = _outgoingRequests.FirstOrDefault(r => r.player_ulid == targetPlayerULID);
        if (request == null) return;

        // Optimistic removal
        _outgoingRequests.Remove(request);
        UIProfileButton.SetState(FriendButtonState.SendRequest);
        UIOutgoingRequestsList.RemoveItem(targetPlayerULID);

        LootLockerSDKManager.CancelFriendRequest(targetPlayerULID, (resp) =>
        {
            if (!resp.success)
            {
                // Rollback
                _outgoingRequests.Add(request);
                UIProfileButton.SetState(FriendButtonState.Pending);
                UIOutgoingRequestsList.AddItem(request);
                ShowToast($"Failed to cancel request: {resp.errorData?.message}");
                return;
            }

            ShowToast($"Cancelled request to {displayName}");
        });
    }

    private void ShowToast(string message)
    {
        // Implementation depends on your UI framework
        Debug.Log(message);
    }
}

public enum FriendButtonState
{
    SendRequest,
    Pending,
    Friends
}
```

{% endtab %}

{% tab title="Unreal C++" %}

```cpp
// Friend Request Management - Unreal C++
// Handles sending and canceling friend requests with optimistic updates

class FFriendRequestController
{
public:
    TArray<FLootLockerFriend> OutgoingRequests;
    TSet<FString> FriendIds; // Track current friends for UI state

    void SendFriendRequest(const FString& TargetPlayerULID, const FString& DisplayName)
    {
        if (TargetPlayerULID.IsEmpty()) return;
        
        if (FriendIds.Contains(TargetPlayerULID))
        {
            ShowToast(TEXT("Already friends"));
            return;
        }
        
        // Check if request already exists
        if (OutgoingRequests.ContainsByPredicate([&](const FLootLockerFriend& Request) {
            return Request.Player_ulid == TargetPlayerULID;
        }))
        {
            ShowToast(TEXT("Request already sent"));
            return;
        }

        // Optimistic update
        FLootLockerFriend TempRequest;
        TempRequest.Player_ulid = TargetPlayerULID;
        TempRequest.Player_name = DisplayName;
        TempRequest.Created_at = FDateTime::Now().ToString();
        
        OutgoingRequests.Add(TempRequest);
        UProfileButtonWidget::SetState(EFriendButtonState::Pending);
        UOutgoingRequestsWidget::AddItem(TempRequest);

        ULootLockerSDKManager::SendFriendRequest(TargetPlayerULID,
            FLootLockerFriendActionResponseDelegate::CreateLambda([this, TargetPlayerULID, DisplayName](const FLootLockerFriendActionResponse& Response)
        {
            if (!Response.success)
            {
                // Rollback
                OutgoingRequests.RemoveAll([&](const FLootLockerFriend& Request) {
                    return Request.Player_ulid == TargetPlayerULID;
                });
                UProfileButtonWidget::SetState(EFriendButtonState::SendRequest);
                UOutgoingRequestsWidget::RemoveItem(TargetPlayerULID);
                ShowToast(FString::Printf(TEXT("Failed to send request: %s"), *Response.Error));
                return;
            }

            ShowToast(FString::Printf(TEXT("Friend request sent to %s"), *DisplayName));
        }));
    }

    void CancelFriendRequest(const FString& TargetPlayerULID, const FString& DisplayName)
    {
        // Find the request
        FLootLockerFriend* RequestPtr = OutgoingRequests.FindByPredicate([&](const FLootLockerFriend& Request) {
            return Request.Player_ulid == TargetPlayerULID;
        });
        
        if (!RequestPtr) return;
        FLootLockerFriend RequestCopy = *RequestPtr;

        // Optimistic removal
        OutgoingRequests.RemoveAll([&](const FLootLockerFriend& Request) {
            return Request.Player_ulid == TargetPlayerULID;
        });
        UProfileButtonWidget::SetState(EFriendButtonState::SendRequest);
        UOutgoingRequestsWidget::RemoveItem(TargetPlayerULID);

        ULootLockerSDKManager::CancelFriendRequest(TargetPlayerULID,
            FLootLockerFriendActionResponseDelegate::CreateLambda([this, RequestCopy, DisplayName](const FLootLockerFriendActionResponse& Response)
        {
            if (!Response.success)
            {
                // Rollback
                OutgoingRequests.Add(RequestCopy);
                UProfileButtonWidget::SetState(EFriendButtonState::Pending);
                UOutgoingRequestsWidget::AddItem(RequestCopy);
                ShowToast(FString::Printf(TEXT("Failed to cancel request: %s"), *Response.Error));
                return;
            }

            ShowToast(FString::Printf(TEXT("Cancelled request to %s"), *DisplayName));
        }));
    }

private:
    void ShowToast(const FString& Message)
    {
        UE_LOG(LogTemp, Log, TEXT("%s"), *Message);
        // Add your toast UI implementation
    }
};

UENUM(BlueprintType)
enum class EFriendButtonState : uint8
{
    SendRequest,
    Pending,
    Friends
};
```

{% endtab %}

{% tab title="Unreal Blueprints" %}
Coming soon...
{% endtab %}

{% tab title="REST" %}
Coming soon...
{% endtab %}
{% endtabs %}

## Accept and Decline Friend Requests

Handle incoming friend requests to build your friends network.

{% tabs %}
{% tab title="Unity" %}

```csharp
// Accept and Decline Incoming Friend Requests
// Move requests between lists when processed

public class IncomingRequestsController
{
    private readonly List<LootLockerFriend> _incomingRequests;
    private readonly List<LootLockerAcceptedFriend> _friends;

    public IncomingRequestsController(List<LootLockerFriend> incomingRequests, List<LootLockerAcceptedFriend> friends)
    {
        _incomingRequests = incomingRequests;
        _friends = friends;
    }

    public void AcceptFriendRequest(string fromPlayerULID)
    {
        var request = _incomingRequests.FirstOrDefault(r => r.player_ulid == fromPlayerULID);
        if (request == null) return;

        // Optimistic update: move from incoming to friends
        _incomingRequests.Remove(request);
        var newFriend = new LootLockerAcceptedFriend
        {
            player_ulid = request.player_ulid,
            player_name = request.player_name,
            player_id = request.player_id,
            created_at = request.created_at,
            accepted_at = DateTime.Now
        };
        _friends.Add(newFriend);

        UIIncomingRequestsList.RemoveItem(fromPlayerULID);
        UIFriendsList.AddItem(newFriend);
        ShowToast($"Now friends with {request.player_name}");

        LootLockerSDKManager.AcceptFriendRequest(fromPlayerULID, (resp) =>
        {
            if (!resp.success)
            {
                // Rollback: move back to incoming
                _friends.Remove(newFriend);
                _incomingRequests.Add(request);
                UIFriendsList.RemoveItem(fromPlayerULID);
                UIIncomingRequestsList.AddItem(request);
                ShowToast($"Failed to accept request: {resp.errorData?.message}");
            }
        });
    }

    public void DeclineFriendRequest(string fromPlayerULID)
    {
        var request = _incomingRequests.FirstOrDefault(r => r.player_ulid == fromPlayerULID);
        if (request == null) return;

        // Optimistic removal
        _incomingRequests.Remove(request);
        UIIncomingRequestsList.RemoveItem(fromPlayerULID);
        ShowToast($"Declined request from {request.player_name}");

        LootLockerSDKManager.DeclineFriendRequest(fromPlayerULID, (resp) =>
        {
            if (!resp.success)
            {
                // Rollback
                _incomingRequests.Add(request);
                UIIncomingRequestsList.AddItem(request);
                ShowToast($"Failed to decline request: {resp.errorData?.message}");
            }
        });
    }

    private void ShowToast(string message)
    {
        Debug.Log(message);
    }
}
```

{% endtab %}

{% tab title="Unreal C++" %}

```cpp
// Accept and Decline Friend Requests - Unreal C++

class FIncomingRequestsController
{
public:
    TArray<FLootLockerFriend> IncomingRequests;
    TArray<FLootLockerAcceptedFriend> Friends;

    void AcceptFriendRequest(const FString& FromPlayerULID)
    {
        // Find the request
        FLootLockerFriend* RequestPtr = IncomingRequests.FindByPredicate([&](const FLootLockerFriend& Request) {
            return Request.Player_ulid == FromPlayerULID;
        });
        
        if (!RequestPtr) return;
        FLootLockerFriend RequestCopy = *RequestPtr;

        // Optimistic update: move from incoming to friends
        IncomingRequests.RemoveAll([&](const FLootLockerFriend& Request) {
            return Request.Player_ulid == FromPlayerULID;
        });

        FLootLockerAcceptedFriend NewFriend;
        NewFriend.Player_ulid = RequestCopy.Player_ulid;
        NewFriend.Player_name = RequestCopy.Player_name;
        NewFriend.Player_id = RequestCopy.Player_id;
        NewFriend.Created_at = RequestCopy.Created_at;
        NewFriend.Accepted_at = FDateTime::Now().ToString();
        
        Friends.Add(NewFriend);
        UIncomingRequestsWidget::RemoveItem(FromPlayerULID);
        UFriendsListWidget::AddItem(NewFriend);
        ShowToast(FString::Printf(TEXT("Now friends with %s"), *RequestCopy.Player_name));

        ULootLockerSDKManager::AcceptFriendRequest(FromPlayerULID,
            FLootLockerFriendActionResponseDelegate::CreateLambda([this, RequestCopy, NewFriend, FromPlayerULID](const FLootLockerFriendActionResponse& Response)
        {
            if (!Response.success)
            {
                // Rollback: move back to incoming
                Friends.RemoveAll([&](const FLootLockerAcceptedFriend& Friend) {
                    return Friend.Player_ulid == FromPlayerULID;
                });
                IncomingRequests.Add(RequestCopy);
                UFriendsListWidget::RemoveItem(FromPlayerULID);
                UIncomingRequestsWidget::AddItem(RequestCopy);
                ShowToast(FString::Printf(TEXT("Failed to accept request: %s"), *Response.Error));
            }
        }));
    }

    void DeclineFriendRequest(const FString& FromPlayerULID)
    {
        // Find the request
        FLootLockerFriend* RequestPtr = IncomingRequests.FindByPredicate([&](const FLootLockerFriend& Request) {
            return Request.Player_ulid == FromPlayerULID;
        });
        
        if (!RequestPtr) return;
        FLootLockerFriend RequestCopy = *RequestPtr;

        // Optimistic removal
        IncomingRequests.RemoveAll([&](const FLootLockerFriend& Request) {
            return Request.Player_ulid == FromPlayerULID;
        });
        UIncomingRequestsWidget::RemoveItem(FromPlayerULID);
        ShowToast(FString::Printf(TEXT("Declined request from %s"), *RequestCopy.Player_name));

        ULootLockerSDKManager::DeclineFriendRequest(FromPlayerULID,
            FLootLockerFriendActionResponseDelegate::CreateLambda([this, RequestCopy](const FLootLockerFriendActionResponse& Response)
        {
            if (!Response.success)
            {
                // Rollback
                IncomingRequests.Add(RequestCopy);
                UIncomingRequestsWidget::AddItem(RequestCopy);
                ShowToast(FString::Printf(TEXT("Failed to decline request: %s"), *Response.Error));
            }
        }));
    }

private:
    void ShowToast(const FString& Message)
    {
        UE_LOG(LogTemp, Log, TEXT("%s"), *Message);
    }
};
```

{% endtab %}

{% tab title="Unreal Blueprints" %}
Coming soon...
{% endtab %}

{% tab title="REST" %}
Coming soon...
{% endtab %}
{% endtabs %}

## Block and Unblock Players

Manage your blocked players list to prevent unwanted interactions.

{% tabs %}
{% tab title="Unity" %}

```csharp
// Block and Unblock Players
// Blocking removes from friends and prevents future friend requests

public class BlockingController
{
    private readonly List<LootLockerBlockedPlayer> _blockedPlayers;
    private readonly List<LootLockerAcceptedFriend> _friends;

    public BlockingController(List<LootLockerBlockedPlayer> blockedPlayers, List<LootLockerAcceptedFriend> friends)
    {
        _blockedPlayers = blockedPlayers;
        _friends = friends;
    }

    public void BlockPlayer(string targetPlayerULID, string displayName)
    {
        if (string.IsNullOrEmpty(targetPlayerULID)) return;

        // Check if already blocked
        if (_blockedPlayers.Any(p => p.player_ulid == targetPlayerULID))
        {
            ShowToast("Player already blocked");
            return;
        }

        // Optimistic update: remove from friends if present, add to blocked
        var existingFriend = _friends.FirstOrDefault(f => f.player_ulid == targetPlayerULID);
        if (existingFriend != null)
        {
            _friends.Remove(existingFriend);
            UIFriendsList.RemoveItem(targetPlayerULID);
        }

        var blockedPlayer = new LootLockerBlockedPlayer
        {
            player_ulid = targetPlayerULID,
            player_name = displayName,
            blocked_at = DateTime.Now
        };
        _blockedPlayers.Add(blockedPlayer);
        UIBlockedPlayersList.AddItem(blockedPlayer);
        UIProfileButton.SetState(FriendButtonState.Blocked);

        LootLockerSDKManager.BlockPlayer(targetPlayerULID, (resp) =>
        {
            if (!resp.success)
            {
                // Rollback
                _blockedPlayers.Remove(blockedPlayer);
                UIBlockedPlayersList.RemoveItem(targetPlayerULID);
                if (existingFriend != null)
                {
                    _friends.Add(existingFriend);
                    UIFriendsList.AddItem(existingFriend);
                    UIProfileButton.SetState(FriendButtonState.Friends);
                }
                else
                {
                    UIProfileButton.SetState(FriendButtonState.SendRequest);
                }
                ShowToast($"Failed to block player: {resp.errorData?.message}");
                return;
            }

            ShowToast($"Blocked {displayName}");
        });
    }

    public void UnblockPlayer(string targetPlayerULID, string displayName)
    {
        var blockedPlayer = _blockedPlayers.FirstOrDefault(p => p.player_ulid == targetPlayerULID);
        if (blockedPlayer == null) return;

        // Optimistic removal
        _blockedPlayers.Remove(blockedPlayer);
        UIBlockedPlayersList.RemoveItem(targetPlayerULID);
        UIProfileButton.SetState(FriendButtonState.SendRequest);

        LootLockerSDKManager.UnblockPlayer(targetPlayerULID, (resp) =>
        {
            if (!resp.success)
            {
                // Rollback
                _blockedPlayers.Add(blockedPlayer);
                UIBlockedPlayersList.AddItem(blockedPlayer);
                UIProfileButton.SetState(FriendButtonState.Blocked);
                ShowToast($"Failed to unblock player: {resp.errorData?.message}");
                return;
            }

            ShowToast($"Unblocked {displayName}");
        });
    }

    private void ShowToast(string message)
    {
        Debug.Log(message);
    }
}
```

{% endtab %}

{% tab title="Unreal C++" %}

```cpp
// Block and Unblock Players - Unreal C++

class FBlockingController
{
public:
    TArray<FLootLockerBlockedPlayer> BlockedPlayers;
    TArray<FLootLockerAcceptedFriend> Friends;

    void BlockPlayer(const FString& TargetPlayerULID, const FString& DisplayName)
    {
        if (TargetPlayerULID.IsEmpty()) return;

        // Check if already blocked
        if (BlockedPlayers.ContainsByPredicate([&](const FLootLockerBlockedPlayer& Player) {
            return Player.Player_ulid == TargetPlayerULID;
        }))
        {
            ShowToast(TEXT("Player already blocked"));
            return;
        }

        // Remove from friends if present
        FLootLockerAcceptedFriend* ExistingFriend = Friends.FindByPredicate([&](const FLootLockerAcceptedFriend& Friend) {
            return Friend.Player_ulid == TargetPlayerULID;
        });

        FLootLockerAcceptedFriend FriendCopy;
        bool bWasFriend = false;
        if (ExistingFriend)
        {
            FriendCopy = *ExistingFriend;
            bWasFriend = true;
            Friends.RemoveAll([&](const FLootLockerAcceptedFriend& Friend) {
                return Friend.Player_ulid == TargetPlayerULID;
            });
            UFriendsListWidget::RemoveItem(TargetPlayerULID);
        }

        // Add to blocked
        FLootLockerBlockedPlayer BlockedPlayer;
        BlockedPlayer.Player_ulid = TargetPlayerULID;
        BlockedPlayer.Player_name = DisplayName;
        BlockedPlayer.Blocked_at = FDateTime::Now().ToString();
        
        BlockedPlayers.Add(BlockedPlayer);
        UBlockedPlayersWidget::AddItem(BlockedPlayer);
        UProfileButtonWidget::SetState(EFriendButtonState::Blocked);

        ULootLockerSDKManager::BlockPlayer(TargetPlayerULID,
            FLootLockerFriendActionResponseDelegate::CreateLambda([this, BlockedPlayer, FriendCopy, bWasFriend, TargetPlayerULID, DisplayName](const FLootLockerFriendActionResponse& Response)
        {
            if (!Response.success)
            {
                // Rollback
                BlockedPlayers.RemoveAll([&](const FLootLockerBlockedPlayer& Player) {
                    return Player.Player_ulid == TargetPlayerULID;
                });
                UBlockedPlayersWidget::RemoveItem(TargetPlayerULID);
                
                if (bWasFriend)
                {
                    Friends.Add(FriendCopy);
                    UFriendsListWidget::AddItem(FriendCopy);
                    UProfileButtonWidget::SetState(EFriendButtonState::Friends);
                }
                else
                {
                    UProfileButtonWidget::SetState(EFriendButtonState::SendRequest);
                }
                
                ShowToast(FString::Printf(TEXT("Failed to block player: %s"), *Response.Error));
                return;
            }

            ShowToast(FString::Printf(TEXT("Blocked %s"), *DisplayName));
        }));
    }

    void UnblockPlayer(const FString& TargetPlayerULID, const FString& DisplayName)
    {
        FLootLockerBlockedPlayer* BlockedPlayerPtr = BlockedPlayers.FindByPredicate([&](const FLootLockerBlockedPlayer& Player) {
            return Player.Player_ulid == TargetPlayerULID;
        });
        
        if (!BlockedPlayerPtr) return;
        FLootLockerBlockedPlayer BlockedPlayerCopy = *BlockedPlayerPtr;

        // Optimistic removal
        BlockedPlayers.RemoveAll([&](const FLootLockerBlockedPlayer& Player) {
            return Player.Player_ulid == TargetPlayerULID;
        });
        UBlockedPlayersWidget::RemoveItem(TargetPlayerULID);
        UProfileButtonWidget::SetState(EFriendButtonState::SendRequest);

        ULootLockerSDKManager::UnblockPlayer(TargetPlayerULID,
            FLootLockerFriendActionResponseDelegate::CreateLambda([this, BlockedPlayerCopy, DisplayName](const FLootLockerFriendActionResponse& Response)
        {
            if (!Response.success)
            {
                // Rollback
                BlockedPlayers.Add(BlockedPlayerCopy);
                UBlockedPlayersWidget::AddItem(BlockedPlayerCopy);
                UProfileButtonWidget::SetState(EFriendButtonState::Blocked);
                ShowToast(FString::Printf(TEXT("Failed to unblock player: %s"), *Response.Error));
                return;
            }

            ShowToast(FString::Printf(TEXT("Unblocked %s"), *DisplayName));
        }));
    }

private:
    void ShowToast(const FString& Message)
    {
        UE_LOG(LogTemp, Log, TEXT("%s"), *Message);
    }
};
```

{% endtab %}

{% tab title="Unreal Blueprints" %}
Coming soon...
{% endtab %}

{% tab title="REST" %}
Coming soon...
{% endtab %}
{% endtabs %}

## Check Friendship Status

Determine the current relationship between players for UI state management.

{% tabs %}
{% tab title="Unity" %}

```csharp
// Check Friendship Status Using GetFriend
// Returns specific friend data if they are friends, null/error otherwise

public static class FriendshipProbe
{
    public static void CheckFriendshipStatus(string targetPlayerULID, System.Action<FriendshipStatus> onResult)
    {
        if (string.IsNullOrEmpty(targetPlayerULID)) 
        { 
            onResult?.Invoke(FriendshipStatus.None); 
            return; 
        }

        LootLockerSDKManager.GetFriend(targetPlayerULID, (resp) =>
        {
            if (resp.success && !string.IsNullOrEmpty(resp.player_ulid))
            {
                // They are friends - check when friendship was established
                onResult?.Invoke(FriendshipStatus.Friends);
            }
            else
            {
                // Not friends - could be pending request, blocked, or no relationship
                // You might need additional checks to distinguish these states
                onResult?.Invoke(FriendshipStatus.None);
            }
        });
    }
}

public enum FriendshipStatus
{
    None,           // No relationship
    Friends,        // Mutual friends
    IncomingRequest, // They sent us a request
    OutgoingRequest, // We sent them a request
    Blocked         // One party blocked the other
}

// Usage:
// FriendshipProbe.CheckFriendshipStatus(otherPlayerUid, (status) => {
//     switch(status) {
//         case FriendshipStatus.Friends:
//             UIProfileButton.SetState(FriendButtonState.Friends);
//             break;
//         case FriendshipStatus.None:
//             UIProfileButton.SetState(FriendButtonState.SendRequest);
//             break;
//     }
// });
```

{% endtab %}

{% tab title="Unreal C++" %}

```cpp
// Friendship Status Check - Unreal C++

UENUM(BlueprintType)
enum class EFriendshipStatus : uint8
{
    None,           // No relationship
    Friends,        // Mutual friends  
    IncomingRequest, // They sent us a request
    OutgoingRequest, // We sent them a request
    Blocked         // One party blocked the other
};

class FFriendshipProbe
{
public:
    static void CheckFriendshipStatus(const FString& TargetPlayerULID, TFunction<void(EFriendshipStatus)> Callback)
    {
        if (TargetPlayerULID.IsEmpty()) 
        { 
            Callback(EFriendshipStatus::None); 
            return; 
        }

        ULootLockerSDKManager::GetFriend(TargetPlayerULID,
            FLootLockerGetFriendResponseDelegate::CreateLambda([Callback](const FLootLockerGetFriendResponse& Response)
        {
            if (Response.success && !Response.Player_ulid.IsEmpty())
            {
                // They are friends
                Callback(EFriendshipStatus::Friends);
            }
            else
            {
                // Not friends - could be pending, blocked, or no relationship
                // Additional logic could check pending requests lists
                Callback(EFriendshipStatus::None);
            }
        }));
    }
};

// Usage:
// FFriendshipProbe::CheckFriendshipStatus(OtherPlayerUid, [](EFriendshipStatus Status) {
//     switch(Status) {
//         case EFriendshipStatus::Friends:
//             UProfileButtonWidget::SetState(EFriendButtonState::Friends);
//             break;
//         case EFriendshipStatus::None:
//             UProfileButtonWidget::SetState(EFriendButtonState::SendRequest);
//             break;
//     }
// });
```

{% endtab %}

{% tab title="Unreal Blueprints" %}
Coming soon...
{% endtab %}

{% tab title="REST" %}
Coming soon...
{% endtab %}
{% endtabs %}

## Displaying Counts and Status

Show meaningful counts and relationship indicators in your UI.

{% tabs %}
{% tab title="Unity" %}

```csharp
// Friends Counts and Status Display
// Use first page loads to estimate counts and provide status context

public class FriendsCountsManager
{
    private int _friendsCount = 0;
    private int _incomingRequestsCount = 0;
    private int _outgoingRequestsCount = 0;

    public void LoadAllCounts()
    {
        // Load first pages to get count estimates
        LoadFriendsCount();
        LoadIncomingRequestsCount();
        LoadOutgoingRequestsCount();
    }

    private void LoadFriendsCount()
    {
        LootLockerSDKManager.ListFriendsPaginated(1, 0, (resp) =>
        {
            if (resp.success)
            {
                UIFriendsHeader.SetCount(resp.pagination.total.ToString());
            }
        });
    }

    private void LoadIncomingRequestsCount()
    {
        LootLockerSDKManager.ListIncomingFriendRequestsPaginated(1, 0, (resp) =>
        {
            if (resp.success && resp.incoming != null)
            {
                _incomingRequestsCount = resp.pagination.total;
                UIIncomingRequestsHeader.SetCount(_incomingRequestsCount);
                
                // Show notification badge if there are pending requests
                if (_incomingRequestsCount > 0)
                {
                    UIFriendsTabButton.ShowBadge(_incomingRequestsCount);
                }
            }
        });
    }

    private void LoadOutgoingRequestsCount()
    {
        LootLockerSDKManager.ListOutGoingFriendRequestsPaginated(1, 0, (resp) =>
        {
            if (resp.success && resp.outgoing != null)
            {
                UIOutgoingRequestsHeader.SetCount(resp.pagination.total);
            }
        });
    }
}
```

{% endtab %}

{% tab title="Unreal C++" %}

```cpp
// Friends Counts Display - Unreal C++

class FFriendsCountsManager
{
public:
    int32 FriendsCount = 0;
    int32 IncomingRequestsCount = 0;
    int32 OutgoingRequestsCount = 0;

    void LoadAllCounts()
    {
        LoadFriendsCount();
        LoadIncomingRequestsCount();
        LoadOutgoingRequestsCount();
    }

private:
    void LoadFriendsCount()
    {
        ULootLockerSDKManager::ListFriendsPaginated(1, 0,
            FLootLockerListFriendsResponseDelegate::CreateRaw(this, &FFriendsCountsManager::OnFriendsCountLoaded));
    }

    void LoadIncomingRequestsCount()
    {
        ULootLockerSDKManager::ListIncomingFriendRequestsPaginated(1, 0,
            FLootLockerListIncomingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsCountsManager::OnIncomingCountLoaded));
    }

    void LoadOutgoingRequestsCount()
    {
        ULootLockerSDKManager::ListOutgoingFriendRequestsPaginated(1, 0,
            FLootLockerListOutgoingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsCountsManager::OnOutgoingCountLoaded));
    }

    void OnFriendsCountLoaded(const FLootLockerListFriendsResponse& Response)
    {
        if (Response.success)
        {
            UFriendsHeaderWidget::SetCount(FString::FromInt(Response.Pagination.Total));
        }
    }

    void OnIncomingCountLoaded(const FLootLockerListIncomingFriendRequestsResponse& Response)
    {
        if (Response.success)
        {
            IncomingRequestsCount = Response.Pagination.Total;
            UIncomingRequestsHeaderWidget::SetCount(FString::FromInt(IncomingRequestsCount));
            
            // Show notification badge if there are pending requests
            if (IncomingRequestsCount > 0)
            {
                UFriendsTabButtonWidget::ShowBadge(IncomingRequestsCount);
            }
        }
    }

    void OnOutgoingCountLoaded(const FLootLockerListOutgoingFriendRequestsResponse& Response)
    {
        if (Response.success)
        {
            OutgoingRequestsCount = Response.Pagination.Total;
            UOutgoingRequestsHeaderWidget::SetCount(FString::FromInt(OutgoingRequestsCount));
        }
    }
};
```

{% endtab %}

{% tab title="Unreal Blueprints" %}
Coming soon...
{% endtab %}

{% tab title="REST" %}
Coming soon...
{% endtab %}
{% endtabs %}

## Performance Tips

* **Batch relationship checks**: When showing many players (leaderboards for example), only fetch data for visible items
* **Cache friend IDs locally**: Maintain a set of friend ULIDs for instant button state rendering
* **Moderate page sizes**: 20-50 items per page balances responsiveness with network efficiency
* **Debounce rapid requests**: Prevent spam-clicking send/cancel buttons
* **Lazy load secondary lists**: Load friends first, then incoming/outgoing requests when tabs are opened

## Example Feature Ideas

* **Mutual Friends**: "You have 3 mutual friends with this player"
* **Friend Recommendations**: Suggest friends-of-friends or players with similar interests
* **Private Groups**: Create invite-only spaces using your friends list
* **Co-op Quick Join**: Join friends' game sessions directly from the friends list

## Conclusion

In this How-to we implemented friend requests, list management, acceptance/declining workflows, blocking functionality, and relationship status checking. The friends system provides the foundation for deeper social features like messaging, co-op play, and community building.

Key differences from followers include the mutual consent model, request management workflow, and offset-based pagination. Next, consider adding [Followers](https://docs.lootlocker.com/players/friends-and-followers/how-to/use-followers-in-game) for asymmetric relationships or explore [Web Console management](https://docs.lootlocker.com/players/friends-and-followers/how-to/manage-relationships-web-console) for administrative oversight.
