DeFiCh/wallet

View on GitHub
mobile-app/app/screens/AppNavigator/screens/Portfolio/PortfolioScreen.tsx

Summary

Maintainability
D
2 days
Test Coverage
import {
  CommonActions,
  useIsFocused,
  useScrollToTop,
} from "@react-navigation/native";
import {
  ThemedIcon,
  ThemedScrollViewV2,
  ThemedTextV2,
  ThemedTouchableOpacityV2,
  ThemedViewV2,
} from "@components/themed";
import { useDisplayBalancesContext } from "@contexts/DisplayBalancesContext";
import { useWalletContext } from "@shared-contexts/WalletContext";
import {
  useThemeContext,
  useWalletPersistenceContext,
  useWhaleApiClient,
  useWhaleRpcClient,
} from "@waveshq/walletkit-ui";
import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
import { StackScreenProps } from "@react-navigation/stack";
import {
  dexPricesSelectorByDenomination,
  fetchDexPrice,
  fetchTokens,
  ocean,
  tokensSelector,
  WalletToken,
} from "@waveshq/walletkit-ui/dist/store";
import { tailwind } from "@tailwind";
import BigNumber from "bignumber.js";
import React, {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { batch, useSelector } from "react-redux";
import { Announcements } from "@screens/AppNavigator/screens/Portfolio/components/Announcements";
import { DFIBalanceCard } from "@screens/AppNavigator/screens/Portfolio/components/DFIBalanceCard";
import { translate } from "@translations";
import { Platform, RefreshControl, View } from "react-native";
import { RootState } from "@store";
import { BottomSheetModalMethods } from "@gorhom/bottom-sheet/lib/typescript/types";
import {
  activeVaultsSelector,
  fetchCollateralTokens,
  fetchLoanTokens,
  fetchVaults,
} from "@store/loans";
import {
  SkeletonLoader,
  SkeletonLoaderScreen,
} from "@components/SkeletonLoader";
import { LoanVaultActive } from "@defichain/whale-api-client/dist/api/loan";
import { fetchExecutionBlock, fetchFutureSwaps } from "@store/futureSwap";
import { useAppDispatch } from "@hooks/useAppDispatch";
import {
  AssetsFilterRow,
  ButtonGroupTabKey,
} from "@screens/AppNavigator/screens/Portfolio/components/AssetsFilterRow";
import { BottomSheetAddressDetailV2 } from "@screens/AppNavigator/screens/Portfolio/components/BottomSheetAddressDetailV2";
import {
  BottomSheetWebWithNavV2,
  BottomSheetWithNavV2,
} from "@components/BottomSheetWithNavV2";
import { CreateOrEditAddressLabelForm } from "@screens/AppNavigator/screens/Portfolio/components/CreateOrEditAddressLabelForm";
import { BottomSheetHeaderBackButton } from "@screens/AppNavigator/screens/Portfolio/components/BottomSheetHeaderBackButton";
import { BottomSheetHeader } from "@components/BottomSheetHeader";
import * as SplashScreen from "expo-splash-screen";
import { useLogger } from "@shared-contexts/NativeLoggingProvider";
import { bottomTabDefaultRoutes } from "@screens/AppNavigator/constants/DefaultRoutes";
import { DomainType, useDomainContext } from "@contexts/DomainContext";
import { WalletAlert } from "@components/WalletAlert";
import { useAnalytics } from "@shared-contexts/AnalyticsProvider";
import { AddressSelectionButtonV2 } from "./components/AddressSelectionButtonV2";
import { ActionButtons } from "./components/ActionButtons";
import {
  BottomSheetAssetSortList,
  PortfolioSortType,
} from "./components/BottomSheetAssetSortList";
import { useDenominationCurrency } from "./hooks/PortfolioCurrency";
import { PortfolioCard } from "./components/PortfolioCard";
import {
  LockedBalance,
  useTokenLockedBalance,
} from "./hooks/TokenLockedBalance";
import {
  PortfolioButtonGroupTabKey,
  TotalPortfolio,
} from "./components/TotalPortfolio";
import { useTokenPrice } from "./hooks/TokenPrice";
import { PortfolioParamList } from "./PortfolioNavigator";
import { useEvmTokenBalances } from "./hooks/EvmTokenBalances";

type Props = StackScreenProps<PortfolioParamList, "PortfolioScreen">;

export interface PortfolioRowToken extends WalletToken {
  usdAmount: BigNumber;
}

export function PortfolioScreen({ navigation }: Props): JSX.Element {
  const { isLight } = useThemeContext();
  const { domain } = useDomainContext();
  const isEvmDomain = domain === DomainType.EVM;
  const isFocused = useIsFocused();
  const height = useBottomTabBarHeight();
  const client = useWhaleApiClient();
  const whaleRpcClient = useWhaleRpcClient();
  const { address } = useWalletContext();
  const { hasAnalyticsModalBeenShown, setStorage } = useAnalytics();
  const { denominationCurrency, setDenominationCurrency } =
    useDenominationCurrency();

  const { getTokenPrice } = useTokenPrice(denominationCurrency);
  const prices = useSelector((state: RootState) =>
    dexPricesSelectorByDenomination(state.wallet, denominationCurrency),
  );
  const { wallets } = useWalletPersistenceContext();
  const lockedTokens = useTokenLockedBalance({ denominationCurrency }) as Map<
    string,
    LockedBalance
  >;
  const {
    isBalancesDisplayed,
    toggleDisplayBalances: onToggleDisplayBalances,
  } = useDisplayBalancesContext();
  const blockCount = useSelector((state: RootState) => state.block.count);
  const vaults = useSelector((state: RootState) =>
    activeVaultsSelector(state.loans),
  );

  const dispatch = useAppDispatch();
  const [refreshing, setRefreshing] = useState(false);
  const [isZeroBalance, setIsZeroBalance] = useState(true);
  const [isEvmZeroBalance, setIsEvmZeroBalance] = useState(true);
  const { hasFetchedToken, allTokens } = useSelector(
    (state: RootState) => state.wallet,
  );
  const { hasFetchedEvmTokens } = useSelector((state: RootState) => state.evm);
  const ref = useRef(null);
  const logger = useLogger();
  useScrollToTop(ref);

  useEffect(() => {
    dispatch(ocean.actions.setHeight(height));
  }, [height, wallets]);

  useEffect(() => {
    if (isFocused) {
      batch(() => {
        dispatch(
          fetchFutureSwaps({
            client: whaleRpcClient,
            address,
          }),
        );
        dispatch(fetchExecutionBlock({ client: whaleRpcClient }));
      });
    }
  }, [address, blockCount, isFocused]);

  useEffect(() => {
    batch(() => {
      // fetch only once to decide flag to display locked balance breakdown
      dispatch(fetchCollateralTokens({ client }));
      dispatch(fetchLoanTokens({ client }));
    });
  }, []);

  useEffect(() => {
    setTimeout(() => {
      if (hasAnalyticsModalBeenShown === "false") {
        WalletAlert({
          title: translate(
            "screens/AnalyticsScreen",
            "Data is now collected to improve experience.",
          ),
          message: translate(
            "screens/AnalyticsScreen",
            "As of the latest version, this wallet is now collecting non-identifiable performance-related data. You can choose to opt-out anytime from the settings page.",
          ),
          buttons: [
            {
              text: translate("screens/AnalyticsScreen", "Continue"),
              style: "cancel",
              onPress: async () => {
                setStorage("IS_ANALYTICS_MODAL_SHOWN", "true");
              },
            },
          ],
        });
      }
    }, 1000);
  }, [hasAnalyticsModalBeenShown]);

  const fetchPortfolioData = (): void => {
    batch(() => {
      dispatch(
        fetchTokens({
          client,
          address,
        }),
      );
      dispatch(
        fetchVaults({
          client,
          address,
        }),
      );
    });
  };

  // TODO: check if can reduce API calls. Already being called on WalletDataProvider
  // But doesn't use the denominationCurrency
  useEffect(() => {
    dispatch(
      fetchDexPrice({
        client,
        denomination: denominationCurrency,
      }),
    );
  }, [blockCount, denominationCurrency]);

  const onRefresh = useCallback(async () => {
    setRefreshing(true);
    fetchPortfolioData();
    setRefreshing(false);
  }, [address, client, dispatch]);

  const tokens = useSelector((state: RootState) =>
    tokensSelector(state.wallet),
  );
  const { evmTokens } = useEvmTokenBalances();
  const { totalAvailableValue, dstTokens } = useMemo(() => {
    return (isEvmDomain ? evmTokens : tokens).reduce(
      (
        {
          totalAvailableValue,
          dstTokens,
        }: { totalAvailableValue: BigNumber; dstTokens: PortfolioRowToken[] },
        token,
      ) => {
        const usdAmount = getTokenPrice(
          token.symbol,
          new BigNumber(token.amount),
          token.isLPS,
        );
        if (token.symbol === "DFI") {
          return {
            // `token.id === '0_unified'` to avoid repeated DFI price to get added in totalAvailableValue
            totalAvailableValue:
              token.id === "0_unified"
                ? totalAvailableValue
                : totalAvailableValue.plus(usdAmount.isNaN() ? 0 : usdAmount),
            dstTokens,
          };
        }
        return {
          totalAvailableValue: totalAvailableValue.plus(
            usdAmount.isNaN() ? 0 : usdAmount,
          ),
          dstTokens: [
            ...dstTokens,
            {
              ...token,
              usdAmount,
            },
          ],
        };
      },
      {
        totalAvailableValue: new BigNumber(0),
        dstTokens: [],
      },
    );
  }, [prices, tokens, domain, evmTokens]);

  // add token that are 100% locked as collateral into dstTokens
  const combinedTokens = useMemo(() => {
    if (lockedTokens === undefined || lockedTokens.size === 0 || isEvmDomain) {
      return dstTokens;
    }

    const dstTokenSymbols = dstTokens.map((token) => token.displaySymbol);
    const lockedTokensArray: PortfolioRowToken[] = [];
    lockedTokens.forEach((_lockedBalance, displaySymbol) => {
      if (displaySymbol === "DFI") {
        return;
      }

      const tokenExist = dstTokenSymbols.includes(displaySymbol);
      if (!tokenExist) {
        const tokenData = allTokens[displaySymbol];
        if (tokenData !== undefined) {
          lockedTokensArray.push({
            id: tokenData.id,
            amount: "0",
            symbol: tokenData.symbol,
            displaySymbol: tokenData.displaySymbol,
            symbolKey: tokenData.symbolKey,
            name: tokenData.name,
            isDAT: tokenData.isDAT,
            isLPS: tokenData.isLPS,
            isLoanToken: tokenData.isLoanToken,
            avatarSymbol: tokenData.displaySymbol,
            usdAmount: new BigNumber(0),
          });
        }
      }
    });
    return [...dstTokens, ...lockedTokensArray];
  }, [dstTokens, allTokens, lockedTokens]);

  const [filteredTokens, setFilteredTokens] = useState(combinedTokens);
  // portfolio tab items
  const onPortfolioButtonGroupChange = (
    portfolioButtonGroupTabKey: PortfolioButtonGroupTabKey,
  ): void => {
    setDenominationCurrency(portfolioButtonGroupTabKey);
  };

  const portfolioButtonGroup = [
    {
      id: PortfolioButtonGroupTabKey.USDT,
      label: translate("screens/PortfolioScreen", "USDT"),
      handleOnPress: () =>
        onPortfolioButtonGroupChange(PortfolioButtonGroupTabKey.USDT),
    },
    {
      id: PortfolioButtonGroupTabKey.DFI,
      label: translate("screens/PortfolioScreen", "DFI"),
      handleOnPress: () =>
        onPortfolioButtonGroupChange(PortfolioButtonGroupTabKey.DFI),
    },
    {
      id: PortfolioButtonGroupTabKey.BTC,
      label: translate("screens/PortfolioScreen", "BTC"),
      handleOnPress: () =>
        onPortfolioButtonGroupChange(PortfolioButtonGroupTabKey.BTC),
    },
    {
      id: PortfolioButtonGroupTabKey.DUSD,
      label: translate("screens/PortfolioScreen", "DUSD"),
      handleOnPress: () =>
        onPortfolioButtonGroupChange(PortfolioButtonGroupTabKey.DUSD),
    },
    {
      id: PortfolioButtonGroupTabKey.USDC,
      label: translate("screens/PortfolioScreen", "USDC"),
      handleOnPress: () =>
        onPortfolioButtonGroupChange(PortfolioButtonGroupTabKey.USDC),
    },
    {
      id: PortfolioButtonGroupTabKey.EUROC,
      label: translate("screens/PortfolioScreen", "EUROC"),
      handleOnPress: () =>
        onPortfolioButtonGroupChange(PortfolioButtonGroupTabKey.EUROC),
    },
  ];

  // Asset sort bottom sheet list
  const [assetSortType, setAssetSortType] = useState<PortfolioSortType>(
    PortfolioSortType.HighestDenominationValue,
  ); // to display selected sorted type text
  const [isSorted, setIsSorted] = useState<boolean>(false); // to display acsending/descending icon
  const [showAssetSortBottomSheet, setShowAssetSortBottomSheet] =
    useState(false);
  const sortTokensAssetOnType = useCallback(
    (assetSortType: PortfolioSortType): PortfolioRowToken[] => {
      let sortTokensFunc: (
        a: PortfolioRowToken,
        b: PortfolioRowToken,
      ) => number;
      switch (assetSortType) {
        case PortfolioSortType.HighestDenominationValue:
          sortTokensFunc = (a, b) => b.usdAmount.minus(a.usdAmount).toNumber();
          break;
        case PortfolioSortType.LowestDenominationValue:
          sortTokensFunc = (a, b) => a.usdAmount.minus(b.usdAmount).toNumber();
          break;
        case PortfolioSortType.HighestTokenAmount:
          sortTokensFunc = (a, b) =>
            new BigNumber(b.amount).minus(new BigNumber(a.amount)).toNumber();
          break;
        case PortfolioSortType.LowestTokenAmount:
          sortTokensFunc = (a, b) =>
            new BigNumber(a.amount).minus(new BigNumber(b.amount)).toNumber();
          break;
        case PortfolioSortType.AtoZ:
          sortTokensFunc = (a, b) => a.symbol.localeCompare(b.symbol);
          break;
        case PortfolioSortType.ZtoA:
          sortTokensFunc = (a, b) => b.symbol.localeCompare(a.symbol);
          break;
        default:
          sortTokensFunc = (a, b) => b.usdAmount.minus(a.usdAmount).toNumber();
      }

      return filteredTokens.sort(sortTokensFunc);
    },
    [domain, filteredTokens, assetSortType, denominationCurrency],
  );

  useEffect(() => {
    setAssetSortType(PortfolioSortType.HighestDenominationValue); // reset sorting state upon denominationCurrency change
  }, [denominationCurrency]);

  // Reset button group in EVM domain
  useEffect(() => {
    setActiveButtonGroup(ButtonGroupTabKey.AllTokens);
  }, [domain]);

  // token tab items
  const [activeButtonGroup, setActiveButtonGroup] = useState<ButtonGroupTabKey>(
    ButtonGroupTabKey.AllTokens,
  );
  const handleButtonFilter = useCallback(
    (buttonGroupTabKey: ButtonGroupTabKey) => {
      const filterTokens = combinedTokens.filter((token) => {
        switch (buttonGroupTabKey) {
          case ButtonGroupTabKey.LPTokens:
            return token.isLPS;
          case ButtonGroupTabKey.Crypto:
            return token.isDAT && !token.isLoanToken && !token.isLPS;
          case ButtonGroupTabKey.dTokens:
            return token.isLoanToken;
          // for All token tab will return true for list of dstToken
          default:
            return true;
        }
      });
      setFilteredTokens(filterTokens);
    },
    [combinedTokens],
  );

  const totalLockedValue = useMemo(() => {
    if (lockedTokens === undefined) {
      return new BigNumber(0);
    }
    return [...lockedTokens.values()].reduce(
      (totalLockedValue: BigNumber, value: LockedBalance) =>
        totalLockedValue.plus(value.tokenValue.isNaN() ? 0 : value.tokenValue),
      new BigNumber(0),
    );
  }, [lockedTokens, prices]);

  const totalLoansValue = useMemo(() => {
    if (vaults === undefined) {
      return new BigNumber(0);
    }
    return vaults.reduce(
      (totalLoansValue: BigNumber, vault: LoanVaultActive) => {
        const totalVaultLoansValue = vault.loanAmounts.reduce(
          (totalVaultLoansValue, loanToken) => {
            const tokenValue = getTokenPrice(
              loanToken.symbol,
              new BigNumber(loanToken.amount),
            );
            return totalVaultLoansValue.plus(
              new BigNumber(tokenValue).isNaN() ? 0 : tokenValue,
            );
          },
          new BigNumber(0),
        );
        return totalLoansValue.plus(
          new BigNumber(totalVaultLoansValue).isNaN()
            ? 0
            : totalVaultLoansValue,
        );
      },
      new BigNumber(0),
    );
  }, [prices, vaults]);

  // to update filter list from selected token tab
  useEffect(() => {
    handleButtonFilter(activeButtonGroup);
  }, [activeButtonGroup, combinedTokens]);

  useEffect(() => {
    setIsZeroBalance(
      !tokens.some((token) => new BigNumber(token.amount).isGreaterThan(0)),
    );
    setIsEvmZeroBalance(
      !evmTokens.some((token) => new BigNumber(token.amount).isGreaterThan(0)),
    );
  }, [tokens, evmTokens]);

  const assetSortBottomSheetScreen = useMemo(() => {
    return [
      {
        stackScreenName: "AssetSortList",
        component: BottomSheetAssetSortList({
          onButtonPress: (item: PortfolioSortType) => {
            setIsSorted(true);
            setAssetSortType(item);
            sortTokensAssetOnType(item);
            setShowAssetSortBottomSheet(false);
            dismissModal(true);
          },
          denominationCurrency,
          selectedAssetSortType: assetSortType,
        }),
        option: {
          headerStatusBarHeight: 1,
          headerTitle: "",
          headerBackTitleVisible: false,
          header: (): JSX.Element => {
            return (
              <BottomSheetHeader
                headerText={translate("screens/PortfolioScreen", "Sort Assets")}
                onClose={() => {
                  setShowAssetSortBottomSheet(false);
                  dismissModal(true);
                }}
              />
            );
          },
        },
      },
    ];
  }, [denominationCurrency, assetSortType]);

  // Address selection bottom sheet
  const bottomSheetRef = useRef<BottomSheetModalMethods>(null);
  const bottomSheetSortRef = useRef<BottomSheetModalMethods>(null);
  const containerRef = useRef(null);
  const [isModalDisplayed, setIsModalDisplayed] = useState(false);
  const modalSnapPoints = { ios: ["75%"], android: ["75%"] };
  const modalSortingSnapPoints = { ios: ["55%"], android: ["55%"] };
  const expandModal = useCallback((isSortBottomSheet: boolean) => {
    if (Platform.OS === "web") {
      setIsModalDisplayed(true);
    } else if (isSortBottomSheet) {
      bottomSheetSortRef.current?.present();
    } else {
      bottomSheetRef.current?.present();
    }
  }, []);
  const dismissModal = useCallback((isSortBottomSheet: boolean) => {
    if (Platform.OS === "web") {
      setIsModalDisplayed(false);
    } else if (isSortBottomSheet) {
      bottomSheetSortRef.current?.close();
    } else {
      bottomSheetRef.current?.close();
    }
  }, []);

  const addressBottomSheetHeader = {
    headerStatusBarHeight: 1,
    headerTitle: "",
    headerBackTitleVisible: false,
    headerStyle: tailwind("rounded-t-xl-v2", {
      "bg-mono-light-v2-100": isLight,
      "bg-mono-dark-v2-100": !isLight,
    }),
    header: (): JSX.Element => {
      return (
        <ThemedViewV2
          style={tailwind("pt-5 pb-3 rounded-t-xl-v2 border-b-0 relative px-5")}
        >
          <ThemedViewV2
            style={tailwind("flex flex-row justify-center items-center")}
          >
            <ThemedViewV2
              dark={tailwind("bg-mono-dark-v2-00 border-mono-dark-v2-200")}
              light={tailwind("bg-mono-light-v2-00 border-mono-light-v2-200")}
              style={tailwind(
                "flex flex-row items-center rounded-2xl border-0.5 px-4 py-2",
              )}
            >
              <View
                style={tailwind("w-1.5 h-1.5 bg-green-v2 rounded-full mr-1")}
              />
              <ThemedTextV2
                style={tailwind("text-xs font-normal-v2 pr-1")}
                dark={tailwind("text-mono-dark-v2-700")}
                light={tailwind("text-mono-light-v2-700")}
                testID="bottomsheet-address-header"
              >
                {domain}
              </ThemedTextV2>
              <ThemedTextV2
                style={tailwind("text-xs font-normal-v2")}
                dark={tailwind("text-mono-dark-v2-700")}
                light={tailwind("text-mono-light-v2-700")}
              >
                {translate("screens/PortfolioScreen", "network")}
              </ThemedTextV2>
            </ThemedViewV2>
          </ThemedViewV2>
          <ThemedTouchableOpacityV2
            style={tailwind("border-0 absolute right-5 top-6")}
            onPress={() => dismissModal(false)}
            testID="close_bottom_sheet_button"
          >
            <ThemedIcon iconType="Feather" name="x-circle" size={22} />
          </ThemedTouchableOpacityV2>
        </ThemedViewV2>
      );
    },
  };

  const resetNavigationStack = () => {
    navigation.dispatch(
      CommonActions.reset({
        routes: bottomTabDefaultRoutes,
      }),
    );
  };

  const addressBottomSheetScreen = useMemo(() => {
    return [
      {
        stackScreenName: "AddressDetail",
        component: BottomSheetAddressDetailV2({
          address: address,
          addressLabel: "TODO: get label from storage api",
          onReceiveButtonPress: () => {
            dismissModal(false);
            navigation.navigate("Receive");
          },
          onTransactionsButtonPress: () => {
            dismissModal(false);
            navigation.navigate("TransactionsScreen");
          },
          onCloseButtonPress: () => dismissModal(false),
          navigateToScreen: {
            screenName: "CreateOrEditAddressLabelForm",
          },
          onSwitchAddress: resetNavigationStack,
        }),
        option: addressBottomSheetHeader,
      },
      {
        stackScreenName: "CreateOrEditAddressLabelForm",
        component: CreateOrEditAddressLabelForm,
        option: {
          ...addressBottomSheetHeader,
          headerLeft: (): JSX.Element => <BottomSheetHeaderBackButton />,
        },
      },
    ];
  }, [address, isLight, domain]);

  // Hide splashscreen when first page is loaded to prevent white screen
  // It is wrapped on a timeout so it will execute once the JS stack is cleared up
  useEffect(() => {
    setTimeout(() => {
      SplashScreen.hideAsync().catch(logger.error);
    });
  }, []);

  return (
    <View ref={containerRef} style={tailwind("flex-1")}>
      <ThemedScrollViewV2
        ref={ref}
        contentContainerStyle={tailwind("pb-12")}
        testID="portfolio_list"
        refreshControl={
          <RefreshControl onRefresh={onRefresh} refreshing={refreshing} />
        }
      >
        <ThemedViewV2
          light={tailwind("bg-mono-light-v2-00")}
          dark={tailwind("bg-mono-dark-v2-00")}
          style={tailwind("px-5 flex flex-row items-center")}
        >
          <AddressSelectionButtonV2 onPress={() => expandModal(false)} />
          <ThemedTouchableOpacityV2
            testID="toggle_balance"
            style={tailwind("ml-2")}
            light={tailwind("bg-transparent")}
            dark={tailwind("bg-transparent")}
            onPress={onToggleDisplayBalances}
          >
            <ThemedIcon
              iconType="MaterialCommunityIcons"
              dark={tailwind("text-mono-dark-v2-900")}
              light={tailwind("text-mono-light-v2-900")}
              name={isBalancesDisplayed ? "eye" : "eye-off"}
              size={18}
            />
          </ThemedTouchableOpacityV2>
        </ThemedViewV2>

        <TotalPortfolio
          totalAvailableValue={totalAvailableValue}
          totalLockedValue={totalLockedValue}
          totalLoansValue={totalLoansValue}
          portfolioButtonGroup={portfolioButtonGroup}
          denominationCurrency={denominationCurrency}
          setDenominationCurrency={setDenominationCurrency}
        />
        <ActionButtons />
        <Announcements />

        {domain === DomainType.DVM && (
          <AssetsFilterRow
            activeButtonGroup={activeButtonGroup}
            onButtonGroupPress={handleButtonFilter}
            setActiveButtonGroup={setActiveButtonGroup}
          />
        )}
        {/* to show bottom sheet for asset sort */}
        <AssetSortRow
          assetSortType={assetSortType}
          onPress={() => {
            setShowAssetSortBottomSheet(true);
            expandModal(true);
          }}
          isSorted={isSorted}
          denominationCurrency={denominationCurrency}
          isEvmDomain={isEvmDomain}
        />
        {activeButtonGroup === ButtonGroupTabKey.AllTokens && (
          <DFIBalanceCard denominationCurrency={denominationCurrency} />
        )}
        {!hasFetchedToken || (isEvmDomain && !hasFetchedEvmTokens) ? (
          <View style={tailwind("px-5")}>
            <SkeletonLoader row={2} screen={SkeletonLoaderScreen.Portfolio} />
          </View>
        ) : (
          <PortfolioCard
            isZeroBalance={isZeroBalance}
            isEvmZeroBalance={isEvmZeroBalance}
            filteredTokens={sortTokensAssetOnType(assetSortType)}
            navigation={navigation}
            buttonGroupOptions={{
              activeButtonGroup: activeButtonGroup,
              setActiveButtonGroup: setActiveButtonGroup,
              onButtonGroupPress: handleButtonFilter,
            }}
            denominationCurrency={denominationCurrency}
            isEvmDomain={isEvmDomain}
          />
        )}
        {Platform.OS === "web" ? (
          <BottomSheetWebWithNavV2
            modalRef={containerRef}
            screenList={
              showAssetSortBottomSheet
                ? assetSortBottomSheetScreen
                : addressBottomSheetScreen
            }
            isModalDisplayed={isModalDisplayed}
            modalStyle={{
              position: "absolute",
              bottom: "0",
              height: "505px",
              width: "375px",
              zIndex: 50,
              borderTopLeftRadius: 15,
              borderTopRightRadius: 15,
              overflow: "hidden",
            }}
          />
        ) : (
          <>
            <BottomSheetWithNavV2
              modalRef={bottomSheetSortRef}
              screenList={assetSortBottomSheetScreen}
              snapPoints={modalSortingSnapPoints}
            />
            <BottomSheetWithNavV2
              modalRef={bottomSheetRef}
              screenList={addressBottomSheetScreen}
              snapPoints={modalSnapPoints}
            />
          </>
        )}
      </ThemedScrollViewV2>
    </View>
  );
}

function AssetSortRow(props: {
  isSorted: boolean;
  assetSortType: PortfolioSortType;
  denominationCurrency: string;
  onPress: () => void;
  isEvmDomain: boolean;
}): JSX.Element {
  const highestCurrencyValue = translate(
    "screens/PortfolioScreen",
    "Highest value ({{denominationCurrency}})",
    { denominationCurrency: props.denominationCurrency },
  );
  const lowestCurrencyValue = translate(
    "screens/PortfolioScreen",
    "Lowest value ({{denominationCurrency}})",
    { denominationCurrency: props.denominationCurrency },
  );
  const getDisplayedSortText = (text: PortfolioSortType): string => {
    if (text === PortfolioSortType.HighestDenominationValue) {
      return highestCurrencyValue;
    } else if (text === PortfolioSortType.LowestDenominationValue) {
      return lowestCurrencyValue;
    }
    return text;
  };

  return (
    <View
      style={tailwind("px-10 flex flex-row justify-between", {
        "mt-8": props.isEvmDomain,
      })}
      testID="toggle_sorting_assets"
    >
      <ThemedTextV2
        style={tailwind("text-xs pr-1 font-normal-v2")}
        light={tailwind("text-mono-light-v2-500")}
        dark={tailwind("text-mono-dark-v2-500")}
      >
        {translate("screens/PortfolioScreen", "ASSETS")}
      </ThemedTextV2>
      {!props.isEvmDomain && (
        <ThemedTouchableOpacityV2
          style={tailwind("flex flex-row items-center")}
          onPress={props.onPress}
          testID="your_assets_dropdown_arrow"
        >
          <ThemedTextV2
            light={tailwind("text-mono-light-v2-800")}
            dark={tailwind("text-mono-dark-v2-800")}
            style={tailwind("text-xs font-normal-v2")}
          >
            {translate(
              "screens/PortfolioScreen",
              props.isSorted
                ? getDisplayedSortText(props.assetSortType)
                : "Sort by",
            )}
          </ThemedTextV2>
          <ThemedIcon
            style={tailwind("ml-1 font-medium")}
            light={tailwind("text-mono-light-v2-800")}
            dark={tailwind("text-mono-dark-v2-800")}
            iconType="Feather"
            name="menu"
            size={16}
          />
        </ThemedTouchableOpacityV2>
      )}
    </View>
  );
}