Skip to content

Commit 0203a4a

Browse files
committed
fix: sync perps advanced chart pagination and header price
1 parent 1b281fa commit 0203a4a

7 files changed

Lines changed: 163 additions & 14 deletions

File tree

app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
selectPerpsAdvancedChartEnabledFlag,
1919
selectPerpsRelatedMarketsEnabledFlag,
2020
} from '../../selectors/featureFlags';
21-
import type { PerpsMarketData } from '@metamask/perps-controller';
21+
import { TimeDuration, type PerpsMarketData } from '@metamask/perps-controller';
2222

2323
const mockPerpsAdvancedChartMount = jest.fn();
2424
const mockPerpsAdvancedChartUnmount = jest.fn();
@@ -1482,6 +1482,47 @@ describe('PerpsMarketDetailsView', () => {
14821482
expect(mockPerpsAdvancedChartMount).toHaveBeenCalledTimes(2);
14831483
});
14841484

1485+
it('passes YearToDate pagination to advanced chart and uses its latest price for the header', async () => {
1486+
const { useSelector } = jest.requireMock('react-redux');
1487+
const mockSelectPerpsEligibility = jest.requireMock(
1488+
'../../selectors/perpsController',
1489+
).selectPerpsEligibility;
1490+
useSelector.mockImplementation((selector: unknown) => {
1491+
if (selector === mockSelectPerpsEligibility) {
1492+
return true;
1493+
}
1494+
if (selector === selectPerpsAdvancedChartEnabledFlag) {
1495+
return true;
1496+
}
1497+
if (selector === selectPerpsRelatedMarketsEnabledFlag) {
1498+
return false;
1499+
}
1500+
return undefined;
1501+
});
1502+
1503+
const { getByTestId, getAllByText } = renderWithProvider(
1504+
<PerpsConnectionProvider>
1505+
<PerpsMarketDetailsView />
1506+
</PerpsConnectionProvider>,
1507+
{
1508+
state: initialState,
1509+
},
1510+
);
1511+
1512+
const advancedChart = getByTestId('mock-perps-advanced-chart');
1513+
expect(advancedChart.props.paginationDuration).toBe(
1514+
TimeDuration.YearToDate,
1515+
);
1516+
1517+
act(() => {
1518+
advancedChart.props.onLatestPriceChange(47000);
1519+
});
1520+
1521+
await waitFor(() => {
1522+
expect(getAllByText('$47,000').length).toBeGreaterThan(0);
1523+
});
1524+
});
1525+
14851526
it('refreshes candle data when position tab is active', async () => {
14861527
// Arrange
14871528
const mockRefreshPosition = jest.fn();

app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,17 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
445445
const lastCandle = candleData.candles.at(-1);
446446
return lastCandle?.close ? Number.parseFloat(lastCandle.close) : 0;
447447
}, [candleData]);
448+
const [advancedChartCurrentPrice, setAdvancedChartCurrentPrice] = useState<
449+
number | undefined
450+
>(undefined);
451+
const syncedChartCurrentPrice =
452+
isAdvancedChartEnabled && advancedChartCurrentPrice !== undefined
453+
? advancedChartCurrentPrice
454+
: chartCurrentPrice;
455+
456+
useEffect(() => {
457+
setAdvancedChartCurrentPrice(undefined);
458+
}, [isAdvancedChartEnabled, market?.symbol, selectedCandlePeriod]);
448459

449460
// Auto-zoom to latest candle when interval changes and new data arrives
450461
// This ensures the chart shows the most recent data after interval change
@@ -533,10 +544,12 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
533544
);
534545

535546
// Compute TP/SL lines for the chart based on existing position
536-
// Use chartCurrentPrice (from candle close) to ensure price line syncs with live candle
547+
// Use the active chart candle close so the header and current price line stay in sync.
537548
const tpslLines = useMemo(() => {
538549
const chartPriceStr =
539-
chartCurrentPrice > 0 ? chartCurrentPrice.toString() : undefined;
550+
syncedChartCurrentPrice > 0
551+
? syncedChartCurrentPrice.toString()
552+
: undefined;
540553

541554
if (existingPosition) {
542555
return {
@@ -550,7 +563,7 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
550563

551564
// Even without position, show current price line on chart
552565
return chartPriceStr ? { currentPrice: chartPriceStr } : undefined;
553-
}, [existingPosition, chartCurrentPrice]);
566+
}, [existingPosition, syncedChartCurrentPrice]);
554567

