diff --git a/src/components/change-wallet/AddressRow.tsx b/src/components/change-wallet/AddressRow.tsx index b05b9daf9ca..a5af90c724b 100644 --- a/src/components/change-wallet/AddressRow.tsx +++ b/src/components/change-wallet/AddressRow.tsx @@ -13,7 +13,6 @@ import { Icon } from '../icons'; import { Centered, Column, ColumnWithMargins, Row } from '../layout'; import { Text, TruncatedText } from '../text'; import ContextMenuButton from '@/components/native-context-menu/contextMenu'; -import ContextMenuAndroid from '@/components/native-context-menu/contextMenu.android'; import useExperimentalFlag, { NOTIFICATIONS } from '@/config/experimentalHooks'; import { removeFirstEmojiFromString, @@ -25,6 +24,7 @@ import { abbreviations, deviceUtils, profileUtils } from '@/utils'; import { EditWalletContextMenuActions } from '@/screens/ChangeWalletSheet'; import { toChecksumAddress } from '@/handlers/web3'; import { IS_IOS, IS_ANDROID } from '@/env'; +import { ContextMenu } from '../context-menu'; const maxAccountLabelWidth = deviceUtils.dimensions.width - 88; const NOOP = () => undefined; @@ -219,6 +219,25 @@ export default function AddressRow({ menuTitle: walletName, }; + const handleSelectActionMenuItem = useCallback( + (buttonIndex: number) => { + switch (buttonIndex) { + case 0: + contextMenuActions?.edit(walletId, address); + break; + case 1: + contextMenuActions?.notifications(walletName, address); + break; + case 2: + contextMenuActions?.remove(walletId, address); + break; + default: + break; + } + }, + [contextMenuActions, walletName, walletId, address] + ); + const handleSelectMenuItem = useCallback( // @ts-expect-error ContextMenu is an untyped JS component and can't type its onPress handler properly ({ nativeEvent: { actionKey } }) => { @@ -316,14 +335,15 @@ export default function AddressRow({ ) : ( - // @ts-expect-error js component - item.actionTitle)} isAnchoredToRight - onPressMenuItem={handleSelectMenuItem} + onPressActionSheet={handleSelectActionMenuItem} > - - + + + + ))} diff --git a/src/components/gas/GasSpeedButton.js b/src/components/gas/GasSpeedButton.js index 4bff48b0017..79ff68abd30 100644 --- a/src/components/gas/GasSpeedButton.js +++ b/src/components/gas/GasSpeedButton.js @@ -1,3 +1,4 @@ +/* eslint-disable no-nested-ternary */ /* eslint-disable no-undef */ import AnimateNumber from '@bankify/react-native-animate-number'; import lang from 'i18n-js'; @@ -34,6 +35,8 @@ import styled from '@/styled-thing'; import { fonts, fontWithWidth, margin, padding } from '@/styles'; import { ethereumUtils, gasUtils } from '@/utils'; import { getNetworkObj } from '@/networks'; +import { IS_ANDROID } from '@/env'; +import { ContextMenu } from '../context-menu'; const { GAS_EMOJIS, @@ -249,6 +252,7 @@ const GasSpeedButton = ({ shouldOpenCustomGasSheet.focusTo, flashbotTransaction, speeds, + fallbackColor, ]); const openCustomOptions = useCallback( @@ -377,6 +381,25 @@ const GasSpeedButton = ({ [handlePressSpeedOption] ); + const handlePressActionSheet = useCallback( + buttonIndex => { + switch (buttonIndex) { + case 0: + handlePressSpeedOption(NORMAL); + break; + case 1: + handlePressSpeedOption(FAST); + break; + case 2: + handlePressSpeedOption(URGENT); + break; + case 3: + handlePressSpeedOption(CUSTOM); + } + }, + [handlePressSpeedOption] + ); + const nativeFeeCurrency = useMemo(() => { switch (currentNetwork) { case networkTypes.polygon: @@ -399,6 +422,8 @@ const GasSpeedButton = ({ const menuConfig = useMemo(() => { const menuOptions = speedOptions.map(gasOption => { + if (IS_ANDROID) return gasOption; + const totalGwei = add( gasFeeParamsBySpeed[gasOption]?.maxBaseFee?.gwei, gasFeeParamsBySpeed[gasOption]?.maxPriorityFeePerGas?.gwei @@ -441,6 +466,7 @@ const GasSpeedButton = ({ gasFeeParamsBySpeed, selectedGasFeeOption, speedOptions, + isL2, ]); const gasOptionsAvailable = useMemo(() => speedOptions.length > 1, [ @@ -477,6 +503,24 @@ const GasSpeedButton = ({ /> ); if (!gasOptionsAvailable || gasIsNotReady) return pager; + + if (IS_ANDROID) { + return ( + + {pager} + + ); + } + return ( diff --git a/src/components/positions/PositionsCard.tsx b/src/components/positions/PositionsCard.tsx index 5efdd314261..67ef48e329f 100644 --- a/src/components/positions/PositionsCard.tsx +++ b/src/components/positions/PositionsCard.tsx @@ -72,7 +72,7 @@ export const PositionCard = ({ position }: PositionCardProps) => { }, [navigate, position]); const depositTokens: CoinStackToken[] = useMemo(() => { - let tokens: CoinStackToken[] = []; + const tokens: CoinStackToken[] = []; position.deposits.forEach((deposit: RainbowDeposit) => { deposit.underlying.forEach(({ asset }) => { tokens.push({ diff --git a/src/screens/ENSIntroSheet.tsx b/src/screens/ENSIntroSheet.tsx index 317ad38804f..e6252c7d81e 100644 --- a/src/screens/ENSIntroSheet.tsx +++ b/src/screens/ENSIntroSheet.tsx @@ -3,7 +3,7 @@ import { useRoute } from '@react-navigation/native'; import { IS_TESTING } from 'react-native-dotenv'; import lang from 'i18n-js'; import React, { useCallback, useMemo } from 'react'; -import { InteractionManager } from 'react-native'; +import { InteractionManager, View } from 'react-native'; import { MenuActionConfig } from 'react-native-ios-context-menu'; import LinearGradient from 'react-native-linear-gradient'; import ActivityIndicator from '../components/ActivityIndicator'; @@ -35,6 +35,8 @@ import { } from '@/hooks'; import Routes from '@/navigation/routesNames'; import { useTheme } from '@/theme'; +import { IS_ANDROID } from '@/env'; +import ContextMenu from '@/components/context-menu/ContextMenu.android'; enum AnotherENSEnum { search = 'search', @@ -45,6 +47,92 @@ const topPadding = android ? 29 : 19; const minHeight = 740; +type ContextMenuRendererProps = { + children: React.ReactNode; + handleSelectExistingName: () => void; + handleNavigateToSearch: () => void; +}; + +const ContextMenuRenderer = ({ + children, + handleSelectExistingName, + handleNavigateToSearch, +}: ContextMenuRendererProps) => { + const menuConfig = useMemo(() => { + return { + menuItems: [ + { + actionKey: AnotherENSEnum.my_ens, + actionTitle: lang.t('profiles.intro.my_ens_names'), + icon: { + iconType: 'SYSTEM', + iconValue: 'rectangle.stack.badge.person.crop', + }, + }, + { + actionKey: AnotherENSEnum.search, + actionTitle: lang.t('profiles.intro.search_new_ens'), + icon: { + iconType: 'SYSTEM', + iconValue: 'magnifyingglass', + }, + }, + ] as MenuActionConfig[], + menuTitle: '', + }; + }, []); + + const handlePressMenuItem = useCallback( + // @ts-expect-error ContextMenu is an untyped JS component and can't type its onPress handler properly + ({ nativeEvent: { actionKey } }) => { + if (actionKey === AnotherENSEnum.my_ens) { + handleSelectExistingName(); + } else if (actionKey === AnotherENSEnum.search) { + handleNavigateToSearch(); + } + }, + [handleNavigateToSearch, handleSelectExistingName] + ); + + const handlePressActionSheet = useCallback( + (buttonIndex: number) => { + switch (buttonIndex) { + case 0: + handleSelectExistingName(); + break; + case 1: + handleNavigateToSearch(); + break; + } + }, + [handleNavigateToSearch, handleSelectExistingName] + ); + + if (IS_ANDROID) { + return ( + i.actionTitle)} + > + {children} + + ); + } + + return ( + + {children} + + ); +}; + export default function ENSIntroSheet() { const { width: deviceWidth, height: deviceHeight } = useDimensions(); const { colors } = useTheme(); @@ -64,6 +152,8 @@ export default function ENSIntroSheet() { enabled: Boolean(uniqueDomain?.name), }); + console.log(nonPrimaryDomains.length); + // We are not using `isSmallPhone` from `useDimensions` here as we // want to explicitly set a min height. const isSmallPhone = deviceHeight < minHeight; @@ -121,42 +211,6 @@ export default function ENSIntroSheet() { }); }, [navigate, navigateToAssignRecords]); - const menuConfig = useMemo(() => { - return { - menuItems: [ - { - actionKey: AnotherENSEnum.my_ens, - actionTitle: lang.t('profiles.intro.my_ens_names'), - icon: { - iconType: 'SYSTEM', - iconValue: 'rectangle.stack.badge.person.crop', - }, - }, - { - actionKey: AnotherENSEnum.search, - actionTitle: lang.t('profiles.intro.search_new_ens'), - icon: { - iconType: 'SYSTEM', - iconValue: 'magnifyingglass', - }, - }, - ] as MenuActionConfig[], - menuTitle: '', - }; - }, []); - - const handlePressMenuItem = useCallback( - // @ts-expect-error ContextMenu is an untyped JS component and can't type its onPress handler properly - ({ nativeEvent: { actionKey } }) => { - if (actionKey === AnotherENSEnum.my_ens) { - handleSelectExistingName(); - } else if (actionKey === AnotherENSEnum.search) { - handleNavigateToSearch(); - } - }, - [handleNavigateToSearch, handleSelectExistingName] - ); - return ( )} {nonPrimaryDomains?.length > 0 ? ( - - + ) : ( void; - contact?: Contact; - account?: RainbowAccount; -}; +type ContextMenuRendererProps = { + children: React.ReactNode; + name: string | undefined; + color: number | null; + formattedAddress: string | undefined; + fetchedEnsName: string | undefined; +} & Omit; -export const TransactionDetailsAddressRow: React.FC = ({ +const ContextMenuRenderer = ({ + children, + account, address, - title, - onAddressCopied, contact, - account, -}) => { - const formattedAddress = formatAddressForDisplay(address); - const [fetchedEnsName, setFetchedEnsName] = useState(); - const [fetchedEnsImage, setFetchedEnsImage] = useState(); - const [imageLoaded, setImageLoaded] = useState(!!account?.image); - const ensNameSharedValue = useTiming(!!fetchedEnsName, { - duration: 420, - easing: Easing.linear, - }); - - const accountEmoji = useMemo(() => returnStringFirstEmoji(account?.label), [ - account, - ]); - const accountName = useMemo( - () => removeFirstEmojiFromString(account?.label), - [] - ); - const color = - account?.color ?? contact?.color ?? addressHashedColorIndex(address); - const emoji = accountEmoji || addressHashedEmoji(address); - const name = - accountName || contact?.nickname || contact?.ens || formattedAddress; - - const imageUrl = fetchedEnsImage ?? account?.image; - const ensAvatarSharedValue = useTiming(!!imageUrl && imageLoaded, { - duration: account?.image ? 0 : 420, - }); - - useEffect(() => { - if (!contact?.nickname && !accountName) { - fetchReverseRecord(address).then(name => { - if (name) { - setFetchedEnsName(name); - } - }); - } - }, []); - - useEffect(() => { - if (!account?.image && (fetchedEnsName || contact?.ens)) { - const ens = fetchedEnsName ?? contact?.ens; - if (ens) { - fetchENSAvatar(ens, { cacheFirst: true }).then(avatar => { - if (avatar?.imageUrl) { - setFetchedEnsImage(avatar.imageUrl); - } - }); - } - } - }, [fetchedEnsName]); - - const addressAnimatedStyle = useAnimatedStyle(() => ({ - opacity: interpolate(ensNameSharedValue.value, [0, 0.5, 1], [1, 0, 0]), - })); - const ensNameAnimatedStyle = useAnimatedStyle(() => ({ - opacity: interpolate(ensNameSharedValue.value, [0, 0.5, 1], [0, 0, 1]), - })); - - const emojiAvatarAnimatedStyle = useAnimatedStyle(() => ({ - opacity: interpolate(ensAvatarSharedValue.value, [0, 1], [1, 0]), - })); - const ensAvatarAnimatedStyle = useAnimatedStyle(() => ({ - opacity: ensAvatarSharedValue.value, - })); - + name, + color, + formattedAddress, + fetchedEnsName, + onAddressCopied, +}: ContextMenuRendererProps) => { const menuConfig = useMemo( () => ({ menuTitle: '', @@ -151,7 +93,7 @@ export const TransactionDetailsAddressRow: React.FC = ({ }, ], }), - [fetchedEnsName, address, contact, account] + [contact, account, formattedAddress, name] ); const onPressMenuItem = useCallback( @@ -193,18 +135,157 @@ export const TransactionDetailsAddressRow: React.FC = ({ return; } }, - [address, fetchedEnsName, contact] + [address, fetchedEnsName, contact, color, onAddressCopied] ); - const onImageLoad = () => { - setImageLoaded(true); - }; + const onPressActionSheetItem = useCallback( + (buttonIndex: number) => { + switch (buttonIndex) { + case 0: + Navigation.handleAction(Routes.SEND_FLOW, { + params: { + address, + }, + screen: Routes.SEND_SHEET, + }); + return; + case 1: + Navigation.handleAction(Routes.MODAL_SCREEN, { + address: address, + color: color, + contact, + ens: fetchedEnsName || contact?.ens, + type: 'contact_profile', + }); + return; + case 2: + onAddressCopied?.(); + haptics.notificationSuccess(); + Clipboard.setString(address); + return; + } + }, + [address, color, contact, fetchedEnsName, onAddressCopied] + ); + + if (IS_ANDROID) { + return ( + i.actionTitle)} + > + {children} + + ); + } return ( + {children} + + ); +}; + +type Props = { + address: string; + title: string; + onAddressCopied?: () => void; + contact?: Contact; + account?: RainbowAccount; +}; + +export const TransactionDetailsAddressRow: React.FC = ({ + address, + title, + onAddressCopied, + contact, + account, +}) => { + const formattedAddress = formatAddressForDisplay(address); + const [fetchedEnsName, setFetchedEnsName] = useState(); + const [fetchedEnsImage, setFetchedEnsImage] = useState(); + const [imageLoaded, setImageLoaded] = useState(!!account?.image); + const ensNameSharedValue = useTiming(!!fetchedEnsName, { + duration: 420, + easing: Easing.linear, + }); + + const accountEmoji = useMemo(() => returnStringFirstEmoji(account?.label), [ + account, + ]); + const accountName = useMemo( + () => removeFirstEmojiFromString(account?.label), + [] + ); + const color = + account?.color ?? contact?.color ?? addressHashedColorIndex(address); + const emoji = accountEmoji || addressHashedEmoji(address); + const name = + accountName || contact?.nickname || contact?.ens || formattedAddress; + + const imageUrl = fetchedEnsImage ?? account?.image; + const ensAvatarSharedValue = useTiming(!!imageUrl && imageLoaded, { + duration: account?.image ? 0 : 420, + }); + + useEffect(() => { + if (!contact?.nickname && !accountName) { + fetchReverseRecord(address).then(name => { + if (name) { + setFetchedEnsName(name); + } + }); + } + }, []); + + useEffect(() => { + if (!account?.image && (fetchedEnsName || contact?.ens)) { + const ens = fetchedEnsName ?? contact?.ens; + if (ens) { + fetchENSAvatar(ens, { cacheFirst: true }).then(avatar => { + if (avatar?.imageUrl) { + setFetchedEnsImage(avatar.imageUrl); + } + }); + } + } + }, [fetchedEnsName]); + + const addressAnimatedStyle = useAnimatedStyle(() => ({ + opacity: interpolate(ensNameSharedValue.value, [0, 0.5, 1], [1, 0, 0]), + })); + const ensNameAnimatedStyle = useAnimatedStyle(() => ({ + opacity: interpolate(ensNameSharedValue.value, [0, 0.5, 1], [0, 0, 1]), + })); + + const emojiAvatarAnimatedStyle = useAnimatedStyle(() => ({ + opacity: interpolate(ensAvatarSharedValue.value, [0, 1], [1, 0]), + })); + const ensAvatarAnimatedStyle = useAnimatedStyle(() => ({ + opacity: ensAvatarSharedValue.value, + })); + + const onImageLoad = () => { + setImageLoaded(true); + }; + + return ( + @@ -263,6 +344,6 @@ export const TransactionDetailsAddressRow: React.FC = ({ - + ); };