Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(wallet-mobile): new tx review improvements #3730

Merged
merged 12 commits into from
Nov 9, 2024
4 changes: 2 additions & 2 deletions apps/wallet-mobile/src/features/ReviewTx/common/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ 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 [isOpen, setIsOpen] = React.useState(true)
const animatedHeight = React.useRef(new Animated.Value(1)).current

const toggleSection = () => {
setIsOpen(!isOpen)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ const formatInputs = async (
address,
addressKind: addressKind ?? null,
rewardAddress,
ownAddress: address != null && isOwnedAddress(wallet, address),
ownAddress: address != null ? isOwnedAddress(wallet, address) : null,
txIndex: input.index,
txHash: input.transaction_id,
}
Expand Down
84 changes: 50 additions & 34 deletions apps/wallet-mobile/src/features/ReviewTx/common/operations.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {FullPoolInfo} from '@emurgo/yoroi-lib'
import {useTheme} from '@yoroi/theme'
import {Balance} from '@yoroi/types'
import * as React from 'react'
import {StyleSheet, Text, useWindowDimensions, View} from 'react-native'
import {TouchableOpacity} from 'react-native-gesture-handler'
Expand All @@ -10,13 +11,13 @@ import {Space} from '../../../components/Space/Space'
import {wrappedCsl} from '../../../yoroi-wallets/cardano/wrappedCsl'
import {usePoolInfo} from '../../../yoroi-wallets/hooks'
import {formatTokenWithText} from '../../../yoroi-wallets/utils/format'
import {asQuantity} from '../../../yoroi-wallets/utils/utils'
import {asQuantity, Quantities} from '../../../yoroi-wallets/utils/utils'
import {useSelectedWallet} from '../../WalletManager/common/hooks/useSelectedWallet'
import {useStrings} from './hooks/useStrings'
import {PoolDetails} from './PoolDetails'
import {CertificateType, FormattedTx} from './types'

export const StakeRegistrationOperation = () => {
export const StakeRegistrationOperation = ({fee}: {fee: Balance.Quantity}) => {
const {styles} = useStyles()
const strings = useStrings()
const {wallet} = useSelectedWallet()
Expand All @@ -27,9 +28,7 @@ export const StakeRegistrationOperation = () => {

<Space width="lg" />

<Text style={styles.operationValue}>
{formatTokenWithText(asQuantity(wallet.protocolParams.keyDeposit), wallet.portfolioPrimaryTokenInfo)}
</Text>
<Text style={styles.operationValue}>{formatTokenWithText(fee, wallet.portfolioPrimaryTokenInfo)}</Text>
</View>
)
}
Expand Down Expand Up @@ -128,36 +127,53 @@ export const VoteDelegationOperation = ({drepID}: {drepID: string}) => {
}

export const useOperations = (certificates: FormattedTx['certificates']) => {
if (certificates === null) return []

return certificates.reduce<React.ReactNode[]>((acc, certificate, index) => {
switch (certificate.type) {
case CertificateType.StakeRegistration:
return [...acc, <StakeRegistrationOperation key={index} />]

case CertificateType.StakeDeregistration:
return [...acc, <StakeDeregistrationOperation key={index} />]

case CertificateType.StakeDelegation: {
const poolKeyHash = certificate.value.pool_keyhash ?? null
if (poolKeyHash == null) return acc
return [...acc, <StakeDelegateOperation key={index} poolId={poolKeyHash} />]
}

case CertificateType.VoteDelegation: {
const drep = certificate.value.drep

if (drep === 'AlwaysAbstain') return [...acc, <AbstainOperation key={index} />]
if (drep === 'AlwaysNoConfidence') return [...acc, <NoConfidenceOperation key={index} />]

const drepId = ('KeyHash' in drep ? drep.KeyHash : drep.ScriptHash) ?? ''
return [...acc, <VoteDelegationOperation key={index} drepID={drepId} />]
const {wallet} = useSelectedWallet()
if (certificates === null) return {components: [], totalFee: Quantities.zero}

return certificates.reduce<{components: React.ReactNode[]; totalFee: Balance.Quantity}>(
(acc, certificate, index) => {
switch (certificate.type) {
case CertificateType.StakeRegistration: {
const fee = asQuantity(wallet.protocolParams.keyDeposit)
return {
components: [...acc.components, <StakeRegistrationOperation fee={fee} key={index} />],
totalFee: Quantities.sum([fee, acc.totalFee]),
}
}

case CertificateType.StakeDeregistration:
return {components: [...acc.components, <StakeDeregistrationOperation key={index} />], totalFee: acc.totalFee}

case CertificateType.StakeDelegation: {
const poolKeyHash = certificate.value.pool_keyhash ?? null
if (poolKeyHash == null) return acc
return {
components: [...acc.components, <StakeDelegateOperation key={index} poolId={poolKeyHash} />],
totalFee: acc.totalFee,
}
}

case CertificateType.VoteDelegation: {
const drep = certificate.value.drep

if (drep === 'AlwaysAbstain')
return {components: [...acc.components, <AbstainOperation key={index} />], totalFee: acc.totalFee}
if (drep === 'AlwaysNoConfidence')
return {components: [...acc.components, <NoConfidenceOperation key={index} />], totalFee: acc.totalFee}

const drepId = ('KeyHash' in drep ? drep.KeyHash : drep.ScriptHash) ?? ''
return {
components: [...acc.components, <VoteDelegationOperation key={index} drepID={drepId} />],
totalFee: acc.totalFee,
}
}

default:
return acc
}

default:
return acc
}
}, [])
},
{components: [], totalFee: Quantities.zero},
)
}

export const getDrepBech32Id = async (poolId: string) => {
Expand Down
2 changes: 1 addition & 1 deletion apps/wallet-mobile/src/features/ReviewTx/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type FormattedInput = {
address: string | undefined
addressKind: CredKind | null
rewardAddress: string | null
ownAddress: boolean
ownAddress: boolean | null
txIndex: number
txHash: string
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import {CredKind} from '@emurgo/cross-csl-core'
import {Blockies} from '@yoroi/identicon'
import {useTheme} from '@yoroi/theme'
import {Balance} from '@yoroi/types'
import * as React from 'react'
import {Linking, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View} from 'react-native'

Expand Down Expand Up @@ -34,6 +35,7 @@ export const OverviewTab = ({
details?: {title: string; component: React.ReactNode}
}) => {
const {styles} = useStyles()
const operations = useOperations(tx.certificates)

const notOwnedOutputs = React.useMemo(() => tx.outputs.filter((output) => !output.ownAddress), [tx.outputs])
const ownedOutputs = React.useMemo(() => tx.outputs.filter((output) => output.ownAddress), [tx.outputs])
Expand All @@ -51,9 +53,10 @@ export const OverviewTab = ({
notOwnedOutputs={notOwnedOutputs}
ownedOutputs={ownedOutputs}
receiverCustomTitle={receiverCustomTitle}
operationsFee={operations.totalFee}
/>

<OperationsSection tx={tx} extraOperations={extraOperations} />
<OperationsSection operations={operations.components} extraOperations={extraOperations} />

<Details details={details} />
</View>
Expand Down Expand Up @@ -119,11 +122,13 @@ const SenderSection = ({
notOwnedOutputs,
ownedOutputs,
receiverCustomTitle,
operationsFee,
}: {
tx: FormattedTx
notOwnedOutputs: FormattedOutputs
ownedOutputs: FormattedOutputs
receiverCustomTitle?: React.ReactNode
operationsFee: Balance.Quantity
}) => {
const strings = useStrings()
const {styles} = useStyles()
Expand All @@ -141,7 +146,7 @@ const SenderSection = ({

<Space height="sm" />

<SenderTokens tx={tx} notOwnedOutputs={notOwnedOutputs} />
<SenderTokens tx={tx} notOwnedOutputs={notOwnedOutputs} operationsFee={operationsFee} />

{notOwnedOutputs.length === 1 && (
<ReceiverSection receiverCustomTitle={receiverCustomTitle} notOwnedOutputs={notOwnedOutputs} />
Expand All @@ -151,7 +156,15 @@ const SenderSection = ({
}

// 🚧 TODO: ADD MULTIRECEIVER SUPPORT 🚧
const SenderTokens = ({tx, notOwnedOutputs}: {tx: FormattedTx; notOwnedOutputs: FormattedOutputs}) => {
const SenderTokens = ({
tx,
notOwnedOutputs,
operationsFee,
}: {
tx: FormattedTx
notOwnedOutputs: FormattedOutputs
operationsFee: Balance.Quantity
}) => {
const {styles} = useStyles()

const {wallet} = useSelectedWallet()
Expand All @@ -164,8 +177,8 @@ const SenderTokens = ({tx, notOwnedOutputs}: {tx: FormattedTx; notOwnedOutputs:
[notOwnedOutputs],
)
const totalPrimaryTokenSpent = React.useMemo(
() => Quantities.sum([totalPrimaryTokenSent, tx.fee.quantity]),
[totalPrimaryTokenSent, tx.fee.quantity],
() => Quantities.sum([totalPrimaryTokenSent, tx.fee.quantity, operationsFee]),
[totalPrimaryTokenSent, tx.fee.quantity, operationsFee],
)
const totalPrimaryTokenSpentLabel = formatTokenWithText(totalPrimaryTokenSpent, wallet.portfolioPrimaryTokenInfo)

Expand Down Expand Up @@ -241,10 +254,14 @@ const ReceiverSection = ({
)
}

const OperationsSection = ({tx, extraOperations}: {tx: FormattedTx; extraOperations?: Array<React.ReactNode>}) => {
const operations = useOperations(tx.certificates)

if (extraOperations == null && tx.certificates == null) return null
const OperationsSection = ({
operations,
extraOperations,
}: {
operations: Array<React.ReactNode>
extraOperations?: Array<React.ReactNode>
}) => {
if (extraOperations == null && operations?.length === 0) return null

return (
<View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const ReviewTxScreen = () => {
})

const txBody = useTxBody({cbor, unsignedTx})

const formatedTx = useFormattedTx(txBody)
const formattedMetadata = useFormattedMetadata({txBody, unsignedTx, cbor})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const Inputs = ({inputs}: {inputs: FormattedInputs}) => {

const Input = ({input}: {input: FormattedInput}) => {
const {styles} = useStyles()
if (input?.address === undefined) throw new Error('UTxOsTab: input invalid address')

return (
<View>
Expand All @@ -48,8 +47,8 @@ const Input = ({input}: {input: FormattedInput}) => {

<Space height="lg" />

<CopiableText textToCopy={input.address}>
<Text style={styles.addressText}>{input.address}</Text>
<CopiableText textToCopy={input.address ?? '-'}>
<Text style={styles.addressText}>{input.address ?? '-'}</Text>
</CopiableText>

<Space height="sm" />
Expand Down Expand Up @@ -81,7 +80,6 @@ const Outputs = ({outputs}: {outputs: FormattedOutputs}) => {

const Output = ({output}: {output: FormattedOutput}) => {
const {styles} = useStyles()
if (output?.address === undefined) throw new Error('UTxOsTab: input invalid address')

return (
<View>
Expand All @@ -92,8 +90,8 @@ const Output = ({output}: {output: FormattedOutput}) => {

<Space height="lg" />

<CopiableText textToCopy={output.address}>
<Text style={styles.addressText}>{output.address}</Text>
<CopiableText textToCopy={output.address ?? '-'}>
<Text style={styles.addressText}>{output.address ?? '-'}</Text>
</CopiableText>
</View>

Expand Down Expand Up @@ -133,11 +131,12 @@ const Fee = ({fee}: {fee: string}) => {
)
}

const UtxoTitle = ({isInput, isOwnAdddress}: {isOwnAdddress: boolean; isInput: boolean}) => {
const UtxoTitle = ({isInput, isOwnAdddress}: {isOwnAdddress: boolean | null; isInput: boolean}) => {
const {styles} = useStyles()
const strings = useStrings()

const label = isOwnAdddress ? strings.utxosYourAddressLabel : strings.utxosForeignAddressLabel
const label =
isOwnAdddress != null ? (isOwnAdddress ? strings.utxosYourAddressLabel : strings.utxosForeignAddressLabel) : '-'

return (
<View style={styles.utxoTitle}>
Expand Down
Loading