diff --git a/react-native-expo/.storybook/preview.tsx b/react-native-expo/.storybook/preview.tsx
index 1c1c1a4..f935a8d 100644
--- a/react-native-expo/.storybook/preview.tsx
+++ b/react-native-expo/.storybook/preview.tsx
@@ -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 `` in the root decorator to provide fonts
* to Storybook. But that caused weird import issues. So for now, we'll just add
@@ -13,11 +14,13 @@ import './fonts.css';
const preview: Preview = {
decorators: [
Story => (
-
-
-
-
-
+
+
+
+
+
+
+
),
],
parameters: {
diff --git a/react-native-expo/components/BalanceAndActions/index.tsx b/react-native-expo/components/BalanceAndActions/index.tsx
new file mode 100644
index 0000000..ff83856
--- /dev/null
+++ b/react-native-expo/components/BalanceAndActions/index.tsx
@@ -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 (
+ <>
+
+
+
+ Balance
+
+ 0.00 USDC
+
+
+
+
+
+
+
+
+ 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;
diff --git a/react-native-expo/components/HomeScreen/index.tsx b/react-native-expo/components/HomeScreen/index.tsx
index 29d1fb1..f041f4d 100644
--- a/react-native-expo/components/HomeScreen/index.tsx
+++ b/react-native-expo/components/HomeScreen/index.tsx
@@ -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 (
- <>
+
{/** @todo: Make this a `ScrollView`. */}
-
-
-
- Balance
-
- 0.00 USDC
-
-
-
-
-
-
+
-
-
-
- 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',
px: 'screenHorizontalMargin',
},
} satisfies Record;
diff --git a/react-native-expo/components/PortfolioScreen/BalanceList/AssetActionSheet/index.tsx b/react-native-expo/components/PortfolioScreen/BalanceList/AssetActionSheet/index.tsx
new file mode 100644
index 0000000..5b84ba9
--- /dev/null
+++ b/react-native-expo/components/PortfolioScreen/BalanceList/AssetActionSheet/index.tsx
@@ -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 (
+ dispatch(setSelectedAssetSymbol(undefined))}
+ >
+
+
+
+
+
+
+ {balance?.amount} {balance?.assetSymbol}
+
+
+ {balance?.equivalentValue} USDC
+
+
+
+
+ );
+}
+
+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;
diff --git a/react-native-expo/components/PortfolioScreen/BalanceList/AssetActionSheet/useTransactionsForAsset.ts b/react-native-expo/components/PortfolioScreen/BalanceList/AssetActionSheet/useTransactionsForAsset.ts
new file mode 100644
index 0000000..a46e1f2
--- /dev/null
+++ b/react-native-expo/components/PortfolioScreen/BalanceList/AssetActionSheet/useTransactionsForAsset.ts
@@ -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;
+}
diff --git a/react-native-expo/components/PortfolioScreen/BalanceList/Balance/index.stories.ts b/react-native-expo/components/PortfolioScreen/BalanceList/Balance/index.stories.ts
new file mode 100644
index 0000000..4fb39db
--- /dev/null
+++ b/react-native-expo/components/PortfolioScreen/BalanceList/Balance/index.stories.ts
@@ -0,0 +1,20 @@
+import { Meta, StoryObj } from '@storybook/react';
+
+import Balance from '.';
+import balanceFactory from '@/factories/balance';
+
+const meta: Meta = {
+ component: Balance,
+ tags: ['autodocs'],
+ argTypes: {
+ balance: { control: false },
+ },
+};
+
+export default meta;
+
+export const Basic: StoryObj = {
+ args: {
+ balance: balanceFactory.build(),
+ },
+};
diff --git a/react-native-expo/components/PortfolioScreen/BalanceList/Balance/index.tsx b/react-native-expo/components/PortfolioScreen/BalanceList/Balance/index.tsx
new file mode 100644
index 0000000..7739643
--- /dev/null
+++ b/react-native-expo/components/PortfolioScreen/BalanceList/Balance/index.tsx
@@ -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 (
+ }
+ primaryText={balance.assetSymbol}
+ secondaryText={balance.assetName}
+ suffix={
+
+ {balance.amount}
+ {balance.equivalentValue} USDC
+
+ }
+ onPress={() => dispatch(setSelectedAssetSymbol(balance.assetSymbol))}
+ />
+ );
+}
+
+const sx = {
+ equivalentValue: {
+ variant: 'text.detail',
+ color: 'neutralLight',
+ },
+
+ suffix: {
+ flexDirection: 'column',
+ alignItems: 'flex-end',
+ },
+} satisfies Record;
diff --git a/react-native-expo/components/PortfolioScreen/BalanceList/index.tsx b/react-native-expo/components/PortfolioScreen/BalanceList/index.tsx
new file mode 100644
index 0000000..fc06416
--- /dev/null
+++ b/react-native-expo/components/PortfolioScreen/BalanceList/index.tsx
@@ -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 (
+ <>
+
+ {balances.map(balance => (
+
+ ))}
+
+
+
+ >
+ );
+}
diff --git a/react-native-expo/components/PortfolioScreen/index.tsx b/react-native-expo/components/PortfolioScreen/index.tsx
index 98c10d2..eedd2aa 100644
--- a/react-native-expo/components/PortfolioScreen/index.tsx
+++ b/react-native-expo/components/PortfolioScreen/index.tsx
@@ -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 PortfolioScreen;
+ const balances = useAppSelector(state => state.balances.balances);
+
+ return (
+
+ {/** @todo: Make this a `ScrollView`. */}
+
+
+
+
+ );
}
+
+const sx = {
+ root: {
+ flexGrow: 1,
+ px: 'screenHorizontalMargin',
+ },
+} satisfies Record;
diff --git a/react-native-expo/components/HomeScreen/HomeHeader/index.tsx b/react-native-expo/components/TabScreenHeader/index.tsx
similarity index 76%
rename from react-native-expo/components/HomeScreen/HomeHeader/index.tsx
rename to react-native-expo/components/TabScreenHeader/index.tsx
index f45b659..9ea0e3d 100644
--- a/react-native-expo/components/HomeScreen/HomeHeader/index.tsx
+++ b/react-native-expo/components/TabScreenHeader/index.tsx
@@ -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 } center={} />;
}
diff --git a/react-native-expo/components/HomeScreen/HomeHeader/logo.png b/react-native-expo/components/TabScreenHeader/logo.png
similarity index 100%
rename from react-native-expo/components/HomeScreen/HomeHeader/logo.png
rename to react-native-expo/components/TabScreenHeader/logo.png
diff --git a/react-native-expo/components/TabsLayout/index.tsx b/react-native-expo/components/TabsLayout/index.tsx
index 4f38f14..fd776a9 100644
--- a/react-native-expo/components/TabsLayout/index.tsx
+++ b/react-native-expo/components/TabsLayout/index.tsx
@@ -6,7 +6,7 @@ import Icon from '../Icon';
import { ParamListBase, RouteProp } from '@react-navigation/native';
import { Text } from 'dripsy';
import { useLingui } from '@lingui/react/macro';
-import HomeHeader from '../HomeScreen/HomeHeader';
+import TabScreenHeader from '../TabScreenHeader';
const ICONS_BY_ROUTE: Record = {
index: Home,
@@ -57,11 +57,11 @@ export default function TabsLayout() {
return (
// @ts-expect-error - Types are wrong for `title`
- }} />
+ }} />
-
+ }} />
);
}
diff --git a/react-native-expo/factories/balance.ts b/react-native-expo/factories/balance.ts
new file mode 100644
index 0000000..8f25b4d
--- /dev/null
+++ b/react-native-expo/factories/balance.ts
@@ -0,0 +1,14 @@
+import Balance from '@/types/Balance';
+import * as Factory from 'factory.ts';
+import ASSETS from './mockData/assets';
+
+const randomDecimalWith6DecimalPlaces = () => Math.round(Math.random() * 10 ** 9) / 10 ** 3;
+
+const balanceFactory = Factory.makeFactory({
+ amount: Factory.each(randomDecimalWith6DecimalPlaces),
+ equivalentValue: Factory.each(randomDecimalWith6DecimalPlaces),
+ assetSymbol: Factory.each(i => ASSETS[i % ASSETS.length].symbol),
+ assetName: Factory.each(i => ASSETS[i % ASSETS.length].name),
+});
+
+export default balanceFactory;
diff --git a/react-native-expo/factories/mockData/assets.ts b/react-native-expo/factories/mockData/assets.ts
new file mode 100644
index 0000000..74f472e
--- /dev/null
+++ b/react-native-expo/factories/mockData/assets.ts
@@ -0,0 +1,24 @@
+const ASSETS = [
+ {
+ name: 'Ethereum',
+ symbol: 'ETH',
+ },
+ {
+ name: 'Penumbra',
+ symbol: 'UM',
+ },
+ {
+ name: 'Osmosis',
+ symbol: 'OSMO',
+ },
+ {
+ name: 'Cosmos',
+ symbol: 'ATOM',
+ },
+ {
+ name: 'USDC',
+ symbol: 'USD Coin',
+ },
+];
+
+export default ASSETS;
diff --git a/react-native-expo/factories/transaction.ts b/react-native-expo/factories/transaction.ts
index 7ce31df..5305a09 100644
--- a/react-native-expo/factories/transaction.ts
+++ b/react-native-expo/factories/transaction.ts
@@ -1,9 +1,12 @@
import Transaction from '@/types/Transaction';
import * as Factory from 'factory.ts';
+import ASSETS from './mockData/assets';
const transactionFactory = Factory.makeFactory({
id: Factory.each(i => `id-${i}`),
type: 'receive',
+ assetSymbol: Factory.each(i => ASSETS[i % ASSETS.length].symbol),
+ assetName: Factory.each(i => ASSETS[i % ASSETS.length].name),
memo: Factory.each(i => `Memo for transaction ${i}`),
via: 'link',
});
diff --git a/react-native-expo/store/balances.ts b/react-native-expo/store/balances.ts
new file mode 100644
index 0000000..81e2358
--- /dev/null
+++ b/react-native-expo/store/balances.ts
@@ -0,0 +1,22 @@
+import balanceFactory from '@/factories/balance';
+import Balance from '@/types/Balance';
+import { createSlice } from '@reduxjs/toolkit';
+
+export interface BalancesState {
+ balances: Balance[];
+}
+
+const initialState: BalancesState = {
+ /** @todo: Populate with real data */
+ balances: balanceFactory.buildList(5),
+};
+
+export const balancesSlice = createSlice({
+ name: 'balances',
+ initialState,
+ reducers: {},
+});
+
+export const {} = balancesSlice.actions;
+
+export default balancesSlice.reducer;
diff --git a/react-native-expo/store/index.ts b/react-native-expo/store/index.ts
index f576e0b..441387c 100644
--- a/react-native-expo/store/index.ts
+++ b/react-native-expo/store/index.ts
@@ -1,11 +1,15 @@
import { configureStore } from '@reduxjs/toolkit';
+import balances from './balances';
import depositFlow from './depositFlow';
+import portfolioScreen from './portfolioScreen';
import transactions from './transactions';
const store = configureStore({
reducer: {
+ balances,
depositFlow,
+ portfolioScreen,
transactions,
},
});
diff --git a/react-native-expo/store/portfolioScreen.ts b/react-native-expo/store/portfolioScreen.ts
new file mode 100644
index 0000000..8bdcb21
--- /dev/null
+++ b/react-native-expo/store/portfolioScreen.ts
@@ -0,0 +1,26 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+/** State specific to `PortfolioScreen`. */
+export interface PortfolioScreenState {
+ /**
+ * The symbol of the currently selected asset (for viewing tarnsactions,
+ * sending, etc.).
+ */
+ selectedAssetSymbol?: string;
+}
+
+const initialState: PortfolioScreenState = {};
+
+export const portfolioScreenSlice = createSlice({
+ name: 'portfolioScreen',
+ initialState,
+ reducers: {
+ setSelectedAssetSymbol(state, action: PayloadAction) {
+ state.selectedAssetSymbol = action.payload;
+ },
+ },
+});
+
+export const { setSelectedAssetSymbol } = portfolioScreenSlice.actions;
+
+export default portfolioScreenSlice.reducer;
diff --git a/react-native-expo/types/Balance.ts b/react-native-expo/types/Balance.ts
new file mode 100644
index 0000000..46db6fc
--- /dev/null
+++ b/react-native-expo/types/Balance.ts
@@ -0,0 +1,12 @@
+/** A user's holding of a given asset. */
+export default interface Balance {
+ assetSymbol: string;
+ assetName: string;
+ /** The amount represented as a decimal number of the display denom. */
+ amount: number;
+ /**
+ * The equivalent value represented as a decimal number of the display denom
+ * of the asset selected by the user as their default asset.
+ */
+ equivalentValue: number;
+}
diff --git a/react-native-expo/types/Transaction.ts b/react-native-expo/types/Transaction.ts
index d8ba73e..109e8b2 100644
--- a/react-native-expo/types/Transaction.ts
+++ b/react-native-expo/types/Transaction.ts
@@ -5,6 +5,10 @@ interface TransactionBase {
memo?: string;
/** @todo: add more options as they become available */
via?: 'link';
+ /** The symbol of the asset in this transaction. */
+ assetSymbol: string;
+ /** The name of the asset in this transaction. */
+ assetName: string;
}
interface ReceiveTransaction extends TransactionBase {