diff --git a/app/src/app/(modal)/_layout.tsx b/app/src/app/(modal)/_layout.tsx index 177c3b669..978fcba4c 100644 --- a/app/src/app/(modal)/_layout.tsx +++ b/app/src/app/(modal)/_layout.tsx @@ -1,16 +1,8 @@ -import { Slot, Stack } from 'expo-router'; +import { Slot } from 'expo-router'; export default function SheetLayout() { return ( <> - ); diff --git a/app/src/app/(nav)/[account]/(home)/_layout.tsx b/app/src/app/(nav)/[account]/(home)/_layout.tsx index 9f506e09a..574966919 100644 --- a/app/src/app/(nav)/[account]/(home)/_layout.tsx +++ b/app/src/app/(nav)/[account]/(home)/_layout.tsx @@ -1,11 +1,10 @@ import { Panes } from '#/layout/Panes'; -import { Slot, Stack } from 'expo-router'; +import { Slot } from 'expo-router'; import { HomePane } from './index'; export default function HomeLayout() { return ( - diff --git a/app/src/app/(nav)/[account]/(home)/activity/_layout.tsx b/app/src/app/(nav)/[account]/(home)/activity/_layout.tsx index 11921efcf..900313a0d 100644 --- a/app/src/app/(nav)/[account]/(home)/activity/_layout.tsx +++ b/app/src/app/(nav)/[account]/(home)/activity/_layout.tsx @@ -1,4 +1,3 @@ -import { Panes } from '#/layout/Panes'; import { Slot } from 'expo-router'; import { ActivityPane } from './index'; diff --git a/app/src/app/(nav)/[account]/(home)/index.tsx b/app/src/app/(nav)/[account]/(home)/index.tsx index 84c145a8f..b14e6740b 100644 --- a/app/src/app/(nav)/[account]/(home)/index.tsx +++ b/app/src/app/(nav)/[account]/(home)/index.tsx @@ -63,17 +63,17 @@ function HomePane_() { return ( + } + style={styles.appbar} + noPadding + /> - } - style={styles.appbar} - noPadding - /> diff --git a/app/src/app/(nav)/[account]/(home)/send.tsx b/app/src/app/(nav)/[account]/(home)/send.tsx new file mode 100644 index 000000000..820cffce7 --- /dev/null +++ b/app/src/app/(nav)/[account]/(home)/send.tsx @@ -0,0 +1,135 @@ +import { PaneSkeleton } from '#/skeleton/PaneSkeleton'; +import { withSuspense } from '#/skeleton/withSuspense'; +import { zUAddress } from '~/lib/zod'; +import { AccountParams } from '../_layout'; +import { useLocalParams } from '~/hooks/useLocalParams'; +import { graphql } from 'relay-runtime'; +import { useState } from 'react'; +import Decimal from 'decimal.js'; +import { z } from 'zod'; +import { Pane } from '#/layout/Pane'; +import { TokenAmountInput } from '#/token/TokenAmountInput'; +import { useLazyQuery } from '~/api'; +import { send_SendScreen2Query } from '~/api/__generated__/send_SendScreen2Query.graphql'; +import { useSelectedToken } from '~/hooks/useSelectToken'; +import { asChain } from 'lib'; +import { Scrollable } from '#/Scrollable'; +import { Appbar } from '#/Appbar/Appbar'; +import { View } from 'react-native'; +import { createStyles, useStyles } from '@theme/styles'; +import { SendMode, SendModeChips } from '#/send/SendModeChips'; +import { ItemList } from '#/layout/ItemList'; +import { SendAccount } from '#/send/SendAccount'; +import { SendTo } from '#/send/SendTo'; +import { match } from 'ts-pattern'; +import { TransferMode } from '#/send/TransferMode'; +import { TransferFromMode } from '#/send/TransferFromMode'; +import { Text } from 'react-native-paper'; + +const Query = graphql` + query send_SendScreen2Query($account: UAddress!, $token: UAddress!) { + token(address: $token) @required(action: THROW) { + id + address + decimals + balance(input: { account: $account }) + price { + usd + } + ...TokenAmountInput_token + ...SendAccount_token @arguments(account: $account) + ...TransferMode_token + ...TransferFromMode_token + } + + account(address: $account) @required(action: THROW) { + address + ...useProposeTransaction_account + ...SendAccount_account + ...TransferMode_account + ...TransferFromMode_account + } + } +`; + +export const SendScreenParams = AccountParams.extend({ + to: zUAddress().optional(), +}); +export type SendScreenParams = z.infer; + +function SendScreen() { + const params = useLocalParams(SendScreenParams); + const { styles } = useStyles(stylesheet); + const chain = asChain(params.account); + + const { token, account } = useLazyQuery(Query, { + account: params.account, + token: useSelectedToken(chain), + }); + + const [amount, setAmount] = useState(new Decimal(0)); + const [mode, setMode] = useState('transfer'); + const [to, setTo] = useState(params.to); + + const warning = amount.gt(token.balance) && 'Insufficient balance'; + + return ( + + + + + + + {warning} + + + + + + + + + + + + + {match(mode) + .with('transfer', () => ( + + )) + .with('transferFrom', () => ( + + )) + .exhaustive()} + + + ); +} + +const stylesheet = createStyles(({ colors, padding, negativeMargin }) => ({ + container: { + gap: 8, + paddingHorizontal: padding, + }, + inputContainer: { + marginVertical: 16, + }, + sendModeChipsContainer: { + marginHorizontal: negativeMargin, + }, + item: { + backgroundColor: colors.surface, + }, + warning: { + color: colors.warning, + }, +})); + +export default withSuspense(SendScreen, ); + +export { ErrorBoundary } from '#/ErrorBoundary'; diff --git a/app/src/app/(nav)/[account]/_layout.tsx b/app/src/app/(nav)/[account]/_layout.tsx index 039388dcf..cc8e0a18a 100644 --- a/app/src/app/(nav)/[account]/_layout.tsx +++ b/app/src/app/(nav)/[account]/_layout.tsx @@ -1,5 +1,5 @@ import { useEffect, useLayoutEffect } from 'react'; -import { Redirect, Stack, useLocalSearchParams, useRouter } from 'expo-router'; +import { Redirect, Slot, useLocalSearchParams, useRouter } from 'expo-router'; import { ErrorBoundary as BaseErrorBoundary, ErrorBoundaryProps, @@ -9,7 +9,6 @@ import NotFound from '~/app/+not-found'; import { useLocalParams } from '~/hooks/useLocalParams'; import { useSelectedAccount, useSetSelectedAccont } from '~/hooks/useSelectedAccount'; import { zUAddress } from '~/lib/zod'; -import { AppbarHeader } from '#/Appbar/AppbarHeader'; import { withSuspense } from '#/skeleton/withSuspense'; import { Splash } from '#/Splash'; import { graphql } from 'relay-runtime'; @@ -52,12 +51,7 @@ export function AccountLayout() { // Redirect to the home page if account isn't found if (!found) return ; - return ( - <> - - }} /> - - ); + return ; } export default withSuspense(AccountLayout, ); diff --git a/app/src/app/(nav)/[account]/send.tsx b/app/src/app/(nav)/[account]/send.tsx deleted file mode 100644 index fc3bc0c05..000000000 --- a/app/src/app/(nav)/[account]/send.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { useRouter } from 'expo-router'; -import { useProposeTransaction } from '~/hooks/mutations/useProposeTransaction'; -import { FIAT_DECIMALS, asAddress, asChain, asFp, asUAddress } from 'lib'; -import { useEffect, useState } from 'react'; -import { StyleSheet, View } from 'react-native'; -import { Divider } from 'react-native-paper'; -import { useAddressLabel } from '#/address/AddressLabel'; -import { NumericInput } from '#/fields/NumericInput'; -import { TokenItem } from '#/token/TokenItem'; -import { InputsView, InputType } from '../../../components/InputsView'; -import { Button } from '#/Button'; -import { useInvalidateRecentToken, useSelectToken, useSelectedToken } from '~/hooks/useSelectToken'; -import { createTransferOp } from '~/lib/transfer'; -import { AppbarOptions } from '#/Appbar/AppbarOptions'; -import { z } from 'zod'; -import { zAddress, zUAddress } from '~/lib/zod'; -import { useLocalParams } from '~/hooks/useLocalParams'; -import { Actions } from '#/layout/Actions'; -import { withSuspense } from '#/skeleton/withSuspense'; -import { ScreenSkeleton } from '#/skeleton/ScreenSkeleton'; -import { ScrollableScreenSurface } from '#/layout/ScrollableScreenSurface'; -import Decimal from 'decimal.js'; -import { ampli } from '~/lib/ampli'; -import { graphql } from 'relay-runtime'; -import { useLazyQuery } from '~/api'; -import { send_SendScreenQuery } from '~/api/__generated__/send_SendScreenQuery.graphql'; - -const Query = graphql` - query send_SendScreenQuery($account: UAddress!, $token: UAddress!) { - token(address: $token) { - id - address - decimals - balance(input: { account: $account }) - price { - id - usd - } - ...InputsView_token @arguments(account: $account) - ...TokenItem_token - } - - account(address: $account) @required(action: THROW) { - ...useProposeTransaction_account - } - } -`; - -const SendScreenParams = z.object({ - account: zUAddress(), - to: zAddress(), -}); -export type SendScreenParams = z.infer; - -function SendScreen() { - const { account: accountAddress, to } = useLocalParams(SendScreenParams); - const chain = asChain(accountAddress); - const router = useRouter(); - const propose = useProposeTransaction(); - const toLabel = useAddressLabel(asUAddress(to, chain)); - const invalidateRecent = useInvalidateRecentToken(chain); - const selectToken = useSelectToken(); - const selectedToken = useSelectedToken(chain); - - const { token, account } = useLazyQuery(Query, { - account: accountAddress, - token: selectedToken, - }); - - const [input, setInput] = useState(''); - const [type, setType] = useState(InputType.Token); - - useEffect(() => { - if (!token) invalidateRecent(selectedToken); - }, [chain, invalidateRecent, selectedToken, token]); - - if (!token) return null; - - const inputAmount = input || '0'; - const amount = - type === InputType.Token - ? new Decimal(inputAmount) - : new Decimal(inputAmount).div(token.price?.usd ?? 0); - - return ( - <> - - - - - - - - selectToken({ account: accountAddress })} - /> - - - - - - - - - - ); -} - -const styles = StyleSheet.create({ - spacer: { - flex: 1, - }, - action: { - marginHorizontal: 16, - marginBottom: 16, - alignSelf: 'stretch', - }, -}); - -export default withSuspense(SendScreen, ScreenSkeleton); - -export { ErrorBoundary } from '#/ErrorBoundary'; diff --git a/app/src/app/(nav)/[account]/settings/_layout.tsx b/app/src/app/(nav)/[account]/settings/_layout.tsx index 1da3890c3..d44ab9cd9 100644 --- a/app/src/app/(nav)/[account]/settings/_layout.tsx +++ b/app/src/app/(nav)/[account]/settings/_layout.tsx @@ -1,16 +1,13 @@ import { Panes } from '#/layout/Panes'; -import { Slot, Stack } from 'expo-router'; +import { Slot } from 'expo-router'; import _ from 'lodash'; import { AccountSettingsPane } from './index'; export default function AccountSettingsLayout() { return ( - <> - - - - - - + + + + ); } diff --git a/app/src/app/(nav)/[account]/settings/details.tsx b/app/src/app/(nav)/[account]/settings/details.tsx index e3113f482..2d1b8654b 100644 --- a/app/src/app/(nav)/[account]/settings/details.tsx +++ b/app/src/app/(nav)/[account]/settings/details.tsx @@ -4,7 +4,6 @@ import { AccountSettingsParams } from './index'; import { useForm } from 'react-hook-form'; import { createStyles } from '@theme/styles'; import { Appbar } from '#/Appbar/Appbar'; -import { Surface } from '#/layout/Surface'; import { View } from 'react-native'; import { AccountNameFormField } from '#/fields/AccountNameFormField'; import { Actions } from '#/layout/Actions'; diff --git a/app/src/app/(nav)/[account]/swap.tsx b/app/src/app/(nav)/[account]/swap.tsx index 1805aee9c..7509ea96a 100644 --- a/app/src/app/(nav)/[account]/swap.tsx +++ b/app/src/app/(nav)/[account]/swap.tsx @@ -8,7 +8,6 @@ import { View } from 'react-native'; import { NumericInput } from '#/fields/NumericInput'; import { DateTime } from 'luxon'; import { Button } from '#/Button'; -import { AppbarOptions } from '#/Appbar/AppbarOptions'; import { useLocalParams } from '~/hooks/useLocalParams'; import { withSuspense } from '#/skeleton/withSuspense'; import { ScrollableScreenSurface } from '#/layout/ScrollableScreenSurface'; @@ -31,6 +30,7 @@ import { ScreenSkeleton } from '#/skeleton/ScreenSkeleton'; import { graphql } from 'relay-runtime'; import { useLazyQuery } from '~/api'; import { swap_SwapScreenQuery } from '~/api/__generated__/swap_SwapScreenQuery.graphql'; +import { Appbar } from '#/Appbar/Appbar'; const DownArrow = materialCommunityIcon('arrow-down-thin'); const ICON_BUTTON_SIZE = 24; @@ -112,7 +112,7 @@ function SwapScreen() { return ( <> - + diff --git a/app/src/app/(nav)/[account]/tokens.tsx b/app/src/app/(nav)/[account]/tokens.tsx index b1a5e3510..781af6de8 100644 --- a/app/src/app/(nav)/[account]/tokens.tsx +++ b/app/src/app/(nav)/[account]/tokens.tsx @@ -10,12 +10,12 @@ import { zUAddress } from '~/lib/zod'; import { useLocalParams } from '~/hooks/useLocalParams'; import { withSuspense } from '#/skeleton/withSuspense'; import { ScreenSkeleton } from '#/skeleton/ScreenSkeleton'; -import { SearchbarOptions } from '#/Appbar/SearchbarOptions'; import { ScreenSurface } from '#/layout/ScreenSurface'; import { MenuOrSearchIcon } from '#/Appbar/MenuOrSearchIcon'; import { graphql } from 'relay-runtime'; import { tokens_TokensScreenQuery } from '~/api/__generated__/tokens_TokensScreenQuery.graphql'; import { useLazyQuery } from '~/api'; +import { Searchbar } from '#/Appbar/Searchbar'; const Query = graphql` query tokens_TokensScreenQuery($account: UAddress!, $chain: Chain, $query: String) { @@ -44,7 +44,7 @@ function TokensScreen() { return ( <> - router.push(`/token/add`)} />} diff --git a/app/src/app/(nav)/_layout.tsx b/app/src/app/(nav)/_layout.tsx index 672e485f4..46b410efb 100644 --- a/app/src/app/(nav)/_layout.tsx +++ b/app/src/app/(nav)/_layout.tsx @@ -17,16 +17,15 @@ import { import { DrawerItem as Item } from '#/drawer/DrawerItem'; import { useSelectedAccount } from '~/hooks/useSelectedAccount'; import { CONFIG } from '~/util/config'; -import { useSend } from '~/hooks/useSend'; import { DrawerSurface } from '#/drawer/DrawerSurface'; -import { Link, Stack } from 'expo-router'; -import { AppbarHeader } from '#/Appbar/AppbarHeader'; +import { Link, Slot, useRouter } from 'expo-router'; import { DrawerLogo } from '#/drawer/DrawerLogo'; import { createStyles, useStyles } from '@theme/styles'; import { PressableOpacity } from '#/PressableOpacity'; import { Fab } from '#/Fab'; import { RailSurface } from '#/drawer/RailSurface'; import { RailItem } from '#/drawer/RailItem'; +import { SendScreenParams } from './[account]/(home)/send'; const Section = PaperDrawer.Section; @@ -37,7 +36,7 @@ export const unstable_settings = { export default function DrawerLayout() { return ( - }} /> + ); } @@ -45,7 +44,7 @@ export default function DrawerLayout() { function RailContent() { const { styles } = useStyles(railStylesheet); const account = useSelectedAccount(); - const send = useSend(); + const router = useRouter(); return ( } style={styles.fabContainer} - loading={false} - onPress={() => send({ account })} animated={false} + onPress={() => + router.push({ + pathname: `/(nav)/[account]/send`, + params: { account } satisfies SendScreenParams, + }) + } /> ) } @@ -111,7 +114,6 @@ const railStylesheet = createStyles(({ colors }) => ({ function DrawerContent() { const account = useSelectedAccount(); - const send = useSend(); return ( @@ -137,7 +139,6 @@ function DrawerContent() { href={{ pathname: `/(nav)/[account]/send`, params: { account } }} icon={OutboundIcon} label="Send" - onPress={() => send({ account })} /> )} diff --git a/app/src/app/(nav)/accounts/create.tsx b/app/src/app/(nav)/accounts/create.tsx index a0f4a9b2a..a8e46b39c 100644 --- a/app/src/app/(nav)/accounts/create.tsx +++ b/app/src/app/(nav)/accounts/create.tsx @@ -1,4 +1,4 @@ -import { AppbarOptions } from '#/Appbar/AppbarOptions'; +import { Appbar } from '#/Appbar/Appbar'; import { withSuspense } from '#/skeleton/withSuspense'; import { ScreenSkeleton } from '#/skeleton/ScreenSkeleton'; import { ScrollableScreenSurface } from '#/layout/ScrollableScreenSurface'; @@ -7,7 +7,7 @@ import { CreateAccount } from '#/CreateAccount'; function CreateAccountScreen() { return ( <> - + diff --git a/app/src/app/(nav)/contacts/_layout.tsx b/app/src/app/(nav)/contacts/_layout.tsx index 81ef56872..d41ea6a6f 100644 --- a/app/src/app/(nav)/contacts/_layout.tsx +++ b/app/src/app/(nav)/contacts/_layout.tsx @@ -1,19 +1,16 @@ import { Panes } from '#/layout/Panes'; -import { Slot, Stack } from 'expo-router'; +import { Slot } from 'expo-router'; import { ContactsPane } from './index'; import { Pane } from '#/layout/Pane'; export default function ContactsLayout() { return ( - <> - - - - - + + + + - - - + + ); } diff --git a/app/src/app/(nav)/ledger/link.tsx b/app/src/app/(nav)/ledger/link.tsx index d4039b584..a7b2d0663 100644 --- a/app/src/app/(nav)/ledger/link.tsx +++ b/app/src/app/(nav)/ledger/link.tsx @@ -6,7 +6,7 @@ import { Actions } from '#/layout/Actions'; import { Button } from '#/Button'; import { match } from 'ts-pattern'; import { LedgerItem } from '#/link/ledger/LedgerItem'; -import { AppbarOptions } from '#/Appbar/AppbarOptions'; +import { Appbar } from '#/Appbar/Appbar'; import { useObservable } from '~/hooks/useObservable'; import { bleDevices } from '~/lib/ble/manager'; import { ok } from 'neverthrow'; @@ -36,7 +36,7 @@ function LinkLedgerScreen() { return ( <> - + diff --git a/app/src/app/(nav)/message/[id].tsx b/app/src/app/(nav)/message/[id].tsx index 5bc8674de..deb9e2e45 100644 --- a/app/src/app/(nav)/message/[id].tsx +++ b/app/src/app/(nav)/message/[id].tsx @@ -3,7 +3,7 @@ import { useLocalParams } from '~/hooks/useLocalParams'; import { zUuid } from '~/lib/zod'; import { AppbarMore } from '#/Appbar/AppbarMore'; import { Divider, Menu } from 'react-native-paper'; -import { AppbarOptions } from '#/Appbar/AppbarOptions'; +import { Appbar } from '#/Appbar/Appbar'; import { ScrollableScreenSurface } from '#/layout/ScrollableScreenSurface'; import { MessageStatus } from '#/message/MessageStatus'; import { StyleSheet, View } from 'react-native'; @@ -58,7 +58,7 @@ export default function MessageScreen() { return ( - } mode="large" {...(remove && { diff --git a/app/src/app/(nav)/sessions/index.tsx b/app/src/app/(nav)/sessions/index.tsx index fb82c6aa6..83941e832 100644 --- a/app/src/app/(nav)/sessions/index.tsx +++ b/app/src/app/(nav)/sessions/index.tsx @@ -4,7 +4,7 @@ import { StyleSheet, FlatList } from 'react-native'; import { useWalletConnect } from '~/lib/wc'; import { PairingItem } from '#/walletconnect/PairingItem'; import { Divider, Text } from 'react-native-paper'; -import { AppbarOptions } from '#/Appbar/AppbarOptions'; +import { Appbar } from '#/Appbar/Appbar'; import { withSuspense } from '#/skeleton/withSuspense'; import { ScreenSkeleton } from '#/skeleton/ScreenSkeleton'; import { ScreenSurface } from '#/layout/ScreenSurface'; @@ -24,7 +24,7 @@ function SessionsScreen() { return ( <> - + - + diff --git a/app/src/app/(nav)/settings/notifications.tsx b/app/src/app/(nav)/settings/notifications.tsx index ad22222f2..bc3dab28f 100644 --- a/app/src/app/(nav)/settings/notifications.tsx +++ b/app/src/app/(nav)/settings/notifications.tsx @@ -1,4 +1,4 @@ -import { AppbarOptions } from '#/Appbar/AppbarOptions'; +import { Appbar } from '#/Appbar/Appbar'; import { ScrollableScreenSurface } from '#/layout/ScrollableScreenSurface'; import { NotificationSettings } from '#/NotificationSettings'; import { ScreenSkeleton } from '#/skeleton/ScreenSkeleton'; @@ -8,7 +8,7 @@ import { createStyles } from '@theme/styles'; export function NotificationSettingsScreen() { return ( <> - + diff --git a/app/src/app/(nav)/token/[address].tsx b/app/src/app/(nav)/token/[address].tsx index 36aed88d5..5ca1404c0 100644 --- a/app/src/app/(nav)/token/[address].tsx +++ b/app/src/app/(nav)/token/[address].tsx @@ -7,7 +7,7 @@ import { FormTextField } from '#/fields/FormTextField'; import { Actions } from '#/layout/Actions'; import { AppbarMore } from '#/Appbar/AppbarMore'; import { Menu } from 'react-native-paper'; -import { AppbarOptions } from '#/Appbar/AppbarOptions'; +import { Appbar } from '#/Appbar/Appbar'; import { withSuspense } from '#/skeleton/withSuspense'; import { ScreenSkeleton } from '#/skeleton/ScreenSkeleton'; import { z } from 'zod'; @@ -92,7 +92,7 @@ function TokenScreen_() { return ( - ( diff --git a/app/src/app/(nav)/token/add.tsx b/app/src/app/(nav)/token/add.tsx index 9da42075a..259772183 100644 --- a/app/src/app/(nav)/token/add.tsx +++ b/app/src/app/(nav)/token/add.tsx @@ -1,4 +1,4 @@ -import { AppbarOptions } from '#/Appbar/AppbarOptions'; +import { Appbar } from '#/Appbar/Appbar'; import { FormSelectChip } from '#/fields/FormSelectChip'; import { FormSubmitButton } from '#/fields/FormSubmitButton'; import { FormTextField } from '#/fields/FormTextField'; @@ -31,7 +31,7 @@ export default function AddTokenScreen() { return ( <> - + diff --git a/app/src/app/(nav)/transaction/[id].tsx b/app/src/app/(nav)/transaction/[id].tsx index 85a597b85..869dabf18 100644 --- a/app/src/app/(nav)/transaction/[id].tsx +++ b/app/src/app/(nav)/transaction/[id].tsx @@ -1,7 +1,7 @@ import { AppbarMore } from '#/Appbar/AppbarMore'; import { z } from 'zod'; import { useLocalParams } from '~/hooks/useLocalParams'; -import { AppbarOptions } from '#/Appbar/AppbarOptions'; +import { Appbar } from '#/Appbar/Appbar'; import { ScrollableScreenSurface } from '#/layout/ScrollableScreenSurface'; import { zUuid } from '~/lib/zod'; import { TransactionStatus } from '#/transaction/TransactionStatus'; @@ -92,7 +92,7 @@ function TransactionScreen() { return ( <> - } {...(remove && { trailing: (props) => ( diff --git a/app/src/app/(sheet)/_layout.tsx b/app/src/app/(sheet)/_layout.tsx index c884e22ad..978fcba4c 100644 --- a/app/src/app/(sheet)/_layout.tsx +++ b/app/src/app/(sheet)/_layout.tsx @@ -1,16 +1,8 @@ -import { Slot, Stack } from 'expo-router'; +import { Slot } from 'expo-router'; export default function SheetLayout() { return ( <> - ); diff --git a/app/src/app/_layout.tsx b/app/src/app/_layout.tsx index 352e2bf6e..ede6e2c47 100644 --- a/app/src/app/_layout.tsx +++ b/app/src/app/_layout.tsx @@ -15,7 +15,6 @@ import { NotificationsProvider } from '#/provider/NotificationsProvider'; import { SnackbarProvider } from '#/provider/SnackbarProvider'; import { UpdateProvider } from '#/provider/UpdateProvider'; import { ThemeProvider } from '~/util/theme/ThemeProvider'; -import { AppbarHeader } from '#/Appbar/AppbarHeader'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { Portal as RnpPortal } from 'react-native-paper'; import { TQueryProvider } from '#/provider/TQueryProvider'; @@ -33,13 +32,23 @@ export const unstable_settings = { function Layout() { return ( - }}> - - - - - - + + + ); } diff --git a/app/src/app/index.tsx b/app/src/app/index.tsx index 392be0a34..995e7966c 100644 --- a/app/src/app/index.tsx +++ b/app/src/app/index.tsx @@ -1,4 +1,4 @@ -import { ScrollView, View } from 'react-native'; +import { View } from 'react-native'; import { createStyles, useStyles } from '@theme/styles'; import { LandingHeader } from '#/landing/LandingHeader'; import { PrimarySection } from '#/landing/PrimarySection'; @@ -13,6 +13,7 @@ import { ScreenSkeleton } from '#/skeleton/ScreenSkeleton'; import { graphql } from 'relay-runtime'; import { useLazyQuery } from '~/api'; import { app_LandingScreenQuery } from '~/api/__generated__/app_LandingScreenQuery.graphql'; +import { Scrollable } from '#/Scrollable'; // Must use Query.accounts to avoid potential redirect loop with AccountLayout const Query = graphql` @@ -38,7 +39,7 @@ function LandingScreen() { return ; return ( - + - + ); } diff --git a/app/src/app/scan.tsx b/app/src/app/scan.tsx index a7e2c730f..c65935418 100644 --- a/app/src/app/scan.tsx +++ b/app/src/app/scan.tsx @@ -13,7 +13,7 @@ import { useFocusEffect, useRouter } from 'expo-router'; import { ScanOverlay } from '#/ScanOverlay'; import { Subject } from 'rxjs'; import { useGetEvent } from '~/hooks/useGetEvent'; -import { AppbarOptions } from '#/Appbar/AppbarOptions'; +import { Appbar } from '#/Appbar/Appbar'; import { z } from 'zod'; import { zUAddress } from '~/lib/zod'; import { useLocalParams } from '~/hooks/useLocalParams'; @@ -92,8 +92,7 @@ export default function ScanScreen() { ) : ( - - + Please grant camera permissions in order to scan a QR code diff --git a/app/src/components/Chip.tsx b/app/src/components/Chip.tsx index 16515aff5..ec7cd4c69 100644 --- a/app/src/components/Chip.tsx +++ b/app/src/components/Chip.tsx @@ -1,19 +1,35 @@ -import { ComponentPropsWithoutRef } from 'react'; +import { IconProps } from '@theme/icons'; +import { createStyles, useStyles } from '@theme/styles'; +import { ComponentPropsWithoutRef, FC } from 'react'; import { Chip as Base } from 'react-native-paper'; import { useWithLoading } from '~/hooks/useWithLoading'; type BaseProps = ComponentPropsWithoutRef; -export interface ChipProps extends BaseProps {} +export interface ChipProps extends Omit { + icon?: FC; +} + +export function Chip({ icon: Icon, selected, ...props }: ChipProps) { + const { styles } = useStyles(stylesheet); -export function Chip(props: ChipProps) { const [loading, onPress] = useWithLoading(props.onPress); return ( })} onPress={onPress} {...(loading && { disabled: true })} /> ); } + +const stylesheet = createStyles(({ colors }) => ({ + unselectedIcon: { + color: colors.onSurface, + }, +})); diff --git a/app/src/components/fields/DecimalInput.tsx b/app/src/components/fields/DecimalInput.tsx new file mode 100644 index 000000000..b17f93f40 --- /dev/null +++ b/app/src/components/fields/DecimalInput.tsx @@ -0,0 +1,47 @@ +import Decimal from 'decimal.js'; +import { ComponentType, Dispatch, SetStateAction, startTransition, useState } from 'react'; +import { TextInput as NativeTextInput, TextInputProps } from 'react-native'; +import { z } from 'zod'; + +const decimal = z + .string() + .regex(/^\d*(\.\d*)?$/) + .transform((v, ctx) => { + try { + if (v === '') return new Decimal(0); + return new Decimal(v); + } catch { + ctx.addIssue({ code: 'custom', message: 'Must be a valid decimal' }); + return z.NEVER; + } + }); + +export interface DecimalInputProps extends Omit { + value: Decimal; + onChange: Dispatch>; + as?: ComponentType; +} + +export function DecimalInput({ + as: TextInput = NativeTextInput, + value, + onChange, + ...props +}: DecimalInputProps) { + const [input, setInput] = useState(() => (value.isZero() ? '' : value.toString())); + + const handleChangeText = (input: string) => { + const parsed = decimal.safeParse(input); + if (parsed.data) { + startTransition(() => onChange(parsed.data)); + } else { + // If the input is not a valid decimal, don't update the value + } + + if (parsed.success || input === '.') setInput(input); + }; + + return ( + + ); +} diff --git a/app/src/components/home/QuickActions.tsx b/app/src/components/home/QuickActions.tsx index 93cca90c8..40ffce92c 100644 --- a/app/src/components/home/QuickActions.tsx +++ b/app/src/components/home/QuickActions.tsx @@ -2,10 +2,10 @@ import { OutboundIcon, SwapIcon, ScanIcon, ReceiveIcon } from '@theme/icons'; import { ScrollView, View } from 'react-native'; import { UAddress } from 'lib'; import { Link } from 'expo-router'; -import { useSend } from '~/hooks/useSend'; import { CORNER } from '@theme/paper'; import { createStyles, useStyles } from '@theme/styles'; import { Button } from '#/Button'; +import { SendScreenParams } from '~/app/(nav)/[account]/(home)/send'; export interface QuickActionsProps { account: UAddress; @@ -13,7 +13,6 @@ export interface QuickActionsProps { export function QuickActions({ account }: QuickActionsProps) { const { styles } = useStyles(stylesheet); - const send = useSend(); return ( @@ -22,15 +21,17 @@ export function QuickActions({ account }: QuickActionsProps) { showsHorizontalScrollIndicator={false} contentContainerStyle={styles.content} > - + + + + + ); +} + +const stylesheet = createStyles(({ colors }) => ({ + item: { + backgroundColor: colors.surface, + }, +})); diff --git a/app/src/components/send/TransferMode.tsx b/app/src/components/send/TransferMode.tsx new file mode 100644 index 000000000..2942d703b --- /dev/null +++ b/app/src/components/send/TransferMode.tsx @@ -0,0 +1,77 @@ +import { asAddress, asFp, UAddress } from 'lib'; +import { Actions } from '#/layout/Actions'; +import { Button } from '#/Button'; +import { useProposeTransaction } from '~/hooks/mutations/useProposeTransaction'; +import { graphql } from 'relay-runtime'; +import { TransferMode_account$key } from '~/api/__generated__/TransferMode_account.graphql'; +import { useFragment } from 'react-relay'; +import { TransferMode_token$key } from '~/api/__generated__/TransferMode_token.graphql'; +import { encodeFunctionData } from 'viem'; +import { ERC20 } from 'lib/dapps'; +import Decimal from 'decimal.js'; +import { CheckAllIcon } from '@theme/icons'; +import { useRouter } from 'expo-router'; +import { ampli } from '~/lib/ampli'; + +const Account = graphql` + fragment TransferMode_account on Account { + address + ...useProposeTransaction_account + } +`; + +const Token = graphql` + fragment TransferMode_token on Token { + address + decimals + } +`; + +export interface TransferModeProps { + account: TransferMode_account$key; + token: TransferMode_token$key; + to: UAddress | undefined; + amount: Decimal; +} + +export function TransferMode({ to, amount, ...props }: TransferModeProps) { + const account = useFragment(Account, props.account); + const token = useFragment(Token, props.token); + const propose = useProposeTransaction(); + const router = useRouter(); + + const proposeTransfer = + to && + (async () => { + const transaction = await propose(account, { + operations: [ + { + to: asAddress(token.address), + data: encodeFunctionData({ + abi: ERC20, + functionName: 'transfer', + args: [asAddress(to), asFp(amount, token.decimals, Decimal.ROUND_DOWN)], + }), + }, + ], + // executionGas: TODO: estimate execution gas + }); + router.push({ pathname: `/(nav)/transaction/[id]`, params: { id: transaction } }); + ampli.transferProposal({ token: token.address }); + }); + + return ( + <> + + + + + ); +} diff --git a/app/src/components/token/TokenAmountInput.tsx b/app/src/components/token/TokenAmountInput.tsx new file mode 100644 index 000000000..9bbe035cb --- /dev/null +++ b/app/src/components/token/TokenAmountInput.tsx @@ -0,0 +1,129 @@ +import { DecimalInput } from '#/fields/DecimalInput'; +import { createStyles, useStyles } from '@theme/styles'; +import Decimal from 'decimal.js'; +import { Dispatch, SetStateAction } from 'react'; +import { View } from 'react-native'; +import { Text } from 'react-native-paper'; +import { useFragment } from 'react-relay'; +import { graphql } from 'relay-runtime'; +import { TokenAmountInput_token$key } from '~/api/__generated__/TokenAmountInput_token.graphql'; +import { TokenIcon } from './TokenIcon'; +import { ICON_SIZE } from '@theme/paper'; +import { useSelectToken } from '~/hooks/useSelectToken'; +import { PressableOpacity } from '#/PressableOpacity'; +import { UAddress } from 'lib'; +import { FiatValue } from '#/FiatValue'; + +const Token = graphql` + fragment TokenAmountInput_token on Token { + id + symbol + price { + usd + } + ...TokenIcon_token + } +`; + +export interface TokenAmountInputProps { + account: UAddress; + token: TokenAmountInput_token$key; + amount: Decimal; + onChange: Dispatch>; +} + +export function TokenAmountInput({ account, amount, onChange, ...props }: TokenAmountInputProps) { + const { styles } = useStyles(stylesheet); + const token = useFragment(Token, props.token); + const selectToken = useSelectToken(); + + const value = token.price && amount.mul(token.price.usd); + + return ( + <> + {value && ( + { + alert('secondary'); + }} + > + + {'≈ '} + + + + )} + + + + + + + { + selectToken({ account }); + }} + > + + + + {token.symbol} + + + + + ); +} + +const stylesheet = createStyles(({ colors, fonts, corner, negativeMargin }) => ({ + secondary: { + alignSelf: 'flex-start', + padding: 4, + borderRadius: corner.s, + }, + row: { + flexDirection: 'row', + flexWrap: 'wrap', + alignItems: 'center', + gap: 8, + }, + approximation: { + color: colors.onSurfaceVariant, + }, + inputContainer: { + flex: 1, + flexBasis: '65%', + }, + input: { + ...fonts.displayLarge, + // ...(Platform.OS === 'web' && { outlineStyle: 'none' }), // only on web + outlineStyle: 'none', + }, + placeholder: { + color: colors.tertiary, + }, + selection: { + color: colors.tertiary, + }, + tokenContainer: { + flexShrink: 1, + flexDirection: 'row', + alignItems: 'center', + gap: 8, + padding: 8, + marginHorizontal: -8, + borderRadius: corner.m, + }, + unit: { + color: colors.onSurfaceVariant, + }, +})); diff --git a/app/src/components/token/useTokenAmount.tsx b/app/src/components/token/useTokenAmount.tsx index 93064e1e9..9a7a76d49 100644 --- a/app/src/components/token/useTokenAmount.tsx +++ b/app/src/components/token/useTokenAmount.tsx @@ -47,9 +47,9 @@ export const useTokenAmount = ({ const unit = amount.eq(0) ? token : units.reduce( - // Find the closest unit; bias the smaller unit (-1) + // Find the closest unit (closest, unit) => - Math.abs(unit.decimals - d) - 1 <= Math.abs(closest.decimals - d) ? unit : closest, + Math.abs(unit.decimals - d) <= Math.abs(closest.decimals - d) ? unit : closest, units[0], ); diff --git a/app/src/hooks/useSend.ts b/app/src/hooks/useSend.ts deleted file mode 100644 index 62158eb3f..000000000 --- a/app/src/hooks/useSend.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useSelectAddress } from '~/hooks/useSelectAddress'; -import { SendScreenParams } from '~/app/(nav)/[account]/send'; -import { O } from 'ts-toolbelt'; -import { useRouter } from 'expo-router'; -import { asAddress } from 'lib'; - -export function useSend() { - const router = useRouter(); - const selectAddress = useSelectAddress(); - - return async (params: O.Optional) => { - params.to ??= asAddress( - await selectAddress({ - headline: 'Send to', - include: ['accounts', 'contacts'], - disabled: [params.account], - }), - ); - - if (params.to) router.push({ pathname: `/(nav)/[account]/send`, params }); - }; -} diff --git a/app/src/util/theme/icons.tsx b/app/src/util/theme/icons.tsx index 3f2a6aacc..6962a6832 100644 --- a/app/src/util/theme/icons.tsx +++ b/app/src/util/theme/icons.tsx @@ -49,6 +49,7 @@ export const EditOutlineIcon = materialCommunityIcon('pencil-outline'); export const ScanIcon = materialCommunityIcon('line-scan'); export const ShareIcon = materialCommunityIcon('share-variant'); export const CheckIcon = materialCommunityIcon('check'); +export const CheckAllIcon = materialCommunityIcon('check-all'); export const CloseIcon = materialCommunityIcon('close'); export const CancelIcon = CloseIcon; export const RemoveIcon = CloseIcon;