Skip to content

feat: wire follow/unfollow to SocialController using AuthenticationController profileId#28843

Merged
zone-live merged 19 commits into
mainfrom
TSA-389-follow-unfollow-cta
Apr 17, 2026
Merged

feat: wire follow/unfollow to SocialController using AuthenticationController profileId#28843
zone-live merged 19 commits into
mainfrom
TSA-389-follow-unfollow-cta

Conversation

@Bigshmow

@Bigshmow Bigshmow commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Description

Wire follow/unfollow persistence via SocialController state

  • Bump @metamask/social-controllers to ^1.0.0 which writes followingProfileIds to controller state on follow/unfollow/updateFollowing
  • Add AuthenticationController:getBearerToken delegation to SocialService messenger (required by 1.0.0 for JWT auth on all API requests)
  • Hydrate following state once at Engine startup via SocialController:updateFollowing
  • Read isFollowing from Redux (SocialController.followingProfileIds) instead of ephemeral local state
  • toggleFollow calls SocialController:followTrader/unfollowTrader directly — controller updates state, Redux syncs, UI re-renders

Test plan

  1. Follow a trader on homepage carousel > navigate to leaderboard > follow state persists
  2. Unfollow a trader on leaderboard > navigate away and back > unfollow state persists
  3. Follow/unfollow on trader profile > navigate back to leaderboard > state is consistent
  4. Kill and reopen app > follow state survives (controller state is persisted)

TSA-389

Changelog

CHANGELOG entry: Wired up follow/unfollow functionality

Related issues

Fixes:

Manual testing steps

Feature: my feature name

  Scenario: user [verb for user action]
    Given [describe expected initial app state]

    When user [verb for user action]
    Then [describe expected outcome]

Screenshots/Recordings

Before

After

Pre-merge author checklist

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Introduces new follow/unfollow wiring that calls SocialController via Engine.controllerMessenger and runs a new hydration request during EngineService.start(), which could affect app startup behavior and follow state consistency if messaging/auth fails.

Overview
Moves follow state from local component overrides to persisted SocialController.followingProfileIds in Redux, via a new shared hook useFollowToggle/useFollowToggleMany that performs optimistic follow/unfollow and dispatches SocialController:followTrader / SocialController:unfollowTrader using the session profileId from AuthenticationController.

Adds engine-startup hydration (hydrateSocialFollowing) to refresh the server following list by calling SocialController:updateFollowing (fire-and-forget, non-fatal on failure), plus a new selector selectFollowingProfileIds and expanded unit tests to cover seeding from controller state, optimistic updates, rejection reverts, and in-flight call de-duping.

Reviewed by Cursor Bugbot for commit 4637abc. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@Bigshmow Bigshmow marked this pull request as ready for review April 17, 2026 03:17
@github-actions github-actions Bot added the risk-high Extensive testing required · High bug introduction risk label Apr 17, 2026
@Bigshmow Bigshmow added the team-social-ai Social & AI team label Apr 17, 2026
@socket-security

socket-security Bot commented Apr 17, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednpm/​better-sqlite3@​12.8.0 ⏵ 12.9.010010010092100

View full report

@github-actions github-actions Bot added risk-high Extensive testing required · High bug introduction risk and removed risk-high Extensive testing required · High bug introduction risk labels Apr 17, 2026
Comment thread app/selectors/socialController.ts Outdated
Prevents spurious re-renders when SocialController state is nullish
by using a frozen constant instead of allocating a new array each call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added risk-high Extensive testing required · High bug introduction risk and removed risk-high Extensive testing required · High bug introduction risk labels Apr 17, 2026
Comment thread app/components/Views/Homepage/Sections/TopTraders/hooks/useTopTraders.ts Outdated
Prevents double-tap race conditions by disabling the follow/unfollow
button during the controller call. Applies to TraderRow, TopTraderCard,
and TraderProfileView.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added risk-high Extensive testing required · High bug introduction risk and removed risk-high Extensive testing required · High bug introduction risk labels Apr 17, 2026
Comment thread app/components/Views/Homepage/Sections/TopTraders/hooks/useTopTraders.ts Outdated
Use Set<string> instead of single string for followLoadingIds to
correctly handle multiple concurrent follow/unfollow operations.
Fix Object.freeze type cast and add missing mock properties in tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added risk-high Extensive testing required · High bug introduction risk and removed risk-high Extensive testing required · High bug introduction risk labels Apr 17, 2026
Comment thread app/components/Views/Homepage/Sections/TopTraders/hooks/useTopTraders.ts Outdated
Comment thread app/components/Views/Homepage/Sections/TopTraders/TopTradersSection.tsx Outdated
@github-actions github-actions Bot removed the risk-high Extensive testing required · High bug introduction risk label Apr 17, 2026
@github-actions github-actions Bot added size-L risk-high Extensive testing required · High bug introduction risk and removed size-M risk-high Extensive testing required · High bug introduction risk labels Apr 17, 2026
@github-actions github-actions Bot added risk-high Extensive testing required · High bug introduction risk and removed risk-high Extensive testing required · High bug introduction risk labels Apr 17, 2026
…ow-cta

