Skip to content

Commit 96abde9

Browse files
Bigshmowclaude
andauthored
feat: social leaderboard opt out (#32982)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> <!-- mms-check directive vocabulary — read by .github/scripts/shared/pr-template-checks.ts at module load to build the validation plan. Directives are invisible in rendered markdown and must NOT be removed or edited without updating the validator registry. type=text Section must contain non-placeholder prose. type=changelog Section must have a valid CHANGELOG entry: line. type=issue-link Section must have a Fixes:/Closes:/Refs: line with a value. type=manual-testing Section must have real testing steps or an explicit N/A. type=screenshot Section must have evidence (image/URL) or an explicit N/A. type=checklist Section must have all checkboxes consciously checked. required=true|false Whether a missing/invalid section runs the validator at all. blocking=true|false Whether a failure of this check fails the CI workflow. Default: false — failures are shown as warnings in the sticky comment but do not block the PR. Sections without a directive are checked for structural presence only. --> ## **Description** <!-- mms-check: type=text required=true --> <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> Adds a way for users to opt out of the Top Traders leaderboard from within the app. **Reason:** MetaMask previously had no in-app way for a user to remove their account's addresses from the Top Traders leaderboard. `@metamask/social-controllers@2.4.0` added the `optOutOfLeaderboard` capability (messenger action `SocialController:optOutOfLeaderboard`); this PR wires it into the mobile UI. **Solution:** - Bumps `@metamask/social-controllers` from `2.3.1` → `2.4.0`. - Adds a **Top Traders** entry to the app Settings list (rendered after "Buy & sell crypto", gated by `selectSocialLeaderboardEnabled`). - Adds a `TopTradersSettings` screen with an **Opt out of leaderboard** row. - Adds a `LeaderboardOptOutBottomSheet` (composed from the design-system `BottomSheet`, mirroring the existing `TradingSignalsSetupBottomSheet`) that calls `SocialController:optOutOfLeaderboard`, showing a loading/disabled state, closing on success, and logging + recovering on failure. - New routes `SETTINGS.TOP_TRADERS` and `SOCIAL_LEADERBOARD.OPT_OUT` with navigator registrations, param-list types, i18n strings, and testIDs. ## **Changelog** <!-- mms-check: type=changelog required=true blocking=true --> <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: Added a Top Traders section in Settings that lets users opt out of the Top Traders leaderboard. ## **Related issues** Figma: https://www.figma.com/design/HzPB0InxHBY5hPkOukPCqv/Social?node-id=3017-31866&t=R8yrnl1Yo45ffgJO-4 <!-- mms-check: type=issue-link required=true --> Fixes: [TSA-830] ## **Manual testing steps** <!-- mms-check: type=manual-testing required=true --> ```gherkin Feature: Opt out of the Top Traders leaderboard from Settings Scenario: user opts out of the leaderboard Given the Social Leaderboard feature flag is enabled And the user is on the Settings screen When the user taps "Top Traders" Then the Top Traders settings screen is shown with an "Opt out of leaderboard" option When the user taps "Opt out of leaderboard" Then a bottom sheet appears with the opt-out description and a confirm button When the user taps "Opt out of leaderboard" in the sheet Then the app calls SocialController:optOutOfLeaderboard And the sheet closes once the request succeeds Scenario: opt-out request fails Given the user has opened the opt-out bottom sheet When the user taps "Opt out of leaderboard" And the opt-out request fails Then the error is logged And the sheet stays open with the button re-enabled Scenario: leaderboard feature flag is disabled Given the Social Leaderboard feature flag is disabled When the user is on the Settings screen Then the "Top Traders" entry is not shown ``` ## **Screenshots/Recordings** <!-- mms-check: type=screenshot required=true --> <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <img width="450" height="992" alt="Screenshot 2026-07-08 at 3 28 04 PM" src="https://github.com/user-attachments/assets/a610166e-54ee-475e-8e0f-2886c12ffe0e" /> <!-- Settings list without the "Top Traders" entry --> ### **After** <img width="450" height="961" alt="Screenshot 2026-07-08 at 3 20 47 PM" src="https://github.com/user-attachments/assets/2fef884d-2149-48e5-8d0e-07e57016a54b" /> <!-- Recording of: Settings → Top Traders → Opt out of leaderboard → bottom sheet → confirm --> ## **Pre-merge author checklist** <!-- mms-check: type=checklist required=true --> <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [x] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [x] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [x] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] 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. [TSA-830]: https://consensyssoftware.atlassian.net/browse/TSA-830?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes public visibility of trading accounts via backend opt-in/out; risk is mitigated by feature flags, non-optimistic persistence, and tests, but still touches privacy-sensitive social settings. > > **Overview** > Adds a **Top Traders** block on **Security & privacy** so users can control whether their account appears on the leaderboard, behind both `aiSocialLeaderboardEnabled` and the new `aiSocialLeaderboardOptFlowEnabled` remote flag. > > Toggling calls `SocialController:optInToLeaderboard` / `optOutOfLeaderboard` (with `SocialService` actions delegated on the social controller messenger). Redux `settings.showAccountOnLeaderboard` is updated **only after** a successful backend response (default shown); failures are logged and the toggle stays unchanged. A new MetaMetrics opt-in event **Trader Leaderboard Visibility Toggled** fires on success, and the section is covered by unit tests plus i18n and feature-flag registry entries. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f2929c3. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e2db41a commit 96abde9

12 files changed

Lines changed: 420 additions & 0 deletions

File tree

app/actions/settings/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,10 @@ export function setPerpsChartPreferredCandlePeriod(preferredCandlePeriod) {
107107
preferredCandlePeriod,
108108
};
109109
}
110+
111+
export function setShowAccountOnLeaderboard(showAccountOnLeaderboard) {
112+
return {
113+
type: 'SET_SHOW_ACCOUNT_ON_LEADERBOARD',
114+
showAccountOnLeaderboard,
115+
};
116+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { fireEvent, screen, waitFor } from '@testing-library/react-native';
2+
import React from 'react';
3+
4+
import { strings } from '../../../../../../locales/i18n';
5+
import { MetaMetricsEvents } from '../../../../../core/Analytics';
6+
import renderWithProvider from '../../../../../util/test/renderWithProvider';
7+
import { SecurityPrivacyViewSelectorsIDs } from '../SecurityPrivacyView.testIds';
8+
import TopTradersSection from './TopTradersSection';
9+
10+
const mockMessengerCall = jest.fn().mockResolvedValue(undefined);
11+
const mockLoggerError = jest.fn();
12+
const mockTrackEvent = jest.fn();
13+
const mockAddProperties = jest.fn().mockReturnThis();
14+
const mockBuild = jest.fn().mockReturnValue({});
15+
const mockCreateEventBuilder = jest.fn().mockReturnValue({
16+
addProperties: mockAddProperties,
17+
build: mockBuild,
18+
});
19+
let mockLeaderboardEnabled = true;
20+
let mockOptFlowEnabled = true;
21+
22+
jest.mock('../../../../../core/Engine', () => ({
23+
controllerMessenger: {
24+
call: (...args: unknown[]) => mockMessengerCall(...args),
25+
},
26+
}));
27+
28+
jest.mock('../../../../../util/Logger', () => ({
29+
error: (...args: unknown[]) => mockLoggerError(...args),
30+
}));
31+
32+
jest.mock('../../../../hooks/useAnalytics/useAnalytics', () => ({
33+
useAnalytics: () => ({
34+
trackEvent: mockTrackEvent,
35+
createEventBuilder: mockCreateEventBuilder,
36+
}),
37+
}));
38+
39+
jest.mock(
40+
'../../../../../selectors/featureFlagController/socialLeaderboard',
41+
() => ({
42+
selectSocialLeaderboardEnabled: () => mockLeaderboardEnabled,
43+
selectSocialLeaderboardOptFlowEnabled: () => mockOptFlowEnabled,
44+
}),
45+
);
46+
47+
const renderWith = (showAccountOnLeaderboard = true) =>
48+
renderWithProvider(<TopTradersSection />, {
49+
state: { settings: { showAccountOnLeaderboard } },
50+
});
51+
52+
describe('TopTradersSection', () => {
53+
beforeEach(() => {
54+
jest.clearAllMocks();
55+
mockLeaderboardEnabled = true;
56+
mockOptFlowEnabled = true;
57+
mockMessengerCall.mockResolvedValue(undefined);
58+
});
59+
60+
it('renders the section and toggle when the leaderboard is enabled', () => {
61+
renderWith(true);
62+
63+
expect(
64+
screen.getByTestId(SecurityPrivacyViewSelectorsIDs.TOP_TRADERS_SECTION),
65+
).toBeOnTheScreen();
66+
expect(
67+
screen.getByText(
68+
strings('social_leaderboard.settings.show_account_on_leaderboard'),
69+
),
70+
).toBeOnTheScreen();
71+
expect(
72+
screen.getByTestId(
73+
SecurityPrivacyViewSelectorsIDs.SHOW_ACCOUNT_ON_LEADERBOARD_TOGGLE,
74+
).props.value,
75+
).toBe(true);
76+
});
77+
78+
it('renders nothing when the leaderboard feature is disabled', () => {
79+
mockLeaderboardEnabled = false;
80+
renderWith(true);
81+
82+
expect(
83+
screen.queryByTestId(SecurityPrivacyViewSelectorsIDs.TOP_TRADERS_SECTION),
84+
).toBeNull();
85+
});
86+
87+
it('renders nothing when the leaderboard opt-flow flag is disabled', () => {
88+
mockOptFlowEnabled = false;
89+
renderWith(true);
90+
91+
expect(
92+
screen.queryByTestId(SecurityPrivacyViewSelectorsIDs.TOP_TRADERS_SECTION),
93+
).toBeNull();
94+
});
95+
96+
it('opts out and reflects the toggle off after the backend confirms', async () => {
97+
renderWith(true);
98+
const toggle = screen.getByTestId(
99+
SecurityPrivacyViewSelectorsIDs.SHOW_ACCOUNT_ON_LEADERBOARD_TOGGLE,
100+
);
101+
102+
fireEvent(toggle, 'valueChange', false);
103+
104+
await waitFor(() =>
105+
expect(mockMessengerCall).toHaveBeenCalledWith(
106+
'SocialController:optOutOfLeaderboard',
107+
),
108+
);
109+
// Persisted only after the backend confirms, not optimistically.
110+
await waitFor(() => expect(toggle.props.value).toBe(false));
111+
});
112+
113+
it('opts in when turned on', async () => {
114+
renderWith(false);
115+
const toggle = screen.getByTestId(
116+
SecurityPrivacyViewSelectorsIDs.SHOW_ACCOUNT_ON_LEADERBOARD_TOGGLE,
117+
);
118+
119+
fireEvent(toggle, 'valueChange', true);
120+
121+
await waitFor(() =>
122+
expect(mockMessengerCall).toHaveBeenCalledWith(
123+
'SocialController:optInToLeaderboard',
124+
),
125+
);
126+
});
127+
128+
it('tracks the visibility-toggled event on a successful opt-out', async () => {
129+
renderWith(true);
130+
const toggle = screen.getByTestId(
131+
SecurityPrivacyViewSelectorsIDs.SHOW_ACCOUNT_ON_LEADERBOARD_TOGGLE,
132+
);
133+
134+
fireEvent(toggle, 'valueChange', false);
135+
136+
await waitFor(() =>
137+
expect(mockCreateEventBuilder).toHaveBeenCalledWith(
138+
MetaMetricsEvents.SOCIAL_TRADER_LEADERBOARD_VISIBILITY_TOGGLED,
139+
),
140+
);
141+
expect(mockAddProperties).toHaveBeenCalledWith({
142+
show_account_on_leaderboard: false,
143+
});
144+
expect(mockTrackEvent).toHaveBeenCalledTimes(1);
145+
});
146+
147+
it('tracks the visibility-toggled event on a successful opt-in', async () => {
148+
renderWith(false);
149+
const toggle = screen.getByTestId(
150+
SecurityPrivacyViewSelectorsIDs.SHOW_ACCOUNT_ON_LEADERBOARD_TOGGLE,
151+
);
152+
153+
fireEvent(toggle, 'valueChange', true);
154+
155+
await waitFor(() =>
156+
expect(mockCreateEventBuilder).toHaveBeenCalledWith(
157+
MetaMetricsEvents.SOCIAL_TRADER_LEADERBOARD_VISIBILITY_TOGGLED,
158+
),
159+
);
160+
expect(mockAddProperties).toHaveBeenCalledWith({
161+
show_account_on_leaderboard: true,
162+
});
163+
expect(mockTrackEvent).toHaveBeenCalledTimes(1);
164+
});
165+
166+
it('logs and leaves the toggle unchanged when the request fails', async () => {
167+
mockMessengerCall.mockRejectedValueOnce(new Error('network down'));
168+
renderWith(true);
169+
const toggle = screen.getByTestId(
170+
SecurityPrivacyViewSelectorsIDs.SHOW_ACCOUNT_ON_LEADERBOARD_TOGGLE,
171+
);
172+
173+
fireEvent(toggle, 'valueChange', false);
174+
175+
await waitFor(() => expect(mockLoggerError).toHaveBeenCalled());
176+
// Never persisted optimistically, so nothing to revert — stays as it was.
177+
expect(toggle.props.value).toBe(true);
178+
});
179+
180+
it('does not track the event when the request fails', async () => {
181+
mockMessengerCall.mockRejectedValueOnce(new Error('network down'));
182+
renderWith(true);
183+
const toggle = screen.getByTestId(
184+
SecurityPrivacyViewSelectorsIDs.SHOW_ACCOUNT_ON_LEADERBOARD_TOGGLE,
185+
);
186+
187+
fireEvent(toggle, 'valueChange', false);
188+
189+
await waitFor(() => expect(mockLoggerError).toHaveBeenCalled());
190+
expect(mockTrackEvent).not.toHaveBeenCalled();
191+
});
192+
});
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { Text, TextVariant } from '@metamask/design-system-react-native';
2+
import React, { useCallback, useState } from 'react';
3+
import { View } from 'react-native';
4+
import { useDispatch, useSelector } from 'react-redux';
5+
6+
import { strings } from '../../../../../../locales/i18n';
7+
import { setShowAccountOnLeaderboard } from '../../../../../actions/settings';
8+
import { MetaMetricsEvents } from '../../../../../core/Analytics';
9+
import Engine from '../../../../../core/Engine';
10+
import { RootState } from '../../../../../reducers';
11+
import {
12+
selectSocialLeaderboardEnabled,
13+
selectSocialLeaderboardOptFlowEnabled,
14+
} from '../../../../../selectors/featureFlagController/socialLeaderboard';
15+
import Logger from '../../../../../util/Logger';
16+
import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
17+
import { SecurityOptionToggle } from '../../../../UI/SecurityOptionToggle';
18+
import { useStyles } from '../../../../hooks/useStyles';
19+
import createStyles from '../SecuritySettings.styles';
20+
import { SecurityPrivacyViewSelectorsIDs } from '../SecurityPrivacyView.testIds';
21+
22+
/**
23+
* "Top Traders" section for the Security & privacy screen. Lets the user toggle
24+
* whether their account is shown on the Top Traders leaderboard, calling
25+
* `SocialController:optInToLeaderboard` / `optOutOfLeaderboard`. The displayed
26+
* value is a local (non-AUS) mirror kept in the `settings` reducer, defaulting
27+
* to shown; it is updated optimistically and reverted if the request fails.
28+
*/
29+
const TopTradersSection = () => {
30+
const { styles } = useStyles(createStyles, {});
31+
const dispatch = useDispatch();
32+
const { trackEvent, createEventBuilder } = useAnalytics();
33+
const isSocialLeaderboardEnabled = useSelector(
34+
selectSocialLeaderboardEnabled,
35+
);
36+
const isLeaderboardOptFlowEnabled = useSelector(
37+
selectSocialLeaderboardOptFlowEnabled,
38+
);
39+
const showAccountOnLeaderboard = useSelector(
40+
(state: RootState) => state.settings.showAccountOnLeaderboard ?? true,
41+
);
42+
const [isUpdating, setIsUpdating] = useState(false);
43+
44+
const handleToggle = useCallback(
45+
async (enabled: boolean) => {
46+
if (isUpdating) {
47+
return;
48+
}
49+
setIsUpdating(true);
50+
try {
51+
await (Engine.controllerMessenger.call as CallableFunction)(
52+
enabled
53+
? 'SocialController:optInToLeaderboard'
54+
: 'SocialController:optOutOfLeaderboard',
55+
);
56+
// Update state only after the backend confirms.
57+
dispatch(setShowAccountOnLeaderboard(enabled));
58+
trackEvent(
59+
createEventBuilder(
60+
MetaMetricsEvents.SOCIAL_TRADER_LEADERBOARD_VISIBILITY_TOGGLED,
61+
)
62+
.addProperties({ show_account_on_leaderboard: enabled })
63+
.build(),
64+
);
65+
} catch (error) {
66+
Logger.error(
67+
error as Error,
68+
'TopTradersSection: failed to update leaderboard visibility',
69+
);
70+
}
71+
// Not a `finally`: React Compiler can't lower try/finally and bails on it.
72+
setIsUpdating(false);
73+
},
74+
[createEventBuilder, dispatch, isUpdating, trackEvent],
75+
);
76+
77+
if (!isSocialLeaderboardEnabled || !isLeaderboardOptFlowEnabled) {
78+
return null;
79+
}
80+
81+
return (
82+
<View testID={SecurityPrivacyViewSelectorsIDs.TOP_TRADERS_SECTION}>
83+
<Text variant={TextVariant.HeadingMd} style={styles.subHeading}>
84+
{strings('social_leaderboard.settings.section_title')}
85+
</Text>
86+
{/* Wrap the toggle in `styles.setting` (marginTop) so the gap below the
87+
section heading matches the other sections (e.g. Analytics). */}
88+
<View style={styles.setting}>
89+
<SecurityOptionToggle
90+
title={strings(
91+
'social_leaderboard.settings.show_account_on_leaderboard',
92+
)}
93+
description={strings(
94+
'social_leaderboard.settings.show_account_on_leaderboard_description',
95+
)}
96+
value={showAccountOnLeaderboard}
97+
onOptionUpdated={handleToggle}
98+
testId={
99+
SecurityPrivacyViewSelectorsIDs.SHOW_ACCOUNT_ON_LEADERBOARD_TOGGLE
100+
}
101+
disabled={isUpdating}
102+
/>
103+
</View>
104+
</View>
105+
);
106+
};
107+
108+
export default TopTradersSection;

app/components/Views/Settings/SecuritySettings/SecurityPrivacyView.testIds.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export const SecurityPrivacyViewSelectorsIDs = {
1414
CLEAR_PRIVACY_DATA_BUTTON: 'clear-privacy-data-button',
1515
PROTECT_YOUR_WALLET: 'protect-your-wallet',
1616
DELETE_WALLET_BUTTON: 'security-settings-delete-wallet-buttons',
17+
TOP_TRADERS_SECTION: 'top-traders-section',
18+
SHOW_ACCOUNT_ON_LEADERBOARD_TOGGLE: 'show-account-on-leaderboard-toggle',
1719
};
1820

1921
export const SecurityPrivacyViewSelectorsText = {

app/components/Views/Settings/SecuritySettings/SecuritySettings.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { TextVariant as LibraryTextVariant } from '../../../../component-library
5151
import BasicFunctionalityComponent from '../../../UI/BasicFunctionality/BasicFunctionality';
5252
import Routes from '../../../../constants/navigation/Routes';
5353
import MetaMetricsAndDataCollectionSection from './Sections/MetaMetricsAndDataCollectionSection/MetaMetricsAndDataCollectionSection';
54+
import TopTradersSection from './Sections/TopTradersSection';
5455
import { selectIsMetamaskNotificationsEnabled } from '../../../../selectors/notifications';
5556
import SwitchLoadingModal from '../../../../components/UI/Notification/SwitchLoadingModal';
5657
import { RootState } from '../../../../reducers';
@@ -411,6 +412,7 @@ const Settings: React.FC = () => {
411412
<MetaMetricsAndDataCollectionSection />
412413
<DeleteMetaMetricsData metricsOptin={analyticsEnabled} />
413414
<DeleteWalletData />
415+
<TopTradersSection />
414416
{renderHint()}
415417
</View>
416418
</ScrollView>

app/core/Analytics/MetaMetrics.events.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,7 @@ enum EVENT_NAME {
789789
SOCIAL_QUICK_BUY_INTERACTED = 'Quick Buy Interacted',
790790
SOCIAL_QUICK_TRADE_MODE_TOGGLED = 'Quick Trade Mode Toggled',
791791
SOCIAL_FOLLOW_TRADING_NOTIFICATION_CLICKED = 'Follow Trading Notification Clicked',
792+
SOCIAL_TRADER_LEADERBOARD_VISIBILITY_TOGGLED = 'Trader Leaderboard Visibility Toggled',
792793
SOCIAL_FOLLOW_TRADING_ONBOARDING_VIEWED = 'Follow Trading Onboarding Viewed',
793794
SOCIAL_FOLLOW_TRADING_ONBOARDING_COMPLETED = 'Follow Trading Onboarding Completed',
794795
SOCIAL_FOLLOW_TRADING_ONBOARDING_INTERACTION = 'Follow Trading Onboarding Interaction',
@@ -2116,6 +2117,9 @@ const events = {
21162117
SOCIAL_FOLLOW_TRADING_NOTIFICATION_CLICKED: generateOpt(
21172118
EVENT_NAME.SOCIAL_FOLLOW_TRADING_NOTIFICATION_CLICKED,
21182119
),
2120+
SOCIAL_TRADER_LEADERBOARD_VISIBILITY_TOGGLED: generateOpt(
2121+
EVENT_NAME.SOCIAL_TRADER_LEADERBOARD_VISIBILITY_TOGGLED,
2122+
),
21192123
SOCIAL_FOLLOW_TRADING_ONBOARDING_VIEWED: generateOpt(
21202124
EVENT_NAME.SOCIAL_FOLLOW_TRADING_ONBOARDING_VIEWED,
21212125
),

app/core/Engine/messengers/social-controller-messenger.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export function getSocialControllerMessenger(
3333
'SocialService:follow',
3434
'SocialService:unfollow',
3535
'SocialService:fetchFollowing',
36+
'SocialService:optOutOfLeaderboard',
37+
'SocialService:optInToLeaderboard',
3638
],
3739
messenger,
3840
});

app/reducers/settings/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ const initialState = {
1010
basicFunctionalityEnabled: true,
1111
deepLinkModalDisabled: false,
1212
hapticsEnabled: true,
13+
// Whether this account is shown on the Top Traders leaderboard. Local mirror
14+
// of the backend opt-in/out state; defaults to shown. Not persisted to AUS.
15+
showAccountOnLeaderboard: true,
1316
// Perps chart preferences
1417
perpsChartPreferences: {
1518
preferredCandlePeriod: '15m', // Default to 15 minutes
@@ -73,6 +76,11 @@ const settingsReducer = (state = initialState, action) => {
7376
...state,
7477
hapticsEnabled: action.hapticsEnabled,
7578
};
79+
case 'SET_SHOW_ACCOUNT_ON_LEADERBOARD':
80+
return {
81+
...state,
82+
showAccountOnLeaderboard: action.showAccountOnLeaderboard,
83+
};
7684
case 'SET_PERPS_CHART_PREFERRED_CANDLE_PERIOD':
7785
return {
7886
...state,

0 commit comments

Comments
 (0)