Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create initial portfolio screen #15

Merged
merged 11 commits into from
Jan 6, 2025
13 changes: 8 additions & 5 deletions react-native-expo/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import dripsyTheme from '../utils/dripsyTheme';
import PraxI18nProvider from '../components/PraxI18nProvider';
import type { Preview } from '@storybook/react';
import React from 'react';
import ReduxProvider from '../components/ReduxProvider';
/**
* Ideally, we'd use `<FontProvider />` in the root decorator to provide fonts
* to Storybook. But that caused weird import issues. So for now, we'll just add
Expand All @@ -13,11 +14,13 @@ import './fonts.css';
const preview: Preview = {
decorators: [
Story => (
<PraxI18nProvider>
<DripsyProvider theme={dripsyTheme}>
<Story />
</DripsyProvider>
</PraxI18nProvider>
<ReduxProvider>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed because <Balance />, which has a Storybook story, dispatches Redux actions

<PraxI18nProvider>
<DripsyProvider theme={dripsyTheme}>
<Story />
</DripsyProvider>
</PraxI18nProvider>
</ReduxProvider>
),
],
parameters: {
Expand Down
71 changes: 71 additions & 0 deletions react-native-expo/components/BalanceAndActions/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Trans } from '@lingui/react/macro';
import { Sx, Text, View } from 'dripsy';
import Button from '../Button';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { close, open } from '@/store/depositFlow';
import DepositFlow from '../DepositFlow';

/**
* Renders the user's current balance, as well as buttons for actions related to
* their balance (such as Send/Deposit/Request).
*/
export default function BalanceAndActions() {
const dispatch = useAppDispatch();
const isOpen = useAppSelector(state => state.depositFlow.isOpen);

return (
<>
<View sx={sx.root}>
<View sx={sx.balanceWrapper}>
<Text sx={sx.balanceLabel}>
<Trans>Balance</Trans>
</Text>
<Text sx={sx.balance}>0.00 USDC</Text>
</View>

<View sx={sx.buttons}>
<Button actionType='accent' onPress={() => dispatch(open())}>
<Trans>Deposit</Trans>
</Button>
<Button>
<Trans>Request</Trans>
</Button>
</View>
</View>

<DepositFlow isOpen={isOpen} onClose={() => dispatch(close())} />
</>
);
}

const sx = {
balance: {
variant: 'text.h4',
},

balanceLabel: {
variant: 'text.small',

color: 'neutralLight',
},

balanceWrapper: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
flexGrow: 1,
},

buttons: {
flexDirection: 'row',
flexGrow: 0,
gap: '$2',
px: '$4',
pb: '$4',
},

root: {
flexGrow: 1,
flexDirection: 'column',
},
} satisfies Record<string, Sx>;
63 changes: 6 additions & 57 deletions react-native-expo/components/HomeScreen/index.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,21 @@
import Button from '../Button';
import { Sx, Text, View } from 'dripsy';
import DepositFlow from '../DepositFlow';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { close, open } from '@/store/depositFlow';
import { Trans } from '@lingui/react/macro';
import { Sx, View } from 'dripsy';
import HomeScreenTransactionsList from './HomeScreenTransactionsList';
import BalanceAndActions from '../BalanceAndActions';

export default function HomeScreen() {
const dispatch = useAppDispatch();
const isOpen = useAppSelector(state => state.depositFlow.isOpen);

return (
<>
<View sx={sx.root}>
{/** @todo: Make this a `ScrollView`. */}
<View sx={sx.root}>
<View sx={sx.balanceWrapper}>
<Text sx={sx.balanceLabel}>
<Trans>Balance</Trans>
</Text>
<Text sx={sx.balance}>0.00 USDC</Text>
</View>

<View sx={sx.buttons}>
<Button actionType='accent' onPress={() => dispatch(open())}>
<Trans>Deposit</Trans>
</Button>
<Button>
<Trans>Request</Trans>
</Button>
</View>
<BalanceAndActions />

<HomeScreenTransactionsList />
</View>

<DepositFlow isOpen={isOpen} onClose={() => dispatch(close())} />
</>
<HomeScreenTransactionsList />
</View>
);
}

const sx = {
balance: {
variant: 'text.h4',
},

balanceLabel: {
variant: 'text.small',

color: 'neutralLight',
},

balanceWrapper: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
flexGrow: 1,
},

buttons: {
flexDirection: 'row',
flexGrow: 0,
gap: '$2',
px: '$4',
pb: '$4',
},

root: {
flexGrow: 1,
flexDirection: 'column',
px: 'screenHorizontalMargin',
},
} satisfies Record<string, Sx>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import ActionSheet from '@/components/ActionSheet';
import AssetIcon from '@/components/AssetIcon';
import TransactionList from '@/components/TransactionList';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setSelectedAssetSymbol } from '@/store/portfolioScreen';
import { Sx, Text, View } from 'dripsy';
import useTransactionsForAsset from './useTransactionsForAsset';

