Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React from 'react';
import { fireEvent } from '@testing-library/react-native';
import renderWithProvider from '../../../../../util/test/renderWithProvider';
import { useMoneyAccountTransactions } from '../../hooks/useMoneyAccountTransactions';
import MOCK_MONEY_TRANSACTIONS from '../../constants/mockActivityData';
import {
isMoneyActivityDeposit,
isMoneyActivityTransfer,
} from '../../constants/moneyActivityFilters';
import MoneyActivityView from './MoneyActivityView';
import { MoneyActivityViewTestIds } from './MoneyActivityView.testIds';

const mockGoBack = jest.fn();

jest.mock('@react-navigation/native', () => {
const actualReactNavigation = jest.requireActual('@react-navigation/native');
return {
...actualReactNavigation,
useNavigation: () => ({
goBack: mockGoBack,
navigate: jest.fn(),
}),
};
});

jest.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: () => ({ top: 48, bottom: 34, left: 0, right: 0 }),
}));

jest.mock('../../hooks/useMoneyAccountTransactions', () => ({
useMoneyAccountTransactions: jest.fn(),
}));

jest.mock('../../components/MoneyActivityItem/MoneyActivityItem', () => {
const { View, Text } = jest.requireActual('react-native');
return {
__esModule: true,
default: ({ tx }: { tx: { id: string } }) => (
<View testID={`activity-mock-tx-${tx.id}`}>
<Text>{tx.id}</Text>
</View>
),
};
});

jest.mock('../../../../../../locales/i18n', () => ({
strings: (key: string) => {
const map: Record<string, string> = {
'money.activity.title': 'Activity',
'money.activity.empty': 'No activity yet',
'money.activity.filter_all': 'All',
'money.activity.filter_deposits': 'Deposits',
'money.activity.filter_transfers': 'Transfers',
};
return map[key] ?? key;
},
}));

const mockUseMoneyAccountTransactions = jest.mocked(
useMoneyAccountTransactions,
);

const MOCK_DEPOSITS = MOCK_MONEY_TRANSACTIONS.filter(isMoneyActivityDeposit);
const MOCK_TRANSFERS = MOCK_MONEY_TRANSACTIONS.filter(isMoneyActivityTransfer);

describe('MoneyActivityView', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseMoneyAccountTransactions.mockReturnValue({
allTransactions: MOCK_MONEY_TRANSACTIONS,
deposits: MOCK_DEPOSITS,
transfers: MOCK_TRANSFERS,
submittedTransactions: [],
moneyAddress: '0x0000000000000000000000000000000000000001',
});
});

it('renders the main container', () => {
const { getByTestId } = renderWithProvider(<MoneyActivityView />);

expect(getByTestId(MoneyActivityViewTestIds.CONTAINER)).toBeOnTheScreen();
});

it('renders the activity title', () => {
const { getByTestId } = renderWithProvider(<MoneyActivityView />);

expect(getByTestId(MoneyActivityViewTestIds.TITLE)).toBeOnTheScreen();
});

it('renders filter controls', () => {
const { getByTestId } = renderWithProvider(<MoneyActivityView />);

expect(getByTestId(MoneyActivityViewTestIds.FILTER_ALL)).toBeOnTheScreen();
expect(
getByTestId(MoneyActivityViewTestIds.FILTER_DEPOSITS),
).toBeOnTheScreen();
expect(
getByTestId(MoneyActivityViewTestIds.FILTER_TRANSFERS),
).toBeOnTheScreen();
});

it('pressing the back button calls navigation.goBack', () => {
const { getByTestId } = renderWithProvider(<MoneyActivityView />);

fireEvent.press(getByTestId(MoneyActivityViewTestIds.BACK_BUTTON));

expect(mockGoBack).toHaveBeenCalledTimes(1);
});

it('renders transaction rows from activity data', () => {
const { getByTestId } = renderWithProvider(<MoneyActivityView />);

expect(getByTestId('activity-mock-tx-money-tx-1')).toBeOnTheScreen();
});

