Skip to content

Commit e5e6ae0

Browse files
authored
feat: add more interval support for token details (#33054)
<!-- 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 --> Adds `4h` and `1w` candle intervals to the token details interval picker (IntervalBar) for technical indicators and charts, per the Price API's newly supported OHLCV intervals. Previously, interval definitions were maintained in three separate locations (`QUICK_INTERVALS` in `IntervalBar.tsx`, `TOKEN_OVERVIEW_CHART_INTERVALS` in `tokenOverviewChart.constants.ts`, and `INTERVAL_TO_TIME_PERIOD` in `Price.advanced.tsx`). This PR consolidates them into a single `CHART_INTERVAL_CONFIGS` object in `tokenOverviewChart.constants.ts`, from which the UI list, TypeScript type, type guard, and API `timePeriod` mapping are all derived. Adding a new interval now requires one entry in one file. ## **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 4-hour and 1-week candle intervals to the token details chart interval picker ## **Related issues** <!-- mms-check: type=issue-link required=true --> Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-3580 ## **Manual testing steps** <!-- mms-check: type=manual-testing required=true --> ```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** <!-- mms-check: type=screenshot required=true --> <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> <img width="429" height="919" alt="Screenshot 2026-07-09 at 18 25 32" src="https://github.com/user-attachments/assets/e9b724f7-be94-4a13-a67a-45483025b48d" /> ## **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. --> - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] 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** <!-- 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.
1 parent 758f4d2 commit e5e6ae0

5 files changed

Lines changed: 65 additions & 37 deletions

File tree

app/components/UI/AssetOverview/Price/Price.advanced.test.tsx

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,9 @@ jest.mock('../../Charts/AdvancedChart/IndicatorBar', () => {
207207

208208
jest.mock('../../Charts/AdvancedChart/IntervalBar', () => {
209209
const { View, Pressable, Text } = jest.requireActual('react-native');
210-
const QUICK_INTERVALS = ['1m', '5m', '15m', '1h', '1d'];
210+
const { TOKEN_OVERVIEW_CHART_INTERVALS } = jest.requireActual(
211+
'./tokenOverviewChart.constants',
212+
);
211213
return {
212214
__esModule: true,
213215
default: ({
@@ -216,7 +218,7 @@ jest.mock('../../Charts/AdvancedChart/IntervalBar', () => {
216218
onIntervalSelect?: (interval: string) => void;
217219
}) => (
218220
<View testID="mock-interval-bar">
219-
{QUICK_INTERVALS.map((interval) => (
221+
{TOKEN_OVERVIEW_CHART_INTERVALS.map((interval: string) => (
220222
<Pressable
221223
key={interval}
222224
accessibilityLabel={interval}
@@ -1678,21 +1680,21 @@ describe('PriceAdvanced', () => {
16781680

16791681
// For '1H' timeRange:
16801682
// - WS_INTERVAL_BY_TIME_RANGE['1H'] = '1m'
1681-
// - INTERVAL_TO_TIME_PERIOD['1m'] = '1d'
1683+
// - CHART_INTERVAL_CONFIGS['1m'] = '1d'
16821684
// - TIME_RANGE_CONFIGS['1H'].timePeriod = '1h'
16831685
// With flag OFF, should use '1h', not '1d'
16841686
});
16851687

1686-
it('uses INTERVAL_TO_TIME_PERIOD when technical indicators flag is ON', () => {
1688+
it('uses CHART_INTERVAL_CONFIGS when technical indicators flag is ON', () => {
16871689
mockSelectTechnicalIndicatorsEnabled.mockReturnValue(true);
16881690

16891691
render(<PriceAdvanced {...baseProps} />);
16901692

16911693
// Default timeRange is '1D':
16921694
// - displayInterval starts as wsInterval = WS_INTERVAL_BY_TIME_RANGE['1D'] = '15m'
16931695
// - chartInterval = '15m'
1694-
// - INTERVAL_TO_TIME_PERIOD['15m'] = '1d'
1695-
// With flag ON, should use '1d' from INTERVAL_TO_TIME_PERIOD
1696+
// - CHART_INTERVAL_CONFIGS['15m'] = '1d'
1697+
// With flag ON, should use '1d' from CHART_INTERVAL_CONFIGS
16961698
expect(mockUseOHLCVChart).toHaveBeenCalledWith(
16971699
expect.objectContaining({
16981700
timePeriod: '1d',
@@ -1728,14 +1730,14 @@ describe('PriceAdvanced', () => {
17281730
);
17291731
});
17301732

1731-
it('correctly uses INTERVAL_TO_TIME_PERIOD for candle intervals when flag is ON', () => {
1733+
it('correctly uses CHART_INTERVAL_CONFIGS for candle intervals when flag is ON', () => {
17321734
mockSelectTechnicalIndicatorsEnabled.mockReturnValue(true);
17331735

17341736
render(<PriceAdvanced {...baseProps} />);
17351737

17361738
// For '1D' timeRange:
17371739
// - wsInterval = '15m'
1738-
// - INTERVAL_TO_TIME_PERIOD['15m'] = '1d'
1740+
// - CHART_INTERVAL_CONFIGS['15m'] = '1d'
17391741
expect(mockUseOHLCVChart).toHaveBeenCalledWith(
17401742
expect.objectContaining({
17411743
timePeriod: '1d',
@@ -1882,6 +1884,32 @@ describe('PriceAdvanced', () => {
18821884
);
18831885
});
18841886

1887+
it('maps 4h interval to 1m timePeriod when flag is ON', () => {
1888+
enableIndicatorBar('4h');
1889+
1890+
render(<PriceAdvanced {...baseProps} />);
1891+
1892+
expect(mockUseOHLCVChart).toHaveBeenCalledWith(
1893+
expect.objectContaining({
1894+
timePeriod: '1m',
1895+
interval: '4h',
1896+
}),
1897+
);
1898+
});
1899+
1900+
it('maps 1w interval to 1y timePeriod when flag is ON', () => {
1901+
enableIndicatorBar('1w');
1902+
1903+
render(<PriceAdvanced {...baseProps} />);
1904+
1905+
expect(mockUseOHLCVChart).toHaveBeenCalledWith(
1906+
expect.objectContaining({
1907+
timePeriod: '1y',
1908+
interval: '1w',
1909+
}),
1910+
);
1911+
});
1912+
18851913
it('does not dispatch interval persistence when technical indicators flag is OFF', () => {
18861914
mockSelectTechnicalIndicatorsEnabled.mockReturnValue(false);
18871915
mockUseSelector.mockImplementation((selector: unknown) => {

app/components/UI/AssetOverview/Price/Price.advanced.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { toDateFormat } from '../../../../util/date';
1414
import styleSheet from './Price.styles';
1515
import {
1616
CHART_DATA_THRESHOLD,
17+
CHART_INTERVAL_CONFIGS,
1718
isTokenOverviewChartInterval,
1819
TOKEN_OVERVIEW_CHART_HEIGHT as BASE_CHART_HEIGHT,
1920
} from './tokenOverviewChart.constants';
@@ -33,7 +34,6 @@ import {
3334
import TimeRangeSelector, {
3435
TIME_RANGE_CONFIGS,
3536
type TimeRange,
36-
type OHLCVTimePeriod,
3737
} from '../../Charts/AdvancedChart/TimeRangeSelector';
3838
import { useOHLCVChart } from '../../Charts/AdvancedChart/useOHLCVChart';
3939
import { useOHLCVRealtime } from '../../Charts/AdvancedChart/useOHLCVRealtime';
@@ -79,18 +79,6 @@ const WS_INTERVAL_BY_TIME_RANGE: Record<TimeRange, string> = {
7979
'1Y': '1d',
8080
};
8181

82-
/**
83-
* Maps each candle interval to the API timePeriod that returns enough history.
84-
* Without this, e.g. interval=1d + timePeriod=1d returns only ~1 bar.
85-
*/
86-
const INTERVAL_TO_TIME_PERIOD: Record<string, OHLCVTimePeriod> = {
87-
'1m': '1d',
88-
'5m': '1d',
89-
'15m': '1d',
90-
'1h': '1w',
91-
'1d': '1m',
92-
};
93-
9482
const TIME_RANGE_LABELS: Record<TimeRange, string> = {
9583
'1H': 'asset_overview.chart_time_period.1h',
9684
'1D': 'asset_overview.chart_time_period.1d',
@@ -393,7 +381,7 @@ const PriceAdvanced = ({
393381
const chartInterval = displayInterval.toLowerCase();
394382

395383
const effectiveTimePeriod = isTechnicalIndicatorsEnabled
396-
? INTERVAL_TO_TIME_PERIOD[chartInterval]
384+
? CHART_INTERVAL_CONFIGS[chartInterval]
397385
: config.timePeriod;
398386

399387
const effectiveInterval = isTechnicalIndicatorsEnabled

app/components/UI/AssetOverview/Price/tokenOverviewChart.constants.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Dimensions } from 'react-native';
2+
import type { OHLCVTimePeriod } from '../../Charts/AdvancedChart/TimeRangeSelector';
23

34
/**
45
* Token overview chart column height (AdvancedChart / TradingView + legacy line when shown).
@@ -17,17 +18,30 @@ export const CHART_DATA_THRESHOLD = 5;
1718
*/
1819
export const TOKEN_OVERVIEW_TIME_RANGE_ROW_HEIGHT = 34;
1920

20-
/** Quick-pick candle intervals shown in IntervalBar when technical indicators are enabled. */
21-
export const TOKEN_OVERVIEW_CHART_INTERVALS = [
22-
'1m',
23-
'5m',
24-
'15m',
25-
'1h',
26-
'1d',
27-
] as const;
21+
/**
22+
* Single source of truth for candle intervals in the IntervalBar.
23+
* Maps each interval to the API timePeriod that returns enough history.
24+
*
25+
* To add a new interval: add one entry here. No other files need changes.
26+
* The UI pills, TypeScript type, type guard, and API mapping all derive from this.
27+
*/
28+
export const CHART_INTERVAL_CONFIGS: Record<string, OHLCVTimePeriod> = {
29+
'1m': '1d',
30+
'5m': '1d',
31+
'15m': '1d',
32+
'1h': '1w',
33+
'4h': '1m',
34+
'1d': '1m',
35+
'1w': '1y',
36+
};
37+
38+
/** Ordered list of intervals for the IntervalBar UI pills. */
39+
export const TOKEN_OVERVIEW_CHART_INTERVALS = Object.keys(
40+
CHART_INTERVAL_CONFIGS,
41+
);
2842

29-
export type TokenOverviewChartInterval =
30-
(typeof TOKEN_OVERVIEW_CHART_INTERVALS)[number];
43+
export type TokenOverviewChartInterval = keyof typeof CHART_INTERVAL_CONFIGS &
44+
string;
3145

3246
/** Default interval when none is persisted (matches 1D time-range WS default). */
3347
export const DEFAULT_TOKEN_OVERVIEW_CHART_INTERVAL: TokenOverviewChartInterval =
@@ -36,4 +50,4 @@ export const DEFAULT_TOKEN_OVERVIEW_CHART_INTERVAL: TokenOverviewChartInterval =
3650
export const isTokenOverviewChartInterval = (
3751
value: string | undefined | null,
3852
): value is TokenOverviewChartInterval =>
39-
TOKEN_OVERVIEW_CHART_INTERVALS.includes(value as TokenOverviewChartInterval);
53+
typeof value === 'string' && value in CHART_INTERVAL_CONFIGS;

app/components/UI/Charts/AdvancedChart/IntervalBar.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import {
1111
} from '@metamask/design-system-react-native';
1212
import { ChartType } from './AdvancedChart.types';
1313
import ChartTypeToggle from './ChartTypeToggle';
14-
15-
const QUICK_INTERVALS = ['1m', '5m', '15m', '1h', '1d'] as const;
14+
import { TOKEN_OVERVIEW_CHART_INTERVALS } from '../../AssetOverview/Price/tokenOverviewChart.constants';
1615

1716
const PILL_BASE = 'flex-row items-center justify-center rounded-xl px-2 py-1';
1817

@@ -44,7 +43,7 @@ const IntervalBar: React.FC<IntervalBarProps> = ({
4443
alignItems={BoxAlignItems.Center}
4544
twClassName="flex-1 gap-1"
4645
>
47-
{QUICK_INTERVALS.map((interval) => {
46+
{TOKEN_OVERVIEW_CHART_INTERVALS.map((interval) => {
4847
const isSelected = normalised === interval;
4948
return (
5049
<Pressable

app/reducers/user/selectors.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,6 @@ describe('user state selectors', () => {
209209
});
210210

211211
it('returns default when interval is invalid', () => {
212-
// @ts-expect-error - Testing invalid persisted value
213212
mockState.user.tokenOverviewChartInterval = 'invalid';
214213

215214
const { result } = renderHook(() =>

0 commit comments

Comments
 (0)