DeFiCh/wallet

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

Summary

Maintainability
D
1 day
Test Coverage
import * as React from "react";
import { useEffect, useState } from "react";
import { Linking, TouchableOpacity } from "react-native";
import { tailwind } from "@tailwind";
import BigNumber from "bignumber.js";
import { NumericFormat as NumberFormat } from "react-number-format";
import { StackScreenProps } from "@react-navigation/stack";
import { translate } from "@translations";
import {
  DFITokenSelector,
  DFIUtxoSelector,
  tokensSelector,
  unifiedDFISelector,
  WalletToken,
} from "@waveshq/walletkit-ui/dist/store";
import {
  getMetaScanTokenUrl,
  useDeFiScanContext,
} from "@shared-contexts/DeFiScanContext";
import { PoolPairData } from "@defichain/whale-api-client/dist/api/poolpairs";
import { View } from "@components";
import {
  IconName,
  IconType,
  ThemedIcon,
  ThemedScrollViewV2,
  ThemedTextV2,
  ThemedViewV2,
} from "@components/themed";
import { useSelector } from "react-redux";
import { RootState } from "@store";
import { ButtonV2 } from "@components/ButtonV2";
import { InfoTextLinkV2 } from "@components/InfoTextLink";
import { ThemedTouchableListItem } from "@components/themed/ThemedTouchableListItem";
import { ConvertDirection } from "@screens/enum";
import { DomainType, useDomainContext } from "@contexts/DomainContext";
import { useNetworkContext } from "@waveshq/walletkit-ui";
import { PortfolioParamList } from "../PortfolioNavigator";
import { useTokenPrice } from "../hooks/TokenPrice";
import { useDenominationCurrency } from "../hooks/PortfolioCurrency";
import { TokenBreakdownDetailsV2 } from "../components/TokenBreakdownDetailsV2";
import { getPrecisedTokenValue } from "../../Auctions/helpers/precision-token-value";
import { PortfolioButtonGroupTabKey } from "../components/TotalPortfolio";
import { TokenIcon } from "../components/TokenIcon";
import { useTokenBalance } from "../hooks/TokenBalance";

interface TokenActionItems {
  title: string;
  icon: IconName;
  onPress: () => void;
  testID: string;
  iconType: IconType;
  isLast?: boolean;
}

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

const usePoolPairToken = (
  tokenParam: WalletToken,
): {
  pair?: PoolPairData;
  token: WalletToken;
  swapTokenDisplaySymbol?: string;
} => {
  const pairs = useSelector((state: RootState) => state.wallet.poolpairs);
  const tokens = useSelector((state: RootState) =>
    tokensSelector(state.wallet),
  );

  // state
  const [token, setToken] = useState(tokenParam);
  const [pair, setPair] = useState<PoolPairData>();
  const [swapTokenDisplaySymbol, setSwapTokenDisplaySymbol] =
    useState<string>();

  useEffect(() => {
    const t = tokens.find((t) => t.id === token.id);

    if (t !== undefined) {
      setToken(t);
    }

    const poolpair = pairs.find((p) => {
      if (token.isLPS) {
        return p.data.id === token.id;
      }
      // get pair with same id if token passed is not LP
      if (token.id === p.data.tokenA.id) {
        setSwapTokenDisplaySymbol(p.data.tokenB.displaySymbol);
        return true;
      }
      if (token.id === p.data.tokenB.id) {
        setSwapTokenDisplaySymbol(p.data.tokenA.displaySymbol);
        return true;
      }
      return false;
    })?.data;

    if (poolpair !== undefined) {
      setPair(poolpair);
    }
  }, [token, JSON.stringify(tokens), pairs]);

  return {
    pair,
    token,
    swapTokenDisplaySymbol,
  };
};

