diff --git a/apps/wallet-mobile/.storybook/storybook.requires.js b/apps/wallet-mobile/.storybook/storybook.requires.js index 7dd7477910..41fde2e017 100644 --- a/apps/wallet-mobile/.storybook/storybook.requires.js +++ b/apps/wallet-mobile/.storybook/storybook.requires.js @@ -241,10 +241,9 @@ const getStories = () => { "./src/features/Swap/common/LiquidityPool/LiquidityPool.stories.tsx": require("../src/features/Swap/common/LiquidityPool/LiquidityPool.stories.tsx"), "./src/features/Swap/common/SelectPool/SelectPoolFromList/SelectPoolFromList.stories.tsx": require("../src/features/Swap/common/SelectPool/SelectPoolFromList/SelectPoolFromList.stories.tsx"), "./src/features/Swap/common/ServiceUnavailable/ServiceUnavailable.stories.tsx": require("../src/features/Swap/common/ServiceUnavailable/ServiceUnavailable.stories.tsx"), - "./src/features/Swap/useCases/ConfirmTxScreen/ConfirmTxScreen.stories.tsx": require("../src/features/Swap/useCases/ConfirmTxScreen/ConfirmTxScreen.stories.tsx"), "./src/features/Swap/useCases/ConfirmTxScreen/ShowFailedTxScreen/ShowFailedTxScreen.stories.tsx": require("../src/features/Swap/useCases/ConfirmTxScreen/ShowFailedTxScreen/ShowFailedTxScreen.stories.tsx"), "./src/features/Swap/useCases/ConfirmTxScreen/ShowSubmittedTxScreen/ShowSubmittedTxScreen.stories.tsx": require("../src/features/Swap/useCases/ConfirmTxScreen/ShowSubmittedTxScreen/ShowSubmittedTxScreen.stories.tsx"), - "./src/features/Swap/useCases/ConfirmTxScreen/TransactionSummary.stories.tsx": require("../src/features/Swap/useCases/ConfirmTxScreen/TransactionSummary.stories.tsx"), + "./src/features/Swap/useCases/ReviewSwap/TransactionSummary.stories.tsx": require("../src/features/Swap/useCases/ReviewSwap/TransactionSummary.stories.tsx"), "./src/features/Swap/useCases/ShowPreprodNoticeScreen/ShowPreprodNoticeScreen.stories.tsx": require("../src/features/Swap/useCases/ShowPreprodNoticeScreen/ShowPreprodNoticeScreen.stories.tsx"), "./src/features/Swap/useCases/ShowSanchoNoticeScreen/ShowSanchoNoticeScreen.stories.tsx": require("../src/features/Swap/useCases/ShowSanchoNoticeScreen/ShowSanchoNoticeScreen.stories.tsx"), "./src/features/Swap/useCases/StartOrderSwapScreen/CreateOrder/Actions/AmountActions/AmountActions.stories.tsx": require("../src/features/Swap/useCases/StartOrderSwapScreen/CreateOrder/Actions/AmountActions/AmountActions.stories.tsx"), diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/Accordion.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/Accordion.tsx index 92e7bfb3ac..7256cff2d0 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/Accordion.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/common/Accordion.tsx @@ -1,6 +1,6 @@ import {useTheme} from '@yoroi/theme' import * as React from 'react' -import {Animated, LayoutAnimation, StyleSheet, Text, TouchableOpacity, View} from 'react-native' +import {Animated, Easing, StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {Icon} from '../../../components/Icon' @@ -10,12 +10,12 @@ export const Accordion = ({label, children}: {label: string; children: React.Rea 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, + easing: Easing.ease, }).start() } diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/CopiableText.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/CopiableText.tsx index 9d2865f4c1..45dea2a1e6 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/CopiableText.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/common/CopiableText.tsx @@ -1,25 +1,40 @@ import {useTheme} from '@yoroi/theme' import * as React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {StyleSheet, TouchableOpacity, View, ViewStyle} from 'react-native' import {useCopy} from '../../../components/Clipboard/ClipboardProvider' import {Icon} from '../../../components/Icon' -export const CopiableText = ({children, textToCopy}: {children: React.ReactNode; textToCopy: string}) => { - const {styles, colors} = useStyles() - const {copy} = useCopy() +export const CopiableText = ({ + children, + style, + textToCopy, +}: { + children: React.ReactNode + style?: ViewStyle + textToCopy: string +}) => { + const {styles} = useStyles() return ( - + {children} - copy({text: textToCopy})} activeOpacity={0.5}> - - + ) } +export const CopyButton = ({textToCopy}: {textToCopy: string}) => { + const {colors} = useStyles() + const {copy} = useCopy() + return ( + copy({text: textToCopy})} activeOpacity={0.5}> + + + ) +} + const useStyles = () => { const {atoms, color} = useTheme() const styles = StyleSheet.create({ diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/ReviewTxProvider.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/ReviewTxProvider.tsx index 05dd6ec924..93f5632b61 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/ReviewTxProvider.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/common/ReviewTxProvider.tsx @@ -24,6 +24,9 @@ export const ReviewTxProvider = ({ cborChanged: (cbor: ReviewTxState['cbor']) => dispatch({type: ReviewTxActionType.CborChanged, cbor}), operationsChanged: (operations: ReviewTxState['operations']) => dispatch({type: ReviewTxActionType.OperationsChanged, operations}), + customReceiverTitleChanged: (customReceiverTitle: ReviewTxState['customReceiverTitle']) => + dispatch({type: ReviewTxActionType.CustomReceiverTitleChanged, customReceiverTitle}), + detailsChanged: (details: ReviewTxState['details']) => dispatch({type: ReviewTxActionType.DetailsChanged, details}), onSuccessChanged: (onSuccess: ReviewTxState['onSuccess']) => dispatch({type: ReviewTxActionType.OnSuccessChanged, onSuccess}), onErrorChanged: (onError: ReviewTxState['onError']) => dispatch({type: ReviewTxActionType.OnErrorChanged, onError}), @@ -55,6 +58,14 @@ const reviewTxReducer = (state: ReviewTxState, action: ReviewTxAction) => { draft.operations = action.operations break + case ReviewTxActionType.CustomReceiverTitleChanged: + draft.customReceiverTitle = action.customReceiverTitle + break + + case ReviewTxActionType.DetailsChanged: + draft.details = action.details + break + case ReviewTxActionType.OnSuccessChanged: draft.onSuccess = action.onSuccess break @@ -82,6 +93,14 @@ type ReviewTxAction = type: ReviewTxActionType.OperationsChanged operations: ReviewTxState['operations'] } + | { + type: ReviewTxActionType.CustomReceiverTitleChanged + customReceiverTitle: ReviewTxState['customReceiverTitle'] + } + | { + type: ReviewTxActionType.DetailsChanged + details: ReviewTxState['details'] + } | { type: ReviewTxActionType.OnSuccessChanged onSuccess: ReviewTxState['onSuccess'] @@ -95,6 +114,8 @@ export type ReviewTxState = { unsignedTx: YoroiUnsignedTx | null cbor: string | null operations: Array | null + customReceiverTitle: React.ReactNode | null + details: {title: string; component: React.ReactNode} | null onSuccess: ((signedTx: YoroiSignedTx) => void) | null onError: (() => void) | null } @@ -103,6 +124,8 @@ type ReviewTxActions = { unsignedTxChanged: (unsignedTx: ReviewTxState['unsignedTx']) => void cborChanged: (cbor: ReviewTxState['cbor']) => void operationsChanged: (operations: ReviewTxState['operations']) => void + customReceiverTitleChanged: (customReceiverTitle: ReviewTxState['customReceiverTitle']) => void + detailsChanged: (details: ReviewTxState['details']) => void onSuccessChanged: (onSuccess: ReviewTxState['onSuccess']) => void onErrorChanged: (onError: ReviewTxState['onError']) => void } @@ -111,6 +134,8 @@ const defaultState: ReviewTxState = Object.freeze({ unsignedTx: null, cbor: null, operations: null, + customReceiverTitle: null, + details: null, onSuccess: null, onError: null, }) @@ -124,6 +149,8 @@ const initialReviewTxContext: ReviewTxContext = { unsignedTxChanged: missingInit, cborChanged: missingInit, operationsChanged: missingInit, + customReceiverTitleChanged: missingInit, + detailsChanged: missingInit, onSuccessChanged: missingInit, onErrorChanged: missingInit, } @@ -132,6 +159,8 @@ enum ReviewTxActionType { UnsignedTxChanged = 'unsignedTxChanged', CborChanged = 'cborChanged', OperationsChanged = 'operationsChanged', + CustomReceiverTitleChanged = 'customReceiverTitleChanged', + DetailsChanged = 'detailsChanged', OnSuccessChanged = 'onSuccessChanged', OnErrorChanged = 'onErrorChanged', } diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useAddressType.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useAddressType.tsx deleted file mode 100644 index 6e054df944..0000000000 --- a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useAddressType.tsx +++ /dev/null @@ -1,12 +0,0 @@ -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/useFormattedMetadata.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedMetadata.tsx index 40e8e12323..7b67530d7c 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedMetadata.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedMetadata.tsx @@ -3,10 +3,11 @@ import {TransactionBody} from '../types' export const formatMetadata = (unsignedTx: YoroiUnsignedTx, txBody: TransactionBody) => { const hash = txBody.auxiliary_data_hash ?? null - const metadata = unsignedTx.metadata?.['674'] ?? null + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const metadata = unsignedTx.metadata?.['674']?.['msg' as any] ?? null return { hash, - metadata, + metadata: {msg: [JSON.parse(metadata)]}, } } diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx index ba1a3f87d9..91ef9f2d30 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx @@ -1,3 +1,5 @@ +// import {CredKind} from '@emurgo/csl-mobile-bridge' +import {CredKind} from '@emurgo/cross-csl-core' import {isNonNullable} from '@yoroi/common' import {infoExtractName} from '@yoroi/portfolio' import {Portfolio} from '@yoroi/types' @@ -6,6 +8,7 @@ import {useQuery} from 'react-query' import {YoroiWallet} from '../../../../yoroi-wallets/cardano/types' import {deriveRewardAddressFromAddress} from '../../../../yoroi-wallets/cardano/utils' +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' @@ -14,12 +17,13 @@ import { FormattedFee, FormattedInputs, FormattedOutputs, + FormattedTx, TransactionBody, TransactionInputs, TransactionOutputs, } from '../types' -export const useFormattedTx = (data: TransactionBody) => { +export const useFormattedTx = (data: TransactionBody): FormattedTx => { const {wallet} = useSelectedWallet() const inputs = data?.inputs ?? [] @@ -93,10 +97,14 @@ const formatInputs = async ( 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 addressKind = address != null ? await getAddressKind(address) : null + const rewardAddress = + address != null && addressKind === CredKind.Key + ? await deriveAddress(address, wallet.networkManager.chainId) + : null + const primaryAssets = coin != null ? [ @@ -130,6 +138,7 @@ const formatInputs = async ( return { assets: [...primaryAssets, ...multiAssets].filter(isNonNullable), address, + addressKind: addressKind ?? null, rewardAddress, ownAddress: address != null && isOwnedAddress(wallet, address), txIndex: input.index, @@ -147,9 +156,12 @@ const formatOutputs = async ( 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 addressKind = await getAddressKind(address) + const rewardAddress = + addressKind === CredKind.Key ? await deriveAddress(address, wallet.networkManager.chainId) : null + const primaryAssets = [ { tokenInfo: wallet.portfolioPrimaryTokenInfo, @@ -183,6 +195,7 @@ const formatOutputs = async ( return { assets, address, + addressKind, rewardAddress, ownAddress: isOwnedAddress(wallet, address), } @@ -202,6 +215,26 @@ export const formatFee = (wallet: YoroiWallet, data: TransactionBody): Formatted } } +const deriveAddress = async (address: string, chainId: number) => { + try { + return await deriveRewardAddressFromAddress(address, chainId) + } catch { + return null + } +} + +const getAddressKind = async (addressBech32: string): Promise => { + const {csl, release} = wrappedCsl() + + try { + const address = await csl.Address.fromBech32(addressBech32) + const addressKind = await (await address.paymentCred())?.kind() + return addressKind ?? null + } finally { + release() + } +} + const getUtxoByTxIdAndIndex = (wallet: YoroiWallet, txId: string, index: number) => { return wallet.utxos.find((u) => u.tx_hash === txId && u.tx_index === index) } diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx index 9914016ec2..b6e245d124 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx @@ -11,6 +11,9 @@ export const useStrings = () => { title: intl.formatMessage(messages.title), utxosTab: intl.formatMessage(messages.utxosTab), overviewTab: intl.formatMessage(messages.overviewTab), + metadataTab: intl.formatMessage(messages.metadataTab), + metadataHash: intl.formatMessage(messages.metadataHash), + metadataJsonLabel: intl.formatMessage(messages.metadataJsonLabel), walletLabel: intl.formatMessage(messages.walletLabel), feeLabel: intl.formatMessage(messages.feeLabel), myWalletLabel: intl.formatMessage(messages.myWalletLabel), @@ -52,6 +55,18 @@ const messages = defineMessages({ id: 'txReview.tabLabel.overview', defaultMessage: '!!!Overview', }, + metadataTab: { + id: 'txReview.tabLabel.metadataTab', + defaultMessage: '!!!Metadata', + }, + metadataHash: { + id: 'txReview.metadata.metadataHash', + defaultMessage: '!!!Metadata hash', + }, + metadataJsonLabel: { + id: 'txReview.metadata.metadataJsonLabel', + defaultMessage: '!!!Metadata', + }, walletLabel: { id: 'txReview.overview.wallet', defaultMessage: '!!!Wallet', diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useTxBody.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useTxBody.tsx index 9de33bb075..75588bd47a 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useTxBody.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useTxBody.tsx @@ -2,8 +2,9 @@ import {useQuery} from 'react-query' import {wrappedCsl} from '../../../../yoroi-wallets/cardano/wrappedCsl' import {YoroiUnsignedTx} from '../../../../yoroi-wallets/types/yoroi' +import {TransactionBody} from '../types' -export const useTxBody = ({cbor, unsignedTx}: {cbor?: string; unsignedTx?: YoroiUnsignedTx}) => { +export const useTxBody = ({cbor, unsignedTx}: {cbor?: string; unsignedTx?: YoroiUnsignedTx}): TransactionBody => { const query = useQuery( ['useTxBody', cbor, unsignedTx], async () => { diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/types.ts b/apps/wallet-mobile/src/features/ReviewTx/common/types.ts index 45744a4cf6..74cb9b2451 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/types.ts +++ b/apps/wallet-mobile/src/features/ReviewTx/common/types.ts @@ -3,6 +3,7 @@ import { TransactionInputsJSON, TransactionOutputsJSON, } from '@emurgo/cardano-serialization-lib-nodejs' +import {CredKind} from '@emurgo/cross-csl-core' import {Balance, Portfolio} from '@yoroi/types' export type TransactionBody = TransactionBodyJSON @@ -18,6 +19,7 @@ export type FormattedInput = { isPrimary: boolean }> address: string | undefined + addressKind: CredKind | null rewardAddress: string | null ownAddress: boolean txIndex: number @@ -35,6 +37,7 @@ export type FormattedOutput = { isPrimary: boolean }> address: string + addressKind: CredKind | null rewardAddress: string | null ownAddress: boolean } @@ -54,3 +57,8 @@ export type FormattedTx = { outputs: FormattedOutputs fee: FormattedFee } + +export type Metadata = { + json: string | null + hash: string | null +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/Metadata/MetadataTab.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/Metadata/MetadataTab.tsx index fb22ac5bed..c60e4d1c60 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/Metadata/MetadataTab.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/Metadata/MetadataTab.tsx @@ -8,7 +8,7 @@ import {useStrings} from '../../../common/hooks/useStrings' type Props = { hash: string | null - metadata: string | null + metadata: {msg: Array} | null } export const MetadataTab = ({metadata, hash}: Props) => { 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 index dc1039784a..cd04aeeb68 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/Overview/OverviewTab.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/Overview/OverviewTab.tsx @@ -1,5 +1,6 @@ // 🚧 TODO: grouping by staking address 🚧 +import {CredKind} from '@emurgo/cross-csl-core' import {Blockies} from '@yoroi/identicon' import {useTheme} from '@yoroi/theme' import * as React from 'react' @@ -7,6 +8,7 @@ import {Linking, StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {Divider} from '../../../../../components/Divider/Divider' import {Icon} from '../../../../../components/Icon' +import {useModal} from '../../../../../components/Modal/ModalContext' import {Space} from '../../../../../components/Space/Space' import {formatTokenWithText} from '../../../../../yoroi-wallets/utils/format' import {Quantities} from '../../../../../yoroi-wallets/utils/utils' @@ -14,13 +16,20 @@ import {useSelectedWallet} from '../../../../WalletManager/common/hooks/useSelec import {useWalletManager} from '../../../../WalletManager/context/WalletManagerProvider' import {Accordion} from '../../../common/Accordion' import {CopiableText} from '../../../common/CopiableText' -import {useAddressType} from '../../../common/hooks/useAddressType' import {useStrings} from '../../../common/hooks/useStrings' -import {ReviewTxState} from '../../../common/ReviewTxProvider' +import {ReviewTxState, useReviewTx} from '../../../common/ReviewTxProvider' import {TokenItem} from '../../../common/TokenItem' import {FormattedOutputs, FormattedTx} from '../../../common/types' -export const OverviewTab = ({tx, operations}: {tx: FormattedTx; operations: ReviewTxState['operations']}) => { +export const OverviewTab = ({ + tx, + operations, + details, +}: { + tx: FormattedTx + operations: ReviewTxState['operations'] + details: ReviewTxState['details'] +}) => { const {styles} = useStyles() const notOwnedOutputs = React.useMemo(() => tx.outputs.filter((output) => !output.ownAddress), [tx.outputs]) @@ -37,6 +46,8 @@ export const OverviewTab = ({tx, operations}: {tx: FormattedTx; operations: Revi + +
) } @@ -94,7 +105,7 @@ const SenderSection = ({ }) => { const strings = useStrings() const {styles} = useStyles() - const address = ownedOutputs[0]?.rewardAddress ?? ownedOutputs[0]?.address + const address = ownedOutputs[0]?.rewardAddress ?? ownedOutputs[0]?.address ?? '-' return ( @@ -172,24 +183,30 @@ const SenderSectionLabel = () => { } const ReceiverSection = ({notOwnedOutputs}: {notOwnedOutputs: FormattedOutputs}) => { - const address = notOwnedOutputs[0]?.rewardAddress ?? notOwnedOutputs[0]?.address + const address = notOwnedOutputs[0]?.rewardAddress ?? notOwnedOutputs[0]?.address ?? '-' const {styles} = useStyles() const strings = useStrings() - const addressType = useAddressType(address) - const isScriptAddress = addressType === 'script' + const {customReceiverTitle} = useReviewTx() return ( <> - {isScriptAddress ? strings.receiveToScriptLabel : strings.receiveToLabel}: + + {notOwnedOutputs[0]?.addressKind === CredKind.Script && customReceiverTitle == null + ? strings.receiveToScriptLabel + : strings.receiveToLabel} + : + - - - {address} - - + {customReceiverTitle ?? ( + + + {address} + + + )} ) @@ -221,6 +238,29 @@ const OperationsSection = ({operations}: {operations: ReviewTxState['operations' ) } +const Details = ({details}: {details: ReviewTxState['details']}) => { + const {openModal} = useModal() + const {styles} = useStyles() + + if (details == null) return null + + const handleOnPress = () => { + openModal(details.title ?? '', {details.component}, 550) + } + + return ( + + + + + + {details?.title} + + + + ) +} + const useStyles = () => { const {atoms, color} = useTheme() const styles = StyleSheet.create({ @@ -290,6 +330,18 @@ const useStyles = () => { ...atoms.body_2_md_regular, color: color.text_gray_medium, }, + detailsRow: { + ...atoms.flex_1, + ...atoms.flex_row, + ...atoms.justify_end, + }, + details: { + ...atoms.px_lg, + }, + detailsButton: { + ...atoms.body_2_md_medium, + color: color.text_primary_medium, + }, }) const colors = { diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx index 035dc332c2..9b0550327d 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx @@ -1,15 +1,28 @@ 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 { + FlatList, + StyleProp, + StyleSheet, + Text, + TouchableOpacity, + TouchableOpacityProps, + View, + ViewStyle, +} from 'react-native' import {Button} from '../../../../components/Button/Button' import {SafeArea} from '../../../../components/SafeArea' +import {ScrollView} from '../../../../components/ScrollView/ScrollView' +import {isEmptyString} from '../../../../kernel/utils' +import {formatMetadata} from '../../common/hooks/useFormattedMetadata' 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 {MetadataTab} from './Metadata/MetadataTab' import {OverviewTab} from './Overview/OverviewTab' import {UTxOsTab} from './UTxOs/UTxOsTab' @@ -18,7 +31,7 @@ const MaterialTab = createMaterialTopTabNavigator() export const ReviewTxScreen = () => { const {styles} = useStyles() const strings = useStrings() - const {unsignedTx, operations, onSuccess, onError} = useReviewTx() + const {unsignedTx, operations, details, onSuccess, onError} = useReviewTx() if (unsignedTx === null) throw new Error('ReviewTxScreen: missing unsignedTx') @@ -28,21 +41,27 @@ export const ReviewTxScreen = () => { onError, }) - // TODO: add cbor arguments + // TODO: apply cbor const txBody = useTxBody({unsignedTx}) const formatedTx = useFormattedTx(txBody) + const formattedMetadata = formatMetadata(unsignedTx, txBody) - const OverViewTabMemo = React.memo(() => ) - const UTxOsTabMemo = React.memo(() => ) + const tabsData = [ + [strings.overviewTab, 'overview'], + [strings.utxosTab, 'utxos'], + ] + + if (!isEmptyString(formattedMetadata.hash) && formattedMetadata.metadata != null) + tabsData.push([strings.metadataTab, 'metadata']) return ( - + }> {() => ( /* TODO: make scrollview general to use button border */ - + )} @@ -51,7 +70,16 @@ export const ReviewTxScreen = () => { {() => ( /* TODO: make scrollview general to use button border */ - + + + )} + + + + {() => ( + /* TODO: make scrollview general to use button border */ + + )} @@ -64,16 +92,12 @@ export const ReviewTxScreen = () => { ) } -const TabBar = ({navigation, state}: MaterialTopTabBarProps) => { +const TabBar = ({navigation, state, tabsData}: MaterialTopTabBarProps & {tabsData: Array>}) => { const {styles} = useStyles() - const strings = useStrings() return ( ( navigation.navigate(key)} /> )} @@ -105,9 +129,9 @@ export const Tab = ({ ) } -const Actions = ({children}: {children: React.ReactNode}) => { +const Actions = ({children, style}: {children: React.ReactNode; style?: StyleProp}) => { const {styles} = useStyles() - return {children} + return {children} } const useStyles = () => { @@ -154,5 +178,8 @@ const useStyles = () => { }, }) - return {styles} as const + const colors = { + lightGray: color.gray_200, + } + return {styles, colors} as const } diff --git a/apps/wallet-mobile/src/features/Settings/ManageCollateral/strings.ts b/apps/wallet-mobile/src/features/Settings/ManageCollateral/strings.ts deleted file mode 100644 index 0b5fc162c0..0000000000 --- a/apps/wallet-mobile/src/features/Settings/ManageCollateral/strings.ts +++ /dev/null @@ -1,77 +0,0 @@ -import {defineMessages, useIntl} from 'react-intl' - -export const useStrings = () => { - const intl = useIntl() - return { - lockedAsCollateral: intl.formatMessage(messages.lockedAsCollateral), - removeCollateral: intl.formatMessage(messages.removeCollateral), - collateralSpent: intl.formatMessage(messages.collateralSpent), - generateCollateral: intl.formatMessage(messages.generateCollateral), - notEnoughFundsAlertTitle: intl.formatMessage(messages.notEnoughFundsAlertTitle), - notEnoughFundsAlertMessage: intl.formatMessage(messages.notEnoughFundsAlertMessage), - notEnoughFundsAlertOK: intl.formatMessage(messages.notEnoughFundsAlertOK), - collateralInfoModalLabel: intl.formatMessage(messages.collateralInfoModalLabel), - collateralInfoModalTitle: intl.formatMessage(messages.collateralInfoModalTitle), - collateralInfoModalText: intl.formatMessage(messages.collateralInfoModalText), - initialCollateralInfoModalTitle: intl.formatMessage(messages.initialCollateralInfoModalTitle), - initialCollateralInfoModalText: intl.formatMessage(messages.initialCollateralInfoModalText), - initialCollateralInfoModalButton: intl.formatMessage(messages.initialCollateralInfoModalButton), - } -} - -const messages = defineMessages({ - lockedAsCollateral: { - id: 'components.settings.collateral.lockedAsCollateral', - defaultMessage: '!!!Locked as collateral', - }, - removeCollateral: { - id: 'components.settings.collateral.removeCollateral', - defaultMessage: '!!!If you want to return the amount locked as collateral to your balance press the remove icon', - }, - collateralSpent: { - id: 'components.settings.collateral.collateralSpent', - defaultMessage: '!!!Your collateral is gone, please generate new collateral', - }, - generateCollateral: { - id: 'components.settings.collateral.generateCollateral', - defaultMessage: '!!!Generate collateral', - }, - notEnoughFundsAlertTitle: { - id: 'components.settings.collateral.notEnoughFundsAlertTitle', - defaultMessage: '!!!Not enough funds', - }, - notEnoughFundsAlertMessage: { - id: 'components.settings.collateral.notEnoughFundsAlertMessage', - defaultMessage: '!!!We could not find enough funds in this wallet to create collateral.', - }, - notEnoughFundsAlertOK: { - id: 'components.settings.collateral.notEnoughFundsAlertOK', - defaultMessage: '!!!OK', - }, - collateralInfoModalLabel: { - id: 'components.settings.collateral.collateralInfoModalLabel', - defaultMessage: '!!!Collateral creation', - }, - collateralInfoModalTitle: { - id: 'components.settings.collateral.collateralInfoModalTitle', - defaultMessage: '!!!What is collateral?', - }, - collateralInfoModalText: { - id: 'components.settings.collateral.collateralInfoModalText', - defaultMessage: - '!!!The collateral mechanism is an important feature that has been designed to ensure successful smart contract execution. It is used to guarantee that Cardano nodes are compensated for their work in case phase-2 validation fails.', - }, - initialCollateralInfoModalTitle: { - id: 'components.settings.collateral.initialCollateralInfoModalTitle', - defaultMessage: '!!!Collateral creation', - }, - initialCollateralInfoModalText: { - id: 'components.settings.collateral.initialCollateralInfoModalText', - defaultMessage: - '!!!The collateral mechanism is designed to ensure smart contracts on Cardano execute successfully. It guarantees that nodes are compensated for their work if a contract fails during validation.', - }, - initialCollateralInfoModalButton: { - id: 'components.settings.collateral.initialCollateralInfoModalButton', - defaultMessage: '!!!Add collateral', - }, -}) diff --git a/apps/wallet-mobile/src/features/Swap/common/navigation.ts b/apps/wallet-mobile/src/features/Swap/common/navigation.ts index 452adf22a6..813aa4a5b0 100644 --- a/apps/wallet-mobile/src/features/Swap/common/navigation.ts +++ b/apps/wallet-mobile/src/features/Swap/common/navigation.ts @@ -13,7 +13,6 @@ export const useNavigateTo = () => { selectBuyToken: () => swapNavigation.navigate('swap-select-buy-token'), selectSellToken: () => swapNavigation.navigate('swap-select-sell-token'), startSwap: () => swapNavigation.navigate('swap-start-swap', {screen: 'token-swap'}), - confirmTx: () => swapNavigation.navigate('swap-confirm-tx'), reviewSwap: () => swapNavigation.navigate('swap-review'), submittedTx: (txId: string) => swapNavigation.navigate('swap-submitted-tx', {txId}), failedTx: () => swapNavigation.navigate('swap-failed-tx'), diff --git a/apps/wallet-mobile/src/features/Swap/common/strings.ts b/apps/wallet-mobile/src/features/Swap/common/strings.ts index 404c2308f6..2fc173ebbb 100644 --- a/apps/wallet-mobile/src/features/Swap/common/strings.ts +++ b/apps/wallet-mobile/src/features/Swap/common/strings.ts @@ -57,7 +57,7 @@ export const useStrings = () => { spendingPassword: intl.formatMessage(messages.spendingPassword), sign: intl.formatMessage(messages.sign), searchTokens: intl.formatMessage(messages.searchTokens), - confirm: intl.formatMessage(messages.confirm), + next: intl.formatMessage(messages.next), chooseConnectionMethod: intl.formatMessage(messages.chooseConnectionMethod), selecteAssetTitle: intl.formatMessage(messages.selectAssetTitle), tokens: (qty: number) => intl.formatMessage(globalMessages.tokens, {qty}), @@ -578,9 +578,9 @@ const messages = defineMessages({ id: 'components.send.selectasset.title', defaultMessage: '!!!Select asset', }, - confirm: { - id: 'components.send.confirmscreen.confirmButton', - defaultMessage: '!!!Confirm', + next: { + id: 'global.next', + defaultMessage: '!!!Next', }, assignCollateral: { id: 'components.send.confirmscreen.assignCollateral', diff --git a/apps/wallet-mobile/src/features/Swap/useCases/ConfirmTxScreen/ConfirmTx.tsx b/apps/wallet-mobile/src/features/Swap/useCases/ConfirmTxScreen/ConfirmTx.tsx deleted file mode 100644 index 6db51ff690..0000000000 --- a/apps/wallet-mobile/src/features/Swap/useCases/ConfirmTxScreen/ConfirmTx.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react' -import {ErrorBoundary} from 'react-error-boundary' - -import {ModalError} from '../../../../components/ModalError/ModalError' -import {YoroiWallet} from '../../../../yoroi-wallets/cardano/types' -import {YoroiSignedTx, YoroiUnsignedTx} from '../../../../yoroi-wallets/types/yoroi' -import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet' -import {ConfirmTxWithHW} from './ConfirmTxWithHW' -import {ConfirmTxWithPassword} from './ConfirmTxWithPassword' - -type Props = { - wallet: YoroiWallet - unsignedTx: YoroiUnsignedTx - onCancel: () => void - onSuccess: (signedTx: YoroiSignedTx) => void -} - -export const ConfirmTx = ({wallet, onSuccess, onCancel, unsignedTx}: Props) => { - const {meta} = useSelectedWallet() - return meta.isHW ? ( - ( - - )} - > - - - ) : ( - - ) -} diff --git a/apps/wallet-mobile/src/features/Swap/useCases/ConfirmTxScreen/ConfirmTxScreen.stories.tsx b/apps/wallet-mobile/src/features/Swap/useCases/ConfirmTxScreen/ConfirmTxScreen.stories.tsx deleted file mode 100644 index 5236d812dd..0000000000 --- a/apps/wallet-mobile/src/features/Swap/useCases/ConfirmTxScreen/ConfirmTxScreen.stories.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import {storiesOf} from '@storybook/react-native' -import {mockSwapManager, mockSwapStateDefault, orderMocks, SwapProvider} from '@yoroi/swap' -import React from 'react' - -import {rootStorage} from '../../../../kernel/storage/rootStorage' -import {mocks as walletMocks} from '../../../../yoroi-wallets/mocks/wallet' -import {WalletManagerProviderMock} from '../../../../yoroi-wallets/mocks/WalletManagerProviderMock' -import {buildPortfolioTokenManagers} from '../../../Portfolio/common/helpers/build-token-managers' -import {WalletManagerProvider} from '../../../WalletManager/context/WalletManagerProvider' -import {buildNetworkManagers} from '../../../WalletManager/network-manager/network-manager' -import {WalletManager} from '../../../WalletManager/wallet-manager' -import {mocks} from '../../common/mocks' -import {SwapFormProvider} from '../../common/SwapFormProvider' -import {ConfirmTxScreen} from './ConfirmTxScreen' - -// TODO: should be mocked -const {tokenManagers} = buildPortfolioTokenManagers() -const networkManagers = buildNetworkManagers({tokenManagers}) -const walletManager = new WalletManager({ - rootStorage, - networkManagers, -}) - -storiesOf('Swap ConfirmTxScreen', module) // - .add('swap confirm tx: with password', () => { - return - }) - .add('swap confirm tx: with os', () => { - return - }) - .add('swap confirm tx: with hw', () => { - return - }) - -const ConfirmTxWithPasswordScreen = () => { - return ( - - - - - - - - ) -} -const ConfirmTxWithOSScreen = () => { - return ( - - - - - - - - - - ) -} -const ConfirmTxWithHWScreen = () => { - return ( - - - - - - - - ) -} - -const calculation = orderMocks.mockedOrderCalculations1[0] diff --git a/apps/wallet-mobile/src/features/Swap/useCases/ConfirmTxScreen/ConfirmTxScreen.tsx b/apps/wallet-mobile/src/features/Swap/useCases/ConfirmTxScreen/ConfirmTxScreen.tsx deleted file mode 100644 index 7a6d3281a8..0000000000 --- a/apps/wallet-mobile/src/features/Swap/useCases/ConfirmTxScreen/ConfirmTxScreen.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import {useSwap} from '@yoroi/swap' -import {useTheme} from '@yoroi/theme' -import React from 'react' -import {InteractionManager, StyleSheet, useWindowDimensions, View, ViewProps} from 'react-native' -import {ScrollView} from 'react-native-gesture-handler' -import {SafeAreaView} from 'react-native-safe-area-context' - -import {Button} from '../../../../components/Button/Button' -import {KeyboardAvoidingView} from '../../../../components/KeyboardAvoidingView/KeyboardAvoidingView' -import {LoadingOverlay} from '../../../../components/LoadingOverlay/LoadingOverlay' -import {useModal} from '../../../../components/Modal/ModalContext' -import {Spacer} from '../../../../components/Spacer/Spacer' -import {useMetrics} from '../../../../kernel/metrics/metricsManager' -import {useSignAndSubmitTx} from '../../../../yoroi-wallets/hooks' -import {YoroiSignedTx} from '../../../../yoroi-wallets/types/yoroi' -import {asQuantity, Quantities} from '../../../../yoroi-wallets/utils/utils' -import {useAuthOsWithEasyConfirmation} from '../../../Auth/common/hooks' -import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet' -import {useNavigateTo} from '../../common/navigation' -import {useStrings} from '../../common/strings' -import {ConfirmTx} from './ConfirmTx' -import {TransactionSummary} from './TransactionSummary' - -const BOTTOM_ACTION_SECTION = 220 - -export const ConfirmTxScreen = () => { - const [contentHeight, setContentHeight] = React.useState(0) - const strings = useStrings() - const styles = useStyles() - const {wallet, meta} = useSelectedWallet() - const navigate = useNavigateTo() - const {track} = useMetrics() - const {openModal, closeModal} = useModal() - const {height: deviceHeight} = useWindowDimensions() - const signedTxRef = React.useRef(null) - - const {unsignedTx, orderData} = useSwap() - const sellTokenInfo = orderData.amounts.sell?.info - const buyTokenInfo = orderData.amounts.buy?.info - - const minReceived = Quantities.denominated( - asQuantity(orderData.selectedPoolCalculation?.buyAmountWithSlippage?.quantity.toString() ?? 0), - buyTokenInfo?.decimals ?? 0, - ) - - const couldReceiveNoAssets = Quantities.isZero(minReceived) - - const {authWithOs, isLoading: authenticating} = useAuthOsWithEasyConfirmation( - {id: wallet.id}, - {onSuccess: (rootKey) => signAndSubmitTx({unsignedTx, rootKey})}, - ) - - const trackSwapOrderSubmitted = () => { - if (orderData.selectedPoolCalculation === undefined) return - track.swapOrderSubmitted({ - from_asset: [ - { - asset_name: sellTokenInfo?.name, - asset_ticker: sellTokenInfo?.ticker, - policy_id: sellTokenInfo?.id.split('.')[0], - }, - ], - to_asset: [ - {asset_name: buyTokenInfo?.name, asset_ticker: buyTokenInfo?.ticker, policy_id: buyTokenInfo?.id.split('.')[0]}, - ], - order_type: orderData.type, - slippage_tolerance: orderData.slippage, - from_amount: orderData.amounts.sell?.quantity.toString() ?? '0', - to_amount: orderData.amounts.buy?.quantity.toString() ?? '0', - pool_source: orderData.selectedPoolCalculation.pool.provider, - swap_fees: Number(orderData.selectedPoolCalculation.cost.batcherFee), - }) - } - - const {signAndSubmitTx, isLoading: processingTx} = useSignAndSubmitTx( - {wallet}, - { - signTx: { - useErrorBoundary: true, - onSuccess: (signedTx) => { - signedTxRef.current = signedTx - }, - }, - submitTx: { - onSuccess: () => { - trackSwapOrderSubmitted() - navigate.submittedTx(signedTxRef.current?.signedTx.id ?? '') - }, - onError: () => { - navigate.failedTx() - }, - useErrorBoundary: true, - }, - }, - ) - - const onPasswordTxSuccess = (signedTx: YoroiSignedTx) => { - trackSwapOrderSubmitted() - closeModal() - InteractionManager.runAfterInteractions(() => { - navigate.submittedTx(signedTx.signedTx.id) - }) - } - - const txIsLoading = authenticating || processingTx - const isButtonDisabled = txIsLoading || couldReceiveNoAssets - - return ( - - - - - { - const {height} = event.nativeEvent.layout - setContentHeight(height + BOTTOM_ACTION_SECTION) - }} - > - - - - - - - - - -