Skip to content

Commit

Permalink
Create initial portfolio screen (#15)
Browse files Browse the repository at this point in the history
* Rename header

* Extract BalanceAndActions component; use it in PortfolioScreen

* Create store/type/factory for balances

* Create balance UI in portfolio screen

* Use more precise naming

* Tweak factories

* Wrap Storybook stories in <ReduxProvider />

* Fix ATOM mock data

* Delete BalanceList stories for now

* Build a simple AssetActionSheet

* Add comment
  • Loading branch information
jessepinho authored Jan 6, 2025
1 parent 35f28b0 commit 3f1bc64
Show file tree
Hide file tree
Showing 20 changed files with 396 additions and 68 deletions.
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>
<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;
}
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);

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

0 comments on commit 3f1bc64

Please sign in to comment.