From 0248c7470edbc60fc6d1a94c9120bf08af0b4593 Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Thu, 26 Sep 2024 17:09:12 -0700 Subject: [PATCH 01/27] feat: Add sortAssets utility function --- app/util/assets/sortAssets.ts | 81 +++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 app/util/assets/sortAssets.ts diff --git a/app/util/assets/sortAssets.ts b/app/util/assets/sortAssets.ts new file mode 100644 index 00000000000..bcbfba11375 --- /dev/null +++ b/app/util/assets/sortAssets.ts @@ -0,0 +1,81 @@ +import { get } from 'lodash'; + +export type SortOrder = 'asc' | 'dsc'; +export interface SortCriteria { + key: string; + order?: 'asc' | 'dsc'; + sortCallback: SortCallbackKeys; +} + +export type SortingType = number | string | Date; +type SortCallbackKeys = keyof SortingCallbacksT; + +export interface SortingCallbacksT { + numeric: (a: number, b: number) => number; + stringNumeric: (a: string, b: string) => number; + alphaNumeric: (a: string, b: string) => number; + date: (a: Date, b: Date) => number; +} + +// All sortingCallbacks should be asc order, sortAssets function handles asc/dsc +const sortingCallbacks: SortingCallbacksT = { + numeric: (a: number, b: number) => a - b, + stringNumeric: (a: string, b: string) => parseInt(a, 10) - parseInt(b, 10), + alphaNumeric: (a: string, b: string) => a.localeCompare(b), + date: (a: Date, b: Date) => a.getTime() - b.getTime(), +}; + +// Utility function to access nested properties by key path +function getNestedValue(obj: T, keyPath: string): SortingType { + return get(obj, keyPath) as SortingType; +} + +export function sortAssets(array: T[], criteria: SortCriteria): T[] { + const { key, order = 'asc', sortCallback } = criteria; + + return [...array].sort((a, b) => { + const aValue = getNestedValue(a, key); + const bValue = getNestedValue(b, key); + + // Always move undefined values to the end, regardless of sort order + if (aValue === undefined) { + return 1; + } + + if (bValue === undefined) { + return -1; + } + + let comparison: number; + + switch (sortCallback) { + case 'stringNumeric': + case 'alphaNumeric': + comparison = sortingCallbacks[sortCallback]( + aValue as string, + bValue as string, + ); + break; + case 'numeric': + comparison = sortingCallbacks.numeric( + aValue as number, + bValue as number, + ); + break; + case 'date': + comparison = sortingCallbacks.date(aValue as Date, bValue as Date); + break; + default: + if (aValue < bValue) { + comparison = -1; + } else if (aValue > bValue) { + comparison = 1; + } else { + comparison = 0; + } + } + + // Modify to sort in ascending or descending order + return order === 'asc' ? comparison : -comparison; + }); +} From 6a2e8c7280e03b8ad6ae424f94590e3a50b95325 Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Thu, 26 Sep 2024 20:39:01 -0700 Subject: [PATCH 02/27] chore: Breakup monolithic Tokens/index.tsx into smaller components --- .../UI/Tokens/TokenList/Networth/index.tsx | 162 +++++ .../TokenList/ScamWarningIcon/index.tsx | 48 ++ .../TokenList/ScamWarningModal/index.tsx | 84 +++ .../UI/Tokens/TokenList/StakeButton/index.tsx | 99 +++ .../TokenList/TokenListFooter/index.tsx | 122 ++++ .../Tokens/TokenList/TokenListItem/index.tsx | 225 ++++++ app/components/UI/Tokens/TokenList/index.tsx | 108 +++ app/components/UI/Tokens/index.tsx | 679 +----------------- .../UI/Tokens/util/handleBalance.ts | 71 ++ .../UI/Tokens/util}/sortAssets.ts | 0 10 files changed, 951 insertions(+), 647 deletions(-) create mode 100644 app/components/UI/Tokens/TokenList/Networth/index.tsx create mode 100644 app/components/UI/Tokens/TokenList/ScamWarningIcon/index.tsx create mode 100644 app/components/UI/Tokens/TokenList/ScamWarningModal/index.tsx create mode 100644 app/components/UI/Tokens/TokenList/StakeButton/index.tsx create mode 100644 app/components/UI/Tokens/TokenList/TokenListFooter/index.tsx create mode 100644 app/components/UI/Tokens/TokenList/TokenListItem/index.tsx create mode 100644 app/components/UI/Tokens/TokenList/index.tsx create mode 100644 app/components/UI/Tokens/util/handleBalance.ts rename app/{util/assets => components/UI/Tokens/util}/sortAssets.ts (100%) diff --git a/app/components/UI/Tokens/TokenList/Networth/index.tsx b/app/components/UI/Tokens/TokenList/Networth/index.tsx new file mode 100644 index 00000000000..c11c6e85cd0 --- /dev/null +++ b/app/components/UI/Tokens/TokenList/Networth/index.tsx @@ -0,0 +1,162 @@ +// Core and React imports +import React from 'react'; +import { View } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; + +// Third-party libraries +import { useSelector } from 'react-redux'; + +// Local hooks +import useIsOriginalNativeTokenSymbol from '../../../../hooks/useIsOriginalNativeTokenSymbol/useIsOriginalNativeTokenSymbol'; +import { useMetrics } from '../../../../../components/hooks/useMetrics'; +import { useTheme } from '../../../../../util/theme'; + +// Constants and core functionality +import AppConstants from '../../../../../core/AppConstants'; +import Engine from '../../../../../core/Engine'; +import Routes from '../../../../../constants/navigation/Routes'; +import { MetaMetricsEvents } from '../../../../../core/Analytics'; + +// Selectors +import { + selectChainId, + selectProviderConfig, + selectTicker, +} from '../../../../../selectors/networkController'; +import { selectCurrentCurrency } from '../../../../../selectors/currencyRateController'; +import { RootState } from '../../../../../reducers'; + +// Utilities +import { renderFiat } from '../../../../../util/number'; +import { isTestNet } from '../../../../../util/networks'; +import { isPortfolioUrl } from '../../../../../../app/util/url'; + +// Styles and Components +import createStyles from '../../styles'; +import Button, { + ButtonVariants, + ButtonSize, + ButtonWidthTypes, +} from '../../../../../component-library/components/Buttons/Button'; +import Text from '../../../../../component-library/components/Texts/Text'; +import AggregatedPercentage from '../../../../../component-library/components-temp/Price/AggregatedPercentage'; +import { IconName } from '../../../../../component-library/components/Icons/Icon'; + +// Types +import { BrowserTab } from '../../types'; +import { WalletViewSelectorsIDs } from '../../../../../../e2e/selectors/wallet/WalletView.selectors'; + +// Localization +import { strings } from '../../../../../../locales/i18n'; + +export const Networth = () => { + const { colors } = useTheme(); + const styles = createStyles(colors); + const balance = Engine.getTotalFiatAccountBalance(); + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const navigation = useNavigation>(); + const { trackEvent, isEnabled } = useMetrics(); + + const { type } = useSelector(selectProviderConfig); + const chainId = useSelector(selectChainId); + const ticker = useSelector(selectTicker); + const isDataCollectionForMarketingEnabled = useSelector( + (state: RootState) => state.security.dataCollectionForMarketing, + ); + const currentCurrency = useSelector(selectCurrentCurrency); + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const browserTabs = useSelector((state: any) => state.browser.tabs); + + const isOriginalNativeTokenSymbol = useIsOriginalNativeTokenSymbol( + chainId, + ticker, + type, + ); + + let total; + if (isOriginalNativeTokenSymbol) { + const tokenFiatTotal = balance?.tokenFiat ?? 0; + const ethFiatTotal = balance?.ethFiat ?? 0; + total = tokenFiatTotal + ethFiatTotal; + } else { + total = balance?.tokenFiat ?? 0; + } + + const fiatBalance = `${renderFiat(total, currentCurrency)}`; + + const onOpenPortfolio = () => { + const existingPortfolioTab = browserTabs.find(({ url }: BrowserTab) => + isPortfolioUrl(url), + ); + + let existingTabId; + let newTabUrl; + if (existingPortfolioTab) { + existingTabId = existingPortfolioTab.id; + } else { + const analyticsEnabled = isEnabled(); + const portfolioUrl = new URL(AppConstants.PORTFOLIO.URL); + + portfolioUrl.searchParams.append('metamaskEntry', 'mobile'); + + // Append user's privacy preferences for metrics + marketing on user navigation to Portfolio. + portfolioUrl.searchParams.append( + 'metricsEnabled', + String(analyticsEnabled), + ); + portfolioUrl.searchParams.append( + 'marketingEnabled', + String(!!isDataCollectionForMarketingEnabled), + ); + + newTabUrl = portfolioUrl.href; + } + const params = { + ...(newTabUrl && { newTabUrl }), + ...(existingTabId && { existingTabId, newTabUrl: undefined }), + timestamp: Date.now(), + }; + navigation.navigate(Routes.BROWSER.HOME, { + screen: Routes.BROWSER.VIEW, + params, + }); + trackEvent(MetaMetricsEvents.PORTFOLIO_LINK_CLICKED, { + portfolioUrl: AppConstants.PORTFOLIO.URL, + }); + }; + + return ( + + + + {fiatBalance} + + + {!isTestNet(chainId) ? ( + + ) : null} + +