555568
// Stop loss prompt banner logic
556569
// Hook handles visibility orchestration including fade-out animation
@@ -1302,7 +1315,7 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
13021315
subtitle={
13031316
<LivePriceHeader
13041317
symbol={market.symbol}
1305-
currentPrice={chartCurrentPrice}
1318+
currentPrice={syncedChartCurrentPrice}
13061319
testIDPrice={PerpsMarketHeaderSelectorsIDs.PRICE}
13071320
testIDChange={PerpsMarketHeaderSelectorsIDs.PRICE_CHANGE}
13081321
throttleMs={1000}
@@ -1369,7 +1382,7 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
13691382
</Box>
13701383
<LivePriceHeader
13711384
symbol={market.symbol}
1372-
currentPrice={chartCurrentPrice}
1385+
currentPrice={syncedChartCurrentPrice}
13731386
testIDPrice={
13741387
PerpsMarketHeaderSelectorsIDs.PRICE_TITLE_SECTION
13751388
}
@@ -1422,8 +1435,10 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
14221435
tpslLines={tpslLines}
14231436
positionSize={existingPosition?.size}
14241437
onCrosshairDataChange={setOhlcData}
1438+
onLatestPriceChange={setAdvancedChartCurrentPrice}
14251439
fallbackCandleData={candleData}
14261440
fallbackFetchMoreHistory={fetchMoreHistory}
1441+
paginationDuration={TimeDuration.YearToDate}
14271442
/>
14281443
) : hasHistoricalData ? (
14291444
<TradingViewChart

app/components/UI/Perps/components/PerpsAdvancedChart/PerpsAdvancedChart.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import React, {
55
useRef,
66
useState,
77
} from 'react';
8-
import { CandlePeriod, type CandleData } from '@metamask/perps-controller';
8+
import {
9+
CandlePeriod,
10+
TimeDuration,
11+
type CandleData,
12+
} from '@metamask/perps-controller';
913
import AdvancedChart from '../../../Charts/AdvancedChart/AdvancedChart';
1014
import {
1115
ChartType,
@@ -42,12 +46,15 @@ export interface PerpsAdvancedChartProps {
4246
/** Signed position size string; used to derive long/short side for position lines. */
4347
positionSize?: string;
4448
onCrosshairDataChange?: (data: OhlcData | null) => void;
49+
onLatestPriceChange?: (price: number | undefined) => void;
4550
onError?: (error: string) => void;
4651
onSkeletonHidden?: (payload?: ChartRangeSettlePayload) => void;
4752
/** Fallback candle data for the Lightweight chart if AdvancedChart fails this mount. */
4853
fallbackCandleData: CandleData | null;
4954
/** Fallback fetch-more-history for the Lightweight chart fallback. */
5055
fallbackFetchMoreHistory?: () => void;
56+
/** Duration used for RN-backed older-bar pagination. */
57+
paginationDuration?: TimeDuration;
5158
}
5259

5360
/**
@@ -159,20 +166,28 @@ const PerpsAdvancedChart: React.FC<PerpsAdvancedChartProps> = ({
159166
tpslLines,
160167
positionSize,
161168
onCrosshairDataChange,
169+
onLatestPriceChange,
162170
onError,
163171
onSkeletonHidden,
164172
fallbackCandleData,
165173
fallbackFetchMoreHistory,
174+
paginationDuration,
166175
}) => {
167176
const {
168177
ohlcvData,
169178
realtimeBar,
179+
latestBar,
170180
ohlcvSeriesKey,
171181
visibleFromMs,
172182
visibleToMs,
173183
isLoading,
174184
handleFetchOlderBarsRequest,
175-
} = usePerpsAdvancedChartAdapter({ symbol, interval, visibleCandleCount });
185+
} = usePerpsAdvancedChartAdapter({
186+
symbol,
187+
interval,
188+
visibleCandleCount,
189+
paginationDuration,
190+
});
176191

177192
// Per-mount error fallback: once errored, stay on Lightweight until unmount.
178193
const [hasFailed, setHasFailed] = useState(false);
@@ -191,6 +206,14 @@ const PerpsAdvancedChart: React.FC<PerpsAdvancedChartProps> = ({
191206

192207
const volumeColors = useMemo(() => getPerpsVolumeColors(colors), [colors]);
193208

209+
useEffect(() => {
210+
onLatestPriceChange?.(
211+
latestBar && Number.isFinite(latestBar.close)
212+
? latestBar.close
213+
: undefined,
214+
);
215+
}, [latestBar, onLatestPriceChange]);
216+
194217
// ---- Crosshair + haptics ----
195218

196219
const prevOhlcRef = useRef<OhlcData | null>(null);
@@ -350,6 +373,7 @@ const PerpsAdvancedChart: React.FC<PerpsAdvancedChartProps> = ({
350373
height={height}
351374
visibleCandleCount={visibleCandleCount}
352375
tpslLines={tpslLines}
376+
symbol={symbol}
353377
onNeedMoreHistory={fallbackFetchMoreHistory}
354378
onOhlcDataChange={onCrosshairDataChange}
355379
showOverlay={false}

app/components/UI/Perps/components/PerpsAdvancedChart/__tests__/PerpsAdvancedChart.test.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import { act, render } from '@testing-library/react-native';
3-
import { CandlePeriod } from '@metamask/perps-controller';
3+
import { CandlePeriod, TimeDuration } from '@metamask/perps-controller';
44
import {
55
default as PerpsAdvancedChart,
66
mapTpslToPositionLines,
@@ -68,6 +68,7 @@ jest.mock('react-native-performance', () => ({
6868
const mockAdapterResult = {
6969
ohlcvData: [],
7070
realtimeBar: undefined,
71+
latestBar: undefined,
7172
ohlcvSeriesKey: 'BTC|1h',
7273
visibleFromMs: undefined,
7374
visibleToMs: undefined,
@@ -112,6 +113,38 @@ describe('PerpsAdvancedChart', () => {
112113
);
113114
});
114115

116+
it('passes pagination duration into the adapter', () => {
117+
renderChart({ paginationDuration: TimeDuration.YearToDate });
118+
119+
expect(mockUsePerpsAdvancedChartAdapter).toHaveBeenCalledWith(
120+
expect.objectContaining({
121+
symbol: 'BTC',
122+
interval: CandlePeriod.OneHour,
123+
visibleCandleCount: 100,
124+
paginationDuration: TimeDuration.YearToDate,
125+
}),
126+
);
127+
});
128+
129+
it('notifies the parent when the latest adapter close changes', () => {
130+
const onLatestPriceChange = jest.fn();
131+
mockUsePerpsAdvancedChartAdapter.mockReturnValue({
132+
...mockAdapterResult,
133+
latestBar: {
134+
time: 1000,
135+
open: 42000,
136+
high: 42100,
137+
low: 41900,
138+
close: 42050,
139+
volume: 100,
140+
},
141+
});
142+
143+
renderChart({ onLatestPriceChange });
144+
145+
expect(onLatestPriceChange).toHaveBeenCalledWith(42050);
146+
});
147+
115148
it('passes mapped position lines to AdvancedChart', () => {
116149
renderChart({
117150
tpslLines: {
@@ -304,6 +337,7 @@ describe('PerpsAdvancedChart', () => {
304337
height: 240,
305338
visibleCandleCount: 100,
306339
tpslLines,
340+
symbol: 'BTC',
307341
onNeedMoreHistory: fallbackFetchMoreHistory,
308342
onOhlcDataChange: onCrosshairDataChange,
309343
showOverlay: false,

app/components/UI/Perps/components/PerpsChartFullscreenModal/PerpsChartFullscreenModal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ const PerpsChartFullscreenModal: React.FC<PerpsChartFullscreenModalProps> = ({
218218
candleData={candleData}
219219
height={Math.max(chartHeight - ohlcvHeight, 100)}
220220
tpslLines={tpslLines}
221+
symbol={symbol}
221222
visibleCandleCount={
222223
visibleCandleCount ??
223224
PERPS_CHART_CONFIG.CANDLE_COUNT.FULLSCREEN

app/components/UI/Perps/hooks/__tests__/usePerpsAdvancedChartAdapter.test.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { renderHook, act } from '@testing-library/react-hooks';
2-
import { CandlePeriod, type CandleData } from '@metamask/perps-controller';
2+
import {
3+
CandlePeriod,
4+
TimeDuration,
5+
type CandleData,
6+
} from '@metamask/perps-controller';
37
import { usePerpsStream } from '../../providers/PerpsStreamManager';
48
import type { FetchOlderBarsRequest } from '../../../Charts/AdvancedChart/AdvancedChart.types';
59
import {
@@ -126,12 +130,15 @@ describe('usePerpsAdvancedChartAdapter loading lifecycle', () => {
126130
const SYMBOL = 'BTC';
127131
const INTERVAL = CandlePeriod.OneHour;
128132

129-
const renderAdapter = () =>
133+
const renderAdapter = (
134+
overrides: Partial<Parameters<typeof usePerpsAdvancedChartAdapter>[0]> = {},
135+
) =>
130136
renderHook(() =>
131137
usePerpsAdvancedChartAdapter({
132138
symbol: SYMBOL,
133139
interval: INTERVAL,
134140
visibleCandleCount: 45,
141+
...overrides,
135142
}),
136143
);
137144

@@ -371,7 +378,7 @@ describe('usePerpsAdvancedChartAdapter loading lifecycle', () => {
371378
expect(mockFetchHistoricalCandles).toHaveBeenCalledWith(
372379
SYMBOL,
373380
INTERVAL,
374-
'1w',
381+
TimeDuration.OneWeek,
375382
);
376383
expect(response).toEqual({
377384
requestId: 'older-1',
@@ -384,6 +391,27 @@ describe('usePerpsAdvancedChartAdapter loading lifecycle', () => {
384391
});
385392
});
386393

394+
it('uses the configured duration when fetching older bars', async () => {
395+
const { result } = renderAdapter({
396+
paginationDuration: TimeDuration.YearToDate,
397+
});
398+
399+
act(() => {
400+
subscribeParams().callback({
401+
symbol: SYMBOL,
402+
interval: INTERVAL,
403+
candles: [candle(1000), candle(2000), candle(3000)],
404+
});
405+
});
406+
await result.current.handleFetchOlderBarsRequest(fetchOlderRequest());
407+
408+
expect(mockFetchHistoricalCandles).toHaveBeenCalledWith(
409+
SYMBOL,
410+
INTERVAL,
411+
TimeDuration.YearToDate,
412+
);
413+
});
414+
387415
it('returns noData when handleFetchOlderBarsRequest finds no older bars', async () => {
388416
const { result } = renderAdapter();
389417

app/components/UI/Perps/hooks/usePerpsAdvancedChartAdapter.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,13 @@ export interface UsePerpsAdvancedChartAdapterOptions {
6161
symbol: string;
6262
interval: CandlePeriod;
6363
visibleCandleCount: number;
64+
paginationDuration?: TimeDuration;
6465
}
6566

6667
export interface UsePerpsAdvancedChartAdapterResult {
6768
ohlcvData: OHLCVBar[];
6869
realtimeBar: OHLCVBar | undefined;
70+
latestBar: OHLCVBar | undefined;
6971
ohlcvSeriesKey: string;
7072
visibleFromMs: number | undefined;
7173
visibleToMs: number | undefined;
@@ -87,6 +89,7 @@ export function usePerpsAdvancedChartAdapter({
8789
symbol,
8890
interval,
8991
visibleCandleCount,
92+
paginationDuration = TimeDuration.OneWeek,
9093
}: UsePerpsAdvancedChartAdapterOptions): UsePerpsAdvancedChartAdapterResult {
9194
const stream = usePerpsStream();
9295

@@ -197,7 +200,7 @@ export function usePerpsAdvancedChartAdapter({
197200
await stream.candles.fetchHistoricalCandles(
198201
symbol,
199202
interval,
200-
TimeDuration.OneWeek,
203+
paginationDuration,
201204
);
202205
// fetchHistoricalCandles notifies subscribers synchronously before resolving,
203206
// so latestCandleDataRef.current is up to date after the await.
@@ -225,12 +228,15 @@ export function usePerpsAdvancedChartAdapter({
225228
};
226229
}
227230
},
228-
[symbol, interval, stream],
231+
[symbol, interval, paginationDuration, stream],
229232
);
230233

234+
const latestBar = realtimeBar ?? ohlcvData[ohlcvData.length - 1];
235+
231236
return {
232237
ohlcvData,
233238
realtimeBar,
239+
latestBar,
234240
ohlcvSeriesKey,
235241
visibleFromMs,
236242
visibleToMs,

0 commit comments

Comments
 (0)