Skip to content

Commit fda21e0

Browse files
feat: Tsa 411 wire position view data (#29018)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Inject data into TraderPositionView, replacing all hardcoded mock data with live position, market, and price data. **Summary** - Position card reads currentValueUSD, pnlValueUsd, pnlPercent from Position route params - Closed positions show "Closed position" label with realized P&L and percentage - PriceChart wired in with PriceChartProvider - All tab cascades through 1M > 1W > 1D when 3y (All default) data unavailable - Shared formatters extracted to SocialLeaderboard/utils/formatters.ts **Test plan** 1. Open position: position card shows current value + P&L 2. Closed position: shows "Closed position" with realized P&L 3. Trades filter by time period 4. Chart renders and updates per time period 5. Market cap and price % change display for indexed tokens 6. Unindexed tokens show dashes gracefully <!-- 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: inject real data into position view ## **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** - [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 - [ ] 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** - [ ] 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** > Replaces mocked UI data with live-derived values, adds new fetching logic (market cap + historical prices) and time-based filtering, which could introduce regressions in loading/empty-state handling and API edge cases. > > **Overview** > Trader position screen is refactored from hardcoded mock content into a data-driven view backed by a new `useTraderPositionData` hook, and split into focused subcomponents (header, token info, chart, time-period selector, PnL card, trades list). > > The view now derives **market cap** (from `TokenRatesController` with a `handleFetch` fallback), fetches **historical prices** for multiple periods and computes percent change with fallbacks (e.g., `All` cascades to shorter ranges), filters trades by selected interval, and properly handles *missing position/unsupported chain* by stopping chart loading and showing dashes/empty states. > > Navigation is updated to pass `traderImageUrl` into `TraderPositionView`, shared social leaderboard formatting is extracted to `utils/formatters.ts`, and tests/locales are expanded to cover the new behaviors (closed-position labeling, no-trades state, market-cap fallback, period switching, and chart data fallbacks). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 5a9d983. 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: Joao Santos <jrmsantos15@gmail.com>
1 parent c463847 commit fda21e0

18 files changed

Lines changed: 1239 additions & 431 deletions

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

Lines changed: 292 additions & 52 deletions
Large diffs are not rendered by default.

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

Lines changed: 74 additions & 348 deletions
Large diffs are not rendered by default.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from 'react';
2+
import { TouchableOpacity } from 'react-native';
3+
import {
4+
Box,
5+
Text,
6+
TextVariant,
7+
TextColor,
8+
FontWeight,
9+
} from '@metamask/design-system-react-native';
10+
11+
export interface TimePeriodButtonProps {
12+
label: string;
13+
isActive: boolean;
14+
onPress: () => void;
15+
}
16+
17+
const TimePeriodButton: React.FC<TimePeriodButtonProps> = ({
18+
label,
19+
isActive,
20+
onPress,
21+
}) => (
22+
<TouchableOpacity onPress={onPress}>
23+
<Box
24+
twClassName={`flex-1 items-center justify-center px-2 py-1 rounded ${
25+
isActive ? 'bg-muted' : ''
26+
}`}
27+
>
28+
<Text
29+
variant={TextVariant.BodySm}
30+
fontWeight={FontWeight.Medium}
31+
color={isActive ? TextColor.TextDefault : TextColor.TextAlternative}
32+
>
33+
{label}
34+
</Text>
35+
</Box>
36+
</TouchableOpacity>
37+
);
38+
39+
export default TimePeriodButton;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React from 'react';
2+
import { Image } from 'react-native';
3+
import { useTailwind } from '@metamask/design-system-twrnc-preset';
4+
import {
5+
Box,
6+
Text,
7+
TextVariant,
8+
TextColor,
9+
FontWeight,
10+
BoxFlexDirection,
11+
BoxAlignItems,
12+
AvatarBase,
13+
AvatarBaseSize,
14+
} from '@metamask/design-system-react-native';
15+
import type { Trade } from '@metamask/social-controllers';
16+
import { strings } from '../../../../../../locales/i18n';
17+
import { formatUsd, formatTradeDate } from '../../utils/formatters';
18+
19+
export interface TradeRowProps {
20+
trade: Trade;
21+
traderName: string;
22+
traderImageUrl?: string;
23+
}
24+
25+
const TradeRow: React.FC<TradeRowProps> = ({
26+
trade,
27+
traderName,
28+
traderImageUrl,
29+
}) => {
30+
const tw = useTailwind();
31+
const isEntry = trade.intent === 'enter';
32+
return (
33+
<Box
34+
flexDirection={BoxFlexDirection.Row}
35+
alignItems={BoxAlignItems.Center}
36+
twClassName="px-4 py-3"
37+
testID={`trade-row-${trade.transactionHash}`}
38+
>
39+
<Box
40+
flexDirection={BoxFlexDirection.Row}
41+
alignItems={BoxAlignItems.Center}
42+
gap={4}
43+
twClassName="flex-1 min-w-0 mr-3"
44+
>
45+
{traderImageUrl ? (
46+
<Image
47+
source={{ uri: traderImageUrl }}
48+
style={tw.style('w-[32px] h-[32px] rounded-full bg-muted')}
49+
resizeMode="cover"
50+
/>
51+
) : (
52+
<AvatarBase
53+
size={AvatarBaseSize.Md}
54+
fallbackText={traderName.charAt(0).toUpperCase()}
55+
/>
56+
)}
57+
<Box twClassName="flex-1 min-w-0">
58+
<Text
59+
variant={TextVariant.BodyMd}
60+
fontWeight={FontWeight.Medium}
61+
color={TextColor.TextDefault}
62+
numberOfLines={1}
63+
>
64+
{isEntry
65+
? strings('social_leaderboard.trader_position.bought', {
66+
name: traderName,
67+
})
68+
: strings('social_leaderboard.trader_position.sold', {
69+
name: traderName,
70+
})}
71+
</Text>
72+
<Text
73+
variant={TextVariant.BodySm}
74+
color={TextColor.TextAlternative}
75+
numberOfLines={1}
76+
>
77+
{formatTradeDate(trade.timestamp)}
78+
</Text>
79+
</Box>
80+
</Box>
81+
82+
<Box alignItems={BoxAlignItems.End}>
83+
<Text
84+
variant={TextVariant.BodyMd}
85+
fontWeight={FontWeight.Medium}
86+
twClassName={isEntry ? 'text-success-default' : 'text-error-default'}
87+
>
88+
{formatUsd(
89+
isEntry ? Math.abs(trade.usdCost) : -Math.abs(trade.usdCost),
90+
)}
91+
</Text>
92+
</Box>
93+
</Box>
94+
);
95+
};
96+
97+
export default TradeRow;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from 'react';
2+
import { Box } from '@metamask/design-system-react-native';
3+
import type { TokenPrice } from '../../../../hooks/useTokenHistoricalPrices';
4+
import PriceChart from '../../../../UI/AssetOverview/PriceChart';
5+
import { PriceChartProvider } from '../../../../UI/AssetOverview/PriceChart/PriceChart.context';
6+
7+
export interface TraderPositionChartSectionProps {
8+
historicalPrices: TokenPrice[];
9+
priceDiff: number;
10+
isPricesLoading: boolean;
11+
onChartIndexChange: (index: number) => void;
12+
}
13+
14+
const TraderPositionChartSection: React.FC<TraderPositionChartSectionProps> = ({
15+
historicalPrices,
16+
priceDiff,
17+
isPricesLoading,
18+
onChartIndexChange,
19+
}) => (
20+
<PriceChartProvider>
21+
<Box twClassName="mx-4 my-3">
22+
<PriceChart
23+
prices={historicalPrices}
24+
priceDiff={priceDiff}
25+
isLoading={isPricesLoading}
26+
onChartIndexChange={onChartIndexChange}
27+
/>
28+
</Box>
29+
</PriceChartProvider>
30+
);
31+
32+
export default TraderPositionChartSection;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
import {
3+
Box,
4+
Text,
5+
TextVariant,
6+
TextColor,
7+
FontWeight,
8+
ButtonIcon,
9+
ButtonIconSize,
10+
IconName,
11+
BoxFlexDirection,
12+
BoxAlignItems,
13+
BoxJustifyContent,
14+
} from '@metamask/design-system-react-native';
15+
16+
export interface TraderPositionHeaderProps {
17+
traderName: string;
18+
onClose: () => void;
19+
closeButtonTestID: string;
20+
}
21+
22+
const TraderPositionHeader: React.FC<TraderPositionHeaderProps> = ({
23+
traderName,
24+
onClose,
25+
closeButtonTestID,
26+
}) => (
27+
<Box
28+
flexDirection={BoxFlexDirection.Row}
29+
alignItems={BoxAlignItems.Center}
30+
justifyContent={BoxJustifyContent.Between}
31+
twClassName="px-2 py-2"
32+
>
33+
<Box twClassName="w-10" />
34+
<Text
35+
variant={TextVariant.HeadingSm}
36+
fontWeight={FontWeight.Bold}
37+
color={TextColor.TextDefault}
38+
numberOfLines={1}
39+
>
40+
{traderName}
41+
</Text>
42+
<Box twClassName="w-10 items-end">
43+
<ButtonIcon
44+
iconName={IconName.Close}
45+
size={ButtonIconSize.Md}
46+
onPress={onClose}
47+
testID={closeButtonTestID}
48+
/>
49+
</Box>
50+
</Box>
51+
);
52+
53+
export default TraderPositionHeader;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React from 'react';
2+
import {
3+
Box,
4+
Text,
5+
TextVariant,
6+
TextColor,
7+
FontWeight,
8+
BoxFlexDirection,
9+
BoxAlignItems,
10+
BoxJustifyContent,
11+
} from '@metamask/design-system-react-native';
12+
import { strings } from '../../../../../../locales/i18n';
13+
import { formatPnl } from '../../../../UI/Perps/utils/formatUtils';
14+
import { formatUsd, formatPercent } from '../../utils/formatters';
15+
16+
export interface TraderPositionPnLCardProps {
17+
isClosed: boolean;
18+
positionValue: number | null | undefined;
19+
pnlValue: number | null | undefined;
20+
pnlPercent: number | null;
21+
isPnlPositive: boolean;
22+
}
23+
24+
const TraderPositionPnLCard: React.FC<TraderPositionPnLCardProps> = ({
25+
isClosed,
26+
positionValue,
27+
pnlValue,
28+
pnlPercent,
29+
isPnlPositive,
30+
}) => (
31+
<Box twClassName="mx-4 p-4 bg-muted rounded-2xl">
32+
<Box
33+
flexDirection={BoxFlexDirection.Row}
34+
justifyContent={BoxJustifyContent.Between}
35+
alignItems={BoxAlignItems.Center}
36+
>
37+
{isClosed ? (
38+
<Text
39+
variant={TextVariant.BodyMd}
40+
fontWeight={FontWeight.Medium}
41+
color={TextColor.TextDefault}
42+
>
43+
{strings('social_leaderboard.trader_position.closed_position')}
44+
</Text>
45+
) : (
46+
<Box>
47+
<Text
48+
variant={TextVariant.HeadingMd}
49+
fontWeight={FontWeight.Bold}
50+
color={TextColor.TextDefault}
51+
>
52+
{formatUsd(positionValue)}
53+
</Text>
54+
<Text
55+
variant={TextVariant.BodySm}
56+
fontWeight={FontWeight.Medium}
57+
color={TextColor.TextDefault}
58+
>
59+
{strings('social_leaderboard.trader_position.position')}
60+
</Text>
61+
</Box>
62+
)}
63+
<Box alignItems={BoxAlignItems.End}>
64+
<Text
65+
variant={TextVariant.HeadingMd}
66+
fontWeight={FontWeight.Bold}
67+
twClassName={
68+
isPnlPositive ? 'text-success-default' : 'text-error-default'
69+
}
70+
>
71+
{pnlValue != null ? formatPnl(pnlValue) : '\u2014'}
72+
</Text>
73+
<Text
74+
variant={TextVariant.BodySm}
75+
fontWeight={FontWeight.Medium}
76+
color={TextColor.TextDefault}
77+
>
78+
{formatPercent(pnlPercent)}
79+
</Text>
80+
</Box>
81+
</Box>
82+
</Box>
83+
);
84+
85+
export default TraderPositionPnLCard;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from 'react';
2+
import {
3+
Box,
4+
BoxFlexDirection,
5+
BoxAlignItems,
6+
BoxJustifyContent,
7+
} from '@metamask/design-system-react-native';
8+
import type { TimePeriod } from '../useTraderPositionData';
9+
import TimePeriodButton from './TimePeriodButton';
10+
11+
export interface TraderTimePeriodSelectorProps {
12+
timePeriods: readonly TimePeriod[];
13+
activeTimePeriod: TimePeriod;
14+
onSelectPeriod: (period: TimePeriod) => void;
15+
}
16+
17+
const TraderTimePeriodSelector: React.FC<TraderTimePeriodSelectorProps> = ({
18+
timePeriods,
19+
activeTimePeriod,
20+
onSelectPeriod,
21+
}) => (
22+
<Box
23+
flexDirection={BoxFlexDirection.Row}
24+
alignItems={BoxAlignItems.Center}
25+
justifyContent={BoxJustifyContent.Between}
26+
twClassName="px-4 pb-3"
27+
>
28+
{timePeriods.map((period) => (
29+
<TimePeriodButton
30+
key={period}
31+
label={period}
32+
isActive={activeTimePeriod === period}
33+
onPress={() => onSelectPeriod(period)}
34+
/>
35+
))}
36+
</Box>
37+
);
38+
39+
export default TraderTimePeriodSelector;

0 commit comments

Comments
 (0)