import BigNumber from 'bignumber.js' import React, { useMemo, type ReactNode } from 'react' import { Trans, useTranslation } from 'react-i18next' import { ScrollView, StyleSheet, Text, View } from 'react-native' import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import SkeletonPlaceholder from 'react-native-skeleton-placeholder' import BackButton from 'src/components/BackButton' import ContactCircle from 'src/components/ContactCircle' import CustomHeader from 'src/components/header/CustomHeader' import { formatValueToDisplay } from 'src/components/TokenDisplay' import WalletIcon from 'src/icons/navigator/Wallet' import PhoneIcon from 'src/icons/Phone' import UserIcon from 'src/icons/User' import { LocalCurrencySymbol } from 'src/localCurrency/consts' import { type Recipient } from 'src/recipients/recipient' import Colors from 'src/styles/colors' import { typeScale } from 'src/styles/fonts' import { Spacing } from 'src/styles/styles' import variables from 'src/styles/variables' import { TokenBalance } from 'src/tokens/slice' import Logger from 'src/utils/Logger' export function ReviewTransaction(props: { title: string children: ReactNode headerLeftButton?: ReactNode testID?: string }) { const insets = useSafeAreaInsets() return ( } title={props.title} /> {props.children} ) } export function ReviewContent(props: { children: ReactNode }) { return {props.children} } export function ReviewSummary(props: { children: ReactNode }) { return {props.children} } export function ReviewSummaryItem(props: { label: string icon: ReactNode primaryValue: string secondaryValue?: string testID?: string }) { return ( {props.label} {props.icon} {props.primaryValue} {!!props.secondaryValue && ( {props.secondaryValue} )} ) } export function ReviewSummaryItemContact({ testID, recipient, }: { testID?: string recipient: Recipient }) { const { t } = useTranslation() const contact = useMemo(() => { const phone = recipient.displayNumber || recipient.e164PhoneNumber if (recipient.name) { return { title: recipient.name, subtitle: phone, icon: UserIcon } } if (phone) { return { title: phone, icon: PhoneIcon } } if (recipient.address) { return { title: recipient.address, icon: WalletIcon } } }, [recipient]) // This should never happen if (!contact) { Logger.error( 'ReviewSummaryItemContact', `Transaction review could not render a contact item for recipient` ) return null } return ( } /> ) } export function ReviewDetails(props: { children: ReactNode }) { return {props.children} } export function ReviewDetailsItem({ label, value, variant = 'default', isLoading, testID, }: { label: ReactNode value: ReactNode variant?: 'default' | 'bold' isLoading?: boolean testID?: string }) { const textStyle = variant === 'bold' ? styles.reviewDetailsItemTextBold : styles.reviewDetailsItemText return ( {label} {/* TODO Add for Earn Deposit/Withdrawal */} {isLoading ? ( ) : ( {value} )} ) } export function ReviewFooter(props: { children: ReactNode }) { return {props.children} } export function ReviewTotalValue({ tokenInfo, feeTokenInfo, tokenAmount, localAmount, feeTokenAmount, feeLocalAmount, localCurrencySymbol, }: { tokenInfo: TokenBalance | undefined feeTokenInfo: TokenBalance | undefined tokenAmount: BigNumber | null localAmount: BigNumber | null feeTokenAmount: BigNumber | undefined feeLocalAmount: BigNumber | null localCurrencySymbol: LocalCurrencySymbol }) { const { t } = useTranslation() // if there are not token info or token amount then it should not even be possible to get to the review screen if (!tokenInfo || !tokenAmount) { return null } // if there are no fees then just format token amount if (!feeTokenInfo || !feeTokenAmount) { if (localAmount) { return ( ) } return t('tokenAmountApprox', { tokenAmount: formatValueToDisplay(tokenAmount), tokenSymbol: tokenInfo.symbol, }) } const sameToken = tokenInfo.tokenId === feeTokenInfo.tokenId const haveLocalPrice = !!localAmount && !!feeLocalAmount // if single token and have local price - return token and local amounts if (sameToken && haveLocalPrice) { return ( ) } // if single token but no local price - return token amount if (sameToken && !haveLocalPrice) { return t('tokenAmountApprox', { tokenAmount: formatValueToDisplay(tokenAmount.plus(feeTokenAmount)), tokenSymbol: tokenInfo.symbol, }) } // if multiple tokens and have local price - return local amount if (!sameToken && haveLocalPrice) { return t('localAmountApprox', { localAmount: formatValueToDisplay(localAmount.plus(feeLocalAmount)), localCurrencySymbol, }) } // otherwise there are multiple tokens with no local prices so return multiple token amounts return t('reviewTransaction.multipleTokensWithPlusSign', { amount1: formatValueToDisplay(tokenAmount), symbol1: tokenInfo.symbol, amount2: formatValueToDisplay(feeTokenAmount), symbol2: feeTokenInfo.symbol, }) } const styles = StyleSheet.create({ safeAreaView: { flex: 1, }, header: { paddingHorizontal: variables.contentPadding, }, reviewContainer: { margin: Spacing.Regular16, gap: Spacing.Thick24, flex: 1, justifyContent: 'space-between', }, reviewContent: { gap: Spacing.Thick24, }, reviewSummary: { borderWidth: 1, borderColor: Colors.borderPrimary, borderRadius: Spacing.Small12, backgroundColor: Colors.backgroundSecondary, padding: Spacing.Regular16, gap: Spacing.Regular16, flexShrink: 1, }, reviewSummaryItem: { gap: Spacing.Tiny4, }, reviewSummaryItemLabel: { ...typeScale.labelSmall, color: Colors.contentSecondary, }, reviewSummaryItemContent: { flexDirection: 'row', gap: Spacing.Smallest8, alignItems: 'center', }, reviewSummaryItemValuesWrapper: { flexShrink: 1, }, reviewSummaryItemPrimaryValue: { ...typeScale.labelSemiBoldLarge, }, reviewSummaryItemSecondaryValue: { ...typeScale.bodySmall, color: Colors.contentSecondary, }, reviewDetails: { gap: Spacing.Regular16, width: '100%', }, reviewDetailsItem: { flexDirection: 'row', justifyContent: 'space-between', gap: Spacing.Smallest8, }, reviewDetailsItemLabel: { flexDirection: 'row', alignItems: 'center', gap: Spacing.Tiny4, }, reviewDetailsItemText: { ...typeScale.bodyMedium, color: Colors.contentSecondary, }, reviewDetailsItemTextBold: { ...typeScale.labelSemiBoldMedium, }, reviewFooter: { gap: Spacing.Regular16, }, loaderContainer: { height: 20, width: 96, }, loader: { height: '100%', width: '100%', }, totalPlusFeesLocalAmount: { color: Colors.contentSecondary, }, })