Skip to content
Draft
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
7 changes: 6 additions & 1 deletion .storybook/storybook-store.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { AppThemeKey } from '../app/util/theme/models';
import initialRootState from '../app/util/test/initial-root-state';

export const storybookStore = {
user: { appTheme: AppThemeKey.os },
...initialRootState,
user: {
...initialRootState.user,
appTheme: AppThemeKey.os,
},
};
1 change: 1 addition & 0 deletions .storybook/storybook.requires.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ import { StyleSheet } from 'react-native';
const createStyles = () =>
StyleSheet.create({
accountGroupBalance: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 16,
justifyContent: 'space-between',
paddingTop: 24,
},
balanceContainer: {
flexDirection: 'row',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,40 @@ describe('AccountGroupBalance', () => {
}),
);

const { getByTestId } = renderWithProvider(<AccountGroupBalance />, {
state: testState,
});
const { getByTestId, queryByTestId } = renderWithProvider(
<AccountGroupBalance />,
{
state: testState,
},
);

// Should render balance text, not empty state
expect(getByTestId(WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT)).toBeTruthy();
expect(queryByTestId('account-group-balance-empty-state')).toBeNull();
});

const el = getByTestId(WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT);
expect(el).toBeTruthy();
it('renders balance empty state when balance is zero', () => {
const { selectBalanceBySelectedAccountGroup } = jest.requireMock(
'../../../../../selectors/assets/balances',
);
(selectBalanceBySelectedAccountGroup as jest.Mock).mockImplementation(
() => ({
walletId: 'wallet-1',
groupId: 'wallet-1/group-1',
totalBalanceInUserCurrency: 0, // Zero balance
userCurrency: 'usd',
}),
);

const { getByTestId, queryByTestId } = renderWithProvider(
<AccountGroupBalance />,
{
state: testState,
},
);

// Should render BalanceEmptyState instead of balance text
expect(getByTestId('account-group-balance-empty-state')).toBeDefined();
expect(queryByTestId(WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT)).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { WalletViewSelectorsIDs } from '../../../../../../e2e/selectors/wallet/W
import { Skeleton } from '../../../../../component-library/components/Skeleton';
import { useFormatters } from '../../../../hooks/useFormatters';
import AccountGroupBalanceChange from '../../components/BalanceChange/AccountGroupBalanceChange';
import BalanceEmptyState from '../../../BalanceEmptyState';

const AccountGroupBalance = () => {
const { PreferencesController } = Engine.context;
Expand All @@ -38,10 +39,23 @@ const AccountGroupBalance = () => {
const userCurrency = groupBalance?.userCurrency ?? '';
const displayBalance = formatCurrency(totalBalance, userCurrency);

// Check if balance is zero (empty state) - only check when we have balance data
const hasZeroBalance =
groupBalance && groupBalance.totalBalanceInUserCurrency === 0;

return (
<View style={styles.accountGroupBalance}>
<View>
{groupBalance ? (
{!groupBalance ? (
<View style={styles.skeletonContainer}>
<Skeleton width={100} height={40} />
<Skeleton width={100} height={20} />
</View>
) : hasZeroBalance ? (
<>
<BalanceEmptyState testID="account-group-balance-empty-state" />
</>
) : (
<TouchableOpacity
onPress={() => togglePrivacy(!privacyMode)}
testID="balance-container"
Expand All @@ -66,11 +80,6 @@ const AccountGroupBalance = () => {
/>
)}
</TouchableOpacity>
) : (
<View style={styles.skeletonContainer}>
<Skeleton width={100} height={40} />
<Skeleton width={100} height={20} />
</View>
)}
</View>
</View>
Expand Down
20 changes: 20 additions & 0 deletions app/components/UI/BalanceEmptyState/BalanceEmptyState.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';

import BalanceEmptyState from './BalanceEmptyState';
import { Box, BoxBackgroundColor } from '@metamask/design-system-react-native';

const BalanceEmptyStateMeta = {
title: 'Components / UI / BalanceEmptyState',
component: BalanceEmptyState,
decorators: [
(Story: React.FC) => (
<Box backgroundColor={BoxBackgroundColor.BackgroundDefault} padding={4}>
<Story />
</Box>
),
],
};

export default BalanceEmptyStateMeta;

export const Default = {};
58 changes: 58 additions & 0 deletions app/components/UI/BalanceEmptyState/BalanceEmptyState.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import { fireEvent } from '@testing-library/react-native';
import renderWithProvider from '../../../util/test/renderWithProvider';
import { backgroundState } from '../../../util/test/initial-root-state';
import BalanceEmptyState from './BalanceEmptyState';
import { BalanceEmptyStateProps } from './BalanceEmptyState.types';

// Mock navigation (component requires it)
const mockNavigate = jest.fn();
jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
useNavigation: () => ({
navigate: mockNavigate,
}),
}));

describe('BalanceEmptyState', () => {
beforeEach(() => {
jest.clearAllMocks();
});

const renderComponent = (props: Partial<BalanceEmptyStateProps> = {}) =>
renderWithProvider(
<BalanceEmptyState testID="balance-empty-state" {...props} />,
{
state: {
engine: {
backgroundState,
},
},
},
);

it('renders correctly', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('balance-empty-state')).toBeDefined();
});

it('passes a twClassName to the Box component', () => {
const { getByTestId } = renderComponent({ twClassName: 'mt-4' });
expect(getByTestId('balance-empty-state')).toHaveStyle({
marginTop: 16, // mt-4
});
});

it('has action button that can be pressed', () => {
const { getByTestId } = renderComponent();
const actionButton = getByTestId('balance-empty-state-action-button');

expect(actionButton).toBeDefined();

// Press the button
fireEvent.press(actionButton);

// Verify that navigation was triggered
expect(mockNavigate).toHaveBeenCalled();
});
});
118 changes: 118 additions & 0 deletions app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React from 'react';
import { Image } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useSelector } from 'react-redux';
import {
Box,
Text,
TextVariant,
TextColor,
BoxBackgroundColor,
BoxFlexDirection,
BoxAlignItems,
BoxJustifyContent,
FontWeight,
} from '@metamask/design-system-react-native';
import { useTailwind } from '@metamask/design-system-twrnc-preset';
import ButtonHero from '../../../component-library/components-temp/Buttons/ButtonHero';
import { strings } from '../../../../locales/i18n';
import { MetaMetricsEvents, useMetrics } from '../../hooks/useMetrics';
import { getDecimalChainId } from '../../../util/networks';
import { selectChainId } from '../../../selectors/networkController';
import { trace, TraceName } from '../../../util/trace';
import { createDepositNavigationDetails } from '../Ramp/Deposit/routes/utils';
import { BalanceEmptyStateProps } from './BalanceEmptyState.types';
import bankTransferImage from '../../../images/bank-transfer.png';

