From 3ba032e02740a3a17eebd9e20056d06abd8abbe7 Mon Sep 17 00:00:00 2001 From: Christian Baroni Date: Tue, 21 Nov 2023 16:34:42 -0500 Subject: [PATCH] Simulation fixes (#5194) * Use simulation fetch config * Update green/red colors * Add Moti * Clean up routes, use signTransactionSheetConfig on iOS * UI cleanup, state fixes, make cards expandable * Trim trailing zeros from simulation amounts * Fix GasSpeedButton jank * Block signing sheet dismiss gestures * Revert "Update green/red colors" This reverts commit 50ee10d20a9ae8d94df4b7e578194a2e6e9abe1c. * Fixed: Update green/red colors * Fix button shadows * Fix display nonce * Remove unused code * Fix type * Upgrade reanimated to fix crash * Cleanup * Fix type errors from reanimated upgrade * lint * Fix Android scrolling * Revert "lint" This reverts commit 94fbd40c9676c06a52c375965de44b25fa1e228b. * Revert "Fix type errors from reanimated upgrade" This reverts commit 8cf75da6cf65d8297016e0ef53374006eae27614. * Revert "Upgrade reanimated to fix crash" This reverts commit 22d1b0bc9d171f33ed096263fb197544cfbb2fd6. * Prevent crash without upgrading reanimated Moving the reanimated upgrade to another PR * Fix zIndex * Android fixes --------- Co-authored-by: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Co-authored-by: Ben Goldberg --- package.json | 1 + .../ProfileActionButtonsRow.tsx | 5 - src/components/coin-icon/ChainImage.tsx | 4 +- src/components/contacts/ContactAvatar.js | 25 +- src/components/contacts/ImageAvatar.js | 30 +- src/components/gas/GasSpeedButton.js | 50 +- .../SheetActionButton.tsx | 10 +- src/graphql/index.ts | 2 +- src/helpers/utilities.ts | 18 +- src/languages/en_US.json | 12 +- src/navigation/Routes.android.tsx | 4 - src/navigation/Routes.ios.tsx | 15 +- src/navigation/config.tsx | 1 + src/navigation/effects.tsx | 1 + src/navigation/routesNames.ts | 1 - src/screens/SignTransactionSheet.tsx | 2147 +++++++++++------ src/styles/colors.ts | 7 +- yarn.lock | 106 +- 18 files changed, 1588 insertions(+), 851 deletions(-) diff --git a/package.json b/package.json index 470d4bfdf48..83f071dd1ca 100644 --- a/package.json +++ b/package.json @@ -189,6 +189,7 @@ "make-color-more-chill": "0.2.2", "match-sorter": "6.3.0", "mnemonist": "0.38.1", + "moti": "0.27.2", "multiformats": "9.6.2", "nanoid": "3.2.0", "p-queue": "7.2.0", diff --git a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx index 08bfcd44a0b..7c42236d482 100644 --- a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx @@ -273,11 +273,6 @@ export function MoreButton() { Clipboard.setString(accountAddress); }, [accountAddress, isToastActive, setToastActive]); - const { navigate } = useNavigation(); - const navigateToSigningSheet = React.useCallback(() => { - navigate(Routes.SIGN_TRANSACTION_SHEET); - }, [navigate]); - return ( <> {/* @ts-expect-error JavaScript component */} diff --git a/src/components/coin-icon/ChainImage.tsx b/src/components/coin-icon/ChainImage.tsx index 839ba65e6dc..6e5e622543c 100644 --- a/src/components/coin-icon/ChainImage.tsx +++ b/src/components/coin-icon/ChainImage.tsx @@ -14,11 +14,9 @@ import ZoraBadge from '../../assets/badges/zora.png'; import { ImgixImage } from '../images'; export function ChainImage({ - borderRadius = 20, chain, size = 20, }: { - borderRadius?: number; chain: Network | null | undefined; size?: number; }) { @@ -48,7 +46,7 @@ export function ChainImage({ ); } diff --git a/src/components/contacts/ContactAvatar.js b/src/components/contacts/ContactAvatar.js index 77daee6091a..7c7c33b77eb 100644 --- a/src/components/contacts/ContactAvatar.js +++ b/src/components/contacts/ContactAvatar.js @@ -10,8 +10,7 @@ import ShadowStack from '@/react-native-shadow-stack'; import { IS_ANDROID } from '@/env'; const buildShadows = (color, size, darkMode, colors) => { - // TODO: remove `legacySmall` size once rainbow home screen revamp is released - if (size === 'small' || size === 'legacySmall') { + if (size === 'small') { return [ [0, 3, 5, colors.shadow, 0.14], [ @@ -46,6 +45,17 @@ const buildShadows = (color, size, darkMode, colors) => { 0.4, ], ]; + } else if (size === 'signing') { + return [ + [ + 0, + 4, + 12, + darkMode ? colors.shadow : colors.avatarBackgrounds[color] || color, + darkMode ? 0.16 : 0.2, + ], + [0, 2, 6, colors.trueBlack, 0.02], + ]; } else { return sizeConfigs(colors)[size]['shadow']; } @@ -84,6 +94,10 @@ const sizeConfigs = colors => ({ ], textSize: 'larger', }, + signing: { + dimensions: 44, + textSize: 25, + }, small: { dimensions: 36, textSize: 'large', @@ -93,11 +107,6 @@ const sizeConfigs = colors => ({ textSize: 'large', shadow: [[0, 0, 0, colors.shadow, 0]], }, - // TODO: remove `legacySmall` size once rainbow home screen revamp is released - legacySmall: { - dimensions: 34, - textSize: 'large', - }, smaller: { dimensions: 20, textSize: 'micro', @@ -135,7 +144,7 @@ const ContactAvatar = ({ color, size = 'medium', value, ...props }) => { typeof color === 'number' ? // sometimes the color is gonna be missing so we fallback to white // otherwise there will be only shadows without the the placeholder "circle" - colors.avatarBackgrounds[color] ?? 'white' + colors.avatarBackgrounds[color] ?? colors.white : color; return ( diff --git a/src/components/contacts/ImageAvatar.js b/src/components/contacts/ImageAvatar.js index ea23b18bca0..e82ae065935 100644 --- a/src/components/contacts/ImageAvatar.js +++ b/src/components/contacts/ImageAvatar.js @@ -5,13 +5,14 @@ import styled from '@/styled-thing'; import { borders } from '@/styles'; import ShadowStack from '@/react-native-shadow-stack'; import { IS_ANDROID } from '@/env'; +import { useAccountAccentColor } from '@/hooks'; const buildSmallShadows = (color, colors) => [ [0, 3, 5, colors.shadow, 0.14], [0, 6, 10, colors.avatarBackgrounds[color] || color, 0.2], ]; -const sizeConfigs = (colors, isDarkMode) => ({ +const sizeConfigs = (accentColor, colors, isDarkMode) => ({ header: { dimensions: 34, textSize: 'large', @@ -32,19 +33,25 @@ const sizeConfigs = (colors, isDarkMode) => ({ ], textSize: 28, }, - sim: { - dimensions: 44, - shadow: [ - [0, 4, 6, colors.shadow, 0.04], - [0, 1, 3, colors.shadow, 0.08], - ], - textSize: 'larger', - }, medium: { dimensions: 40, shadow: [[0, 4, 12, colors.shadow, isDarkMode ? 0.3 : 0.15]], textSize: 'larger', }, + signing: { + dimensions: 44, + shadow: [ + [ + 0, + 4, + 12, + !isDarkMode && accentColor ? accentColor : colors.shadow, + isDarkMode ? 0.16 : 0.2, + ], + [0, 2, 6, colors.trueBlack, 0.02], + ], + textSize: 25, + }, small: { dimensions: 30, shadow: [[0, 3, 9, colors.shadow, 0.1]], @@ -87,10 +94,11 @@ const ImageAvatar = ({ onLoad = undefined, ...props }) => { + const { accentColor } = useAccountAccentColor(); const { colors, isDarkMode } = useTheme(); const { dimensions, shadow } = useMemo( - () => sizeConfigs(colors, isDarkMode)[size], - [colors, isDarkMode, size] + () => sizeConfigs(accentColor, colors, isDarkMode)[size], + [accentColor, colors, isDarkMode, size] ); const shadows = useMemo( diff --git a/src/components/gas/GasSpeedButton.js b/src/components/gas/GasSpeedButton.js index 79ff68abd30..1634929098f 100644 --- a/src/components/gas/GasSpeedButton.js +++ b/src/components/gas/GasSpeedButton.js @@ -4,8 +4,10 @@ import AnimateNumber from '@bankify/react-native-animate-number'; import lang from 'i18n-js'; import { isEmpty, isNaN, isNil, upperFirst } from 'lodash'; import makeColorMoreChill from 'make-color-more-chill'; +import { AnimatePresence, MotiView } from 'moti'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { InteractionManager, Keyboard } from 'react-native'; +import { Easing } from 'react-native-reanimated'; import { darkModeThemeColors } from '../../styles/colors'; import { ButtonPressAnimation } from '../animations'; import { ChainBadge, CoinIcon } from '../coin-icon'; @@ -95,7 +97,13 @@ const ChainBadgeContainer = styled.View.attrs({ ...margin.object(0), }); -const NativeCoinIconWrapper = styled(Column)(margin.object(1.5, 5, 0, 0)); +const NativeCoinIconWrapper = styled(Column).attrs({})({ + ...margin.object(1, 5, 0, 0), + alignItems: 'center', + height: 18, + justifyContent: 'center', + width: 18, +}); const Container = styled(Column).attrs({ alignItems: 'center', @@ -591,19 +599,33 @@ const GasSpeedButton = ({ > - {currentNetwork === Network.mainnet ? ( - - ) : ( - - )} + + {!!currentNetwork && ( + + {currentNetwork === Network.mainnet ? ( + + ) : ( + + )} + + )} + diff --git a/src/components/sheet/sheet-action-buttons/SheetActionButton.tsx b/src/components/sheet/sheet-action-buttons/SheetActionButton.tsx index 2bd73e9b403..c8b8022fbf7 100644 --- a/src/components/sheet/sheet-action-buttons/SheetActionButton.tsx +++ b/src/components/sheet/sheet-action-buttons/SheetActionButton.tsx @@ -24,6 +24,7 @@ type Props = PropsWithChildren<{ label?: string; lightShadows?: boolean; marginBottom?: number; + newShadows?: boolean; nftShadows?: boolean; onPress?: () => void; scaleTo?: number; @@ -89,6 +90,7 @@ const SheetActionButton: React.FC = ({ label = null, lightShadows, onPress, + newShadows, nftShadows, scaleTo = 0.9, size = null, @@ -105,7 +107,12 @@ const SheetActionButton: React.FC = ({ const isWhite = color === colors.white; const textColor = givenTextColor || colors.whiteLabel; const shadowsForButtonColor = useMemo(() => { - if (nftShadows) { + if (newShadows) { + return [ + [0, 2, 6, colors.trueBlack, 0.02], + [0, 10, 30, isDarkMode ? colors.shadow : color, 0.4], + ]; + } else if (nftShadows) { return [[0, 10, 30, colors.alpha(colors.shadowBlack, 0.3)]]; } else if (!forceShadows && (disabled || isTransparent)) { return [[0, 0, 0, colors.transparent, 0]]; @@ -128,6 +135,7 @@ const SheetActionButton: React.FC = ({ isTransparent, isDarkMode, lightShadows, + newShadows, nftShadows, isWhite, ]); diff --git a/src/graphql/index.ts b/src/graphql/index.ts index 55515909de8..6e462fc6ca8 100644 --- a/src/graphql/index.ts +++ b/src/graphql/index.ts @@ -10,7 +10,7 @@ export const metadataClient = getMetadataSdk( getFetchRequester(config.metadata) ); export const simulationClient = getMetadataSdk( - getFetchRequester(config.metadata) + getFetchRequester(config.simulation) ); export const arcClient = getArcSdk(getFetchRequester(config.arc)); export const arcDevClient = getArcDevSdk(getFetchRequester(config.arcDev)); diff --git a/src/helpers/utilities.ts b/src/helpers/utilities.ts index 12f33bf7975..d0cc6b5e18a 100644 --- a/src/helpers/utilities.ts +++ b/src/helpers/utilities.ts @@ -374,14 +374,20 @@ export const convertRawAmountToNativeDisplay = ( export const convertRawAmountToBalance = ( value: BigNumberish, asset: { decimals: number; symbol?: string }, - buffer?: number + buffer?: number, + trimTrailingZeros?: boolean ) => { const decimals = asset?.decimals ?? 18; const assetBalance = convertRawAmountToDecimalFormat(value, decimals); return { amount: assetBalance, - display: convertAmountToBalanceDisplay(assetBalance, asset, buffer), + display: convertAmountToBalanceDisplay( + assetBalance, + asset, + buffer, + trimTrailingZeros + ), }; }; @@ -391,11 +397,15 @@ export const convertRawAmountToBalance = ( export const convertAmountToBalanceDisplay = ( value: BigNumberish, asset: { decimals: number; symbol?: string }, - buffer?: number + buffer?: number, + trimTrailingZeros?: boolean ) => { const decimals = asset?.decimals ?? 18; const display = handleSignificantDecimals(value, decimals, buffer); - return `${display} ${asset?.symbol || ''}`; + const formattedDisplay = trimTrailingZeros + ? display.replace(/\.?0+$/, '') + : display; + return `${formattedDisplay} ${asset?.symbol || ''}`; }; /** diff --git a/src/languages/en_US.json b/src/languages/en_US.json index a11aa444d13..8213540a786 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -2363,7 +2363,8 @@ "types": { "chain": "Network", "contract": "Contract", - "contract_created": "Contact Created", + "contract_created": "Contract Created", + "to": "To", "function": "Function", "source_code": "Source Code", "nonce": "Nonce" @@ -2385,6 +2386,15 @@ "confirm": "􀎽 Confirm", "get_native_token": "Get %{symbol}", "buy_native_token": "Buy %{symbol}" + }, + "formatted_dates": { + "today": "Today", + "day_ago": "day ago", + "days_ago": "days ago", + "week_ago": "week ago", + "weeks_ago": "weeks ago", + "month_ago": "month ago", + "months_ago": "months ago" } } }, diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx index bcc8b7dfd6e..c9f90dae4d3 100644 --- a/src/navigation/Routes.android.tsx +++ b/src/navigation/Routes.android.tsx @@ -387,10 +387,6 @@ function BSNavigator() { name={Routes.CONFIRM_REQUEST} options={walletconnectBottomSheetPreset} /> - ); } diff --git a/src/navigation/Routes.ios.tsx b/src/navigation/Routes.ios.tsx index 20c109af370..d43d0c22c79 100644 --- a/src/navigation/Routes.ios.tsx +++ b/src/navigation/Routes.ios.tsx @@ -330,15 +330,7 @@ function NativeStackNavigator() { - { const [provider, setProvider] = useState(null); const [currentNetwork, setCurrentNetwork] = useState(); const [isAuthorizing, setIsAuthorizing] = useState(false); + const [isLoading, setIsLoading] = useState(!isPersonalSign); const [methodName, setMethodName] = useState(null); const calculatingGasLimit = useRef(false); - const [isBalanceEnough, setIsBalanceEnough] = useState(false); + const [isBalanceEnough, setIsBalanceEnough] = useState(); + const [nonceForDisplay, setNonceForDisplay] = useState(); const isFocused = useIsFocused(); const dispatch = useDispatch(); @@ -217,6 +238,13 @@ export const SignTransactionSheet = () => { const simulationUnavailable = isPersonalSign || currentNetwork === Network.zora; + const itemCount = + (simulationData?.in?.length || 0) + + (simulationData?.out?.length || 0) + + (simulationData?.approvals?.length || 0); + + const noChanges = !!(simulationData && itemCount === 0); + const req = transactionDetails?.payload?.params?.[0]; const request = useMemo(() => { return isMessageRequest @@ -296,31 +324,22 @@ export const SignTransactionSheet = () => { transactionDetails?.walletConnectV2RequestValues?.chainId, updateTxFee, // @ts-expect-error Property '_chainId' is private and only accessible within class 'Connector'.ts(2341) - walletConnector?._chainId, ]); const fetchMethodName = useCallback( async (data: string) => { const methodSignaturePrefix = data.substr(0, 10); - let fallbackHandler: NodeJS.Timeout | undefined = undefined; try { - fallbackHandler = setTimeout(() => { - setMethodName(data); - }, 5000); const { name } = await methodRegistryLookupAndParse( methodSignaturePrefix, getNetworkObj(currentNetwork!).id ); if (name) { setMethodName(name); - clearTimeout(fallbackHandler); } } catch (e) { setMethodName(data); - if (fallbackHandler) { - clearTimeout(fallbackHandler); - } } }, [currentNetwork] @@ -372,6 +391,7 @@ export const SignTransactionSheet = () => { return { amount: nativeAsset?.balance?.amount || 0, display: nativeAsset?.balance?.display || `0 ${nativeAsset?.symbol}`, + isLoaded: nativeAsset?.balance?.display !== undefined, symbol: nativeAsset?.symbol || 'ETH', }; }, [ @@ -387,16 +407,11 @@ export const SignTransactionSheet = () => { return; } - if (!isSufficientGas) { - setIsBalanceEnough(false); - return; - } - const { gasFee } = selectedGasFee; - if (!gasFee?.estimatedFee) { - setIsBalanceEnough(false); + if (!walletBalance?.isLoaded || !currentNetwork || !gasFee?.estimatedFee) { return; } + // Get the TX fee Amount const txFeeAmount = fromWei(gasFee?.maxFee?.value?.amount ?? 0); @@ -413,12 +428,11 @@ export const SignTransactionSheet = () => { setIsBalanceEnough(isEnough); }, [ - isBalanceEnough, isMessageRequest, isSufficientGas, currentNetwork, selectedGasFee, - walletBalance?.amount, + walletBalance, req, ]); @@ -480,104 +494,129 @@ export const SignTransactionSheet = () => { }, [currentNetwork, setProvider]); useEffect(() => { - const getNativeAsset = async () => { - const asset = await ethereumUtils.getNativeAssetForNetwork( - currentNetwork!, - accountInfo.address - ); - if (asset) { - provider && setNativeAsset(asset); + (async () => { + if (currentNetwork) { + const asset = await ethereumUtils.getNativeAssetForNetwork( + currentNetwork!, + accountInfo.address + ); + if (asset) { + provider && setNativeAsset(asset); + } } - }; - currentNetwork && getNativeAsset(); + })(); }, [accountInfo.address, currentNetwork, provider]); useEffect(() => { - setTimeout(async () => { - // Message Signing - if (isMessageRequest) { - const simulationData = await simulationClient.simulateMessage({ - address: accountAddress, - chainId: Number( - transactionDetails?.walletConnectV2RequestValues?.chainId || - // @ts-expect-error Property '_chainId' is private and only accessible within class 'Connector'.ts(2341) - - walletConnector?._chainId - ), - message: { - method: transactionDetails?.payload?.method, - params: [request.message], - }, - domain: transactionDetails?.dappUrl, - }); - - if ( - isNil(simulationData?.simulateMessage?.simulation) && - isNil(simulationData?.simulateMessage?.error) - ) { - setSimulationData({ in: [], out: [], approvals: [] }); - return; - } - if (simulationData?.simulateMessage?.error && !simulationUnavailable) { - setSimulationError(simulationData?.simulateMessage?.error?.type); - return; - } - if ( - simulationData.simulateMessage?.simulation && - !simulationUnavailable - ) { - setSimulationData(simulationData.simulateMessage?.simulation); + (async () => { + if ( + accountInfo.address && + currentNetwork && + !isMessageRequest && + !nonceForDisplay + ) { + try { + const nonce = await getNextNonce(); + if (nonce || nonce === 0) { + const nonceAsString = nonce.toString(); + setNonceForDisplay(nonceAsString); + } + } catch (error) { + console.error('Failed to get nonce for display:', error); } - } else { - // TX Signing - const simulationData = await simulationClient.simulateTransactions({ - chainId: Number( - transactionDetails?.walletConnectV2RequestValues?.chainId || - // @ts-expect-error Property '_chainId' is private and only accessible within class 'Connector'.ts(2341) - walletConnector?._chainId - ), - transactions: [ - { - from: req?.from, - to: req?.to, - data: req?.data, - value: req?.value || '0x0', + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [accountInfo.address, currentNetwork, getNextNonce, isMessageRequest]); + + useEffect(() => { + const timeout = setTimeout(async () => { + try { + const chainId = Number( + transactionDetails?.walletConnectV2RequestValues?.chainId || + // @ts-expect-error Property '_chainId' is private and only accessible within class 'Connector'.ts(2341) + walletConnector?._chainId + ); + let simulationData; + if (isMessageRequest) { + // Message Signing + simulationData = await simulationClient.simulateMessage({ + address: accountAddress, + chainId: chainId, + message: { + method: transactionDetails?.payload?.method, + params: [request.message], }, - ], - domain: transactionDetails?.dappUrl, - }); - if ( - isNil(simulationData?.simulateTransactions?.[0]?.simulation) && - isNil(simulationData?.simulateTransactions?.[0]?.error) - ) { - setSimulationData({ in: [], out: [], approvals: [] }); - return; - } - if (simulationData?.simulateTransactions?.[0]?.error) { - setSimulationError( - simulationData?.simulateTransactions?.[0]?.error?.type - ); - return; - } - if (simulationData.simulateTransactions?.[0]?.simulation) { - setSimulationData(simulationData.simulateTransactions[0]?.simulation); + domain: transactionDetails?.dappUrl, + }); + // Handle message simulation response + if ( + isNil(simulationData?.simulateMessage?.simulation) && + isNil(simulationData?.simulateMessage?.error) + ) { + setSimulationData({ in: [], out: [], approvals: [] }); + } else if ( + simulationData?.simulateMessage?.error && + !simulationUnavailable + ) { + setSimulationError(simulationData?.simulateMessage?.error?.type); + setSimulationData(undefined); + } else if ( + simulationData.simulateMessage?.simulation && + !simulationUnavailable + ) { + setSimulationData(simulationData.simulateMessage?.simulation); + } + } else { + // TX Signing + simulationData = await simulationClient.simulateTransactions({ + chainId: chainId, + transactions: [ + { + from: req?.from, + to: req?.to, + data: req?.data, + value: req?.value || '0x0', + }, + ], + domain: transactionDetails?.dappUrl, + }); + // Handle TX simulation response + if ( + isNil(simulationData?.simulateTransactions?.[0]?.simulation) && + isNil(simulationData?.simulateTransactions?.[0]?.error) + ) { + setSimulationData({ in: [], out: [], approvals: [] }); + } else if (simulationData?.simulateTransactions?.[0]?.error) { + setSimulationError( + simulationData?.simulateTransactions?.[0]?.error?.type + ); + setSimulationData(undefined); + } else if (simulationData.simulateTransactions?.[0]?.simulation) { + setSimulationData( + simulationData.simulateTransactions[0]?.simulation + ); + } } + } catch (error) { + logger.error(new RainbowError('Error while simulating'), { error }); + } finally { + setIsLoading(false); } - }, 1000); + }, 750); + + return () => { + clearTimeout(timeout); + }; }, [ accountAddress, currentNetwork, isMessageRequest, isPersonalSign, - req?.data, - req?.from, - req?.to, - req?.value, + req, request.message, simulationUnavailable, - transactionDetails?.dappUrl, - transactionDetails?.payload?.method, - transactionDetails?.walletConnectV2RequestValues?.chainId, + transactionDetails, // @ts-expect-error Property '_chainId' is private and only accessible within class 'Connector'.ts(2341) walletConnector?._chainId, ]); @@ -1041,112 +1080,269 @@ export const SignTransactionSheet = () => { const onPressCancel = useCallback(() => onCancel(), [onCancel]); + const expandedCardBottomInset = + EXPANDED_CARD_BOTTOM_INSET + (isMessageRequest ? 0 : GAS_BUTTON_SPACE); + return ( - - - - - - - - - - - - + + + + + + + + - {transactionDetails.dappName} - - {false && } + + + + + + {transactionDetails.dappName} + + {false && } + + + {isMessageRequest + ? i18n.t( + i18n.l.walletconnect.simulation.titles + .message_request + ) + : i18n.t( + i18n.l.walletconnect.simulation.titles + .transaction_request + )} + + - - {isMessageRequest - ? i18n.t( - i18n.l.walletconnect.simulation.titles.message_request + + + + + {isMessageRequest ? ( + + ) : ( + + )} + + + + + + {accountInfo.accountImage ? ( + + ) : ( + + )} + + + + {i18n.t( + i18n.l.walletconnect.simulation.profile_section + .signing_with + )} + + + {accountInfo.accountName} + + + {isMessageRequest ? ( + + {i18n.t( + i18n.l.walletconnect.simulation.profile_section + .free_to_sign + )} + + ) : ( + + + {!!currentNetwork && walletBalance?.isLoaded && ( + + + + + + + {`${walletBalance?.display} ${i18n.t( + i18n.l.walletconnect.simulation + .profile_section.on_network, + { + network: getNetworkObj( + currentNetwork! + )?.name, + } + )}`} + + + + )} + + )} - - - - - - - - - {isMessageRequest ? ( - + + + + + + - ) : ( - - )} - {/* Hidden scroll view to disable sheet dismiss gestures */} + + + + {/* Extra ScrollView to prevent the sheet from hijacking the real ScrollViews */} + {IS_IOS && ( { > - - - - - - {accountInfo.accountImage ? ( - // size 44 - - ) : ( - - )} - - - - {i18n.t( - i18n.l.walletconnect.simulation.profile_section - .signing_with - )} - - - - {accountInfo.accountName} - - - - - - - - - {isMessageRequest - ? i18n.t( - i18n.l.walletconnect.simulation.profile_section - .free_to_sign - ) - : isZero(walletBalance?.amount) - ? i18n.t( - i18n.l.walletconnect.simulation.profile_section - .no_native_balance, - { symbol: walletBalance?.symbol } - ) - : walletBalance.display} - - {!isBalanceEnough && ( - - {i18n.t( - i18n.l.walletconnect.simulation.profile_section - .on_network, - { network: getNetworkObj(currentNetwork!)?.name } - )} - - )} - - - - - - - - - - - + )} + - {!isMessageRequest && ( - - )} - - + {!isMessageRequest && ( + + + + )} + + + + ); }; -const SimulationCard = ({ - simulation, - isPersonalSign, - currentNetwork, - simulationError, - isBalanceEnough, - walletBalance, -}: { - simulation: TransactionSimulationResult | undefined; - isPersonalSign: boolean; +interface SimulationCardProps { currentNetwork: Network; + expandedCardBottomInset: number; + isBalanceEnough: boolean | undefined; + isLoading: boolean; + isPersonalSign: boolean; + noChanges: boolean; + simulation: TransactionSimulationResult | undefined; simulationError: TransactionErrorType | undefined; - isBalanceEnough: boolean; walletBalance: { amount: string | number; display: string; + isLoaded: boolean; symbol: string; }; -}) => { +} + +const SimulationCard = ({ + currentNetwork, + expandedCardBottomInset, + isBalanceEnough, + isLoading, + isPersonalSign, + noChanges, + simulation, + simulationError, + walletBalance, +}: SimulationCardProps) => { const cardHeight = useSharedValue(COLLAPSED_CARD_HEIGHT); const contentHeight = useSharedValue( COLLAPSED_CARD_HEIGHT - CARD_BORDER_WIDTH * 2 ); const spinnerRotation = useSharedValue(0); - const itemCount = - (simulation?.in?.length || 0) + - (simulation?.out?.length || 0) + - (simulation?.approvals?.length || 0); const simulationUnavailable = isPersonalSign || currentNetwork === Network.zora; - const noChanges = simulation && itemCount === 0; - const listStyle = useAnimatedStyle(() => ({ - opacity: interpolate( - cardHeight.value, - [ - COLLAPSED_CARD_HEIGHT, - contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT - ? MAX_CARD_HEIGHT - : contentHeight.value + CARD_BORDER_WIDTH * 2, - ], - [0, 1] - ), + opacity: noChanges + ? withTiming(1, timingConfig) + : interpolate( + cardHeight.value, + [ + COLLAPSED_CARD_HEIGHT, + contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT + ? MAX_CARD_HEIGHT + : contentHeight.value + CARD_BORDER_WIDTH * 2, + ], + [0, 1] + ), })); const spinnerStyle = useAnimatedStyle(() => { @@ -1340,26 +1440,33 @@ const SimulationCard = ({ }; }); - useEffect(() => { - if (!isBalanceEnough || simulationUnavailable || simulationError) return; - if (!isNil(simulation)) { - spinnerRotation.value = withTiming(360, timingConfig); - } else { - spinnerRotation.value = withRepeat( - withTiming(360, rotationConfig), - -1, - false - ); - } - }, [ - isBalanceEnough, - simulation, - simulationError, - simulationUnavailable, - spinnerRotation, - ]); + useAnimatedReaction( + () => ({ isLoading, simulationUnavailable }), + ( + { isLoading, simulationUnavailable }, + previous = { isLoading: false, simulationUnavailable: false } + ) => { + if (isLoading && !previous?.isLoading) { + spinnerRotation.value = withRepeat( + withTiming(360, rotationConfig), + -1, + false + ); + } else if ( + (!isLoading && previous?.isLoading) || + (simulationUnavailable && + !previous?.simulationUnavailable && + previous?.isLoading) + ) { + spinnerRotation.value = withTiming(360, timingConfig); + } + }, + [isLoading, simulationUnavailable] + ); + + const renderSimulationEventRows = useMemo(() => { + if (isBalanceEnough === false) return null; - const renderSimulationEventRows = () => { return ( <> {simulation?.approvals?.map(change => { @@ -1394,35 +1501,41 @@ const SimulationCard = ({ })} ); - }; + }, [isBalanceEnough, simulation]); - const titleColor = useMemo(() => { - if (!isBalanceEnough) { - return 'accent'; + const titleColor: TextColor = useMemo(() => { + if (isLoading) { + return 'label'; } - - if (simulationError) { - return { custom: colors.red }; + if (isBalanceEnough === false) { + return 'blue'; } - - if (simulationUnavailable) { + if (noChanges || simulationUnavailable) { return 'labelQuaternary'; } - + if (simulationError) { + return 'red'; + } return 'label'; - }, [isBalanceEnough, simulationError, simulationUnavailable]); + }, [ + isBalanceEnough, + isLoading, + noChanges, + simulationError, + simulationUnavailable, + ]); const titleText = useMemo(() => { - if (!isBalanceEnough) { + if (isLoading) { return i18n.t( - i18n.l.walletconnect.simulation.simulation_card.titles - .not_enough_native_balance, - { symbol: walletBalance?.symbol } + i18n.l.walletconnect.simulation.simulation_card.titles.simulating ); } - if (simulationError) { + if (isBalanceEnough === false) { return i18n.t( - i18n.l.walletconnect.simulation.simulation_card.titles.likely_to_fail + i18n.l.walletconnect.simulation.simulation_card.titles + .not_enough_native_balance, + { symbol: walletBalance?.symbol } ); } if (simulationUnavailable) { @@ -1431,36 +1544,65 @@ const SimulationCard = ({ .simulation_unavailable ); } - - // is not laoding check - if (!isNil(simulation)) { + if (noChanges) { + return i18n.t( + i18n.l.walletconnect.simulation.simulation_card.messages.no_changes + ); + } + if (simulationError) { return i18n.t( - i18n.l.walletconnect.simulation.simulation_card.titles.simulation_result + i18n.l.walletconnect.simulation.simulation_card.titles.likely_to_fail ); } return i18n.t( - i18n.l.walletconnect.simulation.simulation_card.titles.simulating + i18n.l.walletconnect.simulation.simulation_card.titles.simulation_result ); }, [ isBalanceEnough, - simulation, + isLoading, + noChanges, simulationError, simulationUnavailable, walletBalance?.symbol, ]); + const isExpanded = useMemo(() => { + if (isLoading || isPersonalSign) { + return false; + } + const shouldExpandOnLoad = + isBalanceEnough === false || + (!isEmpty(simulation) && !noChanges) || + currentNetwork === Network.zora || + !!simulationError; + return shouldExpandOnLoad; + }, [ + currentNetwork, + isBalanceEnough, + isLoading, + isPersonalSign, + noChanges, + simulation, + simulationError, + ]); + return ( - + - - - - {simulationError || !isBalanceEnough ? '􀇿' : '􀬨'} - - - + {!isLoading && (simulationError || isBalanceEnough === false) ? ( + + ) : ( + + {!isLoading && noChanges && !simulationUnavailable ? ( + + {/* The extra space avoids icon clipping */} + {'􀻾 '} + + ) : ( + + + 􀬨 + + + )} + + )} {titleText} + {/* TODO: Unhide once we add explainer sheets */} {/* - - - - 􀁜 - - - + + + + + 􀁜 + + + + */} - {noChanges && - !simulationUnavailable && - !simulationError && - isBalanceEnough && ( - - - - {'􀻾'} - - - - {i18n.t( - i18n.l.walletconnect.simulation.simulation_card.messages - .no_changes - )} - - - )} - {!isBalanceEnough && ( + {isBalanceEnough === false ? ( {i18n.t( i18n.l.walletconnect.simulation.simulation_card.messages @@ -1534,29 +1681,29 @@ const SimulationCard = ({ } )} - )} - {simulationUnavailable && ( - - {isPersonalSign - ? i18n.t( - i18n.l.walletconnect.simulation.simulation_card.messages - .unavailable_personal_sign - ) - : i18n.t( + ) : ( + <> + {simulationUnavailable && !isPersonalSign && ( + + + {i18n.t( + i18n.l.walletconnect.simulation.simulation_card.messages + .unavailable_zora_network + )} + + + )} + {simulationError && ( + + {i18n.t( i18n.l.walletconnect.simulation.simulation_card.messages - .unavailable_zora_network + .failed_to_simulate )} - - )} - {simulationError && ( - - {i18n.t( - i18n.l.walletconnect.simulation.simulation_card.messages - .failed_to_simulate + )} - + )} - {!noChanges && isBalanceEnough && renderSimulationEventRows()} + {renderSimulationEventRows} @@ -1564,23 +1711,29 @@ const SimulationCard = ({ ); }; +interface DetailsCardProps { + currentNetwork: Network; + expandedCardBottomInset: number; + isBalanceEnough: boolean | undefined; + isLoading: boolean; + meta: TransactionSimulationMeta | undefined; + methodName: string; + noChanges: boolean; + nonce: string | undefined; + toAddress: string; +} + const DetailsCard = ({ + currentNetwork, + expandedCardBottomInset, + isBalanceEnough, isLoading, meta, - currentNetwork, methodName, + noChanges, + nonce, toAddress, - isBalanceEnough, -}: { - isLoading: boolean; - meta: TransactionSimulationMeta | undefined; - currentNetwork: Network; - methodName: string; - toAddress: string; - simulationUnavailable: boolean; - simulationError: TransactionErrorType | undefined; - isBalanceEnough: boolean; -}) => { +}: DetailsCardProps) => { const cardHeight = useSharedValue(COLLAPSED_CARD_HEIGHT); const contentHeight = useSharedValue( COLLAPSED_CARD_HEIGHT - CARD_BORDER_WIDTH * 2 @@ -1590,138 +1743,123 @@ const DetailsCard = ({ const listStyle = useAnimatedStyle(() => ({ opacity: interpolate( cardHeight.value, - [COLLAPSED_CARD_HEIGHT, MAX_CARD_HEIGHT], + [ + COLLAPSED_CARD_HEIGHT, + contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT + ? MAX_CARD_HEIGHT + : contentHeight.value + CARD_BORDER_WIDTH * 2, + ], [0, 1] ), })); const collapsedTextColor: TextColor = isLoading ? 'labelQuaternary' : 'blue'; - // we have no details to render if simulation is unavailable - if (!isBalanceEnough) { + const showFunctionRow = + meta?.to?.function || (methodName && methodName.substring(0, 2) !== '0x'); + const isContract = + showFunctionRow || meta?.to?.created || meta?.to?.sourceCodeStatus; + + // Hide DetailsCard if balance is insufficient once loaded + if (!isLoading && isBalanceEnough === false) { return <>; } + return ( - ( - setIsExpanded(true)} - scaleTo={0.96} - > - {children} - - )} + setIsExpanded(true)} > - - - - - - - 􁙠 - - + + + + - {i18n.t(i18n.l.walletconnect.simulation.details_card.title)} + 􁙠 - - - - - { - - } - {(meta?.to?.address || toAddress) && ( - - ethereumUtils.openAddressInBlockExplorer( - meta?.to?.address!, - currentNetwork - ) - } - /> - )} - {meta?.to?.created && ( - - )} - {methodName.substring(0, 2) !== '0x' && ( - - )} - {meta?.to?.sourceCodeStatus && ( - - )} - - - - - + + + {i18n.t(i18n.l.walletconnect.simulation.details_card.title)} + + + + + + { + + } + {!!(meta?.to?.address || toAddress) && ( + + ethereumUtils.openAddressInBlockExplorer( + meta?.to?.address! || toAddress, + currentNetwork + ) + } + value={ + meta?.to?.name || + abbreviations.address(meta?.to?.address || toAddress, 4, 6) || + meta?.to?.address || + toAddress + } + /> + )} + {showFunctionRow && ( + + )} + {!!meta?.to?.sourceCodeStatus && ( + + )} + {!!meta?.to?.created && ( + + )} + {nonce && } + + + + ); }; const MessageCard = ({ + expandedCardBottomInset, message, method, }: { + expandedCardBottomInset: number; message: string; method: RPCMethod; }) => { - const cardHeight = useSharedValue(COLLAPSED_CARD_HEIGHT); - const contentHeight = useSharedValue( - COLLAPSED_CARD_HEIGHT - CARD_BORDER_WIDTH * 2 - ); - const { width: deviceWidth } = useDimensions(); - - const listStyle = useAnimatedStyle(() => ({ - opacity: interpolate( - cardHeight.value, - [ - COLLAPSED_CARD_HEIGHT, - contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT - ? MAX_CARD_HEIGHT - : contentHeight.value + CARD_BORDER_WIDTH * 2, - ], - [0, 1] - ), - })); + const { setClipboard } = useClipboard(); + const [didCopy, setDidCopy] = useState(false); let displayMessage = message; if (isSignTypedData(method)) { @@ -1737,62 +1875,92 @@ const MessageCard = ({ displayMessage = JSON.stringify(displayMessage, null, 4); } + const estimatedMessageHeight = useMemo( + () => estimateMessageHeight(displayMessage), + [displayMessage] + ); + + const cardHeight = useSharedValue( + estimatedMessageHeight > MAX_CARD_HEIGHT + ? MAX_CARD_HEIGHT + : estimatedMessageHeight + CARD_BORDER_WIDTH * 2 + ); + const contentHeight = useSharedValue(estimatedMessageHeight); + + const handleCopyPress = useCallback( + (message: string) => { + if (didCopy) return; + setClipboard(message); + setDidCopy(true); + const copyTimer = setTimeout(() => { + setDidCopy(false); + }, 2000); + return () => clearTimeout(copyTimer); + }, + [didCopy, setClipboard] + ); + return ( - <> - {/* @ts-ignore JS component */} - - - - - {i18n.t(i18n.l.walletconnect.simulation.message_card.copy)} - - - - - - - - - - - 􀙤 - - - - {i18n.t(i18n.l.walletconnect.simulation.message_card.title)} + MAX_CARD_HEIGHT} + isExpanded + skipCollapsedState + > + + + + + + 􀙤 - - - - - {displayMessage} + + + {i18n.t(i18n.l.walletconnect.simulation.message_card.title)} - - - - + + + handleCopyPress(message)} + > + + + + + + {i18n.t( + i18n.l.walletconnect.simulation.message_card.copy + )} + + + + + + + + + {displayMessage} + + + ); }; @@ -1808,32 +1976,56 @@ const SimulatedEventRow = ({ const { colors } = useTheme(); const eventInfo: EventInfo = infoForEventType[eventType]; - const formattedAmounts = - asset?.decimals === 0 - ? amount - : convertRawAmountToBalance( - amount, - { decimals: asset?.decimals || 18, symbol: asset?.symbol }, - 2 - ).display; - - const formattedAmount = `${eventInfo.amountPrefix}${ - amount === 'UNLIMITED' - ? i18n.t( - i18n.l.walletconnect.simulation.simulation_card.event_row.unlimited - ) - : formattedAmounts - }`; - const url = maybeSignUri(asset?.iconURL, { fm: 'png', w: 100 }); + const formattedAmount = useMemo(() => { + if (!asset) return; + + const nftFallbackSymbol = parseFloat(amount) > 1 ? 'NFTs' : 'NFT'; + const assetDisplayName = + asset?.type === TransactionAssetType.Nft + ? asset?.name || asset?.symbol || nftFallbackSymbol + : asset?.symbol || asset?.name; + const shortenedDisplayName = + assetDisplayName.length > 12 + ? `${assetDisplayName.slice(0, 12).trim()}…` + : assetDisplayName; + + const displayAmount = + asset?.decimals === 0 + ? `${amount}${shortenedDisplayName ? ' ' + shortenedDisplayName : ''}` + : convertRawAmountToBalance( + amount, + { decimals: asset?.decimals || 18, symbol: shortenedDisplayName }, + 3, + true + ).display; + + const unlimitedApproval = `${i18n.t( + i18n.l.walletconnect.simulation.simulation_card.event_row.unlimited + )} ${asset?.symbol}`; + + return `${eventInfo.amountPrefix}${ + amount === 'UNLIMITED' ? unlimitedApproval : displayAmount + }`; + }, [amount, asset, eventInfo?.amountPrefix]); + + const url = maybeSignUri(asset?.iconURL, { + fm: 'png', + w: 16 * PixelRatio.get(), + }); let assetCode = asset?.assetCode; // this needs tweaks if (asset?.type === TransactionAssetType.Native) { assetCode = ETH_ADDRESS; } + return ( - + )} @@ -1882,72 +2074,109 @@ const SimulatedEventRow = ({ }; const DetailRow = ({ + currentNetwork, detailType, - value, onPress, + value, }: { + currentNetwork?: Network; detailType: DetailType; - value: string; onPress?: () => void; + value: string; }) => { const detailInfo: DetailInfo = infoForDetailType[detailType]; return ( - ( - - {children} - - )} - > - - - - - - {detailInfo.label} - - - - - {value} - - + + + + + + {detailInfo.label} + - - + + {detailType === 'function' && ( + + )} + {detailType === 'sourceCodeVerification' && ( + + )} + {detailType === 'chain' && currentNetwork && ( + + )} + {detailType !== 'function' && + detailType !== 'sourceCodeVerification' && ( + + {value} + + )} + {(detailType === 'contract' || detailType === 'to') && ( + + + + + 􀂄 + + + + + )} + + + ); }; const EventIcon = ({ eventType }: { eventType: EventType }) => { const eventInfo: EventInfo = infoForEventType[eventType]; - const isApproval = eventType === 'approve'; const hideInnerFill = eventType === 'approve' || eventType === 'revoke'; + const isWarningIcon = + eventType === 'failed' || eventType === 'insufficientBalance'; return ( {!hideInnerFill && ( )} {eventInfo.icon} @@ -1970,6 +2199,94 @@ const DetailIcon = ({ detailInfo }: { detailInfo: DetailInfo }) => { ); }; +const DetailBadge = ({ + type, + value, +}: { + type: 'function' | 'unknown' | 'unverified' | 'verified'; + value: string; +}) => { + const { colors, isDarkMode } = useTheme(); + const separatorTertiary = useForegroundColor('separatorTertiary'); + + const infoForBadgeType: { + [key: string]: { + backgroundColor: string; + borderColor: string; + label?: string; + text: TextColor; + textOpacity?: number; + }; + } = { + function: { + backgroundColor: 'transparent', + borderColor: isDarkMode + ? separatorTertiary + : colors.alpha(separatorTertiary, 0.025), + text: 'labelQuaternary', + }, + unknown: { + backgroundColor: 'transparent', + borderColor: isDarkMode + ? separatorTertiary + : colors.alpha(separatorTertiary, 0.025), + label: 'Unknown', + text: 'labelQuaternary', + }, + unverified: { + backgroundColor: isDarkMode + ? colors.alpha(colors.red, 0.05) + : globalColors.red10, + borderColor: colors.alpha(colors.red, 0.02), + label: 'Unverified', + text: 'red', + textOpacity: 0.76, + }, + verified: { + backgroundColor: isDarkMode + ? colors.alpha(colors.green, 0.05) + : globalColors.green10, + borderColor: colors.alpha(colors.green, 0.02), + label: 'Verified', + text: 'green', + textOpacity: 0.76, + }, + }; + + return ( + + + + {infoForBadgeType[type].label || value} + + + + ); +}; + const VerifiedBadge = () => { return ( @@ -1994,75 +2311,183 @@ const VerifiedBadge = () => { ); }; +const AnimatedCheckmark = ({ visible }: { visible: boolean }) => { + return ( + + {visible && ( + + + + + + 􀁣 + + + + + )} + + ); +}; + const FadedScrollCard = ({ cardHeight, children, contentHeight, - disableGestures, + expandedCardBottomInset = 120, + expandedCardTopInset = 120, + initialScrollEnabled, isExpanded, + onPressCollapsedCard, + skipCollapsedState, }: { cardHeight: SharedValue; children: React.ReactNode; contentHeight: SharedValue; - disableGestures?: boolean; + expandedCardBottomInset?: number; + expandedCardTopInset?: number; + initialScrollEnabled?: boolean; isExpanded: boolean; + onPressCollapsedCard?: () => void; + skipCollapsedState?: boolean; }) => { + const { height: deviceHeight, width: deviceWidth } = useDimensions(); const { isDarkMode } = useTheme(); - const scrollViewRef = useAnimatedRef(); - const [scrollEnabled, setScrollEnabled] = useState(false); + const cardRef = useAnimatedRef(); - const offset = useScrollViewOffset(scrollViewRef); + const [scrollEnabled, setScrollEnabled] = useState(initialScrollEnabled); + const [isFullyExpanded, setIsFullyExpanded] = useState(false); - const topGradientStyle = useAnimatedStyle(() => { - if (!scrollEnabled) { - return { opacity: withTiming(0, timingConfig) }; - } + const yPosition = useSharedValue(0); + + const maxExpandedHeight = + deviceHeight - (expandedCardBottomInset + expandedCardTopInset); + + const containerStyle = useAnimatedStyle(() => { return { - opacity: - offset.value <= 0 - ? withTiming(0, timingConfig) - : withTiming(1, timingConfig), + height: + cardHeight.value > MAX_CARD_HEIGHT || !skipCollapsedState + ? interpolate( + cardHeight.value, + [MAX_CARD_HEIGHT, MAX_CARD_HEIGHT, maxExpandedHeight], + [cardHeight.value, MAX_CARD_HEIGHT, MAX_CARD_HEIGHT], + 'clamp' + ) + : undefined, + zIndex: interpolate( + cardHeight.value, + [0, MAX_CARD_HEIGHT, MAX_CARD_HEIGHT + 1], + [1, 1, 2], + 'clamp' + ), }; }); - const bottomGradientStyle = useAnimatedStyle(() => { - if (!scrollEnabled) { - return { opacity: withTiming(0, timingConfig) }; - } + const backdropStyle = useAnimatedStyle(() => { + const canExpandFully = + contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; return { opacity: - offset.value > contentHeight.value + CARD_BORDER_WIDTH - MAX_CARD_HEIGHT - ? withTiming(0, timingConfig) - : withTiming(1, timingConfig), + canExpandFully && isFullyExpanded + ? withTiming(1, timingConfig) + : withTiming(0, timingConfig), }; }); const cardStyle = useAnimatedStyle(() => { + const canExpandFully = + contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; + const expandedCardHeight = Math.min( + contentHeight.value + CARD_BORDER_WIDTH * 2, + maxExpandedHeight + ); return { - height: cardHeight.value, - }; - }); - - const centerVerticallyWhenCollapsedStyle = useAnimatedStyle(() => { - return { + borderColor: interpolateColor( + cardHeight.value, + [0, MAX_CARD_HEIGHT, expandedCardHeight], + isDarkMode + ? ['#1F2023', '#1F2023', '#242527'] + : ['#F5F7F8', '#F5F7F8', '#FBFCFD'] + ), + height: cardHeight.value > MAX_CARD_HEIGHT ? cardHeight.value : undefined, + position: canExpandFully && isFullyExpanded ? 'absolute' : 'relative', transform: [ { translateY: interpolate( cardHeight.value, + [0, MAX_CARD_HEIGHT, expandedCardHeight], [ - COLLAPSED_CARD_HEIGHT, - contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT - ? MAX_CARD_HEIGHT - : contentHeight.value + CARD_BORDER_WIDTH * 2, - ], - [-2, 0] + 0, + 0, + -yPosition.value + + expandedCardTopInset + + (deviceHeight - + (expandedCardBottomInset + expandedCardTopInset) - + expandedCardHeight) - + (yPosition.value + expandedCardHeight >= + deviceHeight - expandedCardBottomInset + ? 0 + : deviceHeight - + expandedCardBottomInset - + yPosition.value - + expandedCardHeight), + ] ), }, ], }; }); + const centerVerticallyWhenCollapsedStyle = useAnimatedStyle(() => { + return { + transform: skipCollapsedState + ? undefined + : [ + { + translateY: interpolate( + cardHeight.value, + [ + 0, + COLLAPSED_CARD_HEIGHT, + contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT + ? MAX_CARD_HEIGHT + : contentHeight.value + CARD_BORDER_WIDTH * 2, + maxExpandedHeight, + ], + [-2, -2, 0, 0] + ), + }, + ], + }; + }); + + const shadowStyle = useAnimatedStyle(() => { + const canExpandFully = + contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; + return { + shadowOpacity: + canExpandFully && isFullyExpanded + ? withTiming(isDarkMode ? 0.9 : 0.16, timingConfig) + : withTiming(0, timingConfig), + }; + }); + const handleContentSizeChange = useCallback( (width: number, height: number) => { contentHeight.value = Math.round(height); @@ -2070,14 +2495,42 @@ const FadedScrollCard = ({ [contentHeight] ); + const handleOnLayout = useCallback(() => { + runOnUI(() => { + if (cardHeight.value === MAX_CARD_HEIGHT) { + const measurement = measure(cardRef); + if (measurement === null) { + return; + } + if (yPosition.value !== measurement.pageY) { + yPosition.value = measurement.pageY; + } + } + })(); + }, [cardHeight, cardRef, yPosition]); + useAnimatedReaction( - () => ({ contentHeight: contentHeight.value, isExpanded }), - ({ contentHeight, isExpanded }, previous) => { + () => ({ contentHeight: contentHeight.value, isExpanded, isFullyExpanded }), + ({ contentHeight, isExpanded, isFullyExpanded }, previous) => { if ( + isFullyExpanded !== previous?.isFullyExpanded || isExpanded !== previous?.isExpanded || contentHeight !== previous?.contentHeight ) { - if (isExpanded) { + if (isFullyExpanded) { + const expandedCardHeight = + contentHeight + CARD_BORDER_WIDTH * 2 > maxExpandedHeight + ? maxExpandedHeight + : contentHeight + CARD_BORDER_WIDTH * 2; + if ( + contentHeight + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT && + cardHeight.value >= MAX_CARD_HEIGHT + ) { + cardHeight.value = withTiming(expandedCardHeight, timingConfig); + } else { + runOnJS(setIsFullyExpanded)(false); + } + } else if (isExpanded) { cardHeight.value = withTiming( contentHeight + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT @@ -2089,41 +2542,94 @@ const FadedScrollCard = ({ } const enableScroll = - isExpanded && contentHeight + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; + isExpanded && + contentHeight + CARD_BORDER_WIDTH * 2 > + (isFullyExpanded ? maxExpandedHeight : MAX_CARD_HEIGHT); runOnJS(setScrollEnabled)(enableScroll); } } ); return ( - - + { + if (isFullyExpanded) { + setIsFullyExpanded(false); + } + }} + pointerEvents={isFullyExpanded ? 'auto' : 'none'} + style={[ + { + backgroundColor: 'rgba(0, 0, 0, 0.6)', + height: deviceHeight * 3, + left: -deviceWidth * 0.5, + position: 'absolute', + top: -deviceHeight, + width: deviceWidth * 2, + zIndex: -1, + }, + backdropStyle, + ]} + /> + - - {children} + + + { + if (!isFullyExpanded) { + setIsFullyExpanded(true); + } else setIsFullyExpanded(false); + } + } + > + + {children} + + + + + - - - + ); }; @@ -2133,19 +2639,18 @@ const FadeGradient = ({ style, }: { side: 'top' | 'bottom'; - style: StyleProp>>; + style?: StyleProp>>; }) => { const { colors, isDarkMode } = useTheme(); const isTop = side === 'top'; - const solidColor = isDarkMode ? globalColors.white10 : '#FBFCFD'; const transparentColor = colors.alpha(solidColor, 0); return ( @@ -2206,7 +2711,13 @@ const IconContainer = ({ ); }; -type EventType = 'send' | 'receive' | 'approve' | 'revoke'; +type EventType = + | 'send' + | 'receive' + | 'approve' + | 'revoke' + | 'failed' + | 'insufficientBalance'; type EventInfo = { amountPrefix: string; @@ -2253,14 +2764,31 @@ const infoForEventType: { [key: string]: EventInfo } = { ), textColor: 'label', }, + failed: { + amountPrefix: '', + icon: '􀇿', + iconColor: 'red', + label: i18n.t( + i18n.l.walletconnect.simulation.simulation_card.titles.likely_to_fail + ), + textColor: 'red', + }, + insufficientBalance: { + amountPrefix: '', + icon: '􀇿', + iconColor: 'blue', + label: '', + textColor: 'blue', + }, }; type DetailType = | 'chain' | 'contract' - | 'dateCreated' + | 'to' | 'function' | 'sourceCodeVerification' + | 'dateCreated' | 'nonce'; type DetailInfo = { @@ -2277,11 +2805,9 @@ const infoForDetailType: { [key: string]: DetailInfo } = { icon: '􀉆', label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.contract), }, - dateCreated: { - icon: '􀉉', - label: i18n.t( - i18n.l.walletconnect.simulation.details_card.types.contract_created - ), + to: { + icon: '􀉩', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.to), }, function: { icon: '􀡅', @@ -2293,8 +2819,69 @@ const infoForDetailType: { [key: string]: DetailInfo } = { i18n.l.walletconnect.simulation.details_card.types.source_code ), }, + dateCreated: { + icon: '􀉉', + label: i18n.t( + i18n.l.walletconnect.simulation.details_card.types.contract_created + ), + }, nonce: { icon: '􀆃', label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.nonce), }, }; + +const CHARACTERS_PER_LINE = 40; +const LINE_HEIGHT = 11; +const LINE_GAP = 9; + +const estimateMessageHeight = (message: string) => { + const estimatedLines = Math.ceil(message.length / CHARACTERS_PER_LINE); + const messageHeight = + estimatedLines * LINE_HEIGHT + + (estimatedLines - 1) * LINE_GAP + + CARD_ROW_HEIGHT + + 24 * 3 - + CARD_BORDER_WIDTH * 2; + + return messageHeight; +}; + +const formatDate = (dateString: string) => { + const date = new Date(dateString); + const now = new Date(); + const diffTime = Math.abs(now.getTime() - date.getTime()); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + const diffWeeks = Math.floor(diffDays / 7); + const diffMonths = Math.floor(diffDays / 30.44); + + if (diffDays === 0) { + return i18n.t(i18n.l.walletconnect.simulation.formatted_dates.today); + } else if (diffDays === 1) { + return `${diffDays} ${i18n.t( + i18n.l.walletconnect.simulation.formatted_dates.day_ago + )}`; + } else if (diffDays < 7) { + return `${diffDays} ${i18n.t( + i18n.l.walletconnect.simulation.formatted_dates.days_ago + )}`; + } else if (diffWeeks === 1) { + return `${diffWeeks} ${i18n.t( + i18n.l.walletconnect.simulation.formatted_dates.week_ago + )}`; + } else if (diffDays < 30.44) { + return `${diffWeeks} ${i18n.t( + i18n.l.walletconnect.simulation.formatted_dates.weeks_ago + )}`; + } else if (diffMonths === 1) { + return `${diffMonths} ${i18n.t( + i18n.l.walletconnect.simulation.formatted_dates.month_ago + )}`; + } else if (diffDays < 365.25) { + return `${diffMonths} ${i18n.t( + i18n.l.walletconnect.simulation.formatted_dates.months_ago + )}`; + } else { + return date.toLocaleString('default', { month: 'short', year: 'numeric' }); + } +}; diff --git a/src/styles/colors.ts b/src/styles/colors.ts index 0e9b7b80a13..938163ff0b1 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -26,7 +26,7 @@ const darkModeColors = { darkGrey: '#333333', darkModeDark: '#404656', exchangeFallback: 'rgba(60, 66, 82, 0.8)', - green: '#4BD166', + green: '#3ECF5B', grey: '#333333', grey20: '#333333', lighterGrey: '#12131A', @@ -36,6 +36,7 @@ const darkModeColors = { offWhite: '#1F222A', offWhite80: '#1C1F27', placeholder: 'rgba(224, 232, 255, 0.4)', + red: '#FF6257', rowDivider: 'rgba(60, 66, 82, 0.075)', rowDividerExtraLight: 'rgba(60, 66, 82, 0.0375)', rowDividerFaint: 'rgba(60, 66, 82, 0.025)', @@ -115,7 +116,7 @@ const getColorsByTheme = (darkMode?: boolean) => { finiliarPink: '#F89C9C', // '248, 156, 156' finiliarPink06: 'rgba(248, 156, 156, 0.06)', flamingo: '#E540F1', // '229, 64, 241' - green: '#2CCC00', // '58, 166, 134' + green: '#1DB847', // '29, 184, 71' grey: '#A9ADB9', // '169, 173, 185' grey20: '#333333', // '51, 51, 51' lighterGrey: '#F7F7F8', // '247, 247, 248' @@ -144,7 +145,7 @@ const getColorsByTheme = (darkMode?: boolean) => { purpleUniswap: '#FF007A', // '255,0,122', rainbowBlue: '#001E59', // '0, 30, 89', rainbowBlue06: 'rgba(0, 30, 89, 0.06)', // '0, 30, 89, 0.06' - red: '#FF494A', // '255, 73, 74' + red: '#FA423C', // '250, 66, 60' rowDivider: 'rgba(60, 66, 82, 0.03)', // '60, 66, 82, 0.03' rowDividerExtraLight: 'rgba(60, 66, 82, 0.015)', // '60, 66, 82, 0.015' rowDividerFaint: 'rgba(60, 66, 82, 0.01)', // '60, 66, 82, 0.01' diff --git a/yarn.lock b/yarn.lock index d673ee79686..cc00709af81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1565,7 +1565,7 @@ dependencies: "@types/hammerjs" "^2.0.36" -"@emotion/is-prop-valid@^0.8.8": +"@emotion/is-prop-valid@^0.8.2", "@emotion/is-prop-valid@^0.8.8": version "0.8.8" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== @@ -2855,6 +2855,59 @@ tweetnacl "^1.0.3" tweetnacl-util "^0.15.1" +"@motionone/animation@^10.12.0": + version "10.16.3" + resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.16.3.tgz#f5b71e27fd8b88b61f983adb0ed6c8e3e89281f9" + integrity sha512-QUGWpLbMFLhyqKlngjZhjtxM8IqiJQjLK0DF+XOF6od9nhSvlaeEpOY/UMCRVcZn/9Tr2rZO22EkuCIjYdI74g== + dependencies: + "@motionone/easing" "^10.16.3" + "@motionone/types" "^10.16.3" + "@motionone/utils" "^10.16.3" + tslib "^2.3.1" + +"@motionone/dom@10.12.0": + version "10.12.0" + resolved "https://registry.yarnpkg.com/@motionone/dom/-/dom-10.12.0.tgz#ae30827fd53219efca4e1150a5ff2165c28351ed" + integrity sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw== + dependencies: + "@motionone/animation" "^10.12.0" + "@motionone/generators" "^10.12.0" + "@motionone/types" "^10.12.0" + "@motionone/utils" "^10.12.0" + hey-listen "^1.0.8" + tslib "^2.3.1" + +"@motionone/easing@^10.16.3": + version "10.16.3" + resolved "https://registry.yarnpkg.com/@motionone/easing/-/easing-10.16.3.tgz#a62abe0ba2841861f167f286782e287eab8d7466" + integrity sha512-HWTMZbTmZojzwEuKT/xCdvoMPXjYSyQvuVM6jmM0yoGU6BWzsmYMeB4bn38UFf618fJCNtP9XeC/zxtKWfbr0w== + dependencies: + "@motionone/utils" "^10.16.3" + tslib "^2.3.1" + +"@motionone/generators@^10.12.0": + version "10.16.4" + resolved "https://registry.yarnpkg.com/@motionone/generators/-/generators-10.16.4.tgz#4a38708244bce733bfcebd4a26d19f4bbabd36af" + integrity sha512-geFZ3w0Rm0ZXXpctWsSf3REGywmLLujEjxPYpBR0j+ymYwof0xbV6S5kGqqsDKgyWKVWpUInqQYvQfL6fRbXeg== + dependencies: + "@motionone/types" "^10.16.3" + "@motionone/utils" "^10.16.3" + tslib "^2.3.1" + +"@motionone/types@^10.12.0", "@motionone/types@^10.16.3": + version "10.16.3" + resolved "https://registry.yarnpkg.com/@motionone/types/-/types-10.16.3.tgz#9284ea8a52f6b32c51c54b617214f20e43ac6c59" + integrity sha512-W4jkEGFifDq73DlaZs3HUfamV2t1wM35zN/zX7Q79LfZ2sc6C0R1baUHZmqc/K5F3vSw3PavgQ6HyHLd/MXcWg== + +"@motionone/utils@^10.12.0", "@motionone/utils@^10.16.3": + version "10.16.3" + resolved "https://registry.yarnpkg.com/@motionone/utils/-/utils-10.16.3.tgz#ddf07ab6cf3000d89e3bcbdc9a8c3e1fd64f8520" + integrity sha512-WNWDksJIxQkaI9p9Z9z0+K27xdqISGNFy1SsWVGaiedTHq0iaT6iZujby8fT/ZnZxj1EOaxJtSfUPCFNU5CRoA== + dependencies: + "@motionone/types" "^10.16.3" + hey-listen "^1.0.8" + tslib "^2.3.1" + "@multiformats/base-x@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@multiformats/base-x/-/base-x-4.0.1.tgz#95ff0fa58711789d53aefb2590a8b7a4e715d121" @@ -9844,6 +9897,27 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +framer-motion@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.5.1.tgz#802448a16a6eb764124bf36d8cbdfa6dd6b931a7" + integrity sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw== + dependencies: + "@motionone/dom" "10.12.0" + framesync "6.0.1" + hey-listen "^1.0.8" + popmotion "11.0.3" + style-value-types "5.0.0" + tslib "^2.1.0" + optionalDependencies: + "@emotion/is-prop-valid" "^0.8.2" + +framesync@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/framesync/-/framesync-6.0.1.tgz#5e32fc01f1c42b39c654c35b16440e07a25d6f20" + integrity sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA== + dependencies: + tslib "^2.1.0" + fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -10531,6 +10605,11 @@ hexoid@^1.0.0: resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== +hey-listen@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68" + integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q== + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -13395,6 +13474,13 @@ moment@2.29.4, moment@^2.19.3: resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== +moti@0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/moti/-/moti-0.27.2.tgz#617217500a45f3eca93a26a8adb4d7a47cf82843" + integrity sha512-QYH9Id14Zdjx9L75iMPyvxUN+GvHZ4lk6QhUyA63kWgXyqPT2yMnEIbk7hIeQ0WQDW2eLAXEi2/6FJkS7TEwsw== + dependencies: + framer-motion "^6.5.1" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -14560,6 +14646,16 @@ pngjs@^3.0.0, pngjs@^3.3.0, pngjs@^3.3.3: resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== +popmotion@11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-11.0.3.tgz#565c5f6590bbcddab7a33a074bb2ba97e24b0cc9" + integrity sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA== + dependencies: + framesync "6.0.1" + hey-listen "^1.0.8" + style-value-types "5.0.0" + tslib "^2.1.0" + popper.js@^1.14.4: version "1.16.1" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" @@ -17238,6 +17334,14 @@ strtok3@^6.2.4: "@tokenizer/token" "^0.3.0" peek-readable "^4.1.0" +style-value-types@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-5.0.0.tgz#76c35f0e579843d523187989da866729411fc8ad" + integrity sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA== + dependencies: + hey-listen "^1.0.8" + tslib "^2.1.0" + styled-components@5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.2.1.tgz#6ed7fad2dc233825f64c719ffbdedd84ad79101a"