Skip to content

Commit 62ed9c6

Browse files
Bigshmowzone-live
andauthored
feat: deeplinks config social ai (#29383)
<!-- 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. --> ## **Description** * Added social-trader-position as a supported deeplink action. * Added handleSocialTraderPositionUrl to parse required positionId. * Wired universal links so https://link.metamask.io/social-trader-position?positionId=... dispatches to that handler. * Navigates to TraderPositionView with { positionId }. * Missing/blank positionId falls back to wallet home. * Updated TraderPositionView route params to allow position-id-only entry and disabled Buy until hydrated position data exists. * Added analytics route mapping, without adding any signed-link behavior. <!-- 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? --> ## **Changelog** <!-- 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: null ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin 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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- 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) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] 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 - [ ] 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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds a new universal-link action and handler that parses query params and navigates into Social Leaderboard, touching deeplink routing/analytics and navigation param typing where regressions could break link handling. > > **Overview** > Enables a new universal link route `social-trader-position` that parses `positionId`/`traderId` query params and navigates to `Routes.SOCIAL_LEADERBOARD.POSITION`, with a guarded fallback to `Routes.SOCIAL_LEADERBOARD.VIEW` on missing/blank params or navigation errors. > > Wires the new action through deeplink configuration (`ACTIONS`, prefixes, supported-actions lists), `handleUniversalLink` dispatch, and deep-link analytics route mapping/extraction, and updates `TraderPositionView` typing/data plumbing to support deep-link entry where `tokenSymbol` may be absent (defaulting symbol to `''`). Adds focused unit tests for the new handler and universal-link dispatch, plus adjusts `TraderPositionView` tests for optional route params and positionId-driven fetch states. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e2ec83f. 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: António Regadas <antonio.regadas@consensys.net>
1 parent 60f4767 commit 62ed9c6

12 files changed

Lines changed: 278 additions & 16 deletions

File tree

app/components/Views/SocialLeaderboard/TraderPositionView/TraderPositionView.test.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ const mockHandleFetch = handleFetch as jest.MockedFunction<typeof handleFetch>;
1212
const mockPriceChart = jest.fn();
1313

1414
interface MockRouteParams {
15+
positionId?: string;
1516
traderId: string;
16-
traderName: string;
17-
tokenSymbol: string;
17+
traderName?: string;
18+
tokenSymbol?: string;
1819
position?: Position;
1920
}
2021

app/components/Views/SocialLeaderboard/TraderPositionView/useTraderPositionData.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export { TIME_PERIODS };
106106

107107
export function useTraderPositionData(
108108
positionParam: Position | undefined,
109-
tokenSymbol: string,
109+
tokenSymbol?: string,
110110
): TraderPositionData {
111111
const [activeTimePeriod, setActiveTimePeriod] = useState<TimePeriod>('1D');
112112

@@ -268,7 +268,7 @@ export function useTraderPositionData(
268268

269269
// ── Position card ──────────────────────────────────────────────────────
270270

271-
const symbol = positionParam?.tokenSymbol ?? tokenSymbol;
271+
const symbol = positionParam?.tokenSymbol ?? tokenSymbol ?? '';
272272
const isClosed =
273273
positionParam != null &&
274274
positionParam.positionAmount === 0 &&

app/constants/deeplinks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export enum ACTIONS {
4646
PREDICT = 'predict',
4747
ONBOARDING = 'onboarding',
4848
TRENDING = 'trending',
49+
SOCIAL_TRADER_POSITION = 'social-trader-position',
4950
EARN_MUSD = 'earn-musd',
5051
NFT = 'nft',
5152
}
@@ -80,6 +81,7 @@ export const PREFIXES = {
8081
[ACTIONS.CARD_HOME]: '',
8182
[ACTIONS.CARD_KYC_NOTIFICATION]: '',
8283
[ACTIONS.TRENDING]: '',
84+
[ACTIONS.SOCIAL_TRADER_POSITION]: '',
8385
[ACTIONS.EARN_MUSD]: '',
8486
[ACTIONS.NFT]: '',
8587
METAMASK: 'metamask://',
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import NavigationService from '../../../../NavigationService';
2+
import Routes from '../../../../../constants/navigation/Routes';
3+
import DevLogger from '../../../../SDKConnect/utils/DevLogger';
4+
import { handleSocialTraderPositionUrl } from '../handleSocialTraderPositionUrl';
5+
6+
jest.mock('../../../../NavigationService', () => ({
7+
navigation: {
8+
navigate: jest.fn(),
9+
},
10+
}));
11+
12+
jest.mock('../../../../SDKConnect/utils/DevLogger', () => ({
13+
log: jest.fn(),
14+
}));
15+
16+
describe('handleSocialTraderPositionUrl', () => {
17+
const mockNavigate = NavigationService.navigation.navigate as jest.Mock;
18+
19+
beforeEach(() => {
20+
jest.clearAllMocks();
21+
});
22+
23+
it('navigates to TraderPositionView with positionId', () => {
24+
handleSocialTraderPositionUrl({
25+
actionPath:
26+
'?positionId=92d9001b-8b64-4b13-9c1b-ba9292a6099a&traderId=trader-1&deduplication_id=dedup-1&notification_event=follow_newtrade_buy',
27+
});
28+
29+
expect(mockNavigate).toHaveBeenCalledWith(
30+
Routes.SOCIAL_LEADERBOARD.POSITION,
31+
{
32+
positionId: '92d9001b-8b64-4b13-9c1b-ba9292a6099a',
33+
traderId: 'trader-1',
34+
},
35+
);
36+
});
37+
38+
it('navigates with only positionId and traderId route params', () => {
39+
handleSocialTraderPositionUrl({
40+
actionPath: '?positionId=position-1&traderId=trader-1',
41+
});
42+
43+
expect(mockNavigate).toHaveBeenCalledWith(
44+
Routes.SOCIAL_LEADERBOARD.POSITION,
45+
{
46+
positionId: 'position-1',
47+
traderId: 'trader-1',
48+
},
49+
);
50+
});
51+
52+
it('decodes encoded positionId and traderId values', () => {
53+
handleSocialTraderPositionUrl({
54+
actionPath:
55+
'?positionId=position%20id%2Fwith%20reserved%3Fchars&traderId=trader%20id%2Fwith%20reserved%3Fchars&deduplication_id=dedup%20id%2Fwith%20reserved%3Fchars&notification_event=follow%20newtrade%2Fbuy',
56+
});
57+
58+
expect(mockNavigate).toHaveBeenCalledWith(
59+
Routes.SOCIAL_LEADERBOARD.POSITION,
60+
{
61+
positionId: 'position id/with reserved?chars',
62+
traderId: 'trader id/with reserved?chars',
63+
},
64+
);
65+
expect(DevLogger.log).toHaveBeenCalledWith(
66+
'[handleSocialTraderPositionUrl] Parsed navigation parameters:',
67+
{
68+
positionId: 'position id/with reserved?chars',
69+
traderId: 'trader id/with reserved?chars',
70+
deduplicationId: 'dedup id/with reserved?chars',
71+
notificationEvent: 'follow newtrade/buy',
72+
},
73+
);
74+
});
75+
76+
it('falls back to social leaderboard when traderId is missing', () => {
77+
handleSocialTraderPositionUrl({
78+
actionPath: '?positionId=92d9001b-8b64-4b13-9c1b-ba9292a6099a',
79+
});
80+
81+
expect(mockNavigate).toHaveBeenCalledWith(Routes.SOCIAL_LEADERBOARD.VIEW);
82+
});
83+
84+
it('falls back to social leaderboard when positionId is missing', () => {
85+
handleSocialTraderPositionUrl({ actionPath: '' });
86+
87+
expect(mockNavigate).toHaveBeenCalledWith(Routes.SOCIAL_LEADERBOARD.VIEW);
88+
expect(DevLogger.log).toHaveBeenCalledWith(
89+
'[handleSocialTraderPositionUrl] Missing positionId or traderId, falling back to social leaderboard',
90+
);
91+
});
92+
93+
it('falls back to social leaderboard when positionId is blank', () => {
94+
handleSocialTraderPositionUrl({
95+
actionPath: '?positionId=%20%20&traderId=trader-1',
96+
});
97+
98+
expect(mockNavigate).toHaveBeenCalledWith(Routes.SOCIAL_LEADERBOARD.VIEW);
99+
});
100+
101+
it('falls back to social leaderboard on navigation errors', () => {
102+
mockNavigate.mockImplementationOnce(() => {
103+
throw new Error('Navigation error');
104+
});
105+
106+
handleSocialTraderPositionUrl({
107+
actionPath:
108+
'?positionId=92d9001b-8b64-4b13-9c1b-ba9292a6099a&traderId=trader-1',
109+
});
110+
111+
expect(mockNavigate).toHaveBeenCalledTimes(2);
112+
expect(mockNavigate).toHaveBeenLastCalledWith(
113+
Routes.SOCIAL_LEADERBOARD.VIEW,
114+
);
115+
});
116+
});

app/core/DeeplinkManager/handlers/legacy/__tests__/handleUniversalLink.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import handleBrowserUrl from '../handleBrowserUrl';
1616
import { DeepLinkModalLinkType } from '../../../../../components/UI/DeepLinkModal';
1717
import handleMetaMaskDeeplink from '../handleMetaMaskDeeplink';
1818
import { SHIELD_WEBSITE_URL } from '../../../../../constants/shield';
19+
import { handleSocialTraderPositionUrl } from '../handleSocialTraderPositionUrl';
1920
// eslint-disable-next-line import-x/no-namespace
2021
import * as signatureUtils from '../../../utils/verifySignature';
2122

@@ -40,6 +41,7 @@ jest.mock('../handleRewardsUrl');
4041
jest.mock('../handlePredictUrl');
4142
jest.mock('../handleFastOnboarding');
4243
jest.mock('../handleTrendingUrl');
44+
jest.mock('../handleSocialTraderPositionUrl');
4345
jest.mock('../../../../redux', () => ({
4446
__esModule: true,
4547
default: {
@@ -829,6 +831,36 @@ describe('handleUniversalLink', () => {
829831
});
830832
});
831833

834+
describe('ACTIONS.SOCIAL_TRADER_POSITION', () => {
835+
it('calls _handleSocialTraderPosition when action is SOCIAL_TRADER_POSITION', async () => {
836+
const positionUrl = `${PROTOCOLS.HTTPS}://${AppConstants.MM_UNIVERSAL_LINK_HOST}/${ACTIONS.SOCIAL_TRADER_POSITION}?positionId=position-1&traderId=trader-1&deduplication_id=dedup-1&notification_event=follow_newtrade_buy`;
837+
const positionUrlObj = {
838+
...urlObj,
839+
hostname: AppConstants.MM_UNIVERSAL_LINK_HOST,
840+
href: positionUrl,
841+
pathname: `/${ACTIONS.SOCIAL_TRADER_POSITION}`,
842+
search:
843+
'?positionId=position-1&traderId=trader-1&deduplication_id=dedup-1&notification_event=follow_newtrade_buy',
844+
};
845+
846+
await handleUniversalLink({
847+
instance,
848+
handled,
849+
urlObj: positionUrlObj,
850+
browserCallBack: mockBrowserCallBack,
851+
url: positionUrl,
852+
source: 'test-source',
853+
});
854+
855+
expect(mockHandleDeepLinkModalDisplay).not.toHaveBeenCalled();
856+
expect(handleSocialTraderPositionUrl).toHaveBeenCalledWith({
857+
actionPath:
858+
'?positionId=position-1&traderId=trader-1&deduplication_id=dedup-1&notification_event=follow_newtrade_buy',
859+
});
860+
expect(handled).toHaveBeenCalled();
861+
});
862+
});
863+
832864
describe('ACTIONS.WC', () => {
833865
const testCases = [
834866
{
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import NavigationService from '../../../NavigationService';
2+
import Routes from '../../../../constants/navigation/Routes';
3+
import DevLogger from '../../../SDKConnect/utils/DevLogger';
4+
5+
interface HandleSocialTraderPositionUrlParams {
6+
actionPath: string;
7+
}
8+
9+
interface SocialTraderPositionNavigationParams {
10+
positionId?: string;
11+
traderId?: string;
12+
deduplicationId?: string;
13+
notificationEvent?: string;
14+
}
15+
16+
const parseSocialTraderPositionNavigationParams = (
17+
actionPath: string,
18+
): SocialTraderPositionNavigationParams => {
19+
const urlParams = new URLSearchParams(
20+
actionPath.includes('?') ? actionPath.split('?')[1] : '',
21+
);
22+
23+
return {
24+
positionId: urlParams.get('positionId')?.trim() || undefined,
25+
traderId: urlParams.get('traderId')?.trim() || undefined,
26+
deduplicationId: urlParams.get('deduplication_id')?.trim() || undefined,
27+
notificationEvent: urlParams.get('notification_event')?.trim() || undefined,
28+
};
29+
};
30+
31+
const navigateToFallback = () => {
32+
NavigationService.navigation.navigate(Routes.SOCIAL_LEADERBOARD.VIEW);
33+
};
34+
35+
/**
36+
* Handles notification-approved TraderPosition deeplinks.
37+
*
38+
* Supported URL format:
39+
* - https://link.metamask.io/social-trader-position?positionId=<positionId>&traderId=<traderId>&deduplication_id=<deduplicationId>&notification_event=<notificationEvent>
40+
*/
41+
export const handleSocialTraderPositionUrl = ({
42+
actionPath,
43+
}: HandleSocialTraderPositionUrlParams) => {
44+
DevLogger.log(
45+
'[handleSocialTraderPositionUrl] Starting deeplink handling with path:',
46+
actionPath,
47+
);
48+
49+
try {
50+
const { positionId, traderId, deduplicationId, notificationEvent } =
51+
parseSocialTraderPositionNavigationParams(actionPath);
52+
DevLogger.log(
53+
'[handleSocialTraderPositionUrl] Parsed navigation parameters:',
54+
{ positionId, traderId, deduplicationId, notificationEvent },
55+
);
56+
57+
if (!positionId || !traderId) {
58+
DevLogger.log(
59+
'[handleSocialTraderPositionUrl] Missing positionId or traderId, falling back to social leaderboard',
60+
);
61+
navigateToFallback();
62+
return;
63+
}
64+
65+
NavigationService.navigation.navigate(Routes.SOCIAL_LEADERBOARD.POSITION, {
66+
positionId,
67+
traderId,
68+
});
69+
} catch (error) {
70+
DevLogger.log(
71+
'[handleSocialTraderPositionUrl] Failed to handle deeplink:',
72+
error,
73+
);
74+
navigateToFallback();
75+
}
76+
};

app/core/DeeplinkManager/handlers/legacy/handleUniversalLink.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { handleCardOnboarding } from './handleCardOnboarding';
3030
import { handleCardHome } from './handleCardHome';
3131
import { handleCardKycNotification } from './handleCardKycNotification';
3232
import { handleTrendingUrl } from './handleTrendingUrl';
33+
import { handleSocialTraderPositionUrl } from './handleSocialTraderPositionUrl';
3334
import { handleEarnMusd } from './handleEarnMusd';
3435
import { handleAssetUrl } from './handleAssetUrl';
3536
import { handleNftUrl } from './handleNftUrl';
@@ -84,6 +85,7 @@ const SUPPORTED_ACTIONS = {
8485
CARD_HOME: ACTIONS.CARD_HOME,
8586
CARD_KYC_NOTIFICATION: ACTIONS.CARD_KYC_NOTIFICATION,
8687
TRENDING: ACTIONS.TRENDING,
88+
SOCIAL_TRADER_POSITION: ACTIONS.SOCIAL_TRADER_POSITION,
8789
SHIELD: ACTIONS.SHIELD,
8890
EARN_MUSD: ACTIONS.EARN_MUSD,
8991
NFT: ACTIONS.NFT,
@@ -116,6 +118,7 @@ const WHITELISTED_ACTIONS: SUPPORTED_ACTIONS[] = [
116118
SUPPORTED_ACTIONS.SELL,
117119
SUPPORTED_ACTIONS.SELL_CRYPTO,
118120
SUPPORTED_ACTIONS.TRENDING,
121+
SUPPORTED_ACTIONS.SOCIAL_TRADER_POSITION,
119122
SUPPORTED_ACTIONS.SHIELD,
120123
SUPPORTED_ACTIONS.EARN_MUSD,
121124
];
@@ -633,6 +636,12 @@ async function handleUniversalLink({
633636
});
634637
break;
635638
}
639+
case SUPPORTED_ACTIONS.SOCIAL_TRADER_POSITION: {
640+
handleSocialTraderPositionUrl({
641+
actionPath: actionBasedRampPath,
642+
});
643+
break;
644+
}
636645
case SUPPORTED_ACTIONS.EARN_MUSD: {
637646
handleEarnMusd();
638647
break;

app/core/DeeplinkManager/types/deepLink.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export const SUPPORTED_ACTIONS = [
132132
ACTIONS.ONBOARDING,
133133
ACTIONS.PREDICT,
134134
ACTIONS.TRENDING,
135+
ACTIONS.SOCIAL_TRADER_POSITION,
135136
ACTIONS.CARD_ONBOARDING,
136137
ACTIONS.CARD_HOME,
137138
ACTIONS.SHIELD,

app/core/DeeplinkManager/types/deepLinkAnalytics.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export enum DeepLinkRoute {
5151
PREDICT = 'predict',
5252
SHIELD = 'shield',
5353
TRENDING = 'trending',
54+
SOCIAL_TRADER_POSITION = 'social-trader-position',
5455
CARD_ONBOARDING = 'card-onboarding',
5556
CARD_HOME = 'card-home',
5657
NFT = 'nft',

app/core/DeeplinkManager/util/deeplinks/deepLinkAnalytics.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ describe('deepLinkAnalytics', () => {
455455
[ACTIONS.DAPP, DeepLinkRoute.DAPP],
456456
[ACTIONS.WC, DeepLinkRoute.WC],
457457
[ACTIONS.CREATE_ACCOUNT, DeepLinkRoute.CREATE_ACCOUNT],
458+
[ACTIONS.SOCIAL_TRADER_POSITION, DeepLinkRoute.SOCIAL_TRADER_POSITION],
458459
] as const)(
459460
'maps action %s to its corresponding route',
460461
(action, expectedRoute) => {
@@ -519,6 +520,13 @@ describe('deepLinkAnalytics', () => {
519520
expect(result).toBe(DeepLinkRoute.SHIELD);
520521
});
521522

523+
it('extract social trader position route', () => {
524+
const result = extractRouteFromUrl(
525+
'https://link.metamask.io/social-trader-position?positionId=position-1',
526+
);
527+
expect(result).toBe(DeepLinkRoute.SOCIAL_TRADER_POSITION);
528+
});
529+
522530
it('extract home route for empty path', () => {
523531
const result = extractRouteFromUrl('https://link.metamask.io/');
524532
expect(result).toBe(DeepLinkRoute.HOME);

0 commit comments

Comments
 (0)