diff --git a/apps/wallet-mobile/src/components/Warning/Warning.tsx b/apps/wallet-mobile/src/components/Warning/Warning.tsx index ce1a959985..539646cea5 100644 --- a/apps/wallet-mobile/src/components/Warning/Warning.tsx +++ b/apps/wallet-mobile/src/components/Warning/Warning.tsx @@ -8,14 +8,21 @@ import {Space} from '../Space/Space' type Props = { content: ReactNode iconSize?: number + title?: string } -export const Warning = ({content, iconSize = 30}: Props) => { +export const Warning = ({content, iconSize = 30, title = ''}: Props) => { const {styles, colors} = useStyles() return ( - + + + + + + {title} + @@ -36,6 +43,14 @@ const useStyles = () => { ...atoms.body_2_md_regular, color: color.gray_max, }, + titleContainer: { + ...atoms.flex_row, + ...atoms.align_center, + }, + title: { + color: color.text_gray_max, + ...atoms.body_2_md_medium, + }, }) const colors = { diff --git a/apps/wallet-mobile/src/features/Portfolio/common/hooks/useGetPortfolioTokenChart.ts b/apps/wallet-mobile/src/features/Portfolio/common/hooks/useGetPortfolioTokenChart.ts index a745475dcd..83aa0ae562 100644 --- a/apps/wallet-mobile/src/features/Portfolio/common/hooks/useGetPortfolioTokenChart.ts +++ b/apps/wallet-mobile/src/features/Portfolio/common/hooks/useGetPortfolioTokenChart.ts @@ -45,7 +45,9 @@ const getTimestamps = (timeInterval: TokenChartInterval) => { }[timeInterval ?? TokenChartInterval.DAY] const step = (now - from) / resolution - return Array.from({length: resolution}, (_, i) => from + Math.round(step * i)) + const spread = Array.from({length: resolution}, (_, i) => from + Math.round(step * i)) + spread.push(now) + return spread } const ptTicker = networkConfigs[Chain.Network.Mainnet].primaryTokenInfo.ticker diff --git a/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioTokensList/PortfolioWalletTokenList/PortfolioWalletTokenList.tsx b/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioTokensList/PortfolioWalletTokenList/PortfolioWalletTokenList.tsx index a9e115c066..aeaf17c219 100644 --- a/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioTokensList/PortfolioWalletTokenList/PortfolioWalletTokenList.tsx +++ b/apps/wallet-mobile/src/features/Portfolio/useCases/PortfolioTokensList/PortfolioWalletTokenList/PortfolioWalletTokenList.tsx @@ -1,9 +1,10 @@ import {useFocusEffect} from '@react-navigation/native' +import {FlashList} from '@shopify/flash-list' import {infoExtractName, isPrimaryToken} from '@yoroi/portfolio' import {useTheme} from '@yoroi/theme' import {Chain, Portfolio} from '@yoroi/types' import * as React from 'react' -import {FlatList, StyleSheet, Text, View} from 'react-native' +import {StyleSheet, Text, View} from 'react-native' import {Spacer} from '../../../../../components/Spacer/Spacer' import {useMetrics} from '../../../../../kernel/metrics/metricsManager' @@ -90,6 +91,22 @@ export const PortfolioWalletTokenList = () => { const isPreprod = network === Chain.Network.Preprod + const [loadedTokens, setLoadedTokens] = React.useState(getListTokens.slice(0, batchSize)) + const [currentIndex, setCurrentIndex] = React.useState(batchSize) + + React.useEffect(() => { + setLoadedTokens(getListTokens.slice(0, currentIndex + batchSize)) + setCurrentIndex(currentIndex + batchSize) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tokensList]) // must be tokensList + + const handleOnEndReached = React.useCallback(() => { + if (currentIndex >= getListTokens.length) return + const nextBatch = getListTokens.slice(currentIndex, currentIndex + batchSize) + setLoadedTokens([...loadedTokens, ...nextBatch]) + setCurrentIndex(currentIndex + batchSize) + }, [currentIndex, getListTokens, loadedTokens]) + const renderFooterList = () => { if (isSearching) return null if (isLoading) { @@ -126,8 +143,8 @@ export const PortfolioWalletTokenList = () => { return ( - { renderItem={({item}) => } contentContainerStyle={styles.container} ListEmptyComponent={() => } + onEndReached={handleOnEndReached} + onEndReachedThreshold={0.5} + estimatedItemSize={72} /> ) @@ -185,6 +205,8 @@ const SkeletonItem = () => { ) } +const batchSize = 50 + const useStyles = () => { const {atoms, color} = useTheme() const styles = StyleSheet.create({ diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useOnConfirm.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useOnConfirm.tsx index f2c01967e2..74cb199dc3 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useOnConfirm.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useOnConfirm.tsx @@ -30,11 +30,17 @@ export const useOnConfirm = ({ const navigateTo = useNavigateTo() const handleOnSuccess = (signedTx: YoroiSignedTx) => { - onSuccess?.(signedTx) + if (onSuccess) { + onSuccess(signedTx) + return + } navigateTo.showSubmittedTxScreen() } const handleOnError = () => { - onError?.() + if (onError) { + onError() + return + } navigateTo.showFailedTxScreen() } 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 3caba7d7ec..3bf7176085 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx @@ -81,6 +81,9 @@ export const useStrings = () => { operationsLabel: intl.formatMessage(messages.operationsLabel), policyIdLabel: intl.formatMessage(messages.policyIdLabel), createdBy: intl.formatMessage(messages.createdBy), + operationsLogTitle: intl.formatMessage(messages.operationsLogTitle), + operationsLogWarningText: intl.formatMessage(messages.operationsLogWarningText), + operationsLogWarningTitle: intl.formatMessage(messages.operationsLogWarningTitle), } } @@ -309,6 +312,18 @@ const messages = defineMessages({ id: 'txReview.operations.selectAbstain', defaultMessage: '!!!Select abstain', }, + operationsLogTitle: { + id: 'txReview.operations.log.title', + defaultMessage: '!!!Operations log', + }, + operationsLogWarningTitle: { + id: 'txReview.operations.warning.title', + defaultMessage: '!!!Unusual operations detected', + }, + operationsLogWarningText: { + id: 'txReview.operations.warning.text', + defaultMessage: '!!!Please check the operations log before confirming this transaction.', + }, selectNoConfidence: { id: 'txReview.operations.selectNoConfidence', defaultMessage: '!!!Select no confidence', diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts b/apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts index 681388345b..b11b4e9128 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts +++ b/apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts @@ -1354,6 +1354,240 @@ const multiAssetMultiReceiver: FormattedTx = { referenceInputs: [], } +const operationsWarning: FormattedTx = { + inputs: [ + { + assets: [ + { + tokenInfo: { + id: '.', + nature: Portfolio.Token.Nature.Primary, + type: Portfolio.Token.Type.FT, + application: Portfolio.Token.Application.Coin, + status: Portfolio.Token.Status.Valid, + fingerprint: '', + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', + description: 'Cardano', + }, + name: 'ADA', + label: '8.221903 ADA', + quantity: '8221903', + isPrimary: true, + }, + ], + address: + 'addr1q9r502tqdksvqmhs3lwlxx5f5cz0c92cftqqludl3r0urtk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwql6sl74', + addressKind: 0, + rewardAddress: 'stake1u88sshxrn2j075k7r6nqdmjcr2kpm2upvmtgxrn6ndkwehqyy4w9s', + ownAddress: true, + txIndex: 1, + txHash: '1a2070bd83bbbe8b8d5146a06d5eeb00631ed236bb79f5f54451d1e0e777943a', + }, + ], + outputs: [ + { + assets: [ + { + tokenInfo: { + id: '.', + nature: Portfolio.Token.Nature.Primary, + type: Portfolio.Token.Type.FT, + application: Portfolio.Token.Application.Coin, + status: Portfolio.Token.Status.Valid, + fingerprint: '', + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', + description: 'Cardano', + }, + name: 'ADA', + label: '8.048702 ADA', + quantity: '8048702', + isPrimary: true, + }, + ], + address: + 'addr1q9r502tqdksvqmhs3lwlxx5f5cz0c92cftqqludl3r0urtk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwql6sl74', + addressKind: 0, + rewardAddress: 'stake1u88sshxrn2j075k7r6nqdmjcr2kpm2upvmtgxrn6ndkwehqyy4w9s', + ownAddress: true, + }, + ], + fee: { + tokenInfo: { + id: '.', + nature: Portfolio.Token.Nature.Primary, + type: Portfolio.Token.Type.FT, + application: Portfolio.Token.Application.Coin, + status: Portfolio.Token.Status.Valid, + fingerprint: '', + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', + description: 'Cardano', + }, + name: 'ADA', + label: '0.173201 ADA', + quantity: '173201', + isPrimary: true, + }, + certificates: [ + { + type: 'StakeDelegation', + value: { + stake_credential: { + Key: 'cf085cc39aa4ff52de1ea606ee581aac1dab8166d6830e7a9b6cecdc', + }, + pool_keyhash: '2a8294ad7538b15353b9ffd81e26dafe846ffc3f6b9e331d4c1dc030', + }, + }, + { + type: 'StakeRegistration', + value: { + stake_credential: { + Key: 'a7aa605f13f7c7c26df239951a421dd297e955d8eb91273775078f54', + }, + coin: null, + }, + }, + { + type: 'StakeDelegation', + value: { + stake_credential: { + Key: 'a7aa605f13f7c7c26df239951a421dd297e955d8eb91273775078f54', + }, + pool_keyhash: 'dbda39c8d064ff9801e376f8350efafe67c07e9e9244dd613aee5125', + }, + }, + { + type: 'StakeRegistration', + value: { + stake_credential: { + Key: 'a7aa605f13f7c7c26df239951a421dd297e955d8eb91273775078f54', + }, + coin: null, + }, + }, + { + type: 'StakeDelegation', + value: { + stake_credential: { + Key: 'a7aa605f13f7c7c26df239951a421dd297e955d8eb91273775078f54', + }, + pool_keyhash: 'dbda39c8d064ff9801e376f8350efafe67c07e9e9244dd613aee5125', + }, + }, + { + type: 'StakeRegistration', + value: { + stake_credential: { + Key: 'a7aa605f13f7c7c26df239951a421dd297e955d8eb91273775078f54', + }, + coin: null, + }, + }, + { + type: 'StakeDelegation', + value: { + stake_credential: { + Key: 'a7aa605f13f7c7c26df239951a421dd297e955d8eb91273775078f54', + }, + pool_keyhash: 'dbda39c8d064ff9801e376f8350efafe67c07e9e9244dd613aee5125', + }, + }, + { + type: 'StakeRegistration', + value: { + stake_credential: { + Key: 'a7aa605f13f7c7c26df239951a421dd297e955d8eb91273775078f54', + }, + coin: null, + }, + }, + { + type: 'StakeDelegation', + value: { + stake_credential: { + Key: 'a7aa605f13f7c7c26df239951a421dd297e955d8eb91273775078f54', + }, + pool_keyhash: 'dbda39c8d064ff9801e376f8350efafe67c07e9e9244dd613aee5125', + }, + }, + { + type: 'StakeRegistration', + value: { + stake_credential: { + Key: 'a7aa605f13f7c7c26df239951a421dd297e955d8eb91273775078f54', + }, + coin: null, + }, + }, + { + type: 'StakeDelegation', + value: { + stake_credential: { + Key: 'a7aa605f13f7c7c26df239951a421dd297e955d8eb91273775078f54', + }, + pool_keyhash: 'dbda39c8d064ff9801e376f8350efafe67c07e9e9244dd613aee5125', + }, + }, + ], + mint: null, + referenceInputs: [ + { + assets: [ + { + tokenInfo: { + id: '.', + nature: Portfolio.Token.Nature.Primary, + type: Portfolio.Token.Type.FT, + application: Portfolio.Token.Application.Coin, + status: Portfolio.Token.Status.Valid, + fingerprint: '', + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', + description: 'Cardano', + }, + name: 'ADA', + label: '8.221903 ADA', + quantity: '8221903', + isPrimary: true, + }, + ], + address: + 'addr1q9r502tqdksvqmhs3lwlxx5f5cz0c92cftqqludl3r0urtk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwql6sl74', + addressKind: 0, + rewardAddress: 'stake1u88sshxrn2j075k7r6nqdmjcr2kpm2upvmtgxrn6ndkwehqyy4w9s', + ownAddress: true, + txIndex: 1, + txHash: '1a2070bd83bbbe8b8d5146a06d5eeb00631ed236bb79f5f54451d1e0e777943a', + }, + ], +} + export const mocks = { formattedTxs: { onlyAdaOneReceiver, @@ -1362,5 +1596,6 @@ export const mocks = { multiAssetMultiReceiver, onlyAdaOneReceiverMint, onlyAdaOneReceiverReferenceInputs, + operationsWarning, }, } diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/operations.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/operations.tsx index 6fc9eaa6a2..7e67d04739 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/operations.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/common/operations.tsx @@ -6,6 +6,7 @@ import {StyleSheet, Text, useWindowDimensions, View} from 'react-native' import {TouchableOpacity} from 'react-native-gesture-handler' import {useQuery} from 'react-query' +import {Icon} from '../../../components/Icon' import {useModal} from '../../../components/Modal/ModalContext' import {Space} from '../../../components/Space/Space' import {wrappedCsl} from '../../../yoroi-wallets/cardano/wrappedCsl' @@ -17,47 +18,65 @@ import {useStrings} from './hooks/useStrings' import {PoolDetails} from './PoolDetails' import {CertificateType, FormattedTx} from './types' -export const StakeRegistrationOperation = ({fee}: {fee: Balance.Quantity}) => { +export const StakeRegistrationOperation = ({ + fee, + showWarning, + strike, +}: { + fee: Balance.Quantity + showWarning?: boolean + strike?: boolean +}) => { const {styles} = useStyles() const strings = useStrings() const {wallet} = useSelectedWallet() return ( - {strings.registerStakingKey} + ) } -export const StakeDeregistrationOperation = () => { +export const StakeDeregistrationOperation = ({showWarning, strike}: {showWarning?: boolean; strike?: boolean}) => { const {styles} = useStyles() const strings = useStrings() return ( - {strings.deregisterStakingKey} + ) } -export const StakeRewardsWithdrawalOperation = () => { +export const StakeRewardsWithdrawalOperation = ({showWarning, strike}: {showWarning?: boolean; strike?: boolean}) => { const {styles} = useStyles() const strings = useStrings() return ( - {strings.rewardsWithdrawalLabel} + ) } -export const StakeDelegationOperation = ({poolId}: {poolId: string}) => { +export const StakeDelegationOperation = ({ + poolId, + showWarning, + strike, +}: { + poolId: string + showWarning?: boolean + strike?: boolean +}) => { const {styles} = useStyles() const strings = useStrings() const poolInfo = usePoolInfo({poolId}) @@ -72,12 +91,12 @@ export const StakeDelegationOperation = ({poolId}: {poolId: string}) => { return ( - {strings.delegateStake} + ) @@ -87,29 +106,37 @@ export const generatePoolName = (poolInfo: FullPoolInfo) => { return poolInfo.explorer != null ? `[${poolInfo.explorer.ticker}] ${poolInfo.explorer.name}` : null } -export const AbstainOperation = () => { +export const AbstainOperation = ({showWarning, strike}: {showWarning?: boolean; strike?: boolean}) => { const {styles} = useStyles() const strings = useStrings() return ( - {strings.selectAbstain} + ) } -export const NoConfidenceOperation = () => { +export const NoConfidenceOperation = ({showWarning, strike}: {showWarning?: boolean; strike?: boolean}) => { const {styles} = useStyles() const strings = useStrings() return ( - {strings.selectNoConfidence} + ) } -export const VoteDelegationOperation = ({drepID}: {drepID: string}) => { +export const VoteDelegationOperation = ({ + drepID, + showWarning, + strike, +}: { + drepID: string + showWarning?: boolean + strike?: boolean +}) => { const {styles} = useStyles() const strings = useStrings() @@ -117,134 +144,248 @@ export const VoteDelegationOperation = ({drepID}: {drepID: string}) => { return ( - {strings.delegateVotingToDRep} + ) } -export const DrepRegistrationOperation = ({fee}: {fee: Balance.Quantity}) => { +export const DrepRegistrationOperation = ({ + fee, + showWarning, + strike, +}: { + fee: Balance.Quantity + showWarning?: boolean + strike?: boolean +}) => { const {styles} = useStyles() const strings = useStrings() const {wallet} = useSelectedWallet() return ( - {strings.drepRegistration} + ) } -export const DrepDeregistrationOperation = () => { +export const DrepDeregistrationOperation = ({showWarning, strike}: {showWarning?: boolean; strike?: boolean}) => { const {styles} = useStyles() const strings = useStrings() return ( - {strings.drepDeregistration} + ) } -export const PoolRegistrationOperation = ({fee}: {fee: Balance.Quantity}) => { +export const PoolRegistrationOperation = ({ + fee, + showWarning, + strike, +}: { + fee: Balance.Quantity + showWarning?: boolean + strike?: boolean +}) => { const {styles} = useStyles() const strings = useStrings() const {wallet} = useSelectedWallet() return ( - {strings.poolRegistration} + ) } -export const PoolRetirementOperation = () => { +export const PoolRetirementOperation = ({showWarning, strike}: {showWarning?: boolean; strike?: boolean}) => { const {styles} = useStyles() const strings = useStrings() return ( - {strings.poolRetirement} + ) } -export const DrepUpdateOperation = () => { +export const DrepUpdateOperation = ({showWarning, strike}: {showWarning?: boolean; strike?: boolean}) => { const {styles} = useStyles() const strings = useStrings() return ( - {strings.drepUpdate} + ) } -export const MoveInstantaneousRewardsOperation = () => { +export const MoveInstantaneousRewardsOperation = ({showWarning, strike}: {showWarning?: boolean; strike?: boolean}) => { const {styles} = useStyles() const strings = useStrings() return ( - {strings.moveInstantaneousRewards} + ) } -export const CommitteeHotAuthorizationOperation = () => { +export const CommitteeHotAuthorizationOperation = ({ + showWarning, + strike, +}: { + showWarning?: boolean + strike?: boolean +}) => { const {styles} = useStyles() const strings = useStrings() return ( - {strings.committeeHotAuthorization} + ) } -export const CommitteeColdResignOperation = () => { +export const CommitteeColdResignOperation = ({showWarning, strike}: {showWarning?: boolean; strike?: boolean}) => { const {styles} = useStyles() const strings = useStrings() return ( - {strings.committeeColdResign} + ) } +const Label = ({label, showWarning, strike}: {label: string; showWarning?: boolean; strike?: boolean}) => { + const {colors, styles} = useStyles() + return ( + + {label} + + {showWarning && ( + + + + )} + + ) +} + +type OperationsCount = Record +export type Operations = { + components: Array<{ + duplicated: boolean + type: CertificateType + component: React.ReactNode + }> + totalFee: Balance.Quantity + operationCount: OperationsCount +} export const useOperations = (certificates: FormattedTx['certificates']) => { const {wallet} = useSelectedWallet() - if (certificates === null) return {components: [], totalFee: Quantities.zero} + const operationCount = {} as OperationsCount + + if (certificates === null) + return { + components: [] as Operations['components'], + totalFee: Quantities.zero, + operationCount, + } - return certificates.reduce<{components: React.ReactNode[]; totalFee: Balance.Quantity}>( + const certificatesTypes = certificates.map((cert) => cert.type) + certificatesTypes.forEach((cert) => updateOperationsCount(cert, operationCount)) + + return certificates.reduce( (acc, certificate, index) => { + const fistElementIndex = certificatesTypes.indexOf(certificate.type) + const isFistElement = fistElementIndex === index + const isNotFirstElementDuplicated = operationCount[certificate.type] > 1 && !isFistElement + const isFirstElementDuplicated = operationCount[certificate.type] > 1 && isFistElement + switch (certificate.type) { case CertificateType.StakeRegistration: { const fee = asQuantity(wallet.protocolParams.keyDeposit) + return { - components: [...acc.components, ], + components: [ + ...acc.components, + { + component: ( + + ), + duplicated: isNotFirstElementDuplicated, + type: CertificateType.StakeRegistration, + }, + ], totalFee: Quantities.sum([fee, acc.totalFee]), + operationCount, } } case CertificateType.StakeDeregistration: - return {components: [...acc.components, ], totalFee: acc.totalFee} + return { + components: [ + ...acc.components, + { + component: ( + + ), + duplicated: isNotFirstElementDuplicated, + type: CertificateType.StakeDeregistration, + }, + ], + totalFee: acc.totalFee, + operationCount, + } case CertificateType.StakeDelegation: { const poolKeyHash = certificate.value.pool_keyhash ?? null if (poolKeyHash == null) return acc return { - components: [...acc.components, ], + components: [ + ...acc.components, + { + component: ( + + ), + duplicated: isNotFirstElementDuplicated, + type: CertificateType.StakeDelegation, + }, + ], totalFee: acc.totalFee, + operationCount, } } @@ -252,63 +393,235 @@ export const useOperations = (certificates: FormattedTx['certificates']) => { const drep = certificate.value.drep if (drep === 'AlwaysAbstain') - return {components: [...acc.components, ], totalFee: acc.totalFee} + return { + components: [ + ...acc.components, + { + component: ( + + ), + duplicated: isNotFirstElementDuplicated, + type: CertificateType.VoteDelegation, + }, + ], + totalFee: acc.totalFee, + operationCount, + } if (drep === 'AlwaysNoConfidence') - return {components: [...acc.components, ], totalFee: acc.totalFee} + return { + components: [ + ...acc.components, + { + component: ( + + ), + duplicated: isNotFirstElementDuplicated, + type: CertificateType.VoteDelegation, + }, + ], + totalFee: acc.totalFee, + operationCount, + } const drepId = ('KeyHash' in drep ? drep.KeyHash : drep.ScriptHash) ?? '' return { - components: [...acc.components, ], + components: [ + ...acc.components, + { + component: ( + + ), + duplicated: isNotFirstElementDuplicated, + type: CertificateType.VoteDelegation, + }, + ], totalFee: acc.totalFee, + operationCount, } } case CertificateType.DRepRegistration: { const fee = asQuantity(wallet.protocolParams.keyDeposit) return { - components: [...acc.components, ], + components: [ + ...acc.components, + { + component: ( + + ), + duplicated: isNotFirstElementDuplicated, + type: CertificateType.DRepRegistration, + }, + ], totalFee: Quantities.sum([fee, acc.totalFee]), + operationCount, } } case CertificateType.DRepDeregistration: { - return {components: [...acc.components, ], totalFee: acc.totalFee} + return { + components: [ + ...acc.components, + { + component: ( + + ), + duplicated: isNotFirstElementDuplicated, + type: CertificateType.DRepDeregistration, + }, + ], + totalFee: acc.totalFee, + operationCount, + } } case CertificateType.PoolRegistration: { const fee = asQuantity(wallet.protocolParams.poolDeposit) return { - components: [...acc.components, ], + components: [ + ...acc.components, + { + component: ( + + ), + duplicated: isNotFirstElementDuplicated, + type: CertificateType.PoolRegistration, + }, + ], totalFee: Quantities.sum([fee, acc.totalFee]), + operationCount, } } case CertificateType.PoolRetirement: { - return {components: [...acc.components, ], totalFee: acc.totalFee} + return { + components: [ + ...acc.components, + { + component: ( + + ), + duplicated: isNotFirstElementDuplicated, + type: CertificateType.PoolRetirement, + }, + ], + totalFee: acc.totalFee, + operationCount, + } } case CertificateType.DRepUpdate: { - return {components: [...acc.components, ], totalFee: acc.totalFee} + return { + components: [ + ...acc.components, + { + component: ( + + ), + duplicated: isNotFirstElementDuplicated, + type: CertificateType.DRepUpdate, + }, + ], + totalFee: acc.totalFee, + operationCount, + } } case CertificateType.MoveInstantaneousRewardsCert: { return { - components: [...acc.components, ], + components: [ + ...acc.components, + { + component: ( + + ), + duplicated: isNotFirstElementDuplicated, + type: CertificateType.MoveInstantaneousRewardsCert, + }, + ], totalFee: acc.totalFee, + operationCount, } } case CertificateType.CommitteeHotAuth: { return { - components: [...acc.components, ], + components: [ + ...acc.components, + { + component: ( + + ), + duplicated: isNotFirstElementDuplicated, + type: CertificateType.CommitteeHotAuth, + }, + ], totalFee: acc.totalFee, + operationCount, } } case CertificateType.CommitteeColdResign: { return { - components: [...acc.components, ], + components: [ + ...acc.components, + { + component: ( + + ), + duplicated: isNotFirstElementDuplicated, + type: CertificateType.CommitteeColdResign, + }, + ], totalFee: acc.totalFee, + operationCount, } } @@ -316,10 +629,22 @@ export const useOperations = (certificates: FormattedTx['certificates']) => { return acc } }, - {components: [], totalFee: Quantities.zero}, + {components: [], totalFee: Quantities.zero, operationCount: {} as OperationsCount}, ) } +const updateOperationsCount = (operation: CertificateType, operationsCount: OperationsCount) => { + let count = operationsCount[operation] + + if (count != null) { + operationsCount[operation] = ++count + return operationsCount + } + + operationsCount[operation] = 1 + return operationsCount +} + export const getDrepBech32Id = async (poolId: string) => { const {csl, release} = wrappedCsl() try { @@ -353,6 +678,10 @@ const useStyles = () => { ...atoms.body_2_md_regular, color: color.text_gray_low, }, + operationLabelContainer: { + ...atoms.flex_row, + ...atoms.align_center, + }, operationValue: { ...atoms.flex_1, ...atoms.text_right, @@ -363,7 +692,18 @@ const useStyles = () => { ...atoms.body_2_md_regular, color: color.text_primary_medium, }, + strike: { + textDecorationLine: 'line-through', + textDecorationStyle: 'solid', + }, + infoIcon: { + ...atoms.pb_2xs, + }, }) - return {styles} as const + const colors = { + warning: color.sys_orange_500, + } + + return {styles, colors} as const } diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/types.ts b/apps/wallet-mobile/src/features/ReviewTx/common/types.ts index 2376879882..f36e49c878 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/types.ts +++ b/apps/wallet-mobile/src/features/ReviewTx/common/types.ts @@ -101,7 +101,7 @@ export const CertificateType = { VoteRegistrationAndDelegation: 'VoteRegistrationAndDelegation', // NO } as const -export type CerificateType = (typeof CertificateType)[keyof typeof CertificateType] +export type CertificateType = (typeof CertificateType)[keyof typeof CertificateType] // Makes sure CertificateType lists all the certificates in CertificateJSON -export type AssertAllImplementedCertTypes = AssertEqual> +export type AssertAllImplementedCertTypes = AssertEqual> diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/Overview/OverviewTab.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/Overview/OverviewTab.tsx index 95fbc70e8b..2d916eb673 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/Overview/OverviewTab.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/Overview/OverviewTab.tsx @@ -4,13 +4,14 @@ import {useTheme} from '@yoroi/theme' import {Balance} from '@yoroi/types' import {Image} from 'expo-image' import * as React from 'react' -import {Linking, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View} from 'react-native' +import {Linking, ScrollView, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View} from 'react-native' import {Divider} from '../../../../../../components/Divider/Divider' import {Icon} from '../../../../../../components/Icon' import {Info} from '../../../../../../components/Info/Info' import {useModal} from '../../../../../../components/Modal/ModalContext' import {Space} from '../../../../../../components/Space/Space' +import {Warning} from '../../../../../../components/Warning/Warning' import {formatTokenWithText} from '../../../../../../yoroi-wallets/utils/format' import {Quantities} from '../../../../../../yoroi-wallets/utils/utils' import {useSelectedWallet} from '../../../../../WalletManager/common/hooks/useSelectedWallet' @@ -18,7 +19,7 @@ import {useWalletManager} from '../../../../../WalletManager/context/WalletManag import {Accordion} from '../../../../common/Accordion' import {CopiableText} from '../../../../common/CopiableText' import {useStrings} from '../../../../common/hooks/useStrings' -import {useOperations} from '../../../../common/operations' +import {Operations, useOperations} from '../../../../common/operations' import {TokenItem} from '../../../../common/TokenItem' import {FormattedOutput, FormattedOutputs, FormattedTx} from '../../../../common/types' import {WalletBalance} from '../../../../common/WalletBalance' @@ -38,14 +39,24 @@ export const OverviewTab = ({ }) => { const {styles} = useStyles() const operations = useOperations(tx.certificates) + const strings = useStrings() const notOwnedOutputs = React.useMemo(() => tx.outputs.filter((output) => !output.ownAddress), [tx.outputs]) const ownedOutputs = React.useMemo(() => tx.outputs.filter((output) => output.ownAddress), [tx.outputs]) + const componentsDuplicated = operations.components.find((component) => component.duplicated) return ( + {componentsDuplicated && ( + <> + + + + + )} + @@ -64,7 +75,7 @@ export const OverviewTab = ({ {notOwnedOutputs.length > 1 && } - +
@@ -353,11 +364,16 @@ const OperationsSection = ({ operations, extraOperations, }: { - operations: Array + operations: Operations extraOperations?: Array }) => { const strings = useStrings() - if (extraOperations == null && operations?.length === 0) return null + if (extraOperations == null && operations.components?.length === 0) return null + + const componentsNotDuplicated = operations.components + .filter((component) => !component.duplicated) + .map(({component}) => component) + const componentDuplicated = operations.components.filter((component) => component.duplicated) return ( @@ -366,7 +382,40 @@ const OperationsSection = ({ - {[...operations, ...(extraOperations ?? [])].map((operation, index) => { + {[...componentsNotDuplicated, ...(extraOperations ?? [])].map((operation, index) => { + if (index === 0) return operation + + return ( + <> + + + {operation} + + ) + })} + + {componentDuplicated.length > 0 && ( +
}} + /> + )} + + + ) +} + +const OperationsModal = ({operations}: {operations: Operations}) => { + const strings = useStrings() + const components = operations.components.map(({component}) => component) + + return ( + + + + + + + {components.map((operation, index) => { if (index === 0) return operation return ( @@ -389,7 +438,13 @@ const Details = ({details}: {details?: {title: string; component: React.ReactNod if (details == null) return null const handleOnPress = () => { - openModal(details.title ?? '', {details.component}, 550) + openModal( + details.title ?? '', + + {details.component} + , + 400, + ) } return ( @@ -496,7 +551,6 @@ const useStyles = () => { color: color.text_gray_medium, }, detailsRow: { - ...atoms.flex_1, ...atoms.flex_row, ...atoms.justify_end, }, diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReviewTx.stories.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReviewTx.stories.tsx index 5c4e1185f1..909b37e8f1 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReviewTx.stories.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReviewTx.stories.tsx @@ -19,6 +19,7 @@ storiesOf('Review Tx Screen', module) .add('Only Ada Tx / Multi Receiver', () => ) .add('Multi Asset Tx / One Receiver', () => ) .add('Multi Asset Tx / Multi Receiver', () => ) + .add('Operations Warning', () => ) const Component = ({formattedTx}: {formattedTx: FormattedTx}) => { return diff --git a/apps/wallet-mobile/src/features/Send/common/navigation.ts b/apps/wallet-mobile/src/features/Send/common/navigation.ts index 1a61f36940..e7f5972e40 100644 --- a/apps/wallet-mobile/src/features/Send/common/navigation.ts +++ b/apps/wallet-mobile/src/features/Send/common/navigation.ts @@ -15,6 +15,8 @@ export const useNavigateTo = () => { startTx: () => navigation.navigate('send-start-tx'), editAmount: () => navigation.navigate('send-edit-amount'), reader: () => navigation.navigate('scan-start', {insideFeature: 'send'}), + submittedTx: () => navigation.navigate('send-submitted-tx'), + failedTx: () => navigation.navigate('send-failed-tx'), startTxAfterReset: () => navigation.reset({ index: 0, diff --git a/apps/wallet-mobile/src/features/Send/common/strings.tsx b/apps/wallet-mobile/src/features/Send/common/strings.tsx index 2d02071dc1..f0c5300e47 100644 --- a/apps/wallet-mobile/src/features/Send/common/strings.tsx +++ b/apps/wallet-mobile/src/features/Send/common/strings.tsx @@ -253,7 +253,7 @@ const messages = defineMessages({ }, submittedTxTitle: { id: 'components.send.sendscreen.submittedTxTitle', - defaultMessage: '!!!Transaction submitted', + defaultMessage: '!!!Transaction signed', }, submittedTxText: { id: 'components.send.sendscreen.submittedTxText', @@ -261,7 +261,7 @@ const messages = defineMessages({ }, submittedTxButton: { id: 'components.send.sendscreen.submittedTxButton', - defaultMessage: '!!!Go to transactions', + defaultMessage: '!!!Close', }, failedTxTitle: { id: 'components.send.sendscreen.failedTxTitle', diff --git a/apps/wallet-mobile/src/features/Send/useCases/ListAmountsToSend/ListAmountsToSendScreen.tsx b/apps/wallet-mobile/src/features/Send/useCases/ListAmountsToSend/ListAmountsToSendScreen.tsx index 5dbeebbcd4..3d9d90072e 100644 --- a/apps/wallet-mobile/src/features/Send/useCases/ListAmountsToSend/ListAmountsToSendScreen.tsx +++ b/apps/wallet-mobile/src/features/Send/useCases/ListAmountsToSend/ListAmountsToSendScreen.tsx @@ -87,10 +87,13 @@ export const ListAmountsToSendScreen = () => { if (memo.length > 0) { saveMemo({txId: signedTx.signedTx.id, memo: memo.trim()}) } + + navigateTo.submittedTx() } const onError = () => { track.sendSummarySubmitted(sendProperties) + navigateTo.failedTx() } const onNext = () => { diff --git a/apps/wallet-mobile/src/features/Send/useCases/ShowFailedTxScreen/FailedTxScreen.tsx b/apps/wallet-mobile/src/features/Send/useCases/ShowFailedTxScreen/FailedTxScreen.tsx new file mode 100644 index 0000000000..a24c35371d --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/useCases/ShowFailedTxScreen/FailedTxScreen.tsx @@ -0,0 +1,77 @@ +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {StyleSheet, Text, View} from 'react-native' + +import {Button} from '../../../../components/Button/Button' +import {SafeArea} from '../../../../components/SafeArea' +import {Space} from '../../../../components/Space/Space' +import {Spacer} from '../../../../components/Spacer/Spacer' +import {useBlockGoBack, useWalletNavigation} from '../../../../kernel/navigation' +import {FailedTxIcon} from '../../../ReviewTx/illustrations/FailedTxIcon' +import {useStrings} from '../../common/strings' + +export const FailedTxScreen = () => { + useBlockGoBack() + const strings = useStrings() + const {styles} = useStyles() + const {resetToStartTransfer} = useWalletNavigation() + + return ( + + + + + + + + + + {strings.failedTxTitle} + + {strings.failedTxText} + + + + +