Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/social-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` ([#8459](https://github.com/MetaMask/core/pull/8459))

### Changed

Expand Down
90 changes: 81 additions & 9 deletions packages/social-controllers/src/SocialController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ describe('SocialController', () => {
expect(getDefaultSocialControllerState()).toStrictEqual({
leaderboardEntries: [],
followingAddresses: [],
followingProfileIds: [],
});
});
});
Expand All @@ -109,6 +110,7 @@ describe('SocialController', () => {
expect(controller.state).toStrictEqual({
leaderboardEntries: [],
followingAddresses: [],
followingProfileIds: [],
});
});

Expand All @@ -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'],
});
});
});
Expand Down Expand Up @@ -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()
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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 () => {
Expand All @@ -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()
Expand All @@ -316,6 +370,10 @@ describe('SocialController', () => {
'0x1111111111111111111111111111111111111111',
'0x2222222222222222222222222222222222222222',
],
followingProfileIds: [
'550e8400-e29b-41d4-a716-446655440000',
'660e8400-e29b-41d4-a716-446655440001',
],
},
});

Expand All @@ -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 () => {
Expand All @@ -344,7 +405,7 @@ describe('SocialController', () => {

const { controller } = createController({
rootMessenger,
state: { followingAddresses: [] },
state: { followingAddresses: [], followingProfileIds: [] },
});

await controller.unfollowTrader({
Expand All @@ -353,6 +414,7 @@ describe('SocialController', () => {
});

expect(controller.state.followingAddresses).toStrictEqual([]);
expect(controller.state.followingProfileIds).toStrictEqual([]);
});

it('is callable via messenger action', async () => {
Expand All @@ -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],
Expand All @@ -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({
Expand All @@ -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,
Expand All @@ -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 () => {
Expand Down
23 changes: 23 additions & 0 deletions packages/social-controllers/src/SocialController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function getDefaultSocialControllerState(): SocialControllerState {
return {
leaderboardEntries: [],
followingAddresses: [],
followingProfileIds: [],
Comment thread
cursor[bot] marked this conversation as resolved.
};
}

Expand All @@ -109,6 +110,12 @@ const socialControllerMetadata: StateMetadata<SocialControllerState> = {
includeInStateLogs: false,
usedInUi: true,
},
followingProfileIds: {
persist: true,
includeInDebugSnapshot: false,
includeInStateLogs: false,
usedInUi: true,
},
};

// === CONTROLLER ===
Expand Down Expand Up @@ -181,13 +188,20 @@ 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);
const uniqueNewAddresses = newAddresses.filter(
(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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion packages/social-controllers/src/social-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
};
Loading