export function TokenDetailScreen({ route, navigation }: Props): JSX.Element {
  const { denominationCurrency } = useDenominationCurrency();
  const { domain } = useDomainContext();

  const { hasFetchedToken } = useSelector((state: RootState) => state.wallet);
  const { getTokenPrice } = useTokenPrice(denominationCurrency); // input based on selected denomination from portfolio tab
  const DFIUnified = useSelector((state: RootState) =>
    unifiedDFISelector(state.wallet),
  );
  const availableValue = getTokenPrice(
    DFIUnified.symbol,
    new BigNumber(DFIUnified.amount),
  );
  const DFIToken = useSelector((state: RootState) =>
    DFITokenSelector(state.wallet),
  );
  const DFIUtxo = useSelector((state: RootState) =>
    DFIUtxoSelector(state.wallet),
  );
  const { pair, token, swapTokenDisplaySymbol } = usePoolPairToken(
    route.params.token,
  );

  const { dvmTokens, evmTokens } = useTokenBalance();

  // usdAmount for crypto tokens, undefined for DFI token
  const { usdAmount } = route.params.token;
  const isEvmDomain = domain === DomainType.EVM;
  const onNavigateLiquidity = ({
    destination,
    pair,
    token,
  }: {
    destination: "AddLiquidity" | "RemoveLiquidity";
    pair: PoolPairData;
    token: WalletToken;
  }): void => {
    navigation.navigate("Portfolio", {
      screen: destination,
      initial: false,
      params: {
        pair,
        pairInfo: token,
      },
      merge: true,
    });
  };

  const onNavigateSwap = ({
    pair,
    fromToken,
  }: {
    pair?: PoolPairData;
    fromToken?: WalletToken;
  }): void => {
    navigation.navigate("Portfolio", {
      screen: "CompositeSwap",
      initial: false,
      params: {
        pair,
        fromToken,
        tokenSelectOption: {
          from: {
            isDisabled: false,
            isPreselected: true,
          },
          to: {
            isDisabled: false,
            isPreselected: false,
          },
        },
      },
      merge: true,
    });
  };

  return (
    <ThemedScrollViewV2 contentContainerStyle={tailwind("flex-grow")}>
      <TokenSummary
        token={token}
        border
        usdAmount={usdAmount ?? new BigNumber(0)}
        isEvmDomain={isEvmDomain}
      />

      <View style={tailwind("p-5 pb-12")}>
        {!isEvmDomain && (
          <TokenBreakdownDetailsV2
            hasFetchedToken={hasFetchedToken}
            availableAmount={new BigNumber(DFIUnified.amount)}
            availableValue={availableValue}
            testID="dfi"
            dfiUtxo={DFIUtxo}
            dfiToken={DFIToken}
            token={token}
            usdAmount={usdAmount ?? new BigNumber(0)}
            pair={pair}
          />
        )}
        {token.symbol === "DFI" && token.id !== "0_evm" && (
          <ThemedViewV2
            dark={tailwind("border-mono-dark-v2-300")}
            light={tailwind("border-mono-light-v2-300")}
            style={tailwind("pt-1")}
          >
            <InfoTextLinkV2
              onPress={() =>
                navigation.navigate("Portfolio", {
                  screen: "TokensVsUtxoFaq",
                  merge: true,
                  initial: false,
                })
              }
              text="Learn more about DFI"
              testId="dfi_learn_more"
              textStyle={tailwind("px-0")}
            />
          </ThemedViewV2>
        )}
      </View>

      <View style={tailwind("flex-1 flex-col-reverse pb-12")}>
        <View style={tailwind("px-5")}>
          <ThemedViewV2
            dark={tailwind("bg-mono-dark-v2-00")}
            light={tailwind("bg-mono-light-v2-00")}
            style={tailwind("rounded-lg-v2 px-5")}
          >
            {token.id !== "0" && (
              <>
                {!isEvmDomain && (
                  <TokenActionRow
                    icon="arrow-up-right"
                    iconType="Feather"
                    isLast={false}
                    onPress={() =>
                      navigation.navigate({
                        name: "SendScreen",
                        params: { token },
                        merge: true,
                      })
                    }
                    testID="send_button"
                    title={translate(
                      "screens/TokenDetailScreen",
                      "Send to other wallet",
                    )}
                  />
                )}

                <TokenActionRow
                  icon="arrow-down-left"
                  iconType="Feather"
                  isLast={
                    !(
                      token.symbol === "DFI" ||
                      (token.isLPS && pair !== undefined && !isEvmDomain) ||
                      (pair !== undefined && !token.isLPS && !isEvmDomain)
                    )
                  }
                  onPress={() => navigation.navigate("Receive")}
                  testID="receive_button"
                  title={translate("screens/TokenDetailScreen", "Receive")}
                />
              </>
            )}

            {token.symbol === "DFI" && token.id !== "0_evm" && (
              <TokenActionRow
                icon="swap-calls"
                iconType="MaterialIcons"
                onPress={() => {
                  const convertDirection: ConvertDirection =
                    token.id === "0_utxo"
                      ? ConvertDirection.utxosToAccount
                      : ConvertDirection.accountToUtxos;

                  const utxoToken = dvmTokens.find(
                    (token) => token.tokenId === "0_utxo",
                  );
                  const dfiToken = dvmTokens.find(
                    (token) => token.tokenId === "0",
                  );
                  const [sourceToken, targetToken] =
                    convertDirection === ConvertDirection.utxosToAccount
                      ? [utxoToken, dfiToken]
                      : [dfiToken, utxoToken];

                  navigation.navigate({
                    name: "ConvertScreen",
                    params: {
                      sourceToken,
                      targetToken,
                      convertDirection,
                    },
                    merge: true,
                  });
                }}
                testID="convert_button"
                title={translate(
                  "screens/TokenDetailScreen",
                  "Convert to {{symbol}}",
                  { symbol: "Token/UTXO" },
                )}
              />
            )}

            {token.id === "0_evm" && (
              <TokenActionRow
                icon="swap-calls"
                iconType="MaterialIcons"
                onPress={() => {
                  const convertDirection: ConvertDirection =
                    domain === DomainType.DVM
                      ? ConvertDirection.dvmToEvm
                      : ConvertDirection.evmToDvm;

                  const evmToken = evmTokens.find(
                    (token) => token.tokenId === "0_evm",
                  );
                  const dfiToken = dvmTokens.find(
                    (token) => token.tokenId === "0",
                  );
                  const [sourceToken, targetToken] =
                    convertDirection === ConvertDirection.evmToDvm
                      ? [evmToken, dfiToken]
                      : [dfiToken, evmToken];

                  navigation.navigate({
                    name: "ConvertScreen",
                    params: {
                      sourceToken,
                      targetToken,
                      convertDirection,
                    },
                    merge: true,
                  });
                }}
                testID="convert_button"
                title={translate(
                  "screens/TokenDetailScreen",
                  "Convert to {{symbol}}",
                  { symbol: "Token" },
                )}
              />
            )}
            {token.isLPS && pair !== undefined && (
              <TokenActionRow
                icon="minus-circle"
                iconType="Feather"
                onPress={() =>
                  onNavigateLiquidity({
                    destination: "RemoveLiquidity",
                    pair,
                    token,
                  })
                }
                testID="remove_liquidity_button"
                title={translate(
                  "screens/TokenDetailScreen",
                  "Remove liquidity",
                )}
              />
            )}
            {pair !== undefined && !token.isLPS && !isEvmDomain && (
              <TokenActionRow
                icon="plus-circle"
                iconType="Feather"
                onPress={() =>
                  onNavigateLiquidity({
                    destination: "AddLiquidity",
                    pair,
                    token,
                  })
                }
                testID="add_liquidity_button"
                title={translate("screens/TokenDetailScreen", "Add liquidity")}
              />
            )}
          </ThemedViewV2>

          {/*  Show only for LP tokens */}
          <View style={tailwind("px-5")}>
            {pair !== undefined && token.isLPS && !isEvmDomain && (
              <View style={tailwind("pt-4")}>
                <ButtonV2
                  onPress={() =>
                    onNavigateLiquidity({
                      destination: "AddLiquidity",
                      pair,
                      token,
                    })
                  }
                  testID="add_liquidity_button"
                  label={translate(
                    "screens/TokenDetailScreen",
                    "Add liquidity",
                  )}
                />
              </View>
            )}
          </View>
          {token.symbol === "DFI" && !isEvmDomain && (
            <View style={tailwind("pt-4")}>
              <ButtonV2
                onPress={() =>
                  onNavigateSwap({
                    fromToken: {
                      ...DFIUnified,
                      id: "0_unified",
                    },
                  })
                }
                testID="swap_button_dfi"
                label={translate("screens/TokenDetailScreen", "Swap")}
              />
            </View>
          )}

          {!token.isLPS &&
            pair !== undefined &&
            swapTokenDisplaySymbol !== undefined &&
            !isEvmDomain && (
              <View style={tailwind("pt-4")}>
                <ButtonV2
                  onPress={() => onNavigateSwap({ pair })}
                  testID="swap_button"
                  label={translate("screens/TokenDetailScreen", "Swap")}
                  disabled={!pair.status}
                />
              </View>
            )}
        </View>
      </View>
    </ThemedScrollViewV2>
  );
}

