From 5f906f4c7a4c727c0ddd1078665d5f1152a084b1 Mon Sep 17 00:00:00 2001 From: Bigshmow Date: Tue, 14 Apr 2026 19:12:11 -0600 Subject: [PATCH 1/3] feat(social-controllers): add followingProfileIds to controller state --- packages/social-controllers/CHANGELOG.md | 1 + .../src/SocialController.ts | 23 +++++++++++++++++++ .../social-controllers/src/social-types.ts | 4 +++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/social-controllers/CHANGELOG.md b/packages/social-controllers/CHANGELOG.md index b7089e4c097..4afb3e9f5c4 100644 --- a/packages/social-controllers/CHANGELOG.md +++ b/packages/social-controllers/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `intent` and optional `category` fields to `Trade` type ([#8410](https://github.com/MetaMask/core/pull/8410)) - Export `TradeStruct` superstruct schema; derive `Trade` type via `Infer` ([#8410](https://github.com/MetaMask/core/pull/8410)) - Narrow `direction` to `'buy' | 'sell'` and `intent` to `'enter' | 'exit'` on `Trade` type ([#8410](https://github.com/MetaMask/core/pull/8410)) +- Add `followingProfileIds` to `SocialControllerState` — stores Clicker profile IDs alongside existing `followingAddresses` ([#XXXX](https://github.com/MetaMask/core/pull/XXXX)) ### Changed diff --git a/packages/social-controllers/src/SocialController.ts b/packages/social-controllers/src/SocialController.ts index 9e3aa39c0ec..7ef862efeb5 100644 --- a/packages/social-controllers/src/SocialController.ts +++ b/packages/social-controllers/src/SocialController.ts @@ -91,6 +91,7 @@ export function getDefaultSocialControllerState(): SocialControllerState { return { leaderboardEntries: [], followingAddresses: [], + followingProfileIds: [], }; } @@ -109,6 +110,12 @@ const socialControllerMetadata: StateMetadata = { includeInStateLogs: false, usedInUi: true, }, + followingProfileIds: { + persist: true, + includeInDebugSnapshot: false, + includeInStateLogs: false, + usedInUi: true, + }, }; // === CONTROLLER === @@ -181,6 +188,9 @@ export class SocialController extends BaseController< const newAddresses = [ ...new Set(followResponse.followed.map((profile) => profile.address)), ]; + const newProfileIds = [ + ...new Set(followResponse.followed.map((profile) => profile.profileId)), + ]; this.update((state) => { const existing = new Set(state.followingAddresses); @@ -188,6 +198,10 @@ export class SocialController extends BaseController< (address) => !existing.has(address), ); state.followingAddresses.push(...uniqueNewAddresses); + + const existingIds = new Set(state.followingProfileIds); + const uniqueNewIds = newProfileIds.filter((id) => !existingIds.has(id)); + state.followingProfileIds.push(...uniqueNewIds); }); return followResponse; @@ -210,11 +224,17 @@ export class SocialController extends BaseController< const removedAddresses = new Set( unfollowResponse.unfollowed.map((profile) => profile.address), ); + const removedProfileIds = new Set( + unfollowResponse.unfollowed.map((profile) => profile.profileId), + ); this.update((state) => { state.followingAddresses = state.followingAddresses.filter( (address) => !removedAddresses.has(address), ); + state.followingProfileIds = state.followingProfileIds.filter( + (id) => !removedProfileIds.has(id), + ); }); return unfollowResponse; @@ -240,6 +260,9 @@ export class SocialController extends BaseController< state.followingAddresses = followingResponse.following.map( (profile) => profile.address, ); + state.followingProfileIds = followingResponse.following.map( + (profile) => profile.profileId, + ); }); return followingResponse; diff --git a/packages/social-controllers/src/social-types.ts b/packages/social-controllers/src/social-types.ts index e80b9abd57e..4fcfbec6812 100644 --- a/packages/social-controllers/src/social-types.ts +++ b/packages/social-controllers/src/social-types.ts @@ -270,6 +270,8 @@ export type UnfollowOptions = { export type SocialControllerState = { /** Cached ranked trader list from the last `updateLeaderboard` call. */ leaderboardEntries: LeaderboardEntry[]; - /** Addresses the current user follows — drives Follow/Following button state. */ + /** Wallet addresses the current user follows. */ followingAddresses: string[]; + /** Clicker profile IDs the current user follows — used by mobile UI. */ + followingProfileIds: string[]; }; From 52d3647f13f574b23d7f6e7a81dca2130065e40e Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 15 Apr 2026 09:39:44 +0100 Subject: [PATCH 2/3] chore: changelog update --- packages/social-controllers/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/social-controllers/CHANGELOG.md b/packages/social-controllers/CHANGELOG.md index 4afb3e9f5c4..b28444c35ac 100644 --- a/packages/social-controllers/CHANGELOG.md +++ b/packages/social-controllers/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `intent` and optional `category` fields to `Trade` type ([#8410](https://github.com/MetaMask/core/pull/8410)) - Export `TradeStruct` superstruct schema; derive `Trade` type via `Infer` ([#8410](https://github.com/MetaMask/core/pull/8410)) - Narrow `direction` to `'buy' | 'sell'` and `intent` to `'enter' | 'exit'` on `Trade` type ([#8410](https://github.com/MetaMask/core/pull/8410)) -- Add `followingProfileIds` to `SocialControllerState` — stores Clicker profile IDs alongside existing `followingAddresses` ([#XXXX](https://github.com/MetaMask/core/pull/XXXX)) +- Add `followingProfileIds` to `SocialControllerState` — stores Clicker profile IDs alongside existing `followingAddresses` ([#8459](https://github.com/MetaMask/core/pull/8459)) ### Changed From 38e27b9420d3d111c5235e84b661bf5bd59e11d9 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 15 Apr 2026 09:51:37 +0100 Subject: [PATCH 3/3] chore: test updaate --- .../src/SocialController.test.ts | 90 +++++++++++++++++-- 1 file changed, 81 insertions(+), 9 deletions(-) diff --git a/packages/social-controllers/src/SocialController.test.ts b/packages/social-controllers/src/SocialController.test.ts index 3e3e4d8e11d..57f809feb9e 100644 --- a/packages/social-controllers/src/SocialController.test.ts +++ b/packages/social-controllers/src/SocialController.test.ts @@ -98,6 +98,7 @@ describe('SocialController', () => { expect(getDefaultSocialControllerState()).toStrictEqual({ leaderboardEntries: [], followingAddresses: [], + followingProfileIds: [], }); }); }); @@ -109,6 +110,7 @@ describe('SocialController', () => { expect(controller.state).toStrictEqual({ leaderboardEntries: [], followingAddresses: [], + followingProfileIds: [], }); }); @@ -120,6 +122,21 @@ describe('SocialController', () => { expect(controller.state).toStrictEqual({ leaderboardEntries: [], followingAddresses: ['0xaaaa'], + followingProfileIds: [], + }); + }); + + it('merges partial initial followingProfileIds with defaults', () => { + const { controller } = createController({ + state: { + followingProfileIds: ['550e8400-e29b-41d4-a716-446655440000'], + }, + }); + + expect(controller.state).toStrictEqual({ + leaderboardEntries: [], + followingAddresses: [], + followingProfileIds: ['550e8400-e29b-41d4-a716-446655440000'], }); }); }); @@ -210,7 +227,7 @@ describe('SocialController', () => { }); describe('followTrader', () => { - it('calls service via messenger and appends new addresses to state', async () => { + it('calls service via messenger and appends new addresses and profile IDs to state', async () => { const rootMessenger = getRootMessenger(); const follow = jest .fn() @@ -232,9 +249,12 @@ describe('SocialController', () => { expect(controller.state.followingAddresses).toStrictEqual([ '0x1111111111111111111111111111111111111111', ]); + expect(controller.state.followingProfileIds).toStrictEqual([ + '550e8400-e29b-41d4-a716-446655440000', + ]); }); - it('appends multiple new addresses', async () => { + it('appends multiple new addresses and profile IDs', async () => { const rootMessenger = getRootMessenger(); mockServiceAction( rootMessenger, @@ -258,9 +278,13 @@ describe('SocialController', () => { '0x1111111111111111111111111111111111111111', '0x2222222222222222222222222222222222222222', ]); + expect(controller.state.followingProfileIds).toStrictEqual([ + '550e8400-e29b-41d4-a716-446655440000', + '660e8400-e29b-41d4-a716-446655440001', + ]); }); - it('deduplicates addresses within the same batch', async () => { + it('deduplicates addresses and profile IDs within the same batch', async () => { const rootMessenger = getRootMessenger(); mockServiceAction( rootMessenger, @@ -280,6 +304,36 @@ describe('SocialController', () => { expect(controller.state.followingAddresses).toStrictEqual([ '0x1111111111111111111111111111111111111111', ]); + expect(controller.state.followingProfileIds).toStrictEqual([ + '550e8400-e29b-41d4-a716-446655440000', + ]); + }); + + it('does not duplicate existing addresses or profile IDs across calls', async () => { + const rootMessenger = getRootMessenger(); + mockServiceAction( + rootMessenger, + 'SocialService:follow', + jest.fn().mockResolvedValue({ followed: [mockProfileSummary] }), + ); + + const { controller } = createController({ rootMessenger }); + + await controller.followTrader({ + addressOrUid: '0xuser', + targets: ['0x1111111111111111111111111111111111111111'], + }); + await controller.followTrader({ + addressOrUid: '0xuser', + targets: ['0x1111111111111111111111111111111111111111'], + }); + + expect(controller.state.followingAddresses).toStrictEqual([ + '0x1111111111111111111111111111111111111111', + ]); + expect(controller.state.followingProfileIds).toStrictEqual([ + '550e8400-e29b-41d4-a716-446655440000', + ]); }); it('is callable via messenger action', async () => { @@ -302,7 +356,7 @@ describe('SocialController', () => { }); describe('unfollowTrader', () => { - it('calls service via messenger and removes addresses from state', async () => { + it('calls service via messenger and removes addresses and profile IDs from state', async () => { const rootMessenger = getRootMessenger(); const unfollow = jest .fn() @@ -316,6 +370,10 @@ describe('SocialController', () => { '0x1111111111111111111111111111111111111111', '0x2222222222222222222222222222222222222222', ], + followingProfileIds: [ + '550e8400-e29b-41d4-a716-446655440000', + '660e8400-e29b-41d4-a716-446655440001', + ], }, }); @@ -332,6 +390,9 @@ describe('SocialController', () => { expect(controller.state.followingAddresses).toStrictEqual([ '0x2222222222222222222222222222222222222222', ]); + expect(controller.state.followingProfileIds).toStrictEqual([ + '660e8400-e29b-41d4-a716-446655440001', + ]); }); it('handles unfollowing an address not in state gracefully', async () => { @@ -344,7 +405,7 @@ describe('SocialController', () => { const { controller } = createController({ rootMessenger, - state: { followingAddresses: [] }, + state: { followingAddresses: [], followingProfileIds: [] }, }); await controller.unfollowTrader({ @@ -353,6 +414,7 @@ describe('SocialController', () => { }); expect(controller.state.followingAddresses).toStrictEqual([]); + expect(controller.state.followingProfileIds).toStrictEqual([]); }); it('is callable via messenger action', async () => { @@ -375,7 +437,7 @@ describe('SocialController', () => { }); describe('updateFollowing', () => { - it('calls service via messenger and replaces followingAddresses in state', async () => { + it('calls service via messenger and replaces followingAddresses and followingProfileIds in state', async () => { const rootMessenger = getRootMessenger(); const fetchFollowing = jest.fn().mockResolvedValue({ following: [mockProfileSummary], @@ -389,7 +451,10 @@ describe('SocialController', () => { const { controller } = createController({ rootMessenger, - state: { followingAddresses: ['0xold'] }, + state: { + followingAddresses: ['0xold'], + followingProfileIds: ['old-profile-id'], + }, }); const result = await controller.updateFollowing({ @@ -403,9 +468,12 @@ describe('SocialController', () => { expect(controller.state.followingAddresses).toStrictEqual([ '0x1111111111111111111111111111111111111111', ]); + expect(controller.state.followingProfileIds).toStrictEqual([ + '550e8400-e29b-41d4-a716-446655440000', + ]); }); - it('clears followingAddresses when response is empty', async () => { + it('clears followingAddresses and followingProfileIds when response is empty', async () => { const rootMessenger = getRootMessenger(); mockServiceAction( rootMessenger, @@ -415,12 +483,16 @@ describe('SocialController', () => { const { controller } = createController({ rootMessenger, - state: { followingAddresses: ['0xold'] }, + state: { + followingAddresses: ['0xold'], + followingProfileIds: ['old-profile-id'], + }, }); await controller.updateFollowing({ addressOrUid: '0xuser' }); expect(controller.state.followingAddresses).toStrictEqual([]); + expect(controller.state.followingProfileIds).toStrictEqual([]); }); it('is callable via messenger action', async () => {