Made-with: Cursor

# Conflicts:
#	app/core/Engine/messengers/social-service-messenger.ts
@github-actions github-actions Bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-high Extensive testing required · High bug introduction risk labels Apr 17, 2026
@@ -1,10 +1,13 @@
import { useCallback, useState, useEffect } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Significant Code Duplication — useTopTraders vs useTraderProfile

Both hooks add this same logical sequence. I'll put them side by side using the diff content.

1. State setup — identical concept, only data structure differs

useTraderProfile manages a single trader:

const [optimisticFollow, setOptimisticFollow] = useState<boolean | null>(null);
const inflightRef = useRef(false);

const isFollowing = optimisticFollow ?? reduxFollowing;

useTopTraders manages N traders, so it uses a map — but the concept is the same:

const [optimisticFollowState, setOptimisticFollowState] = useState<Record<string, boolean>>({});
const inflightIdsRef = useRef<Set<string>>(new Set());

// inside useMemo per-trader:
isFollowing: optimisticFollowState[entry.profileId] ?? currentFollowing

The only real difference is "one boolean" vs "a Record<id, boolean>". The logic for why optimistic state exists and how it interacts with Redux is identical.


2. toggleFollow body — nearly verbatim

useTraderProfile:

const toggleFollow = useCallback(async () => {
  if (inflightRef.current) return;           // ← guard
  inflightRef.current = true;

  const nextValue = !isFollowing;
  setOptimisticFollow(nextValue);            // ← optimistic set

  try {
    const { profileId } =
      await Engine.context.AuthenticationController.getSessionProfile();
    const opts = { addressOrUid: profileId, targets: [addressOrId] };
    if (nextValue) {
      await messenger.call('SocialController:followTrader', opts);
    } else {
      await messenger.call('SocialController:unfollowTrader', opts);
    }
  } catch (err) {
    setOptimisticFollow(null);               // ← rollback
    Logger.error(err, '...');
  } finally {
    inflightRef.current = false;             // ← clear guard
  }
}, [isFollowing, addressOrId]);

useTopTraders:

const toggleFollow = useCallback(async (addressOrId: string) => {
  if (inflightIdsRef.current.has(addressOrId)) return;   // ← guard
  inflightIdsRef.current.add(addressOrId);

  const nextValue = !currentlyFollowing;
  setOptimisticFollowState(prev => ({ ...prev, [addressOrId]: nextValue }));  // ← optimistic set

  try {
    const { profileId } =
      await Engine.context.AuthenticationController.getSessionProfile();
    const opts = { addressOrUid: profileId, targets: [addressOrId] };
    if (nextValue) {
      await messenger.call('SocialController:followTrader', opts);
    } else {
      await messenger.call('SocialController:unfollowTrader', opts);
    }
  } catch (err) {
    setOptimisticFollowState(prev => { delete prev[addressOrId]; return prev; }); // ← rollback
    Logger.error(err, '...');
  } finally {
    inflightIdsRef.current.delete(addressOrId);   // ← clear guard
  }
}, [...]);

The four steps — guard → optimistic set → getSessionProfile + messenger call → rollback on error — are word-for-word the same. Only the state mutation shape differs.


3. Cleanup useEffect — same intent, different shape

useTraderProfile — single value, simple:

useEffect(() => {
  if (optimisticFollow !== null && reduxFollowing === optimisticFollow) {
    setOptimisticFollow(null);   // clear once Redux agrees
  }
}, [optimisticFollow, reduxFollowing]);

useTopTraders — iterates over all entries, but same idea:

useEffect(() => {
  setOptimisticFollowState(prev => {
    for (const [id, value] of Object.entries(prev)) {
      if (followingProfileIds.includes(id) === value) {
        // remove it — Redux has caught up
      }
    }
  });
}, [followingProfileIds]);

Both effects exist to answer the same question: "Has Redux caught up to my optimistic value? If yes, discard it."


The Risk

Because this logic is duplicated:

  • A bug in the guard logic (e.g., the inflight ref not being cleared in an edge case) has to be fixed in two places. Right now there's actually a subtle asymmetry: on error, useTraderProfile sets optimisticFollow to null (clearing it), while useTopTraders deletes the key from the map. These are equivalent today but they'll diverge under future edits.
  • If a third surface (e.g., a Watchlist or a Notifications screen) needs follow/unfollow, this pattern gets copy-pasted a third time.

What the Shared Hook Would Look Like

The single-trader hook that both could call:

// app/components/hooks/useFollowToggle.ts