/**
* An action sheet for a given asset, with a list of relevant transactions and
* buttons for actions related to the asset (such as sending).
*/
export default function AssetActionSheet() {
const selectedAssetSymbol = useAppSelector(state => state.portfolioScreen.selectedAssetSymbol);
const balance = useAppSelector(state =>
state.balances.balances.find(balance => balance.assetSymbol === selectedAssetSymbol),
);
const dispatch = useAppDispatch();
const transactions = useTransactionsForAsset(selectedAssetSymbol);

return (
<ActionSheet
isOpen={!!selectedAssetSymbol}
onClose={() => dispatch(setSelectedAssetSymbol(undefined))}
>
<View sx={sx.header}>
<View sx={sx.assetIconWrapper}>
<AssetIcon />
</View>

<Text sx={sx.balance}>
{balance?.amount} {balance?.assetSymbol}
</Text>

<Text sx={sx.equivalentValue}>{balance?.equivalentValue} USDC</Text>
</View>

<TransactionList transactions={transactions} showTitle />
</ActionSheet>
);
}

const sx = {
assetIconWrapper: {
pb: '$2',
},

balance: {
variant: 'text.h4',

textAlign: 'center',
},

equivalentValue: {
variant: 'text.small',

color: 'neutralLight',
textAlign: 'center',
},

header: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
flexGrow: 1,
gap: '$1',
mb: '$4',
},
} satisfies Record<string, Sx>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useAppSelector } from '@/store/hooks';
import { useMemo } from 'react';

/** Returns a memoized array of the transactions for a given asset. */
export default function useTransactionsForAsset(
/** The symbol of the asset that transactions should be filtered by. */
assetSymbol?: string,
) {
const transactions = useAppSelector(state => state.transactions.transactions);

const transactionsForAsset = useMemo(
() =>
assetSymbol
? transactions.filter(transaction => transaction.assetSymbol === assetSymbol)
: [],
[transactions, assetSymbol],
);

return transactionsForAsset;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Meta, StoryObj } from '@storybook/react';

import Balance from '.';
import balanceFactory from '@/factories/balance';

const meta: Meta<typeof Balance> = {
component: Balance,
tags: ['autodocs'],
argTypes: {
balance: { control: false },
},
};

export default meta;

export const Basic: StoryObj<typeof Balance> = {
args: {
balance: balanceFactory.build(),
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import AssetIcon from '@/components/AssetIcon';
import ListItem from '@/components/ListItem';
import { useAppDispatch } from '@/store/hooks';
import { setSelectedAssetSymbol } from '@/store/portfolioScreen';
import IBalance from '@/types/Balance';
import { Sx, Text, View } from 'dripsy';

export interface BalanceProps {
balance: IBalance;
}

export default function Balance({ balance }: BalanceProps) {
const dispatch = useAppDispatch();

return (
<ListItem
avatar={<AssetIcon />}
primaryText={balance.assetSymbol}
secondaryText={balance.assetName}
suffix={
<View sx={sx.suffix}>
<Text variant='small'>{balance.amount}</Text>
<Text sx={sx.equivalentValue}>{balance.equivalentValue} USDC</Text>
</View>
}
onPress={() => dispatch(setSelectedAssetSymbol(balance.assetSymbol))}
/>
);
}

const sx = {
equivalentValue: {
variant: 'text.detail',
color: 'neutralLight',
},

suffix: {
flexDirection: 'column',
alignItems: 'flex-end',
},
} satisfies Record<string, Sx>;
26 changes: 26 additions & 0 deletions react-native-expo/components/PortfolioScreen/BalanceList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import List from '@/components/List';
import IBalance from '@/types/Balance';
import { useLingui } from '@lingui/react/macro';
import Balance from './Balance';
import AssetActionSheet from './AssetActionSheet';

export interface BalanceListProps {
balances: IBalance[];
}

/** Shows a list of the user's balances in every asset they hold. */
export default function BalanceList({ balances }: BalanceListProps) {
const { t } = useLingui();

return (
<>
<List title={t`Assets`}>
{balances.map(balance => (
<Balance key={balance.assetSymbol} balance={balance} />
))}
</List>

<AssetActionSheet />
</>
);
}
23 changes: 21 additions & 2 deletions react-native-expo/components/PortfolioScreen/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
import { Text } from 'dripsy';
import { Sx, View } from 'dripsy';
import BalanceAndActions from '../BalanceAndActions';
import BalanceList from './BalanceList';
import { useAppSelector } from '@/store/hooks';

export default function PortfolioScreen() {
return <Text>PortfolioScreen</Text>;
const balances = useAppSelector(state => state.balances.balances);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: general thoughts on identifying areas that could take advantage of memoized selectors, using reselect, to avoid unnecessary re-renders?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — I've actually started doing that in #19


return (
<View sx={sx.root}>
{/** @todo: Make this a `ScrollView`. */}
<BalanceAndActions />

<BalanceList balances={balances} />
</View>
);
}

const sx = {
root: {
flexGrow: 1,
px: 'screenHorizontalMargin',
},
} satisfies Record<string, Sx>;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import Header from '@/components/Header';
import { Image, Sx } from 'dripsy';
import logo from './logo.png';

export default function HomeHeader() {
/** The header used by the primary screens accessed via tabs. */
export default function TabScreenHeader() {
return <Header left={<Avatar />} center={<Image sx={sx.logo} source={logo} />} />;
}

Expand Down
Loading