diff --git a/.prettierignore b/.prettierignore index 6e14cd21921..a841f71a5c6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,4 +10,4 @@ rainbow-scripts .vscode __generated__ coverage -InjectedJSBundle.js \ No newline at end of file +InjectedJSBundle.js diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 39843e734e9..1329d8cb085 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -2008,6 +2008,7 @@ "to": "To", "value": "Value", "hash": "Tx Hash", + "you": "You", "network_fee": "Network Fee", "hash_copied": "\uDBC0\uDC63 Transaction hash copied", "address_copied": "\uDBC0\uDC63 Address copied", diff --git a/src/screens/transaction-details/components/TransactionDetailsFromToSection.tsx b/src/screens/transaction-details/components/TransactionDetailsFromToSection.tsx index 822d8b4042d..bf76c97a9ef 100644 --- a/src/screens/transaction-details/components/TransactionDetailsFromToSection.tsx +++ b/src/screens/transaction-details/components/TransactionDetailsFromToSection.tsx @@ -1,11 +1,8 @@ -import React, { useMemo } from 'react'; -import { Box, Stack } from '@/design-system'; -import * as i18n from '@/languages'; -import { TransactionDetailsAddressRow } from '@/screens/transaction-details/components/TransactionDetailsAddressRow'; -import { useContacts, useUserAccounts } from '@/hooks'; -import { isLowerCaseMatch } from '@/utils'; +import React from 'react'; +import { Box } from '@/design-system'; import { TransactionDetailsDivider } from '@/screens/transaction-details/components/TransactionDetailsDivider'; import { RainbowTransaction } from '@/entities'; +import TransactionMasthead from './TransactionMasthead'; type Props = { transaction: RainbowTransaction; @@ -13,62 +10,11 @@ type Props = { }; export const TransactionDetailsFromToSection: React.FC = ({ transaction, presentToast }) => { - const from = transaction.from ?? undefined; - const to = transaction.to ?? undefined; - const { contacts } = useContacts(); - const fromContact = from ? contacts[from] : undefined; - const toContact = to ? contacts[to] : undefined; - - const { userAccounts, watchedAccounts } = useUserAccounts(); - - const fromAccount = useMemo(() => { - if (!from) { - return undefined; - } else { - return ( - userAccounts.find(account => isLowerCaseMatch(account.address, from)) ?? - watchedAccounts.find(account => isLowerCaseMatch(account.address, from)) - ); - } - }, [from]); - const toAccount = useMemo(() => { - if (!to) { - return undefined; - } else { - return ( - userAccounts.find(account => isLowerCaseMatch(account.address, to)) ?? - watchedAccounts.find(account => isLowerCaseMatch(account.address, to)) - ); - } - }, [to]); - - if (!from && !to) { - return null; - } return ( <> - - {from && ( - - )} - {to && ( - - )} - + ); diff --git a/src/screens/transaction-details/components/TransactionDetailsStatusActionsAndTimestampSection.tsx b/src/screens/transaction-details/components/TransactionDetailsStatusActionsAndTimestampSection.tsx index 1c8785bbaf9..7aaf0ce0307 100644 --- a/src/screens/transaction-details/components/TransactionDetailsStatusActionsAndTimestampSection.tsx +++ b/src/screens/transaction-details/components/TransactionDetailsStatusActionsAndTimestampSection.tsx @@ -24,7 +24,7 @@ type Props = { }; export const TransactionDetailsStatusActionsAndTimestampSection: React.FC = ({ transaction, hideIcon }) => { - const { minedAt, status, from } = transaction; + const { minedAt, status, type, from } = transaction; const dispatch = useDispatch(); const { navigate, goBack } = useNavigation(); const accountAddress = useSelector((state: AppState) => state.settings.accountAddress); @@ -112,7 +112,7 @@ export const TransactionDetailsStatusActionsAndTimestampSection: React.FC - {status && !hideIcon && ( + {type && !hideIcon && ( )} + - {status && ( + {type && ( - {capitalize(status)} + {/* @ts-ignore */} + {i18n.t(i18n.l.transactions.type[transaction?.title])} )} {date && ( diff --git a/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx b/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx index 33098b35ee5..7f60bfe1c26 100644 --- a/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx +++ b/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx @@ -32,12 +32,10 @@ export const TransactionDetailsValueAndFeeSection: React.FC = ({ transact const change = transaction?.changes?.[0]; const value = change?.value || transaction.balance?.display; - const valueDisplay = convertRawAmountToBalance(value || '', assetData!).display || ''; - const nativeCurrencyValue = convertAmountAndPriceToNativeDisplay( - change?.asset?.balance?.amount || '', - change?.asset?.price?.value || '', - nativeCurrency - ).display; + const valueDisplay = value ? convertRawAmountToBalance(value || '', assetData!).display : ''; + const nativeCurrencyValue = change?.asset?.price?.value + ? convertAmountAndPriceToNativeDisplay(change?.asset?.balance?.amount || '', change?.asset?.price?.value || '', nativeCurrency).display + : ''; const feeValue = fee?.value.display ?? ''; const feeNativeCurrencyValue = fee?.native?.display ?? ''; diff --git a/src/screens/transaction-details/components/TransactionMasthead.tsx b/src/screens/transaction-details/components/TransactionMasthead.tsx new file mode 100644 index 00000000000..5d815e11dbf --- /dev/null +++ b/src/screens/transaction-details/components/TransactionMasthead.tsx @@ -0,0 +1,399 @@ +// @refresh reset + +import React, { useEffect, useMemo, useState } from 'react'; +import { NativeCurrencyKey, ParsedAddressAsset, RainbowTransaction } from '@/entities'; + +import { Bleed, Box, Columns, Cover, Row, Rows, Separator, Stack, Text, TextProps } from '@/design-system'; + +import styled from '@/styled-thing'; +import { position } from '@/styles'; +import { ThemeContextProps, useTheme } from '@/theme'; +import { usePersistentDominantColorFromImage } from '@/hooks/usePersistentDominantColorFromImage'; +import RowWithMargins from '@/components/layout/RowWithMargins'; +import { IS_ANDROID } from '@/env'; +import { convertAmountAndPriceToNativeDisplay, convertAmountToBalanceDisplay } from '@/helpers/utilities'; +import { fetchENSAvatar } from '@/hooks/useENSAvatar'; +import { fetchReverseRecord } from '@/handlers/ens'; + +import { address, formatAddressForDisplay } from '@/utils/abbreviations'; +import { ContactAvatar } from '@/components/contacts'; +import { isLowerCaseMatch } from '@/utils'; +import { useSelector } from 'react-redux'; +import { AppState } from '@/redux/store'; +import { useContacts, useUserAccounts } from '@/hooks'; +import { useTiming } from 'react-native-redash'; +import Animated, { Easing, SharedValue, interpolate, useAnimatedStyle } from 'react-native-reanimated'; +import { removeFirstEmojiFromString, returnStringFirstEmoji } from '@/helpers/emojiHandler'; +import { addressHashedColorIndex, addressHashedEmoji } from '@/utils/profileUtils'; +import ImageAvatar from '@/components/contacts/ImageAvatar'; +import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; +import { Network } from '@/networks/types'; +import * as lang from '@/languages'; + +const TransactionMastheadHeight = android ? 153 : 135; + +const Container = styled(Box).attrs({ + direction: 'column', +})({ + borderRadius: 30, + flex: 1, + paddingHorizontal: 20, + height: TransactionMastheadHeight, + overflow: 'hidden', + zIndex: 0, + ...(android ? { paddingTop: 4 } : {}), + justifyContent: 'center', + alitnItems: 'center', +}); + +const Gradient = styled(Box).attrs(({ theme: { colors }, color }: { theme: ThemeContextProps; color: string }) => ({ + backgroundColor: colors.alpha(color, 0.08), +}))({ + ...position.coverAsObject, +}); + +function CurrencyTile({ + contactAddress, + title, + subtitle, + address = '', + asset, + showAsset, + image, + fallback, + onAddressCopied, +}: { + asset?: ParsedAddressAsset; + showAsset?: boolean; + contactAddress?: string; + title?: string; + subtitle?: string; + image?: string; + fallback?: string; + address?: string; + onAddressCopied: () => void; +}) { + const accountAddress = useSelector((state: AppState) => state.settings.accountAddress); + const theme = useTheme(); + + // @ts-ignore + const { contacts } = useContacts(); + + const { userAccounts, watchedAccounts } = useUserAccounts(); + const addressContact = address ? contacts[address] : undefined; + const addressAccount = useMemo(() => { + if (!address) { + return undefined; + } else { + return ( + userAccounts.find(account => isLowerCaseMatch(account.address, address)) ?? + watchedAccounts.find(account => isLowerCaseMatch(account.address, address)) + ); + } + }, [address]); + + const formattedAddress = formatAddressForDisplay(address, 4, 6); + const [fetchedEnsName, setFetchedEnsName] = useState(); + const [fetchedEnsImage, setFetchedEnsImage] = useState(); + const [imageLoaded, setImageLoaded] = useState(!!addressAccount?.image); + + const accountEmoji = useMemo(() => returnStringFirstEmoji(addressAccount?.label), [addressAccount]); + const accountName = useMemo(() => removeFirstEmojiFromString(addressAccount?.label), []); + const avatarColor = + addressAccount?.color ?? addressContact?.color ?? theme.colors.avatarBackgrounds[addressHashedColorIndex(address) || 1]; + const emoji = accountEmoji || addressHashedEmoji(address); + + const name = accountName || fetchedEnsName || addressContact?.nickname || addressContact?.ens || formattedAddress; + + if (accountAddress?.toLowerCase() === address?.toLowerCase() && !showAsset) { + title = lang.t(lang.l.transaction_details.you); + } + + const shouldShowAddress = (!name.includes('...') || name === lang.t(lang.l.transaction_details.you)) && !showAsset; + const imageUrl = fetchedEnsImage ?? addressAccount?.image; + const ensAvatarSharedValue = useTiming(!!image || (!!imageUrl && imageLoaded), { + duration: image || addressAccount?.image ? 0 : 420, + }); + + useEffect(() => { + if (!addressContact?.nickname && !accountName) { + fetchReverseRecord(address).then(name => { + if (name) { + setFetchedEnsName(name); + } + }); + } + }, []); + + useEffect(() => { + if (!addressAccount?.image && (fetchedEnsName || addressContact?.ens)) { + const ens = fetchedEnsName ?? addressContact?.ens; + if (ens) { + fetchENSAvatar(ens, { cacheFirst: true }).then(avatar => { + if (avatar?.imageUrl) { + setFetchedEnsImage(avatar.imageUrl); + } + }); + } + } + }, [fetchedEnsName]); + + const colorForAsset = usePersistentDominantColorFromImage(showAsset ? asset?.icon_url : imageUrl) || avatarColor; + + const colorToUse = colorForAsset; + + const emojiAvatarAnimatedStyle = useAnimatedStyle(() => ({ + opacity: interpolate(ensAvatarSharedValue.value, [0, 1], [1, 0]), + })); + const ensAvatarAnimatedStyle = useAnimatedStyle(() => ({ + opacity: ensAvatarSharedValue.value, + })); + + const onImageLoad = () => { + setImageLoaded(true); + }; + + return ( + + + + + + + {showAsset ? ( + + ) : ( + <> + + {/*add coin icon*/} + + + + + + + + + )} + + + + + + + + + + + + + + + + + + + + + ); +} + +type AnimatedTextProps = Omit & { + text: string; + loadedText: string | undefined; +}; +const AnimatedText = ({ text, loadedText, size, weight, color, align, ...props }: AnimatedTextProps) => { + const loadedTextValue = useTiming(!!loadedText, { + duration: 420, + easing: Easing.linear, + }); + const textStyle = useAnimatedStyle(() => ({ + opacity: interpolate(loadedTextValue.value, [0, 0.5, 1], [1, 0, 0]), + })); + const loadedTextStyle = useAnimatedStyle(() => ({ + opacity: interpolate(loadedTextValue.value, [0, 0.5, 1], [0, 0, 1]), + })); + + return ( + + + + {text} + + + + + + {loadedText} + + + + + ); +}; +const DoubleChevron = () => ( + + + + 􀯻 + + + + 􀯻 + + + + +); + +export default function TransactionMasthead({ transaction }: { transaction: RainbowTransaction }) { + const nativeCurrency = useSelector((state: AppState) => state.settings.nativeCurrency); + + const inputAsset = useMemo(() => { + const inAsset = transaction?.changes?.find(a => a?.direction === 'in')?.asset; + if (!inAsset) return undefined; + + return { + inAssetValueDisplay: convertAmountToBalanceDisplay(inAsset?.balance?.amount || '0', inAsset), + inAssetNativeDisplay: inAsset?.price?.value + ? convertAmountAndPriceToNativeDisplay(inAsset?.balance?.amount || '0', inAsset?.price?.value || '0', nativeCurrency)?.display + : '-', + ...inAsset, + }; + }, []); + + const outputAsset = useMemo(() => { + const outAsset = transaction?.changes?.find(a => a?.direction === 'out')?.asset; + if (!outAsset) return undefined; + + return { + image: outAsset?.icon_url || '', + inAssetValueDisplay: convertAmountToBalanceDisplay(outAsset?.balance?.amount || '0', outAsset), + inAssetNativeDisplay: outAsset?.price?.value + ? convertAmountAndPriceToNativeDisplay(outAsset?.balance?.amount || '0', outAsset?.price?.value || '0', nativeCurrency)?.display + : '-', + ...outAsset, + }; + }, []); + + const contractImage = transaction?.contract?.iconUrl; + const contractName = transaction?.contract?.name; + + // if its a mint then we want to show the mint contract first + const toAddress = (transaction.type === 'mint' ? transaction?.from : transaction?.to) || undefined; + const fromAddress = (transaction.type === 'mint' ? transaction?.to : transaction?.from) || undefined; + + const getRightMasteadData = (): { title?: string; subtitle?: string; image?: string } => { + if (transaction.type === 'swap') { + return { + title: inputAsset?.inAssetValueDisplay, + subtitle: inputAsset?.inAssetNativeDisplay, + }; + } + if (transaction.type === 'contract_interaction' || transaction.type === 'approve') { + return { + title: contractName, + subtitle: transaction?.from || '', + image: contractImage, + }; + } + + return { + title: undefined, + subtitle: undefined, + image: undefined, + }; + }; + + const getLeftMasteadData = (): { title?: string; subtitle?: string; image?: string } => { + if (transaction.type === 'swap') { + return { + title: outputAsset?.inAssetValueDisplay, + subtitle: outputAsset?.inAssetNativeDisplay, + }; + } + return { + title: undefined, + subtitle: undefined, + image: undefined, + }; + }; + + const leftMasteadData = getLeftMasteadData(); + const rightMasteadData = getRightMasteadData(); + + return ( + + + + + + {}} + /> + + {}} + /> + + + + + + + + + ); +}