it('renders empty state when there are no transactions', () => {
mockUseMoneyAccountTransactions.mockReturnValue({
allTransactions: [],
deposits: [],
transfers: [],
submittedTransactions: [],
moneyAddress: '0x0000000000000000000000000000000000000001',
});

const { getByTestId } = renderWithProvider(<MoneyActivityView />);

expect(getByTestId(MoneyActivityViewTestIds.EMPTY_LIST)).toBeOnTheScreen();
expect(
getByTestId(MoneyActivityViewTestIds.EMPTY_LIST_MESSAGE),
).toBeOnTheScreen();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const MoneyActivityViewTestIds = {
CONTAINER: 'money-activity-view-container',
BACK_BUTTON: 'money-activity-view-back',
TITLE: 'money-activity-view-title',
FILTER_ALL: 'money-activity-view-filter-all',
FILTER_DEPOSITS: 'money-activity-view-filter-deposits',
FILTER_TRANSFERS: 'money-activity-view-filter-transfers',
DATE_HEADER: 'money-activity-view-date-header',
EMPTY_LIST: 'money-activity-view-empty-list',
EMPTY_LIST_MESSAGE: 'money-activity-view-empty-list-message',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import React, { useCallback, useMemo, useState } from 'react';
import { SectionList, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native';
import type { TransactionMeta } from '@metamask/transaction-controller';
import {
Box,
BoxAlignItems,
BoxFlexDirection,
BoxJustifyContent,
Button,
ButtonIconSize,
ButtonIcon,
ButtonSize,
ButtonVariant,
FontWeight,
IconName,
Text,
TextColor,
TextVariant,
} from '@metamask/design-system-react-native';
import { strings } from '../../../../../../locales/i18n';
import { useTheme } from '../../../../../util/theme';
import MoneyActivityItem from '../../components/MoneyActivityItem';
import { useMoneyAccountTransactions } from '../../hooks/useMoneyAccountTransactions';
import { getMoneyActivityDateKeyUtc } from '../../constants/moneyActivityFilters';
import { MoneyActivityFilter } from '../../constants/mockActivityData';
import { showMoneyActivityUnderConstructionAlert } from '../../constants/showMoneyActivityUnderConstructionAlert';
import { MoneyActivityViewTestIds } from './MoneyActivityView.testIds';

const styles = StyleSheet.create({
safeArea: { flex: 1 },
filterButtonSpacing: {
minWidth: 0,
paddingHorizontal: 16,
paddingVertical: 12,
minHeight: 48,
},
});

interface DateSection {
title: string;
data: TransactionMeta[];
}

function groupByDate(transactions: TransactionMeta[]): DateSection[] {
const groups = new Map<string, TransactionMeta[]>();
for (const tx of transactions) {
const key = getMoneyActivityDateKeyUtc(tx);
const existing = groups.get(key);
if (existing) {
existing.push(tx);
} else {
groups.set(key, [tx]);
}
}
return Array.from(groups.entries()).map(([dateKey, data]) => ({
title: new Date(`${dateKey}T00:00:00.000Z`).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
}),
data,
}));
}

const MoneyActivityView = () => {
const navigation = useNavigation();
const insets = useSafeAreaInsets();
const { colors } = useTheme();
const [filter, setFilter] = useState(MoneyActivityFilter.All);

const { allTransactions, deposits, transfers, moneyAddress } =
useMoneyAccountTransactions();

const handleBackPress = useCallback(() => {
navigation.goBack();
}, [navigation]);

const handleItemPress = useCallback(() => {
showMoneyActivityUnderConstructionAlert();
}, []);

const filtered = useMemo(() => {
if (filter === MoneyActivityFilter.All) {
return allTransactions;
}
if (filter === MoneyActivityFilter.Deposits) {
return deposits;
}
return transfers;
}, [filter, allTransactions, deposits, transfers]);

const sections = useMemo(() => groupByDate(filtered), [filtered]);

const renderSectionHeader = ({ section }: { section: DateSection }) => (
<Box twClassName="px-4 pt-2 pb-1 bg-default">
<Text
variant={TextVariant.BodySm}
color={TextColor.TextAlternative}
testID={MoneyActivityViewTestIds.DATE_HEADER}
>
{section.title}
</Text>
</Box>
);

const renderItem = ({ item }: { item: TransactionMeta }) => (
<MoneyActivityItem
tx={item}
moneyAddress={moneyAddress}
onPress={handleItemPress}
/>
);

const isActive = (f: MoneyActivityFilter) => f === filter;

return (
<Box
style={[
styles.safeArea,
{ paddingTop: insets.top, backgroundColor: colors.background.default },
]}
twClassName="flex-1 bg-default"
testID={MoneyActivityViewTestIds.CONTAINER}
>
<Box
flexDirection={BoxFlexDirection.Row}
alignItems={BoxAlignItems.Center}
justifyContent={BoxJustifyContent.Start}
paddingHorizontal={1}
paddingVertical={2}
>
<ButtonIcon
iconName={IconName.ArrowLeft}
size={ButtonIconSize.Md}
onPress={handleBackPress}
accessibilityLabel="Back"
testID={MoneyActivityViewTestIds.BACK_BUTTON}
/>
</Box>

<Box paddingHorizontal={4} paddingTop={4} paddingBottom={6}>
<Text
variant={TextVariant.HeadingLg}
fontWeight={FontWeight.Medium}
testID={MoneyActivityViewTestIds.TITLE}
>
{strings('money.activity.title')}
</Text>
</Box>

<Box
flexDirection={BoxFlexDirection.Row}
gap={4}
paddingHorizontal={4}
paddingBottom={3}
>
<Button
variant={
isActive(MoneyActivityFilter.All)
? ButtonVariant.Primary
: ButtonVariant.Secondary
}
size={ButtonSize.Md}
style={styles.filterButtonSpacing}
onPress={() => setFilter(MoneyActivityFilter.All)}
testID={MoneyActivityViewTestIds.FILTER_ALL}
>
{strings('money.activity.filter_all')}
</Button>
<Button
variant={
isActive(MoneyActivityFilter.Deposits)
? ButtonVariant.Primary
: ButtonVariant.Secondary
}
size={ButtonSize.Md}
style={styles.filterButtonSpacing}
onPress={() => setFilter(MoneyActivityFilter.Deposits)}
testID={MoneyActivityViewTestIds.FILTER_DEPOSITS}
>
{strings('money.activity.filter_deposits')}
</Button>
<Button
variant={
isActive(MoneyActivityFilter.Transfers)
? ButtonVariant.Primary
: ButtonVariant.Secondary
}
size={ButtonSize.Md}
style={styles.filterButtonSpacing}
onPress={() => setFilter(MoneyActivityFilter.Transfers)}
testID={MoneyActivityViewTestIds.FILTER_TRANSFERS}
>
{strings('money.activity.filter_transfers')}
</Button>
</Box>

{sections.length === 0 ? (
<Box
flexDirection={BoxFlexDirection.Row}
alignItems={BoxAlignItems.Center}
justifyContent={BoxJustifyContent.Center}
twClassName="flex-1 px-6 pb-8"
testID={MoneyActivityViewTestIds.EMPTY_LIST}
>
<Text
variant={TextVariant.BodyMd}
color={TextColor.TextAlternative}
testID={MoneyActivityViewTestIds.EMPTY_LIST_MESSAGE}
>
{strings('money.activity.empty')}
</Text>
</Box>
) : (
<SectionList
sections={sections}
keyExtractor={(item) => item.id}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
stickySectionHeadersEnabled={false}
/>
)}
</Box>
);
};

export default MoneyActivityView;
1 change: 1 addition & 0 deletions app/components/UI/Money/Views/MoneyActivityView/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './MoneyActivityView';
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { StyleSheet } from 'react-native';
import type { Theme } from '../../../../../util/theme/models';

const styleSheet = () =>
const styleSheet = (params: { theme: Theme }) =>
StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: params.theme.colors.background.default,
},
scrollContent: {
paddingBottom: 100,
Expand Down
Loading
Loading