diff --git a/apps/wallet-mobile/.storybook/storybook.requires.js b/apps/wallet-mobile/.storybook/storybook.requires.js index 7046d09d9b..21011eaba6 100644 --- a/apps/wallet-mobile/.storybook/storybook.requires.js +++ b/apps/wallet-mobile/.storybook/storybook.requires.js @@ -88,6 +88,7 @@ const getStories = () => { "./src/components/PairedBalance/PairedBalance.stories.tsx": require("../src/components/PairedBalance/PairedBalance.stories.tsx"), "./src/components/PressableIcon/PressableIcon.stories.tsx": require("../src/components/PressableIcon/PressableIcon.stories.tsx"), "./src/components/ShareQRCodeCard/ShareQRCodeCard.stories.tsx": require("../src/components/ShareQRCodeCard/ShareQRCodeCard.stories.tsx"), + "./src/components/SimpleTab/SimpleTab.stories.tsx": require("../src/components/SimpleTab/SimpleTab.stories.tsx"), "./src/components/SomethingWentWrong/SomethingWentWrong.stories.tsx": require("../src/components/SomethingWentWrong/SomethingWentWrong.stories.tsx"), "./src/components/StepperProgress/StepperProgress.stories.tsx": require("../src/components/StepperProgress/StepperProgress.stories.tsx"), "./src/components/TextInput/TextInput.stories.tsx": require("../src/components/TextInput/TextInput.stories.tsx"), @@ -121,7 +122,6 @@ const getStories = () => { "./src/features/Discover/useCases/SearchDappInBrowser/SearchDappInBrowserScreen.stories.tsx": require("../src/features/Discover/useCases/SearchDappInBrowser/SearchDappInBrowserScreen.stories.tsx"), "./src/features/Discover/useCases/SelectDappFromList/CountDAppsAvailable/CountDAppsAvailable.stories.tsx": require("../src/features/Discover/useCases/SelectDappFromList/CountDAppsAvailable/CountDAppsAvailable.stories.tsx"), "./src/features/Discover/useCases/SelectDappFromList/CountDAppsConnected/CountDAppsConnected.stories.tsx": require("../src/features/Discover/useCases/SelectDappFromList/CountDAppsConnected/CountDAppsConnected.stories.tsx"), - "./src/features/Discover/useCases/SelectDappFromList/DAppExplorerTabItem/DAppExplorerTabItem.stories.tsx": require("../src/features/Discover/useCases/SelectDappFromList/DAppExplorerTabItem/DAppExplorerTabItem.stories.tsx"), "./src/features/Discover/useCases/SelectDappFromList/DAppListItem/DAppItemSkeleton.stories.tsx": require("../src/features/Discover/useCases/SelectDappFromList/DAppListItem/DAppItemSkeleton.stories.tsx"), "./src/features/Discover/useCases/SelectDappFromList/DAppListItem/DAppListItem.stories.tsx": require("../src/features/Discover/useCases/SelectDappFromList/DAppListItem/DAppListItem.stories.tsx"), "./src/features/Discover/useCases/SelectDappFromList/DAppTypes/DAppTypes.stories.tsx": require("../src/features/Discover/useCases/SelectDappFromList/DAppTypes/DAppTypes.stories.tsx"), @@ -173,7 +173,6 @@ const getStories = () => { "./src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/OpenDeviceAppSettingsButton.stories.tsx": require("../src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/OpenDeviceAppSettingsButton.stories.tsx"), "./src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/ShowCameraPermissionDeniedScreen.stories.tsx": require("../src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/ShowCameraPermissionDeniedScreen.stories.tsx"), "./src/features/Send/common/ButtonGroup/ButtonGroup.stories.tsx": require("../src/features/Send/common/ButtonGroup/ButtonGroup.stories.tsx"), - "./src/features/Send/useCases/ConfirmTx/ConfirmTxScreen.stories.tsx": require("../src/features/Send/useCases/ConfirmTx/ConfirmTxScreen.stories.tsx"), "./src/features/Send/useCases/ConfirmTx/FailedTx/FailedTxScreen.stories.tsx": require("../src/features/Send/useCases/ConfirmTx/FailedTx/FailedTxScreen.stories.tsx"), "./src/features/Send/useCases/ConfirmTx/SubmittedTx/SubmittedTxScreen.stories.tsx": require("../src/features/Send/useCases/ConfirmTx/SubmittedTx/SubmittedTxScreen.stories.tsx"), "./src/features/Send/useCases/ListAmountsToSend/AddToken/AddToken.stories.tsx": require("../src/features/Send/useCases/ListAmountsToSend/AddToken/AddToken.stories.tsx"), @@ -195,7 +194,6 @@ const getStories = () => { "./src/features/Settings/Currency/ChangeCurrencyScreen.stories.tsx": require("../src/features/Settings/Currency/ChangeCurrencyScreen.stories.tsx"), "./src/features/Settings/EasyConfirmation/EasyConfirmationScreen.stories.tsx": require("../src/features/Settings/EasyConfirmation/EasyConfirmationScreen.stories.tsx"), "./src/features/Settings/EnableLoginWithOs/EnableLoginWithOsScreen.stories.tsx": require("../src/features/Settings/EnableLoginWithOs/EnableLoginWithOsScreen.stories.tsx"), - "./src/features/Settings/ManageCollateral/ConfirmTx/ConfirmTxScreen.stories.tsx": require("../src/features/Settings/ManageCollateral/ConfirmTx/ConfirmTxScreen.stories.tsx"), "./src/features/Settings/ManageCollateral/ConfirmTx/FailedTx/FailedTxScreen.stories.tsx": require("../src/features/Settings/ManageCollateral/ConfirmTx/FailedTx/FailedTxScreen.stories.tsx"), "./src/features/Settings/ManageCollateral/ConfirmTx/SubmittedTx/SubmittedTxScreen.stories.tsx": require("../src/features/Settings/ManageCollateral/ConfirmTx/SubmittedTx/SubmittedTxScreen.stories.tsx"), "./src/features/Settings/ManageCollateral/ManageCollateralScreen.stories.tsx": require("../src/features/Settings/ManageCollateral/ManageCollateralScreen.stories.tsx"), diff --git a/apps/wallet-mobile/src/WalletNavigator.tsx b/apps/wallet-mobile/src/WalletNavigator.tsx index b67c47d16a..c1d3857a7f 100644 --- a/apps/wallet-mobile/src/WalletNavigator.tsx +++ b/apps/wallet-mobile/src/WalletNavigator.tsx @@ -24,6 +24,7 @@ import {useLinksShowActionResult} from './features/Links/common/useLinksShowActi import {MenuNavigator} from './features/Menu/Menu' import {PortfolioNavigator} from './features/Portfolio/PortfolioNavigator' import {CatalystNavigator} from './features/RegisterCatalyst/CatalystNavigator' +import {ReviewTxNavigator} from './features/ReviewTx/ReviewTxNavigator' import {SearchProvider} from './features/Search/SearchContext' import {SettingsScreenNavigator} from './features/Settings' import {NetworkTag} from './features/Settings/ChangeNetwork/NetworkTag' @@ -258,6 +259,8 @@ export const WalletNavigator = () => { + + { divider: color.gray_200, } - return {colors, styles} + return {colors, styles} as const } const messages = defineMessages({ diff --git a/apps/wallet-mobile/src/YoroiApp.tsx b/apps/wallet-mobile/src/YoroiApp.tsx index fc98586531..655676b9c4 100644 --- a/apps/wallet-mobile/src/YoroiApp.tsx +++ b/apps/wallet-mobile/src/YoroiApp.tsx @@ -15,6 +15,7 @@ import {ErrorBoundary} from './components/ErrorBoundary/ErrorBoundary' import {AuthProvider} from './features/Auth/AuthProvider' import {BrowserProvider} from './features/Discover/common/BrowserProvider' import {PortfolioTokenActivityProvider} from './features/Portfolio/common/PortfolioTokenActivityProvider' +import {ReviewTxProvider} from './features/ReviewTx/common/ReviewTxProvider' import {CurrencyProvider} from './features/Settings/Currency/CurrencyContext' import {AutomaticWalletOpenerProvider} from './features/WalletManager/context/AutomaticWalletOpeningProvider' import {WalletManagerProvider} from './features/WalletManager/context/WalletManagerProvider' @@ -65,7 +66,9 @@ const Yoroi = () => { - + + + diff --git a/apps/wallet-mobile/src/components/Icon/Direction.tsx b/apps/wallet-mobile/src/components/Icon/Direction.tsx index 3f803c624d..7d1e4f714b 100644 --- a/apps/wallet-mobile/src/components/Icon/Direction.tsx +++ b/apps/wallet-mobile/src/components/Icon/Direction.tsx @@ -1,6 +1,6 @@ import {ThemedPalette, useTheme} from '@yoroi/theme' import React from 'react' -import {StyleSheet, View} from 'react-native' +import {StyleSheet, View, ViewStyle} from 'react-native' import {TransactionDirection, TransactionInfo} from '../../yoroi-wallets/types/other' import {Received} from '../Icon/Received' @@ -11,9 +11,10 @@ import {MultiParty} from './MultiParty' type Props = { transaction: TransactionInfo size?: number + containerStyle?: ViewStyle } -export const Direction = ({transaction, size = defaultSize}: Props) => { +export const Direction = ({transaction, size = defaultSize, containerStyle}: Props) => { const {color} = useTheme() const {direction} = transaction @@ -21,7 +22,7 @@ export const Direction = ({transaction, size = defaultSize}: Props) => { const IconComponent = iconMap[direction] return ( - + ) diff --git a/apps/wallet-mobile/src/components/Info/Info.tsx b/apps/wallet-mobile/src/components/Info/Info.tsx new file mode 100644 index 0000000000..b4272cc14e --- /dev/null +++ b/apps/wallet-mobile/src/components/Info/Info.tsx @@ -0,0 +1,47 @@ +import {useTheme} from '@yoroi/theme' +import React, {ReactNode} from 'react' +import {StyleSheet, Text, View} from 'react-native' + +import {Icon} from '../Icon' +import {Space} from '../Space/Space' + +type Props = { + content: ReactNode + iconSize?: number +} + +export const Info = ({content, iconSize = 30}: Props) => { + const {styles, colors} = useStyles() + + return ( + + + + + + {content} + + ) +} + +const useStyles = () => { + const {color, atoms} = useTheme() + const styles = StyleSheet.create({ + notice: { + backgroundColor: color.sys_cyan_100, + ...atoms.p_md, + ...atoms.rounded_sm, + }, + text: { + ...atoms.body_2_md_regular, + color: color.text_gray_max, + }, + }) + + const colors = { + yellow: color.sys_orange_500, + blue: color.primary_500, + } + + return {colors, styles} as const +} diff --git a/apps/wallet-mobile/src/features/Portfolio/common/MediaPreview/MediaPreview.tsx b/apps/wallet-mobile/src/components/MediaPreview/MediaPreview.tsx similarity index 90% rename from apps/wallet-mobile/src/features/Portfolio/common/MediaPreview/MediaPreview.tsx rename to apps/wallet-mobile/src/components/MediaPreview/MediaPreview.tsx index 8dacd28a7c..36f43371e1 100644 --- a/apps/wallet-mobile/src/features/Portfolio/common/MediaPreview/MediaPreview.tsx +++ b/apps/wallet-mobile/src/components/MediaPreview/MediaPreview.tsx @@ -5,9 +5,9 @@ import React from 'react' import {ImageStyle, StyleSheet, View} from 'react-native' import SkeletonPlaceholder from 'react-native-skeleton-placeholder' -import placeholderLight from '../../../../assets/img/nft-placeholder.png' -import placeholderDark from '../../../../assets/img/nft-placeholder-dark.png' -import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet' +import placeholderLight from '../../assets/img/nft-placeholder.png' +import placeholderDark from '../../assets/img/nft-placeholder-dark.png' +import {useSelectedWallet} from '../../features/WalletManager/common/hooks/useSelectedWallet' type MediaPreviewProps = { info: Portfolio.Token.Info diff --git a/apps/wallet-mobile/src/components/SimpleTab/SimpleTab.stories.tsx b/apps/wallet-mobile/src/components/SimpleTab/SimpleTab.stories.tsx new file mode 100644 index 0000000000..549ead2f95 --- /dev/null +++ b/apps/wallet-mobile/src/components/SimpleTab/SimpleTab.stories.tsx @@ -0,0 +1,26 @@ +import {action} from '@storybook/addon-actions' +import {storiesOf} from '@storybook/react-native' +import * as React from 'react' +import {View} from 'react-native' + +import {SimpleTab} from './SimpleTab' + +storiesOf('SimpleTab', module) + .add('Active', () => ) + .add('Inactive', () => ) + +const Active = () => { + return ( + + + + ) +} + +const Inactive = () => { + return ( + + + + ) +} diff --git a/apps/wallet-mobile/src/features/Discover/useCases/SelectDappFromList/DAppExplorerTabItem/DAppExplorerTabItem.tsx b/apps/wallet-mobile/src/components/SimpleTab/SimpleTab.tsx similarity index 89% rename from apps/wallet-mobile/src/features/Discover/useCases/SelectDappFromList/DAppExplorerTabItem/DAppExplorerTabItem.tsx rename to apps/wallet-mobile/src/components/SimpleTab/SimpleTab.tsx index e140c97e43..e72787c55d 100644 --- a/apps/wallet-mobile/src/features/Discover/useCases/SelectDappFromList/DAppExplorerTabItem/DAppExplorerTabItem.tsx +++ b/apps/wallet-mobile/src/components/SimpleTab/SimpleTab.tsx @@ -8,7 +8,7 @@ type Props = { onPress: () => void } -export const DAppExplorerTabItem = ({name, onPress, isActive}: Props) => { +export const SimpleTab = ({name, onPress, isActive}: Props) => { const {styles} = useStyles() return ( @@ -34,5 +34,5 @@ const useStyles = () => { }, }) - return {styles} + return {styles} as const } diff --git a/apps/wallet-mobile/src/components/Warning/Warning.tsx b/apps/wallet-mobile/src/components/Warning/Warning.tsx index 320ee76732..ce1a959985 100644 --- a/apps/wallet-mobile/src/components/Warning/Warning.tsx +++ b/apps/wallet-mobile/src/components/Warning/Warning.tsx @@ -5,7 +5,10 @@ import {StyleSheet, Text, View} from 'react-native' import {Icon} from '../Icon' import {Space} from '../Space/Space' -type Props = {content: ReactNode; iconSize?: number} +type Props = { + content: ReactNode + iconSize?: number +} export const Warning = ({content, iconSize = 30}: Props) => { const {styles, colors} = useStyles() @@ -26,8 +29,8 @@ const useStyles = () => { const styles = StyleSheet.create({ notice: { backgroundColor: color.sys_yellow_100, - padding: 12, - borderRadius: 8, + ...atoms.p_md, + ...atoms.rounded_sm, }, text: { ...atoms.body_2_md_regular, @@ -39,5 +42,5 @@ const useStyles = () => { yellow: color.sys_orange_500, } - return {colors, styles} + return {colors, styles} as const } diff --git a/apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/ReviewTransaction.tsx b/apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/ReviewTransaction.tsx index bfbf280cd5..2bd93f55f2 100644 --- a/apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/ReviewTransaction.tsx +++ b/apps/wallet-mobile/src/features/Discover/useCases/ReviewTransaction/ReviewTransaction.tsx @@ -447,7 +447,7 @@ const useConnectorPromptRootKey = () => { }, [promptRootKey]) } -const useSignTxWithHW = () => { +export const useSignTxWithHW = () => { const {confirmHWConnection, closeModal} = useConfirmHWConnectionModal() const {wallet, meta} = useSelectedWallet() diff --git a/apps/wallet-mobile/src/features/Discover/useCases/SelectDappFromList/DAppExplorerTabItem/DAppExplorerTabItem.stories.tsx b/apps/wallet-mobile/src/features/Discover/useCases/SelectDappFromList/DAppExplorerTabItem/DAppExplorerTabItem.stories.tsx deleted file mode 100644 index 626ac02ba0..0000000000 --- a/apps/wallet-mobile/src/features/Discover/useCases/SelectDappFromList/DAppExplorerTabItem/DAppExplorerTabItem.stories.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import {action} from '@storybook/addon-actions' -import {storiesOf} from '@storybook/react-native' -import * as React from 'react' -import {View} from 'react-native' - -import {DAppExplorerTabItem} from './DAppExplorerTabItem' - -storiesOf('Discover DAppExplorerTabItem', module) - .add('connected', () => ) - .add('recommended', () => ) - -const Connected = () => { - return ( - - - - ) -} - -const Recommended = () => { - return ( - - - - ) -} diff --git a/apps/wallet-mobile/src/features/Discover/useCases/SelectDappFromList/SelectDappFromListScreen.tsx b/apps/wallet-mobile/src/features/Discover/useCases/SelectDappFromList/SelectDappFromListScreen.tsx index a21de2ae63..39a2ee4b6c 100644 --- a/apps/wallet-mobile/src/features/Discover/useCases/SelectDappFromList/SelectDappFromListScreen.tsx +++ b/apps/wallet-mobile/src/features/Discover/useCases/SelectDappFromList/SelectDappFromListScreen.tsx @@ -3,6 +3,7 @@ import {useTheme} from '@yoroi/theme' import * as React from 'react' import {FlatList, StyleSheet, View} from 'react-native' +import {SimpleTab} from '../../../../components/SimpleTab/SimpleTab' import {Spacer} from '../../../../components/Spacer/Spacer' import {useMetrics} from '../../../../kernel/metrics/metricsManager' import {useSearch, useSearchOnNavBar} from '../../../Search/SearchContext' @@ -13,7 +14,6 @@ import {useDAppsConnected} from '../../common/useDAppsConnected' import {useStrings} from '../../common/useStrings' import {CountDAppsAvailable} from './CountDAppsAvailable/CountDAppsAvailable' import {CountDAppsConnected} from './CountDAppsConnected/CountDAppsConnected' -import {DAppExplorerTabItem} from './DAppExplorerTabItem/DAppExplorerTabItem' import {DAppListItem} from './DAppListItem/DAppListItem' import {DAppTypes} from './DAppTypes/DAppTypes' import {WelcomeDAppModal} from './WelcomeDAppModal' @@ -153,13 +153,13 @@ const HeaderControl = ({ <> {hasConnectedDapps && ( - onTabChange(DAppTabs.connected)} /> - onTabChange(DAppTabs.recommended)} diff --git a/apps/wallet-mobile/src/features/Portfolio/common/MediaDetailsScreen/MediaDetailsScreen.tsx b/apps/wallet-mobile/src/features/Portfolio/common/MediaDetailsScreen/MediaDetailsScreen.tsx index bcd4e24f25..f2cff3c32c 100644 --- a/apps/wallet-mobile/src/features/Portfolio/common/MediaDetailsScreen/MediaDetailsScreen.tsx +++ b/apps/wallet-mobile/src/features/Portfolio/common/MediaDetailsScreen/MediaDetailsScreen.tsx @@ -20,6 +20,7 @@ import {Boundary} from '../../../../components/Boundary/Boundary' import {CopyButton} from '../../../../components/CopyButton' import {FadeIn} from '../../../../components/FadeIn' import {Hr} from '../../../../components/Hr/Hr' +import {MediaPreview} from '../../../../components/MediaPreview/MediaPreview' import {Spacer} from '../../../../components/Spacer/Spacer' import {Tab, TabPanel, TabPanels, Tabs} from '../../../../components/Tabs/Tabs' import {Text} from '../../../../components/Text' @@ -28,7 +29,6 @@ import {useMetrics} from '../../../../kernel/metrics/metricsManager' import {NftRoutes} from '../../../../kernel/navigation' import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet' import {usePortfolioImageInvalidate} from '../hooks/usePortfolioImage' -import {MediaPreview} from '../MediaPreview/MediaPreview' import {useNavigateTo} from '../navigation' export const MediaDetailsScreen = () => { diff --git a/apps/wallet-mobile/src/features/Portfolio/common/MediaGallery/MediaGallery.tsx b/apps/wallet-mobile/src/features/Portfolio/common/MediaGallery/MediaGallery.tsx index 149d062a5f..273b193caf 100644 --- a/apps/wallet-mobile/src/features/Portfolio/common/MediaGallery/MediaGallery.tsx +++ b/apps/wallet-mobile/src/features/Portfolio/common/MediaGallery/MediaGallery.tsx @@ -4,8 +4,8 @@ import {Balance, Portfolio} from '@yoroi/types' import * as React from 'react' import {StyleSheet, Text, TouchableOpacity, useWindowDimensions, View} from 'react-native' +import {MediaPreview} from '../../../../components/MediaPreview/MediaPreview' import {Spacer} from '../../../../components/Spacer/Spacer' -import {MediaPreview} from '../MediaPreview/MediaPreview' type Props = { amounts: ReadonlyArray diff --git a/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioDashboard/DashboardNFTsList/DashboardNFTsList.tsx b/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioDashboard/DashboardNFTsList/DashboardNFTsList.tsx index d60950805a..a3e6402aa3 100644 --- a/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioDashboard/DashboardNFTsList/DashboardNFTsList.tsx +++ b/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioDashboard/DashboardNFTsList/DashboardNFTsList.tsx @@ -5,12 +5,12 @@ import {FlatList, Image, StyleSheet, Text, TouchableOpacity, useWindowDimensions import placeholderLight from '../../../../../assets/img/nft-placeholder.png' import placeholderDark from '../../../../../assets/img/nft-placeholder-dark.png' import {Icon} from '../../../../../components/Icon' +import {MediaPreview} from '../../../../../components/MediaPreview/MediaPreview' import {Spacer} from '../../../../../components/Spacer/Spacer' import {useSelectedWallet} from '../../../../WalletManager/common/hooks/useSelectedWallet' import {useNavigateTo} from '../../../common/hooks/useNavigateTo' import {usePortfolioBalances} from '../../../common/hooks/usePortfolioBalances' import {useStrings} from '../../../common/hooks/useStrings' -import {MediaPreview} from '../../../common/MediaPreview/MediaPreview' export const DashboardNFTsList = () => { const {styles, cardItemWidth} = useStyles() diff --git a/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioTokensList/PortfolioWalletTokenList/ListMediaGalleryScreen/ZoomMediaImageScreen.tsx b/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioTokensList/PortfolioWalletTokenList/ListMediaGalleryScreen/ZoomMediaImageScreen.tsx index fc4fdb9071..b655b30e32 100644 --- a/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioTokensList/PortfolioWalletTokenList/ListMediaGalleryScreen/ZoomMediaImageScreen.tsx +++ b/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioTokensList/PortfolioWalletTokenList/ListMediaGalleryScreen/ZoomMediaImageScreen.tsx @@ -6,11 +6,11 @@ import {StyleSheet, useWindowDimensions, View} from 'react-native' import ViewTransformer from 'react-native-easy-view-transformer' import {FadeIn} from '../../../../../../components/FadeIn' +import {MediaPreview} from '../../../../../../components/MediaPreview/MediaPreview' import {useMetrics} from '../../../../../../kernel/metrics/metricsManager' import {NftRoutes, useParams} from '../../../../../../kernel/navigation' import {isEmptyString} from '../../../../../../kernel/utils' import {useSelectedWallet} from '../../../../../WalletManager/common/hooks/useSelectedWallet' -import {MediaPreview} from '../../../../common/MediaPreview/MediaPreview' type Params = NftRoutes['nft-details'] diff --git a/apps/wallet-mobile/src/features/ReviewTx/ReviewTxNavigator.tsx b/apps/wallet-mobile/src/features/ReviewTx/ReviewTxNavigator.tsx new file mode 100644 index 0000000000..4f28322aff --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/ReviewTxNavigator.tsx @@ -0,0 +1,36 @@ +import {createStackNavigator} from '@react-navigation/stack' +import {Atoms, ThemedPalette, useTheme} from '@yoroi/theme' +import React from 'react' + +import {Boundary} from '../../components/Boundary/Boundary' +import {defaultStackNavigationOptions, ReviewTxRoutes} from '../../kernel/navigation' +import {useStrings} from './common/hooks/useStrings' +import {ReviewTxScreen} from './useCases/ReviewTxScreen/ReviewTxScreen' + +export const Stack = createStackNavigator() + +export const ReviewTxNavigator = () => { + const {atoms, color} = useTheme() + const strings = useStrings() + + return ( + + + {() => ( + + + + )} + + + ) +} + +const screenOptions = (atoms: Atoms, color: ThemedPalette) => ({ + ...defaultStackNavigationOptions(atoms, color), + gestureEnabled: true, +}) diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/Accordion.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/Accordion.tsx new file mode 100644 index 0000000000..92e7bfb3ac --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/Accordion.tsx @@ -0,0 +1,69 @@ +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {Animated, LayoutAnimation, StyleSheet, Text, TouchableOpacity, View} from 'react-native' + +import {Icon} from '../../../components/Icon' + +export const Accordion = ({label, children}: {label: string; children: React.ReactNode}) => { + const {styles, colors} = useStyles() + const [isOpen, setIsOpen] = React.useState(false) + const animatedHeight = React.useRef(new Animated.Value(0)).current + + const toggleSection = () => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + setIsOpen(!isOpen) + Animated.timing(animatedHeight, { + toValue: isOpen ? 0 : 1, + duration: 300, + useNativeDriver: false, + }).start() + } + + return ( + + + {label} + + + + + + {children} + + + ) +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + sectionHeader: { + ...atoms.flex_row, + ...atoms.justify_between, + }, + sectionHeaderText: { + ...atoms.body_1_lg_medium, + color: color.text_gray_medium, + }, + childrenContainer: { + overflow: 'hidden', + }, + }) + + const colors = { + chevron: color.gray_900, + } + + return {styles, colors} as const +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/CopiableText.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/CopiableText.tsx new file mode 100644 index 0000000000..b09812b5a6 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/CopiableText.tsx @@ -0,0 +1,37 @@ +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {StyleSheet, TouchableOpacity, View} from 'react-native' + +import {Icon} from '../../../components/Icon' +import {useCopy} from '../../../hooks/useCopy' + +export const CopiableText = ({children, textToCopy}: {children: React.ReactNode; textToCopy: string}) => { + const {styles, colors} = useStyles() + const [, copy] = useCopy() + + return ( + + {children} + + copy(textToCopy)} activeOpacity={0.5}> + + + + ) +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + text: { + ...atoms.flex_row, + ...atoms.justify_between, + }, + }) + + const colors = { + copy: color.gray_900, + } + + return {styles, colors} as const +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/Divider.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/Divider.tsx new file mode 100644 index 0000000000..1733930fda --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/Divider.tsx @@ -0,0 +1,31 @@ +import {SpacingSize, useTheme} from '@yoroi/theme' +import * as React from 'react' +import {StyleSheet, View} from 'react-native' + +import {Space} from '../../../components/Space/Space' + +export const Divider = ({verticalSpace = 'none'}: {verticalSpace?: SpacingSize}) => { + const {styles} = useStyles() + return ( + <> + + + + + + + ) +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + divider: { + height: 1, + ...atoms.align_stretch, + backgroundColor: color.gray_200, + }, + }) + + return {styles} as const +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/ReviewTxProvider.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/ReviewTxProvider.tsx new file mode 100644 index 0000000000..05dd6ec924 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/ReviewTxProvider.tsx @@ -0,0 +1,141 @@ +import {castDraft, produce} from 'immer' +import _ from 'lodash' +import React from 'react' + +import {YoroiSignedTx, YoroiUnsignedTx} from '../../../yoroi-wallets/types/yoroi' + +export const useReviewTx = () => React.useContext(ReviewTxContext) + +export const ReviewTxProvider = ({ + children, + initialState, +}: { + children: React.ReactNode + initialState?: Partial +}) => { + const [state, dispatch] = React.useReducer(reviewTxReducer, { + ...defaultState, + ...initialState, + }) + + const actions = React.useRef({ + unsignedTxChanged: (unsignedTx: ReviewTxState['unsignedTx']) => + dispatch({type: ReviewTxActionType.UnsignedTxChanged, unsignedTx}), + cborChanged: (cbor: ReviewTxState['cbor']) => dispatch({type: ReviewTxActionType.CborChanged, cbor}), + operationsChanged: (operations: ReviewTxState['operations']) => + dispatch({type: ReviewTxActionType.OperationsChanged, operations}), + onSuccessChanged: (onSuccess: ReviewTxState['onSuccess']) => + dispatch({type: ReviewTxActionType.OnSuccessChanged, onSuccess}), + onErrorChanged: (onError: ReviewTxState['onError']) => dispatch({type: ReviewTxActionType.OnErrorChanged, onError}), + }).current + + const context = React.useMemo( + () => ({ + ...state, + ...actions, + }), + [state, actions], + ) + + return {children} +} + +const reviewTxReducer = (state: ReviewTxState, action: ReviewTxAction) => { + return produce(state, (draft) => { + switch (action.type) { + case ReviewTxActionType.UnsignedTxChanged: + draft.unsignedTx = castDraft(action.unsignedTx) + break + + case ReviewTxActionType.CborChanged: + draft.cbor = action.cbor + break + + case ReviewTxActionType.OperationsChanged: + draft.operations = action.operations + break + + case ReviewTxActionType.OnSuccessChanged: + draft.onSuccess = action.onSuccess + break + + case ReviewTxActionType.OnErrorChanged: + draft.onError = action.onError + break + + default: + throw new Error('[ReviewTxContext] invalid action') + } + }) +} + +type ReviewTxAction = + | { + type: ReviewTxActionType.UnsignedTxChanged + unsignedTx: ReviewTxState['unsignedTx'] + } + | { + type: ReviewTxActionType.CborChanged + cbor: ReviewTxState['cbor'] + } + | { + type: ReviewTxActionType.OperationsChanged + operations: ReviewTxState['operations'] + } + | { + type: ReviewTxActionType.OnSuccessChanged + onSuccess: ReviewTxState['onSuccess'] + } + | { + type: ReviewTxActionType.OnErrorChanged + onError: ReviewTxState['onError'] + } + +export type ReviewTxState = { + unsignedTx: YoroiUnsignedTx | null + cbor: string | null + operations: Array | null + onSuccess: ((signedTx: YoroiSignedTx) => void) | null + onError: (() => void) | null +} + +type ReviewTxActions = { + unsignedTxChanged: (unsignedTx: ReviewTxState['unsignedTx']) => void + cborChanged: (cbor: ReviewTxState['cbor']) => void + operationsChanged: (operations: ReviewTxState['operations']) => void + onSuccessChanged: (onSuccess: ReviewTxState['onSuccess']) => void + onErrorChanged: (onError: ReviewTxState['onError']) => void +} + +const defaultState: ReviewTxState = Object.freeze({ + unsignedTx: null, + cbor: null, + operations: null, + onSuccess: null, + onError: null, +}) + +function missingInit() { + console.error('[ReviewTxContext] missing initialization') +} + +const initialReviewTxContext: ReviewTxContext = { + ...defaultState, + unsignedTxChanged: missingInit, + cborChanged: missingInit, + operationsChanged: missingInit, + onSuccessChanged: missingInit, + onErrorChanged: missingInit, +} + +enum ReviewTxActionType { + UnsignedTxChanged = 'unsignedTxChanged', + CborChanged = 'cborChanged', + OperationsChanged = 'operationsChanged', + OnSuccessChanged = 'onSuccessChanged', + OnErrorChanged = 'onErrorChanged', +} + +type ReviewTxContext = ReviewTxState & ReviewTxActions + +const ReviewTxContext = React.createContext(initialReviewTxContext) diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/TokenDetails.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/TokenDetails.tsx new file mode 100644 index 0000000000..6f590decfa --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/TokenDetails.tsx @@ -0,0 +1,396 @@ +import {usePortfolioTokenDiscovery} from '@yoroi/portfolio' +import {useTheme} from '@yoroi/theme' +import {Portfolio} from '@yoroi/types' +import * as React from 'react' +import {Linking, ScrollView, StyleSheet, Text, TouchableOpacity, View} from 'react-native' + +import {Icon} from '../../../components/Icon' +import {MediaPreview} from '../../../components/MediaPreview/MediaPreview' +import {SimpleTab} from '../../../components/SimpleTab/SimpleTab' +import {Space} from '../../../components/Space/Space' +import {useCopy} from '../../../hooks/useCopy' +import {time} from '../../../kernel/constants' +import {isEmptyString} from '../../../kernel/utils' +import {useSelectedWallet} from '../../WalletManager/common/hooks/useSelectedWallet' +import {CopiableText} from './CopiableText' +import {useStrings} from './hooks/useStrings' + +export const TokenDetails = ({tokenInfo}: {tokenInfo: Portfolio.Token.Info}) => { + const {styles} = useStyles() + + return ( + +
+ + + + + + ) +} + +const Header = ({info}: {info: Portfolio.Token.Info}) => { + const {styles} = useStyles() + const [policyId, assetName] = info.id.split('.') + + const title = !isEmptyString(info.ticker) ? info.ticker : !isEmptyString(info.name) ? info.name : '' + + return ( + + + + + + {!isEmptyString(title) && {title}} + + {`(${assetName})`} + + + + + + + + + + ) +} + +const Info = ({info}: {info: Portfolio.Token.Info}) => { + const {styles} = useStyles() + const strings = useStrings() + const {wallet} = useSelectedWallet() + const [activeTab, setActiveTab] = React.useState<'overview' | 'json'>('overview') + + const {tokenDiscovery} = usePortfolioTokenDiscovery( + { + id: info.id, + network: wallet.networkManager.network, + getTokenDiscovery: wallet.networkManager.tokenManager.api.tokenDiscovery, + }, + { + staleTime: time.session, + }, + ) + + return ( + + + setActiveTab('overview')} + isActive={activeTab === 'overview'} + /> + + setActiveTab('json')} isActive={activeTab === 'json'} /> + + + + + {/* ↓↓↓ TABS CONTENT ↓↓↓ */} + + + + + + ) +} + +const Json = ({discovery, isActive}: {discovery?: Portfolio.Token.Discovery; isActive: boolean}) => { + const {styles, colors} = useStyles() + const strings = useStrings() + const [, copy] = useCopy() + + if (!isActive || !discovery) return null + + const stringifiedMetadata = JSON.stringify(discovery.originalMetadata, null, 2) + + return ( + + + {strings.metadata} + + copy(stringifiedMetadata)} activeOpacity={0.5}> + + + + + + + + {stringifiedMetadata} + + + ) +} + +const Overview = ({ + info, + discovery, + isActive, +}: { + info: Portfolio.Token.Info + discovery?: Portfolio.Token.Discovery + isActive: boolean +}) => { + if (!isActive) return null + + if (info.type === 'ft') { + return ( + + + + + + + + + + + + ) + } + return ( + + + + + + + + ) +} + +const PolicyId = ({policyId}: {policyId: string}) => { + const {styles} = useStyles() + const strings = useStrings() + + if (isEmptyString(policyId)) return null + + return ( + + {strings.policyId} + + + + + + {policyId} + + + + ) +} + +const Fingerprint = ({info}: {info: Portfolio.Token.Info}) => { + const {styles} = useStyles() + const strings = useStrings() + + if (isEmptyString(info.fingerprint)) return null + + return ( + + {strings.fingerprint} + + + + + + {info.fingerprint} + + + + ) +} + +const Name = ({info}: {info: Portfolio.Token.Info}) => { + const {styles} = useStyles() + const strings = useStrings() + + if (isEmptyString(info.name)) return null + + return ( + + {strings.name} + + {info.name} + + ) +} + +const TokenSupply = ({discovery}: {discovery?: Portfolio.Token.Discovery}) => { + const {styles} = useStyles() + const strings = useStrings() + + if (!discovery || isEmptyString(discovery.supply)) return null + + return ( + + + + + {strings.tokenSupply} + + {discovery.supply} + + + ) +} + +const Symbol = ({info}: {info: Portfolio.Token.Info}) => { + const {styles} = useStyles() + const strings = useStrings() + + if (isEmptyString(info.ticker)) return null + + return ( + + + + + {strings.symbol} + + {info.ticker} + + + ) +} + +const Description = ({info}: {info: Portfolio.Token.Info}) => { + const {styles} = useStyles() + const strings = useStrings() + + if (isEmptyString(info.description)) return null + + return ( + + + + {strings.description} + + {info.description} + + ) +} + +const Links = ({info}: {info: Portfolio.Token.Info}) => { + const {styles} = useStyles() + const {wallet} = useSelectedWallet() + const strings = useStrings() + + const handleOpenLink = async (direction: 'cardanoscan' | 'adaex') => { + if (info == null) return + if (direction === 'cardanoscan') { + await Linking.openURL(wallet.networkManager.explorers.cardanoscan.token(info.id)) + } else { + await Linking.openURL(wallet.networkManager.explorers.cexplorer.token(info.id)) + } + } + + return ( + + + + {strings.details} + + + handleOpenLink('cardanoscan')}> + Cardanoscan + + + handleOpenLink('adaex')}> + Adaex + + + + ) +} + +const Row = ({children}: {children: React.ReactNode}) => { + const {styles} = useStyles() + return {children} +} + +const useStyles = () => { + const {atoms, color} = useTheme() + + const styles = StyleSheet.create({ + root: { + ...atoms.flex_1, + ...atoms.px_lg, + }, + header: { + ...atoms.align_center, + }, + headerText: { + ...atoms.body_1_lg_medium, + ...atoms.text_center, + color: color.text_gray_medium, + maxWidth: 300, + }, + row: { + ...atoms.flex_row, + ...atoms.justify_between, + }, + label: { + ...atoms.body_2_md_regular, + color: color.text_gray_low, + }, + value: { + ...atoms.flex_1, + ...atoms.text_right, + ...atoms.body_2_md_regular, + color: color.text_gray_max, + }, + description: { + ...atoms.body_2_md_regular, + color: color.text_gray_max, + }, + tabs: { + ...atoms.flex_row, + }, + link: { + ...atoms.link_1_lg_underline, + color: color.text_primary_medium, + }, + linkGroup: { + ...atoms.flex_row, + ...atoms.gap_lg, + }, + copiableText: { + ...atoms.flex_1, + ...atoms.align_center, + }, + json: { + ...atoms.flex_1, + ...atoms.pt_lg, + borderRadius: 8, + backgroundColor: color.bg_color_min, + }, + jsonHeader: { + ...atoms.px_lg, + ...atoms.flex_row, + ...atoms.justify_between, + }, + jsonLabel: { + ...atoms.body_1_lg_medium, + color: color.text_gray_medium, + }, + jsonContent: { + ...atoms.px_lg, + }, + info: { + ...atoms.flex_1, + }, + metadata: { + ...atoms.body_2_md_regular, + color: color.text_gray_medium, + }, + }) + + const colors = { + copy: color.gray_900, + } + + return {styles, colors} as const +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/TokenItem.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/TokenItem.tsx new file mode 100644 index 0000000000..fabea93b81 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/TokenItem.tsx @@ -0,0 +1,100 @@ +import {useTheme} from '@yoroi/theme' +import {Portfolio} from '@yoroi/types' +import * as React from 'react' +import {StyleSheet, Text, TouchableOpacity, useWindowDimensions} from 'react-native' + +import {useModal} from '../../../components/Modal/ModalContext' +import {useStrings} from './hooks/useStrings' +import {TokenDetails} from './TokenDetails' + +export const TokenItem = ({ + tokenInfo, + isPrimaryToken = true, + isSent = true, + label, +}: { + tokenInfo: Portfolio.Token.Info + isPrimaryToken?: boolean + isSent?: boolean + label: string +}) => { + const {styles} = useStyles() + const strings = useStrings() + const {openModal} = useModal() + const {height: windowHeight} = useWindowDimensions() + + const handleShowTokenDetails = () => { + openModal(strings.tokenDetailsTitle, , windowHeight * 0.8) + } + + if (!isSent) + return ( + + + {label} + + + ) + + return ( + + {label} + + ) +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + sentTokenItem: { + ...atoms.flex, + ...atoms.flex_row, + ...atoms.align_center, + ...atoms.py_xs, + ...atoms.px_md, + borderRadius: 8, + backgroundColor: color.primary_500, + }, + receivedTokenItem: { + ...atoms.flex, + ...atoms.flex_row, + ...atoms.align_center, + ...atoms.py_xs, + ...atoms.px_md, + borderRadius: 8, + backgroundColor: color.secondary_300, + }, + tokenSentItemText: { + ...atoms.body_2_md_regular, + color: color.white_static, + }, + tokenReceivedItemText: { + ...atoms.body_2_md_regular, + color: color.text_gray_max, + }, + notPrimarySentTokenItem: { + backgroundColor: color.primary_100, + }, + notPrimaryReceivedTokenItem: { + backgroundColor: color.secondary_100, + }, + notPrimarySentTokenItemText: { + color: color.text_primary_medium, + }, + notPrimaryReceivedTokenItemText: { + color: color.secondary_700, + }, + }) + + return {styles} as const +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useAddressType.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useAddressType.tsx new file mode 100644 index 0000000000..6e054df944 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useAddressType.tsx @@ -0,0 +1,12 @@ +import {useQuery} from 'react-query' + +import {getAddressType} from '../../../../yoroi-wallets/cardano/utils' + +export const useAddressType = (address: string) => { + const query = useQuery(['useAddressType', address], () => getAddressType(address), { + suspense: true, + }) + + if (query.data === undefined) throw new Error('invalid address type') + return query.data +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx new file mode 100644 index 0000000000..7949d9541d --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx @@ -0,0 +1,230 @@ +import {invalid, isNonNullable} from '@yoroi/common' +import {infoExtractName} from '@yoroi/portfolio' +import {Portfolio} from '@yoroi/types' +import _ from 'lodash' +import {useQuery} from 'react-query' + +import {YoroiWallet} from '../../../../yoroi-wallets/cardano/types' +import {wrappedCsl} from '../../../../yoroi-wallets/cardano/wrappedCsl' +import {formatTokenWithText} from '../../../../yoroi-wallets/utils/format' +import {asQuantity} from '../../../../yoroi-wallets/utils/utils' +import {usePortfolioTokenInfos} from '../../../Portfolio/common/hooks/usePortfolioTokenInfos' +import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet' +import { + FormattedFee, + FormattedInputs, + FormattedOutputs, + TransactionBody, + TransactionInputs, + TransactionOutputs, +} from '../types' + +export const useFormattedTx = (data: TransactionBody) => { + const {wallet} = useSelectedWallet() + + const inputs = data?.inputs ?? [] + const outputs = data?.outputs ?? [] + + const inputTokenIds = inputs.flatMap((i) => { + const receiveUTxO = getUtxoByTxIdAndIndex(wallet, i.transaction_id, i.index) + return receiveUTxO?.assets.map((a) => `${a.policyId}.${a.assetId}` as Portfolio.Token.Id) ?? [] + }) + + const outputTokenIds = outputs.flatMap((o) => { + if (!o.amount.multiasset) return [] + const policyIds = Object.keys(o.amount.multiasset) + const tokenIds = policyIds.flatMap((policyId) => { + const assetIds = Object.keys(o.amount.multiasset?.[policyId] ?? {}) + return assetIds.map((assetId) => `${policyId}.${assetId}` as Portfolio.Token.Id) + }) + return tokenIds + }) + + const tokenIds = _.uniq([...inputTokenIds, ...outputTokenIds]) + const portfolioTokenInfos = usePortfolioTokenInfos({wallet, tokenIds}, {suspense: true}) + + const formattedInputs = useFormattedInputs(wallet, inputs, portfolioTokenInfos) + const formattedOutputs = useFormattedOutputs(wallet, outputs, portfolioTokenInfos) + const formattedFee = formatFee(wallet, data) + + return {inputs: formattedInputs, outputs: formattedOutputs, fee: formattedFee} +} + +export const useFormattedInputs = ( + wallet: YoroiWallet, + inputs: TransactionInputs, + tokenInfosResult: ReturnType, +) => { + const query = useQuery( + ['useFormattedInputs', inputs], + async () => formatInputs(wallet, inputs, tokenInfosResult), + { + suspense: true, + }, + ) + + if (!query.data) throw new Error('invalid formatted inputs') + return query.data +} + +export const useFormattedOutputs = ( + wallet: YoroiWallet, + outputs: TransactionOutputs, + portfolioTokenInfos: ReturnType, +) => { + const query = useQuery( + ['useFormattedOutputs', outputs], + () => formatOutputs(wallet, outputs, portfolioTokenInfos), + { + suspense: true, + }, + ) + + if (!query.data) throw new Error('invalid formatted outputs') + return query.data +} + +const formatInputs = async ( + wallet: YoroiWallet, + inputs: TransactionInputs, + portfolioTokenInfos: ReturnType, +): Promise => { + return Promise.all( + inputs.map(async (input) => { + const receiveUTxO = getUtxoByTxIdAndIndex(wallet, input.transaction_id, input.index) + const address = receiveUTxO?.receiver + const rewardAddress = + address !== undefined ? await deriveRewardAddressFromAddress(address, wallet.networkManager.chainId) : null + const coin = receiveUTxO?.amount != null ? asQuantity(receiveUTxO.amount) : null + + const primaryAssets = + coin != null + ? [ + { + tokenInfo: wallet.portfolioPrimaryTokenInfo, + name: wallet.portfolioPrimaryTokenInfo.name, + label: formatTokenWithText(coin, wallet.portfolioPrimaryTokenInfo), + quantity: coin, + isPrimary: true, + }, + ] + : [] + + const multiAssets = + receiveUTxO?.assets + .map((a) => { + const tokenInfo = portfolioTokenInfos.tokenInfos?.get(a.assetId as Portfolio.Token.Id) + if (!tokenInfo) return null + const quantity = asQuantity(a.amount) + + return { + tokenInfo, + name: infoExtractName(tokenInfo), + label: formatTokenWithText(quantity, tokenInfo), + quantity: quantity, + isPrimary: false, + } + }) + .filter(Boolean) ?? [] + + return { + assets: [...primaryAssets, ...multiAssets].filter(isNonNullable), + address, + rewardAddress, + ownAddress: address != null && isOwnedAddress(wallet, address), + txIndex: input.index, + txHash: input.transaction_id, + } + }), + ) +} + +const formatOutputs = async ( + wallet: YoroiWallet, + outputs: TransactionOutputs, + portfolioTokenInfos: ReturnType, +): Promise => { + return Promise.all( + outputs.map(async (output) => { + const address = output.address + const rewardAddress = await deriveRewardAddressFromAddress(address, wallet.networkManager.chainId) + const coin = asQuantity(output.amount.coin) + + const primaryAssets = [ + { + tokenInfo: wallet.portfolioPrimaryTokenInfo, + name: wallet.portfolioPrimaryTokenInfo.name, + label: formatTokenWithText(coin, wallet.portfolioPrimaryTokenInfo), + quantity: coin, + isPrimary: true, + }, + ] + + const multiAssets = output.amount.multiasset + ? Object.entries(output.amount.multiasset).flatMap(([policyId, assets]) => { + return Object.entries(assets).map(([assetId, amount]) => { + const tokenInfo = portfolioTokenInfos.tokenInfos?.get(`${policyId}.${assetId}`) + if (tokenInfo == null) return null + const quantity = asQuantity(amount) + + return { + tokenInfo, + name: infoExtractName(tokenInfo), + label: formatTokenWithText(quantity, tokenInfo), + quantity, + isPrimary: false, + } + }) + }) + : [] + + const assets = [...primaryAssets, ...multiAssets].filter(isNonNullable) + + return { + assets, + address, + rewardAddress, + ownAddress: isOwnedAddress(wallet, address), + } + }), + ) +} + +export const formatFee = (wallet: YoroiWallet, data: TransactionBody): FormattedFee => { + const fee = asQuantity(data?.fee ?? '0') + + return { + tokenInfo: wallet.portfolioPrimaryTokenInfo, + name: wallet.portfolioPrimaryTokenInfo.name, + label: formatTokenWithText(fee, wallet.portfolioPrimaryTokenInfo), + quantity: fee, + isPrimary: true, + } +} + +export const deriveRewardAddressFromAddress = async (address: string, chainId: number): Promise => { + const {csl, release} = wrappedCsl() + + try { + const result = await csl.Address.fromBech32(address) + .then((address) => csl.BaseAddress.fromAddress(address)) + .then((baseAddress) => baseAddress?.stakeCred() ?? invalid('invalid base address')) + .then((stakeCredential) => csl.RewardAddress.new(chainId, stakeCredential)) + .then((rewardAddress) => rewardAddress.toAddress()) + .then((rewardAddrAsAddress) => rewardAddrAsAddress.toBech32(undefined)) + .catch((error) => error) + + if (typeof result !== 'string') throw new Error('Its not possible to derive reward address') + return result + } finally { + release() + } +} + +const getUtxoByTxIdAndIndex = (wallet: YoroiWallet, txId: string, index: number) => { + return wallet.utxos.find((u) => u.tx_hash === txId && u.tx_index === index) +} + +const isOwnedAddress = (wallet: YoroiWallet, bech32Address: string) => { + return wallet.internalAddresses.includes(bech32Address) || wallet.externalAddresses.includes(bech32Address) +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useOnConfirm.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useOnConfirm.tsx new file mode 100644 index 0000000000..38188b6e93 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useOnConfirm.tsx @@ -0,0 +1,71 @@ +import * as React from 'react' + +import {ConfirmTxWithHwModal} from '../../../../components/ConfirmTxWithHwModal/ConfirmTxWithHwModal' +import {ConfirmTxWithOsModal} from '../../../../components/ConfirmTxWithOsModal/ConfirmTxWithOsModal' +import {ConfirmTxWithSpendingPasswordModal} from '../../../../components/ConfirmTxWithSpendingPasswordModal/ConfirmTxWithSpendingPasswordModal' +import {useModal} from '../../../../components/Modal/ModalContext' +import {YoroiSignedTx, YoroiUnsignedTx} from '../../../../yoroi-wallets/types/yoroi' +import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet' +import {useStrings} from './useStrings' + +// TODO: make it compatible with CBOR signing +export const useOnConfirm = ({ + unsignedTx, + onSuccess, + onError, + onNotSupportedCIP1694, +}: { + onSuccess?: ((txId: YoroiSignedTx) => void) | null + onError?: (() => void) | null + cbor?: string + unsignedTx?: YoroiUnsignedTx + onNotSupportedCIP1694?: () => void +}) => { + if (unsignedTx === undefined) throw new Error('useOnConfirm: unsignedTx missing') + + const {meta} = useSelectedWallet() + const {openModal, closeModal} = useModal() + const strings = useStrings() + + const onConfirm = () => { + if (meta.isHW) { + openModal( + strings.signTransaction, + onSuccess?.(signedTx)} + onNotSupportedCIP1694={onNotSupportedCIP1694} + />, + 400, + ) + return + } + + if (!meta.isHW && !meta.isEasyConfirmationEnabled) { + openModal( + strings.signTransaction, + onSuccess?.(signedTx)} + onError={onError ?? undefined} + />, + ) + return + } + + if (!meta.isHW && meta.isEasyConfirmationEnabled) { + openModal( + strings.signTransaction, + onSuccess?.(signedTx)} + onError={onError ?? undefined} + />, + ) + return + } + } + + return {onConfirm} as const +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx new file mode 100644 index 0000000000..9914016ec2 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx @@ -0,0 +1,139 @@ +import {defineMessages, useIntl} from 'react-intl' + +import {txLabels} from '../../../../kernel/i18n/global-messages' + +export const useStrings = () => { + const intl = useIntl() + + return { + signTransaction: intl.formatMessage(txLabels.signingTx), + confirm: intl.formatMessage(messages.confirm), + title: intl.formatMessage(messages.title), + utxosTab: intl.formatMessage(messages.utxosTab), + overviewTab: intl.formatMessage(messages.overviewTab), + walletLabel: intl.formatMessage(messages.walletLabel), + feeLabel: intl.formatMessage(messages.feeLabel), + myWalletLabel: intl.formatMessage(messages.myWalletLabel), + sendLabel: intl.formatMessage(messages.sendLabel), + receiveToLabel: intl.formatMessage(messages.receiveToLabel), + receiveToScriptLabel: intl.formatMessage(messages.receiveToScriptLabel), + utxosInputsLabel: intl.formatMessage(messages.utxosInputsLabel), + utxosOutputsLabel: intl.formatMessage(messages.utxosOutputsLabel), + utxosYourAddressLabel: intl.formatMessage(messages.utxosYourAddressLabel), + utxosForeignAddressLabel: intl.formatMessage(messages.utxosForeignAddressLabel), + overview: intl.formatMessage(messages.overview), + json: intl.formatMessage(messages.json), + metadata: intl.formatMessage(messages.metadata), + policyId: intl.formatMessage(messages.policyId), + fingerprint: intl.formatMessage(messages.fingerprint), + name: intl.formatMessage(messages.name), + tokenSupply: intl.formatMessage(messages.tokenSupply), + symbol: intl.formatMessage(messages.symbol), + description: intl.formatMessage(messages.description), + details: intl.formatMessage(messages.details), + tokenDetailsTitle: intl.formatMessage(messages.tokenDetailsTitle), + } +} + +const messages = defineMessages({ + confirm: { + id: 'txReview.confirm', + defaultMessage: '!!!Confirm', + }, + title: { + id: 'txReview.title', + defaultMessage: '!!!UTxOs', + }, + utxosTab: { + id: 'txReview.tabLabel.utxos', + defaultMessage: '!!!UTxOs', + }, + overviewTab: { + id: 'txReview.tabLabel.overview', + defaultMessage: '!!!Overview', + }, + walletLabel: { + id: 'txReview.overview.wallet', + defaultMessage: '!!!Wallet', + }, + feeLabel: { + id: 'txReview.fee', + defaultMessage: '!!!Fee', + }, + myWalletLabel: { + id: 'txReview.overview.myWalletLabel', + defaultMessage: '!!!Your Wallet', + }, + sendLabel: { + id: 'txReview.overview.sendLabel', + defaultMessage: '!!!Send', + }, + receiveToLabel: { + id: 'txReview.overview.receiveToLabel', + defaultMessage: '!!!To', + }, + receiveToScriptLabel: { + id: 'txReview.overview.receiveToScriptLabel', + defaultMessage: '!!!To script', + }, + utxosInputsLabel: { + id: 'txReview.utxos.utxosInputsLabel', + defaultMessage: '!!!Inputs', + }, + utxosOutputsLabel: { + id: 'txReview.utxos.utxosOutputsLabel', + defaultMessage: '!!!Outputs', + }, + utxosYourAddressLabel: { + id: 'txReview.utxos.utxosYourAddressLabel', + defaultMessage: '!!!Your address', + }, + utxosForeignAddressLabel: { + id: 'txReview.utxos.utxosForeignAddressLabel', + defaultMessage: '!!!Foreign address', + }, + overview: { + id: 'txReview.tokenDetails.overViewTab.title', + defaultMessage: '!!!Overview', + }, + json: { + id: 'txReview.tokenDetails.jsonTab.title', + defaultMessage: '!!!JSON', + }, + metadata: { + id: 'txReview.tokenDetails.jsonTab.metadata', + defaultMessage: '!!!Metadata', + }, + policyId: { + id: 'txReview.tokenDetails.policyId.label', + defaultMessage: '!!!Policy ID', + }, + fingerprint: { + id: 'txReview.tokenDetails.fingerprint.label', + defaultMessage: '!!!Fingerprint', + }, + name: { + id: 'txReview.tokenDetails.overViewTab.name.label', + defaultMessage: '!!!Name', + }, + tokenSupply: { + id: 'txReview.tokenDetails.overViewTab.tokenSupply.label', + defaultMessage: '!!!Token Supply', + }, + symbol: { + id: 'txReview.tokenDetails.overViewTab.symbol.label', + defaultMessage: '!!!Symbol', + }, + description: { + id: 'txReview.tokenDetails.overViewTab.description.label', + defaultMessage: '!!!Description', + }, + details: { + id: 'txReview.tokenDetails.overViewTab.details.label', + defaultMessage: '!!!Details on', + }, + tokenDetailsTitle: { + id: 'txReview.tokenDetails.title', + defaultMessage: '!!!Asset Details', + }, +}) diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useTxBody.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useTxBody.tsx new file mode 100644 index 0000000000..9de33bb075 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useTxBody.tsx @@ -0,0 +1,44 @@ +import {useQuery} from 'react-query' + +import {wrappedCsl} from '../../../../yoroi-wallets/cardano/wrappedCsl' +import {YoroiUnsignedTx} from '../../../../yoroi-wallets/types/yoroi' + +export const useTxBody = ({cbor, unsignedTx}: {cbor?: string; unsignedTx?: YoroiUnsignedTx}) => { + const query = useQuery( + ['useTxBody', cbor, unsignedTx], + async () => { + if (cbor !== undefined) { + return getCborTxBody(cbor) + } else if (unsignedTx !== undefined) { + return getUnsignedTxTxBody(unsignedTx) + } else { + throw new Error('useTxBody: missing cbor and unsignedTx') + } + }, + { + useErrorBoundary: true, + suspense: true, + }, + ) + + if (query.data === undefined) throw new Error('useTxBody: cannot extract txBody') + return query.data +} +const getCborTxBody = async (cbor: string) => { + const {csl, release} = wrappedCsl() + try { + const tx = await csl.Transaction.fromHex(cbor) + const jsonString = await tx.toJson() + return JSON.parse(jsonString).body + } finally { + release() + } +} + +const getUnsignedTxTxBody = async (unsignedTx: YoroiUnsignedTx) => { + const { + unsignedTx: {txBody}, + } = unsignedTx + const txBodyjson = await txBody.toJson() + return JSON.parse(txBodyjson) +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts b/apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts new file mode 100644 index 0000000000..12fbe9274a --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts @@ -0,0 +1,148 @@ +import {TransactionBody} from './types' + +export const adaTransactionSingleReceiver: TransactionBody = { + inputs: [ + { + transaction_id: '46fe71d85a733d970fe7bb8e6586624823803936d18c7e14601713d05b5b287a', + index: 0, + }, + { + transaction_id: '9638640d421875f068d10a0125023601bbd7e83e7f17b721c9c06c97cc29ff66', + index: 1, + }, + ], + outputs: [ + { + address: + 'addr1qyf4x8lvcyrwcxzkyz3lykyzfu7s7x307dlafgsu89qzge8lfl229ahk888cgakug24y86qtduvn065c3gw7dg5002cqdskm74', + amount: { + coin: '12000000', + multiasset: null, + }, + plutus_data: null, + script_ref: null, + }, + { + address: + 'addr1q9xy5p0cz2zsjrzpg4tl59mltjmfh07yc28alchxjlanygk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwqde82xg', + amount: { + coin: '23464562', + multiasset: null, + }, + plutus_data: null, + script_ref: null, + }, + ], + fee: '174345', + ttl: '220373661', + certs: null, + withdrawals: null, + update: null, + auxiliary_data_hash: null, + validity_start_interval: null, + mint: null, + script_data_hash: null, + collateral: null, + required_signers: null, + network_id: null, + collateral_return: null, + total_collateral: null, + reference_inputs: null, + voting_procedures: null, + voting_proposals: null, + donation: null, + current_treasury_value: null, +} + +export const multiAssetsTransactionOneReceiver: TransactionBody = { + inputs: [ + { + transaction_id: '46fe71d85a733d970fe7bb8e6586624823803936d18c7e14601713d05b5b287a', + index: 0, + }, + { + transaction_id: 'bddd3e0b43b9b93f6d49190a9d4d55c3cd28e3d270b0f1bbc0f83b8ecc3e373a', + index: 1, + }, + ], + outputs: [ + { + address: + 'addr1qyf4x8lvcyrwcxzkyz3lykyzfu7s7x307dlafgsu89qzge8lfl229ahk888cgakug24y86qtduvn065c3gw7dg5002cqdskm74', + amount: { + coin: '10000000', + multiasset: { + cdaaee586376139ee8c3cc4061623968810d177ca5c300afb890b48a: { + '43415354': '5', + }, + f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a: { + '000de1406a6176696275656e6f': '1', + }, + }, + }, + plutus_data: null, + script_ref: null, + }, + { + address: + 'addr1q9xy5p0cz2zsjrzpg4tl59mltjmfh07yc28alchxjlanygk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwqde82xg', + amount: { + coin: '2228270', + multiasset: { + '2441ab3351c3b80213a98f4e09ddcf7dabe4879c3c94cc4e7205cb63': { + '46495245': '2531', + }, + '279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f': { + '534e454b': '204', + }, + '4cb48d60d1f7823d1307c61b9ecf472ff78cf22d1ccc5786d59461f8': { + '4144414d4f4f4e': '4983996', + }, + a0028f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c235: { + '484f534b59': '115930085', + }, + cdaaee586376139ee8c3cc4061623968810d177ca5c300afb890b48a: { + '43415354': '4503', + }, + e0c4c2d7c4a0ed2cf786753fd845dee82c45512cee03e92adfd3fb8d: { + '6a6176696275656e6f2e616461': '1', + }, + fc411f546d01e88a822200243769bbc1e1fbdde8fa0f6c5179934edb: { + '6a6176696275656e6f': '1', + }, + }, + }, + plutus_data: null, + script_ref: null, + }, + { + address: + 'addr1q9xy5p0cz2zsjrzpg4tl59mltjmfh07yc28alchxjlanygk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwqde82xg', + amount: { + coin: '2300311', + multiasset: null, + }, + plutus_data: null, + script_ref: null, + }, + ], + fee: '189349', + ttl: '93045', + certs: null, + withdrawals: null, + update: null, + auxiliary_data_hash: null, + validity_start_interval: null, + mint: null, + script_data_hash: null, + collateral: null, + required_signers: null, + network_id: null, + collateral_return: null, + total_collateral: null, + reference_inputs: null, + voting_procedures: null, + voting_proposals: null, + donation: null, + current_treasury_value: null, +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/types.ts b/apps/wallet-mobile/src/features/ReviewTx/common/types.ts new file mode 100644 index 0000000000..45744a4cf6 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/common/types.ts @@ -0,0 +1,56 @@ +import { + TransactionBodyJSON, + TransactionInputsJSON, + TransactionOutputsJSON, +} from '@emurgo/cardano-serialization-lib-nodejs' +import {Balance, Portfolio} from '@yoroi/types' + +export type TransactionBody = TransactionBodyJSON +export type TransactionInputs = TransactionInputsJSON +export type TransactionOutputs = TransactionOutputsJSON + +export type FormattedInput = { + assets: Array<{ + tokenInfo: Portfolio.Token.Info + name: string + label: string + quantity: Balance.Quantity + isPrimary: boolean + }> + address: string | undefined + rewardAddress: string | null + ownAddress: boolean + txIndex: number + txHash: string +} + +export type FormattedInputs = Array + +export type FormattedOutput = { + assets: Array<{ + tokenInfo: Portfolio.Token.Info + name: string + label: string + quantity: Balance.Quantity + isPrimary: boolean + }> + address: string + rewardAddress: string | null + ownAddress: boolean +} + +export type FormattedOutputs = Array + +export type FormattedFee = { + tokenInfo: Portfolio.Token.Info + name: string + label: string + quantity: Balance.Quantity + isPrimary: boolean +} + +export type FormattedTx = { + inputs: FormattedInputs + outputs: FormattedOutputs + fee: FormattedFee +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/Overview/OverviewTab.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/Overview/OverviewTab.tsx new file mode 100644 index 0000000000..92278d03ba --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/Overview/OverviewTab.tsx @@ -0,0 +1,396 @@ +// 🚧 TODO: grouping by staking address 🚧 + +import {Blockies} from '@yoroi/identicon' +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {Linking, StyleSheet, Text, TouchableOpacity, View} from 'react-native' + +import {Icon} from '../../../../../components/Icon' +import {Space} from '../../../../../components/Space/Space' +import {formatTokenWithText} from '../../../../../yoroi-wallets/utils/format' +import {Quantities} from '../../../../../yoroi-wallets/utils/utils' +import {useSelectedWallet} from '../../../../WalletManager/common/hooks/useSelectedWallet' +import {useWalletManager} from '../../../../WalletManager/context/WalletManagerProvider' +import {Accordion} from '../../../common/Accordion' +import {CopiableText} from '../../../common/CopiableText' +import {Divider} from '../../../common/Divider' +import {useAddressType} from '../../../common/hooks/useAddressType' +import {useStrings} from '../../../common/hooks/useStrings' +import {ReviewTxState} from '../../../common/ReviewTxProvider' +import {TokenItem} from '../../../common/TokenItem' +import {FormattedOutputs, FormattedTx} from '../../../common/types' + +export const OverviewTab = ({tx, operations}: {tx: FormattedTx; operations: ReviewTxState['operations']}) => { + const {styles} = useStyles() + + const notOwnedOutputs = React.useMemo(() => tx.outputs.filter((output) => !output.ownAddress), [tx.outputs]) + const ownedOutputs = React.useMemo(() => tx.outputs.filter((output) => output.ownAddress), [tx.outputs]) + + return ( + + + + + + + + + + + + ) +} + +const WalletInfoSection = ({tx}: {tx: FormattedTx}) => { + const {styles} = useStyles() + const strings = useStrings() + const {wallet, meta} = useSelectedWallet() + const {walletManager} = useWalletManager() + const {plate, seed} = walletManager.checksum(wallet.publicKeyHex) + const seedImage = new Blockies({seed}).asBase64() + + return ( + <> + + {strings.walletLabel} + + + + + + + {`${plate} | ${meta.name}`} + + + + + + + + ) +} + +const FeeInfoItem = ({fee}: {fee: string}) => { + const {styles} = useStyles() + const strings = useStrings() + + return ( + + {strings.feeLabel} + + {`-${fee}`} + + ) +} + +const SenderSection = ({ + tx, + notOwnedOutputs, + ownedOutputs, +}: { + tx: FormattedTx + notOwnedOutputs: FormattedOutputs + ownedOutputs: FormattedOutputs +}) => { + const strings = useStrings() + const {styles} = useStyles() + const address = ownedOutputs[0]?.rewardAddress ?? ownedOutputs[0]?.address + + return ( + + + + + + {address} + + + + + + + + {notOwnedOutputs.length === 1 && } + + ) +} + +// 🚧 TODO: ADD MULTIRECEIVER SUPPORT 🚧 +const SenderTokens = ({tx, notOwnedOutputs}: {tx: FormattedTx; notOwnedOutputs: FormattedOutputs}) => { + const {styles} = useStyles() + + const {wallet} = useSelectedWallet() + + const totalPrimaryTokenSent = React.useMemo( + () => + notOwnedOutputs + .flatMap((output) => output.assets.filter((asset) => asset.isPrimary)) + .reduce((previous, current) => Quantities.sum([previous, current.quantity]), Quantities.zero), + [notOwnedOutputs], + ) + const totalPrimaryTokenSpent = React.useMemo( + () => Quantities.sum([totalPrimaryTokenSent, tx.fee.quantity]), + [totalPrimaryTokenSent, tx.fee.quantity], + ) + const totalPrimaryTokenSpentLabel = formatTokenWithText(totalPrimaryTokenSpent, wallet.portfolioPrimaryTokenInfo) + + const notPrimaryTokenSent = React.useMemo( + () => notOwnedOutputs.flatMap((output) => output.assets.filter((asset) => !asset.isPrimary)), + [notOwnedOutputs], + ) + + return ( + + + + + + + + + {notPrimaryTokenSent.map((token, index) => ( + + ))} + + + ) +} + +const SenderSectionLabel = () => { + const {styles, colors} = useStyles() + const strings = useStrings() + + return ( + + + + + + {strings.sendLabel} + + ) +} + +const ReceiverSection = ({notOwnedOutputs}: {notOwnedOutputs: FormattedOutputs}) => { + const address = notOwnedOutputs[0]?.rewardAddress ?? notOwnedOutputs[0]?.address + const {styles} = useStyles() + const strings = useStrings() + const addressType = useAddressType(address) + const isScriptAddress = addressType === 'script' + + return ( + <> + + + + {isScriptAddress ? strings.receiveToScriptLabel : strings.receiveToLabel}: + + + + {address} + + + + + ) +} + +const OperationsSection = ({operations}: {operations: ReviewTxState['operations']}) => { + if (operations === null || (Array.isArray(operations) && operations.length === 0)) return null + + return ( + + + + + + + {operations.map((operation, index) => { + if (index === 0) return operation + + return ( + <> + + + {operation} + + ) + })} + + + ) +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + root: { + ...atoms.flex_1, + ...atoms.px_lg, + backgroundColor: color.bg_color_max, + }, + infoItem: { + ...atoms.flex_row, + ...atoms.justify_between, + }, + infoLabel: { + ...atoms.body_2_md_regular, + color: color.gray_600, + }, + walletInfoText: { + ...atoms.body_2_md_medium, + color: color.text_primary_medium, + }, + plate: { + ...atoms.flex_row, + ...atoms.align_center, + }, + fee: { + color: color.gray_900, + ...atoms.body_2_md_regular, + }, + link: { + color: color.text_primary_medium, + ...atoms.body_2_md_medium, + }, + receiverAddress: { + ...atoms.flex_row, + ...atoms.align_center, + ...atoms.flex_row, + ...atoms.justify_between, + }, + tokenSectionLabel: { + ...atoms.body_2_md_regular, + color: color.gray_900, + }, + senderTokenItems: { + ...atoms.flex_wrap, + ...atoms.flex_row, + ...atoms.justify_end, + ...atoms.flex_1, + ...atoms.gap_sm, + }, + tokensSection: { + ...atoms.flex_row, + ...atoms.justify_between, + }, + tokensSectionLabel: { + ...atoms.flex_row, + ...atoms.align_center, + }, + walletChecksum: { + width: 24, + height: 24, + }, + receiverSectionAddress: { + maxWidth: 260, + }, + addressText: { + ...atoms.flex_1, + ...atoms.body_2_md_regular, + color: color.text_gray_medium, + }, + }) + + const colors = { + send: color.primary_500, + received: color.green_static, + } + + return {styles, colors} as const +} + +// 🚧 WORK IN PROGRESS BELOW 🚧 + +// 🚧 TODO: WIP 🚧 +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const CreatedByInfoItem = () => { + const {styles} = useStyles() + + return ( + + Created By + + + {/* */} + + + + Linking.openURL('https://google.com')}> + dapp.org + + + + ) +} + +// 🚧 TODO: WIP 🚧 +// eslint-disable-next-line @typescript-eslint/no-unused-vars +/* const ReceiverTokensSectionMultiReceiver = () => { + const {styles} = useStyles() + + return ( + <> + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} */ + +// 🚧 TODO: WIP 🚧 +/* const ReceiverSectionLabel = () => { + const {styles, colors} = useStyles() + + return ( + + + + + + Receive + + ) +} */ diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx new file mode 100644 index 0000000000..c1d728dbe5 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx @@ -0,0 +1,158 @@ +import {createMaterialTopTabNavigator, MaterialTopTabBarProps} from '@react-navigation/material-top-tabs' +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {FlatList, ScrollView, StyleSheet, Text, TouchableOpacity, TouchableOpacityProps, View} from 'react-native' + +import {Button} from '../../../../components/Button/Button' +import {SafeArea} from '../../../../components/SafeArea' +import {useFormattedTx} from '../../common/hooks/useFormattedTx' +import {useOnConfirm} from '../../common/hooks/useOnConfirm' +import {useStrings} from '../../common/hooks/useStrings' +import {useTxBody} from '../../common/hooks/useTxBody' +import {useReviewTx} from '../../common/ReviewTxProvider' +import {OverviewTab} from './Overview/OverviewTab' +import {UTxOsTab} from './UTxOs/UTxOsTab' + +const MaterialTab = createMaterialTopTabNavigator() + +export const ReviewTxScreen = () => { + const {styles} = useStyles() + const strings = useStrings() + const {unsignedTx, operations, onSuccess, onError} = useReviewTx() + + if (unsignedTx === null) throw new Error('ReviewTxScreen: missing unsignedTx') + + const {onConfirm} = useOnConfirm({ + unsignedTx, + onSuccess, + onError, + }) + + // TODO: add cbor arguments + const txBody = useTxBody({unsignedTx}) + const formatedTx = useFormattedTx(txBody) + + const OverViewTabMemo = React.memo(() => ) + const UTxOsTabMemo = React.memo(() => ) + + return ( + + + + {() => ( + /* TODO: make scrollview general to use button border */ + + + + )} + + + + {() => ( + /* TODO: make scrollview general to use button border */ + + + + )} + + + + +