function TokenSummary(props: {
  token: WalletToken;
  border?: boolean;
  usdAmount: BigNumber;
  isEvmDomain?: boolean;
}): JSX.Element {
  const { denominationCurrency } = useDenominationCurrency();
  const { getTokenUrl } = useDeFiScanContext();
  const { network } = useNetworkContext();
  const onTokenUrlPressed = async (): Promise<void> => {
    const id =
      props.token.id === "0_utxo" ||
      props.token.id === "0_unified" ||
      props.token.id === "0_evm"
        ? 0
        : props.token.id;
    const url = props.token.id.includes("_evm")
      ? getMetaScanTokenUrl(network, props.token.id)
      : getTokenUrl(id);
    await Linking.openURL(url);
  };

  return (
    <ThemedViewV2
      light={tailwind("border-mono-light-v2-300")}
      dark={tailwind("border-mono-dark-v2-300")}
      style={tailwind("pt-8 pb-5 mx-5", { "border-b-0.5": props.border })}
    >
      <View style={tailwind("flex-row items-center")}>
        <TokenIcon
          token={{
            isLPS: props.token.isLPS,
            displaySymbol: props.token.displaySymbol,
            id: props.token.id,
          }}
          size={40}
          isEvmToken={props.isEvmDomain}
        />
        <View style={tailwind("flex-col ml-3")}>
          <ThemedTextV2 style={tailwind("font-semibold-v2")}>
            {props.token.displaySymbol}
          </ThemedTextV2>
          <TouchableOpacity
            onPress={onTokenUrlPressed}
            testID="token_detail_explorer_url"
          >
            <View style={tailwind("flex-row")}>
              <ThemedTextV2
                light={tailwind("text-mono-light-v2-700")}
                dark={tailwind("text-mono-dark-v2-700")}
                style={tailwind("text-sm font-normal-v2")}
              >
                {props.token.name || props.token.symbol}
              </ThemedTextV2>
              <View style={tailwind("ml-1 flex-grow-0 justify-center")}>
                <ThemedIcon
                  light={tailwind("text-mono-light-v2-700")}
                  dark={tailwind("text-mono-dark-v2-700")}
                  iconType="Feather"
                  name="external-link"
                  size={16}
                />
              </View>
            </View>
          </TouchableOpacity>
        </View>
        {props.token.isLPS ? (
          <></>
        ) : (
          <View style={[tailwind("flex-col"), { marginLeft: "auto" }]}>
            <NumberFormat
              displayType="text"
              renderText={(value) => (
                <ThemedTextV2
                  style={tailwind("flex-wrap font-semibold-v2 text-right")}
                  testID="token_detail_amount"
                >
                  {value}
                </ThemedTextV2>
              )}
              thousandSeparator
              value={new BigNumber(props.token.amount).toFixed(8)}
            />
            <NumberFormat
              displayType="text"
              prefix={
                denominationCurrency === PortfolioButtonGroupTabKey.USDT
                  ? "$"
                  : undefined
              }
              suffix={
                denominationCurrency !== PortfolioButtonGroupTabKey.USDT
                  ? ` ${denominationCurrency}`
                  : undefined
              }
              renderText={(value) => (
                <ThemedTextV2
                  style={tailwind(
                    "flex-wrap text-sm font-normal-v2 text-right",
                  )}
                  light={tailwind("text-mono-light-v2-700")}
                  dark={tailwind("text-mono-dark-v2-700")}
                  testID="token_detail_usd_amount"
                >
                  {value}
                </ThemedTextV2>
              )}
              thousandSeparator
              value={getPrecisedTokenValue(props.usdAmount)}
            />
          </View>
        )}
      </View>
    </ThemedViewV2>
  );
}

function TokenActionRow({
  title,
  icon,
  onPress,
  testID,
  iconType,
  isLast,
}: TokenActionItems): JSX.Element {
  return (
    <ThemedTouchableListItem onPress={onPress} isLast={isLast} testID={testID}>
      <ThemedTextV2
        dark={tailwind("text-mono-dark-v2-900")}
        light={tailwind("text-mono-light-v2-900")}
        style={tailwind("font-normal-v2 text-sm")}
      >
        {title}
      </ThemedTextV2>

      <ThemedIcon
        dark={tailwind("text-mono-dark-v2-700")}
        light={tailwind("text-mono-light-v2-700")}
        iconType={iconType}
        name={icon}
        size={20}
      />
    </ThemedTouchableListItem>
  );
}