export function useFollowToggle(addressOrId: string): {
  isFollowing: boolean;
  toggleFollow: () => Promise<void>;
} {
  const followingProfileIds = useSelector(selectFollowingProfileIds);
  const reduxFollowing = followingProfileIds.includes(addressOrId);

  const [optimistic, setOptimistic] = useState<boolean | null>(null);
  const inflightRef = useRef(false);

  const isFollowing = optimistic ?? reduxFollowing;

  const toggleFollow = useCallback(async () => {
    if (inflightRef.current) return;
    inflightRef.current = true;
    const next = !isFollowing;
    setOptimistic(next);
    try {
      const { profileId } =
        await Engine.context.AuthenticationController.getSessionProfile();
      const opts = { addressOrUid: profileId, targets: [addressOrId] };
      await (Engine.controllerMessenger.call as CallableFunction)(
        next ? 'SocialController:followTrader' : 'SocialController:unfollowTrader',
        opts,
      );
    } catch (err) {
      setOptimistic(null);
      Logger.error(err as Error, 'useFollowToggle failed');
    } finally {
      inflightRef.current = false;
    }
  }, [isFollowing, addressOrId]);

  useEffect(() => {
    if (optimistic !== null && reduxFollowing === optimistic) {
      setOptimistic(null);
    }
  }, [optimistic, reduxFollowing]);

  return { isFollowing, toggleFollow };
}

useTraderProfile then becomes a one-liner call:

const { isFollowing, toggleFollow } = useFollowToggle(addressOrId);

useTopTraders calls it once per trader in useMemo, or calls it at render and passes the result into the list — the multi-trader case needs a small adapter, but the core logic lives in one place.

The duplication is real and worth surfacing in review, even if the author chooses to defer the refactor to a follow-up.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

zone-live
zone-live previously approved these changes Apr 17, 2026
… hooks

Removed Redux dependencies and optimized follow state management by integrating useFollowToggle and useFollowToggleMany hooks. This change enhances code clarity and performance by eliminating unnecessary state management and improving the optimistic follow behavior for traders and profiles.
@github-actions github-actions Bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Apr 17, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit be833fe. Configure here.

Comment thread app/components/Views/Homepage/Sections/TopTraders/hooks/useTopTraders.test.ts Outdated
Changed the import statement in the `useTopTraders.test.ts` file from `@testing-library/react-hooks` to `@testing-library/react-native` to align with the testing framework used in the project.
@github-actions github-actions Bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Apr 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeWalletPlatform
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: medium
  • AI Confidence: 82%
click to see 🤖 AI reasoning details

E2E Test Selection:
The changes introduce a new Social Controller hydration mechanism and follow/unfollow toggle hooks that power the Top Traders section of the Trending tab. Key changes:

  1. EngineService.ts: Adds hydrateSocialFollowing() as a fire-and-forget call during Engine initialization. This is a critical path change (Engine startup), but it's non-blocking and failures are caught/logged. Low risk of breaking other flows.

  2. social-controller-hydration.ts: New utility that calls SocialController:updateFollowing via the controller messenger at startup. Depends on AuthenticationController.getSessionProfile().

  3. socialController.ts selector: New Redux selector reading SocialController.followingProfileIds from engine background state.

  4. useFollowToggle.ts: New shared hook providing optimistic follow/unfollow state management against SocialController. Used by both useTopTraders and useTraderProfile.

  5. useTopTraders.ts / useTraderProfile.ts: Refactored to use the new shared hook instead of local state overrides — this changes the follow state source from ephemeral local state to persisted Redux state backed by SocialController.

Tag Selection Rationale:

  • SmokeWalletPlatform: The Top Traders section is part of the Trending tab, which is explicitly covered by SmokeWalletPlatform (Trending discovery tab: search functionality, browsing content feeds including Tokens, Perps, Sites sections). The follow/unfollow functionality in the leaderboard is directly affected. The Trending feed tests in tests/smoke/trending/ are tagged with SmokeWalletPlatform.

Tags NOT selected:

  • SmokeIdentity: While AuthenticationController is used, the changes don't touch identity sync flows or account sync features directly.
  • SmokeConfirmations, SmokeTrade, SmokePerps, SmokePredictions: No changes to transaction flows, swap/bridge, perps trading, or prediction markets.
  • Other tags: No impact on accounts, networks, ramps, multichain, snaps, or seedless onboarding.

Performance Test Selection:
The hydrateSocialFollowing() call added to EngineService is fire-and-forget and non-blocking, so it should not meaningfully impact app launch or login performance. The follow toggle hooks use optimistic state updates which are lightweight. No performance tests are warranted for these changes.

View GitHub Actions results

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

E2E Fixture Validation — Schema is up to date
12 value mismatches detected (expected — fixture represents an existing user).
View details

@zone-live zone-live added this pull request to the merge queue Apr 17, 2026
Merged via the queue into main with commit 61a0b3c Apr 17, 2026
99 checks passed
@zone-live zone-live deleted the TSA-389-follow-unfollow-cta branch April 17, 2026 14:09
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 17, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.75.0 Issue or pull request that will be included in release 7.75.0 label Apr 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.75.0 Issue or pull request that will be included in release 7.75.0 risk-medium Moderate testing recommended · Possible bug introduction risk size-L team-portfolio team-social-ai Social & AI team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants