Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 10 additions & 2 deletions app/__mocks__/rive-react-native.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { forwardRef, useImperativeHandle } from 'react';
import React, { forwardRef, useImperativeHandle, useEffect } from 'react';
import { View, ViewProps } from 'react-native';

export interface RiveRef {
Expand Down Expand Up @@ -27,6 +27,7 @@ type MockRiveProps = ViewProps & {
alignment?: string;
autoplay?: boolean;
stateMachineName?: string;
onPlay?: () => void;
};

const DEFAULT_TEST_ID = 'mock-rive-animation';
Expand All @@ -48,12 +49,19 @@ const updateLastMockedMethods = (methods: RiveRef) => {
};

const RiveMock = forwardRef<RiveRef, MockRiveProps>(
({ testID = DEFAULT_TEST_ID, mockedMethods, ...viewProps }, ref) => {
({ testID = DEFAULT_TEST_ID, mockedMethods, onPlay, ...viewProps }, ref) => {
const methods = createMockedMethods(mockedMethods);
updateLastMockedMethods(methods);

useImperativeHandle(ref, () => methods, [methods]);

useEffect(() => {
if (onPlay) {
onPlay();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return <View testID={testID} {...viewProps} />;
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { AnimationDuration } from '../../../../../constants/animation.constants'
* The animation duration used for initial render.
*/
export const DEFAULT_BOTTOMSHEETDIALOG_DISPLAY_DURATION =
AnimationDuration.Regularly;
AnimationDuration.Fast;
/**
* This number represents the swipe speed to meet the velocity threshold.
*/
Expand Down
14 changes: 10 additions & 4 deletions app/components/UI/FoxAnimation/FoxAnimation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { StyleSheet, Platform, View } from 'react-native';
import Rive, { Alignment, Fit, RiveRef } from 'rive-react-native';
import { useSafeAreaInsets, EdgeInsets } from 'react-native-safe-area-context';
Expand Down Expand Up @@ -77,6 +77,8 @@ const FoxAnimation = ({
const insets = useSafeAreaInsets();
const styles = createStyles(hasFooter, insets);

const [isPlaying, setIsPlaying] = useState(false);

const showFoxAnimation = useCallback(async () => {
if (foxRef.current && trigger) {
try {
Expand All @@ -88,8 +90,10 @@ const FoxAnimation = ({
}, [foxRef, trigger]);

useEffect(() => {
showFoxAnimation();
}, [showFoxAnimation]);
if (isPlaying) {
showFoxAnimation();
}
}, [showFoxAnimation, isPlaying]);

return (
<View style={[styles.foxAnimationWrapper]}>
Expand All @@ -99,9 +103,11 @@ const FoxAnimation = ({
source={FoxAnimationRive}
fit={Fit.Contain}
alignment={Alignment.Center}
autoplay={false}
stateMachineName="FoxRaiseUp"
testID="fox-animation"
onPlay={() => {
setIsPlaying(true);
}}
/>
</View>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ import {
} from '../Bridge/utils/transaction-history';
import { ethers } from 'ethers';
import { formatAmountWithThreshold } from '../../../util/number';
import BadgeWrapper from '../../../component-library/components/Badges/BadgeWrapper';
import Badge, {
BadgeVariant,
} from '../../../component-library/components/Badges/Badge';
import { getNetworkImageSource } from '../../../util/networks';
import { parseCaipAssetType } from '@metamask/utils';

const MultichainBridgeTransactionListItem = ({
transaction,
Expand Down Expand Up @@ -60,7 +66,25 @@ const MultichainBridgeTransactionListItem = ({
appTheme,
osColorScheme,
);
return <Image source={icon} style={style.icon} resizeMode="stretch" />;
const chainId = parseCaipAssetType(
bridgeHistoryItem.quote.srcAsset.assetId,
).chainId;
if (!chainId)
return <Image source={icon} style={style.icon} resizeMode="stretch" />;

const networkImageSource = getNetworkImageSource({ chainId });
return (
<BadgeWrapper
badgeElement={
<Badge
variant={BadgeVariant.Network}
imageSource={networkImageSource}
/>
}
>
<Image source={icon} style={style.icon} resizeMode="stretch" />
</BadgeWrapper>
);
};

// Does not apply to swaps
Expand Down
18 changes: 14 additions & 4 deletions app/components/UI/OnboardingAnimation/OnboardingAnimation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { View, Animated, Easing, StyleSheet } from 'react-native';
import Rive, { Fit, Alignment, RiveRef } from 'rive-react-native';

Expand Down Expand Up @@ -67,6 +73,8 @@ const OnboardingAnimation = ({
const { themeAppearance } = useAppThemeFromContext();
const styles = createStyles();

const [isPlaying, setIsPlaying] = useState(false);

const moveLogoUp = useCallback(() => {
Animated.parallel([
Animated.timing(logoPosition, {
Expand Down Expand Up @@ -118,10 +126,10 @@ const OnboardingAnimation = ({
]);

useEffect(() => {
if (startOnboardingAnimation) {
if (startOnboardingAnimation && isPlaying) {
startRiveAnimation();
}
}, [startOnboardingAnimation, startRiveAnimation]);
}, [startRiveAnimation, startOnboardingAnimation, isPlaying]);

return (
<>
Expand All @@ -141,9 +149,11 @@ const OnboardingAnimation = ({
source={MetaMaskWordmarkAnimation}
fit={Fit.Contain}
alignment={Alignment.Center}
autoplay={false}
stateMachineName="WordmarkBuildUp"
testID="metamask-wordmark-animation"
onPlay={() => {
setIsPlaying(true);
}}
/>
</Animated.View>
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,23 +205,25 @@ jest.mock('../../hooks/usePerpsMarketStats', () => ({
}),
}));

const mockRefreshCandleData = jest.fn();
jest.mock('../../hooks/usePerpsPositionData', () => ({
usePerpsPositionData: () => ({
candleData: [
{
time: 1234567890,
open: 45000,
high: 45500,
low: 44500,
close: 45200,
volume: 1000,
},
],
isLoadingHistory: false,
error: null,
refreshCandleData: mockRefreshCandleData,
jest.mock('../../hooks/stream/usePerpsLiveCandles', () => ({
usePerpsLiveCandles: () => ({
candleData: {
coin: 'BTC',
interval: '1h',
candles: [
{
time: 1234567890,
open: '45000',
high: '45500',
low: '44500',
close: '45200',
volume: '1000',
},
],
},
isLoading: false,
hasHistoricalData: true,
error: null,
}),
}));

Expand Down Expand Up @@ -464,7 +466,6 @@ describe('PerpsMarketDetailsView', () => {
// Clean up mocks after each test
afterEach(() => {
jest.clearAllMocks();
mockRefreshCandleData.mockClear();
mockRefreshOrders.mockClear();
mockRefreshMarketStats.mockClear();
mockNavigate.mockClear();
Expand Down Expand Up @@ -756,8 +757,8 @@ describe('PerpsMarketDetailsView', () => {
await refreshControl.props.onRefresh();
});

// Should refresh candle data by default
expect(mockRefreshCandleData).toHaveBeenCalledTimes(1);
// Note: Candle data now uses WebSocket streaming (usePerpsLiveCandles)
// so no manual refresh is needed - data updates automatically
});

it('refreshes candle data when position tab is active', async () => {
Expand Down Expand Up @@ -790,8 +791,7 @@ describe('PerpsMarketDetailsView', () => {
await refreshControl.props.onRefresh();
});

// Assert - Only candle data refreshes since positions update via WebSocket
expect(mockRefreshCandleData).toHaveBeenCalledTimes(1);
// Assert - Candle data uses WebSocket streaming, no manual refresh needed
// refreshPosition is a no-op for WebSocket, so we don't expect it to be called
expect(mockRefreshPosition).not.toHaveBeenCalled();
});
Expand Down Expand Up @@ -845,9 +845,8 @@ describe('PerpsMarketDetailsView', () => {
await refreshControl.props.onRefresh();
});

// Assert - Only candle data refreshes (all other data updates via WebSocket)
expect(mockRefreshCandleData).toHaveBeenCalledTimes(1);
// Market stats, positions, and orders update via WebSocket, no manual refresh
// Assert - All data now updates via WebSocket, no manual refresh needed
// Market stats, candles, positions, and orders update via WebSocket
expect(mockRefreshMarketStats).not.toHaveBeenCalled();
expect(mockRefreshPosition).not.toHaveBeenCalled();
expect(mockRefreshOrders).not.toHaveBeenCalled();
Expand Down Expand Up @@ -883,8 +882,8 @@ describe('PerpsMarketDetailsView', () => {
await refreshControl.props.onRefresh();
});

// Assert - Only candle data refreshes (positions update via WebSocket)
expect(mockRefreshCandleData).toHaveBeenCalledTimes(1);
// Assert - Candle data now uses WebSocket streaming (no manual refresh)
// Positions also update via WebSocket
expect(mockRefreshPosition).not.toHaveBeenCalled();
});

Expand Down Expand Up @@ -912,15 +911,13 @@ describe('PerpsMarketDetailsView', () => {
await refreshControl.props.onRefresh();
});

// Verify refresh functions were called
expect(mockRefreshCandleData).toHaveBeenCalledTimes(1);
// Note: Candle data now uses WebSocket streaming (no manual refresh needed)
});

it('handles errors during refresh operation', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
const mockRefreshPosition = jest
.fn()
.mockRejectedValue(new Error('Refresh failed'));
it('handles refresh gracefully with WebSocket streaming', async () => {
// Note: Candle data now uses WebSocket streaming, so refresh is a no-op
// This test verifies the refresh control doesn't break with WebSocket data
const mockRefreshPosition = jest.fn();

mockUseHasExistingPosition.mockReturnValue({
hasPosition: false,
Expand All @@ -930,10 +927,6 @@ describe('PerpsMarketDetailsView', () => {
refreshPosition: mockRefreshPosition,
});

mockRefreshCandleData.mockRejectedValue(
new Error('Candle data refresh failed'),
);

const { getByTestId } = renderWithProvider(
<PerpsConnectionProvider>
<PerpsMarketDetailsView />
Expand All @@ -949,18 +942,14 @@ describe('PerpsMarketDetailsView', () => {
);
const refreshControl = scrollView.props.refreshControl;

// Trigger the refresh
// Trigger the refresh - should complete without errors
await act(async () => {
await refreshControl.props.onRefresh();
});

// Should log error
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Failed to refresh'),
expect.any(Error),
);

consoleErrorSpy.mockRestore();
// Refresh control should exist and be functional
expect(refreshControl).toBeDefined();
expect(refreshControl.props.refreshing).toBe(false);
});
});

Expand Down
Loading
Loading