Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
0ac1406
feat: implement Pay With Predict section (#30241)
vinistevam May 26, 2026
c72ed66
fix: dismiss AddWallet sheet before entering HW flow to fix post-conn…
mathieuartu May 26, 2026
664966a
fix(perps): auto-detect and clean stale CocoaPods state in preflight …
abretonc7s May 26, 2026
dbbf7e1
fix(perps): perps Mobile: Screen transition from market detail to ord…
abretonc7s May 26, 2026
37005a8
chore: adds the QuickBuy main sheet with modular buy flow (#30512)
zone-live May 26, 2026
d63e329
fix: remove Save button from trader notifications bottom sheet (#30632)
xavier-brochard May 26, 2026
e4b7792
test: added project name to the browserstack configuration (#30635)
javiergarciavera May 26, 2026
35ec102
fix: update icon for chart switch button (#30525)
sahar-fehri May 26, 2026
09c627d
chore(rewards): vip tier view rework (#30564)
sophieqgu May 26, 2026
8bfb7ff
feat: disable smart account on gas fees sponsored network (#30429)
Battambang May 26, 2026
9c16752
feat: Add dev auto-unlock password support (#30599)
sahar-fehri May 26, 2026
62a5d63
fix(rewards): transparent background of rewards modals (#30562)
sophieqgu May 26, 2026
49ca2c1
feat: add ambient price color A/B test for Token Details page (#30323)
sahar-fehri May 26, 2026
6159478
fix(perps): investigate Failed to execute 'dispatchEvent' on 'EventTa…
abretonc7s May 26, 2026
6d22177
feat(bridge): add fiat source amount input (#29756)
bfullam May 26, 2026
2ff0844
ci: fix auto-rc-build-core permission cp-7.79.0 (#30607)
joaoloureirop May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/auto-rc-ota-build-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ on:
value: ${{ jobs.trigger-build.outputs.android_version_code }}

permissions:
contents: read
contents: write
pull-requests: read
actions: write
id-token: write # required by build.yml
Expand Down
4 changes: 4 additions & 0 deletions .js.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export METAMASK_ENVIRONMENT="dev"
# Build type: "main" or "flask" or "beta"
export METAMASK_BUILD_TYPE="main"

# Optional: automatically unlock an existing wallet after app/Metro refresh in dev.
# Only used when METAMASK_ENVIRONMENT="dev"; must match the wallet password.
# export DEV_AUTO_UNLOCK_PASSWORD=""

# Optional: enable Ramps debug dashboard bridge in __DEV__ (WebSocket + fetch instrumentation).
# See app/components/UI/Ramp/debug/README.md
# export RAMPS_DEBUG_DASHBOARD="true"
Expand Down
17 changes: 13 additions & 4 deletions app/components/Nav/Main/MainNavigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -333,26 +333,35 @@ const RewardsHome = () => {
<Stack.Screen
name={Routes.MODAL.REWARDS_BOTTOM_SHEET_MODAL}
component={RewardsBottomSheetModal}
options={{ presentation: 'transparentModal' }}
options={{
presentation: 'transparentModal',
cardStyle: { backgroundColor: 'transparent' },
}}
/>
<Stack.Screen
name={Routes.MODAL.REWARDS_CLAIM_BOTTOM_SHEET_MODAL}
component={RewardsClaimBottomSheetModal}
options={{ presentation: 'transparentModal' }}
options={{
presentation: 'transparentModal',
cardStyle: { backgroundColor: 'transparent' },
}}
/>
<Stack.Screen
name={Routes.MODAL.REWARDS_OPTIN_ACCOUNT_GROUP_MODAL}
component={RewardOptInAccountGroupModal}
options={{
headerShown: false,
presentation: 'transparentModal',
...clearStackNavigatorOptionsWithTransitionAnimation,
cardStyle: { backgroundColor: 'transparent' },
}}
/>
<Stack.Screen
name={Routes.MODAL.REWARDS_END_OF_SEASON_CLAIM_BOTTOM_SHEET}
component={EndOfSeasonClaimBottomSheet}
options={{ presentation: 'transparentModal' }}
options={{
presentation: 'transparentModal',
cardStyle: { backgroundColor: 'transparent' },
}}
/>
<Stack.Screen
name={Routes.MODAL.REWARDS_SELECT_SHEET}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ const styleSheet = (params: {
theme: Theme;
vars: {
selected: boolean;
selectedColor?: string;
};
}) => {
const {
theme,
vars: { selected },
vars: { selected, selectedColor },
} = params;
const { colors } = theme;
const finalBackgroundColor = selected
? colors.background.muted
? (selectedColor ?? colors.background.muted)
: 'transparent';
/** Matches {@link TimeRangeSelector} segment Pressables: `py-1`, `px-4`, `rounded-lg`, `flex-1`, `bg-muted` when selected. */
return StyleSheet.create({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,34 @@ interface ChartNavigationButtonProps {
onPress: () => void;
label: string;
selected: boolean;
/** Override background color for the selected state (A/B test). */
selectedColor?: string;
}

const ChartNavigationButton = ({
onPress,
label,
selected,
selectedColor,
}: ChartNavigationButtonProps) => {
const { styles } = useStyles(styleSheet, { selected });
const { styles } = useStyles(styleSheet, { selected, selectedColor });

const getTextColor = () => {
if (selected && selectedColor) {
return TextColor.Inverse;
}
if (!selected && selectedColor) {
return selectedColor;
}
return selected ? TextColor.Default : TextColor.Alternative;
};

return (
<TouchableOpacity style={styles.button} onPress={onPress}>
<Text
variant={TextVariant.BodySM}
style={styles.label}
color={selected ? TextColor.Default : TextColor.Alternative}
color={getTextColor()}
>
{label}
</Text>
Expand Down
279 changes: 279 additions & 0 deletions app/components/UI/AssetOverview/Price/Price.advanced.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1049,4 +1049,283 @@ describe('PriceAdvanced', () => {
);
});
});

describe('ambient color logic', () => {
it('returns undefined when useAmbientColor is false', () => {
const { queryByTestId } = render(
<PriceAdvanced {...baseProps} useAmbientColor={false} />,
);

// When useAmbientColor is false, ambientColor should be undefined
// This means we won't render the skeleton and will render the chart directly
expect(queryByTestId('mock-advanced-chart')).toBeOnTheScreen();
});

it('returns success green when displayDiff is null (no data)', () => {
mockUseOHLCVChart.mockReturnValueOnce({
ohlcvData: [
...ohlcvPaddingThree,
{ time: 1000, open: 100, high: 101, low: 99, close: 100, volume: 1 },
{ time: 2000, open: 100, high: 106, low: 100, close: 105, volume: 1 },
],
isLoading: true, // Still loading, so displayDiff will be null
error: undefined,
hasMore: false,
nextCursor: null,
hasEmptyData: false,
});

const { getByTestId } = render(
<PriceAdvanced {...baseProps} useAmbientColor />,
);

// When displayDiff is null, should default to positive (success green)
// The chart should still render because we default to success green
expect(getByTestId('mock-advanced-chart')).toBeOnTheScreen();
});

it('returns success green when displayDiff is positive', () => {
// OHLCV data: reference close = 100, current price = 105
// displayDiff = 105 - 100 = 5 (positive)
const { getByTestId } = render(
<PriceAdvanced {...baseProps} currentPrice={105} useAmbientColor />,
);

// Should render chart with success green color
expect(getByTestId('mock-advanced-chart')).toBeOnTheScreen();
const chart = getByTestId('mock-advanced-chart');
expect(chart.props.lineColorOverride).toBeTruthy();
// In light mode, should use LIGHT_MODE_SUCCESS_GREEN
});

it('returns AMBIENT_NEGATIVE_COLOR when displayDiff is negative', () => {
// Mock OHLCV data with negative price movement
// For 1D range: visibleFromMs = lastBarTime - 86400000ms (24 hours)
// lastBarTime = 100000000, visibleFromMs = 13600000
// First visible candle at time 20000000 has close=100
// Last candle has close=95
// displayDiff = 95 - 100 = -5 (negative)
mockUseOHLCVChart.mockReturnValueOnce({
ohlcvData: [
{ time: 1000000, open: 90, high: 91, low: 89, close: 90, volume: 1 },
{ time: 2000000, open: 90, high: 91, low: 89, close: 91, volume: 1 },
{ time: 3000000, open: 91, high: 92, low: 90, close: 92, volume: 1 },
{
time: 20000000,
open: 100,
high: 101,
low: 99,
close: 100,
volume: 1,
}, // First in visible range
{
time: 100000000,
open: 100,
high: 100,
low: 95,
close: 95,
volume: 1,
}, // Last bar
],
isLoading: false,
error: undefined,
hasMore: false,
nextCursor: null,
hasEmptyData: false,
});

const { getByTestId } = render(
<PriceAdvanced {...baseProps} currentPrice={95} useAmbientColor />,
);

// Should render chart with negative color (#FF5C16)
expect(getByTestId('mock-advanced-chart')).toBeOnTheScreen();
const chart = getByTestId('mock-advanced-chart');
// eslint-disable-next-line @metamask/design-tokens/color-no-hex
expect(chart.props.lineColorOverride).toBe('#FF5C16');
});

it('calls onPriceDirectionChange with true for positive displayDiff', () => {
const mockOnPriceDirectionChange = jest.fn();

render(
<PriceAdvanced
{...baseProps}
currentPrice={105}
useAmbientColor
onPriceDirectionChange={mockOnPriceDirectionChange}
/>,
);

// Should call callback with true for positive price diff
expect(mockOnPriceDirectionChange).toHaveBeenCalledWith(true);
});

it('calls onPriceDirectionChange with false for negative displayDiff', () => {
const mockOnPriceDirectionChange = jest.fn();

// Mock OHLCV data with negative price movement
mockUseOHLCVChart.mockReturnValueOnce({
ohlcvData: [
{ time: 1000000, open: 90, high: 91, low: 89, close: 90, volume: 1 },
{ time: 2000000, open: 90, high: 91, low: 89, close: 91, volume: 1 },
{ time: 3000000, open: 91, high: 92, low: 90, close: 92, volume: 1 },
{
time: 20000000,
open: 100,
high: 101,
low: 99,
close: 100,
volume: 1,
}, // First in visible range
{
time: 100000000,
open: 100,
high: 100,
low: 95,
close: 95,
volume: 1,
}, // Last bar
],
isLoading: false,
error: undefined,
hasMore: false,
nextCursor: null,
hasEmptyData: false,
});

render(
<PriceAdvanced
{...baseProps}
currentPrice={95}
useAmbientColor
onPriceDirectionChange={mockOnPriceDirectionChange}
/>,
);

// Should call callback with false for negative price diff
expect(mockOnPriceDirectionChange).toHaveBeenCalledWith(false);
});

it('does not call onPriceDirectionChange when falling back to legacy', () => {
const mockOnPriceDirectionChange = jest.fn();

mockUseOHLCVChart.mockReturnValueOnce({
ohlcvData: [
{ time: 1000, open: 100, high: 101, low: 99, close: 100, volume: 1 },
],
isLoading: false,
error: undefined,
hasMore: false,
nextCursor: null,
hasEmptyData: false,
});

render(
<PriceAdvanced
{...baseProps}
useAmbientColor
onPriceDirectionChange={mockOnPriceDirectionChange}
/>,
);

// Should not call callback when falling back to legacy (insufficient data)
expect(mockOnPriceDirectionChange).not.toHaveBeenCalled();
});

it('calls onPriceDirectionChange exactly once when OHLCV data is sufficient (>= 5 bars)', () => {
const mockOnPriceDirectionChange = jest.fn();

// Sufficient OHLCV data (5 bars total)
mockUseOHLCVChart.mockReturnValueOnce({
ohlcvData: [
...ohlcvPaddingThree, // 3 bars
{ time: 1000, open: 100, high: 101, low: 99, close: 100, volume: 1 },
{ time: 2000, open: 100, high: 106, low: 100, close: 105, volume: 1 },
],
isLoading: false,
error: undefined,
hasMore: false,
nextCursor: null,
hasEmptyData: false,
});

render(
<PriceAdvanced
{...baseProps}
currentPrice={105}
useAmbientColor
onPriceDirectionChange={mockOnPriceDirectionChange}
/>,
);

// Should call callback exactly once with OHLCV-based direction
expect(mockOnPriceDirectionChange).toHaveBeenCalledTimes(1);
expect(mockOnPriceDirectionChange).toHaveBeenCalledWith(true); // positive price
});

it('does not call onPriceDirectionChange when OHLCV data is insufficient (< 5 bars) - legacy handles it', () => {
const mockOnPriceDirectionChange = jest.fn();

// Insufficient OHLCV data (4 bars total) - should fallback to legacy
mockUseOHLCVChart.mockReturnValueOnce({
ohlcvData: [
{ time: 100, open: 90, high: 91, low: 89, close: 90, volume: 1 },
{ time: 200, open: 90, high: 91, low: 89, close: 91, volume: 1 },
{ time: 1000, open: 100, high: 101, low: 99, close: 100, volume: 1 },
{ time: 2000, open: 100, high: 106, low: 100, close: 105, volume: 1 },
],
isLoading: false,
error: undefined,
hasMore: false,
nextCursor: null,
hasEmptyData: false,
});

render(
<PriceAdvanced
{...baseProps}
currentPrice={105}
useAmbientColor
onPriceDirectionChange={mockOnPriceDirectionChange}
priceDiff={5} // Legacy will use this
/>,
);

// PriceAdvanced should NOT call callback (guarded by shouldFallbackToLegacy)
// PriceLegacy will call it instead when !isLoading
expect(mockOnPriceDirectionChange).not.toHaveBeenCalled();
});

it('prevents stale OHLCV callback from overriding legacy when falling back', () => {
const mockOnPriceDirectionChange = jest.fn();

// Single OHLCV bar (would compute initialPriceDiff = 0, always positive)
// But priceDiff is negative
mockUseOHLCVChart.mockReturnValueOnce({
ohlcvData: [
{ time: 1000, open: 100, high: 101, low: 99, close: 100, volume: 1 },
],
isLoading: false,
error: undefined,
hasMore: false,
nextCursor: null,
hasEmptyData: false,
});

render(
<PriceAdvanced
{...baseProps}
currentPrice={95}
useAmbientColor
onPriceDirectionChange={mockOnPriceDirectionChange}
priceDiff={-5} // Negative - should be used by legacy
/>,
);

// PriceAdvanced should NOT call with stale OHLCV-based value
// This test would FAIL if we remove the !shouldFallbackToLegacy guard
expect(mockOnPriceDirectionChange).not.toHaveBeenCalled();
});
});
});
Loading
Loading