/**
* BalanceEmptyState smart component displays an empty state for wallet balance
* with an illustration, title, subtitle, and action button that navigates to deposit flow.
*/
const BalanceEmptyState: React.FC<BalanceEmptyStateProps> = ({
testID = 'balance-empty-state',
...props
}) => {
const tw = useTailwind();
const chainId = useSelector(selectChainId);
const navigation = useNavigation();
const { trackEvent, createEventBuilder } = useMetrics();

const handleAction = () => {
navigation.navigate(...createDepositNavigationDetails());

trackEvent(
createEventBuilder(
MetaMetricsEvents.CARD_ADD_FUNDS_DEPOSIT_CLICKED,
).build(),
);

trackEvent(
createEventBuilder(MetaMetricsEvents.RAMPS_BUTTON_CLICKED)
.addProperties({
text: 'Add funds',
location: 'BalanceEmptyState',
chain_id_destination: getDecimalChainId(chainId),
ramp_type: 'DEPOSIT',
})
.build(),
);

trace({
name: TraceName.LoadDepositExperience,
});
};

return (
<Box
paddingLeft={4}
paddingRight={4}
paddingTop={3}
paddingBottom={4}
justifyContent={BoxJustifyContent.Center}
backgroundColor={BoxBackgroundColor.BackgroundSection}
gap={5}
testID={testID}
{...props}
twClassName={`rounded-2xl ${props?.twClassName ?? ''}`}
>
<Box
flexDirection={BoxFlexDirection.Column}
gap={1}
alignItems={BoxAlignItems.Center}
>
<Image
source={bankTransferImage}
style={tw.style('w-[100px] h-[100px]')}
resizeMode="cover"
testID={`${testID}-image`}
/>
<Text
variant={TextVariant.HeadingLg}
color={TextColor.TextDefault}
twClassName="text-center"
testID={`${testID}-title`}
>
{strings('wallet.fund_your_wallet')}
</Text>
<Text
variant={TextVariant.BodyMd}
color={TextColor.TextAlternative}
fontWeight={FontWeight.Medium}
twClassName="text-center"
testID={`${testID}-subtitle`}
>
{strings('wallet.get_ready_for_web3')}
</Text>
</Box>
<ButtonHero
onPress={handleAction}
isFullWidth
testID={`${testID}-action-button`}
>
{strings('wallet.add_funds')}
</ButtonHero>
</Box>
);
};

export default BalanceEmptyState;
11 changes: 11 additions & 0 deletions app/components/UI/BalanceEmptyState/BalanceEmptyState.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BoxProps } from '@metamask/design-system-react-native';
/**
* Props for the BalanceEmptyState smart component
*/
export interface BalanceEmptyStateProps extends Omit<BoxProps, 'children'> {
/**
* Test ID for component testing
* @default 'balance-empty-state'
*/
testID?: string;
}
2 changes: 2 additions & 0 deletions app/components/UI/BalanceEmptyState/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './BalanceEmptyState';
export type { BalanceEmptyStateProps } from './BalanceEmptyState.types';
11 changes: 1 addition & 10 deletions app/components/UI/DeFiPositions/DeFiPositionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,6 @@ const DeFiPositionsList: React.FC<DeFiPositionsListProps> = () => {
}
}

if (formattedDeFiPositions.length === 0) {
// No positions found for the current account
return (
<View testID={WalletViewSelectorsIDs.DEFI_POSITIONS_CONTAINER}>
<DeFiPositionsControlBar />
<DefiEmptyState twClassName="mx-auto mt-4" />
</View>
);
}

return (
<View
style={styles.wrapper}
Expand All @@ -163,6 +153,7 @@ const DeFiPositionsList: React.FC<DeFiPositionsListProps> = () => {
`${protocolChainAggregate.chainId}-${protocolChainAggregate.protocolAggregate.protocolDetails.name}`
}
scrollEnabled
ListEmptyComponent={<DefiEmptyState twClassName="mx-auto mt-4" />}
/>
</View>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DefiEmptyState } from './DefiEmptyState';

const DefiEmptyStateMeta = {
title: 'Components/UI/DefiEmptyState',
title: 'Components / UI / DefiEmptyState',
component: DefiEmptyState,
};

Expand Down
Loading
Loading