DeFiCh/wallet

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

Summary

Maintainability
A
3 hrs
Test Coverage
import { tailwind } from "@tailwind";
import {
  ThemedIcon,
  ThemedText,
  ThemedTouchableOpacity,
  ThemedView,
  ThemedScrollView,
  ThemedSectionTitle,
} from "@components/themed";
import { useEffect, useState, useLayoutEffect } from "react";
import {
  MAX_ALLOWED_ADDRESSES,
  useWalletContext,
} from "@shared-contexts/WalletContext";
import { View } from "@components";
import { TouchableOpacity } from "react-native";
import { translate } from "@translations";
import { NavigationProp, useNavigation } from "@react-navigation/native";
import { useLogger } from "@shared-contexts/NativeLoggingProvider";
import { RandomAvatar } from "@screens/AppNavigator/screens/Portfolio/components/RandomAvatar";
import {
  SkeletonLoader,
  SkeletonLoaderScreen,
} from "@components/SkeletonLoader";
import { useSelector } from "react-redux";
import { RootState } from "@store";
import {
  hasTxQueued,
  hasOceanTXQueued,
  wallet as walletReducer,
} from "@waveshq/walletkit-ui/dist/store";
import { loans } from "@store/loans";
import { useAppDispatch } from "@hooks/useAppDispatch";
import { PortfolioParamList } from "../PortfolioNavigator";

export function AddressControlScreen(): JSX.Element {
  const navigation = useNavigation<NavigationProp<PortfolioParamList>>();
  useLayoutEffect(() => {
    navigation.setOptions({
      headerRight: (): JSX.Element => <DiscoverWalletAddress />,
    });
  }, [navigation]);

  return (
    <ThemedScrollView>
      <ThemedSectionTitle
        testID="switch_address_screen_title"
        text={translate(
          "screens/AddressControlScreen",
          "Switch to another address"
        )}
      />
      <AddressControlCard onClose={() => navigation.goBack()} />
    </ThemedScrollView>
  );
}

export function AddressControlModal({
  onClose,
}: {
  onClose: () => void;
}): JSX.Element {
  return (
    <View style={tailwind("w-full pb-16")}>
      <ThemedView
        dark={tailwind("border-gray-700")}
        light={tailwind("border-gray-100")}
        style={tailwind("border-b-2")}
      >
        <View
          style={tailwind(
            "flex flex-row justify-between w-full px-4 pb-4 pt-2"
          )}
        >
          <View style={tailwind("flex flex-row justify-between items-center")}>
            <ThemedText
              dark={tailwind("text-gray-50")}
              light={tailwind("text-gray-900")}
              style={tailwind("ml-2 text-lg font-medium mr-2")}
            >
              {translate(
                "screens/AddressControlScreen",
                "Switch to another address"
              )}
            </ThemedText>
            <DiscoverWalletAddress size={18} />
          </View>
          <TouchableOpacity onPress={onClose}>
            <ThemedIcon
              size={24}
              name="close"
              iconType="MaterialIcons"
              dark={tailwind("text-white text-opacity-70")}
              light={tailwind("text-gray-600")}
            />
          </TouchableOpacity>
        </View>
      </ThemedView>
      <ThemedScrollView
        dark={tailwind("text-gray-50")}
        light={tailwind("text-gray-900")}
        contentContainerStyle={tailwind("pb-8")}
      >
        <AddressControlCard onClose={onClose} />
      </ThemedScrollView>
    </View>
  );
}

export function AddressControlCard({
  onClose,
}: {
  onClose: () => void;
}): JSX.Element {
  const { address, addressLength, setIndex, wallet } = useWalletContext();
  const [availableAddresses, setAvailableAddresses] = useState<string[]>([]);
  const [canCreateAddress, setCanCreateAddress] = useState<boolean>(false);
  const blockCount = useSelector((state: RootState) => state.block.count);
  const dispatch = useAppDispatch();
  const logger = useLogger();

  const fetchAddresses = async (): Promise<void> => {
    const addresses: string[] = [];
    for (let i = 0; i <= addressLength; i++) {
      const account = wallet.get(i);
      const address = await account.getAddress();
      addresses.push(address);
    }
    setAvailableAddresses(addresses);
    await isNextAddressUsable();
  };

  const isNextAddressUsable = async (): Promise<void> => {
    // incremented 1 to check if next account in the wallet is usable.
    const next = addressLength + 1;
    const isUsable = await wallet.isUsable(next);
    setCanCreateAddress(isUsable && MAX_ALLOWED_ADDRESSES > next);
  };

  const onRowPress = async (index: number): Promise<void> => {
    dispatch(walletReducer.actions.setHasFetchedToken(false));
    dispatch(loans.actions.setHasFetchedVaultsData(false));
    await setIndex(index);
    onClose();
  };

  useEffect(() => {
    fetchAddresses().catch(logger.error);
  }, [wallet, addressLength]);

  useEffect(() => {
    isNextAddressUsable().catch(logger.error);
  }, [blockCount]);

  if (address.length === 0) {
    return (
      <SkeletonLoader
        row={addressLength}
        screen={SkeletonLoaderScreen.Address}
      />
    );
  }

  return (
    <>
      {availableAddresses.map((availableAddress: string, index: number) => (
        <AddressItemRow
          key={availableAddress}
          address={availableAddress}
          isActive={address === availableAddress}
          index={index}
          onPress={async () => {
            await onRowPress(index);
          }}
        />
      ))}
      {canCreateAddress && (
        <ThemedTouchableOpacity
          light={tailwind("bg-white border-gray-100")}
          dark={tailwind("bg-gray-900 border-gray-700")}
          style={tailwind("py-4 pl-4 pr-2 border-b ")}
          onPress={async () => {
            await onRowPress(addressLength + 1);
          }}
          testID="create_new_address"
        >
          <View style={tailwind("flex-row items-center flex-grow")}>
            <ThemedIcon
              size={20}
              name="add"
              dark={tailwind("text-darkprimary-500")}
              light={tailwind("text-primary-500")}
              style={tailwind("font-normal")}
              iconType="MaterialIcons"
            />

            <View style={tailwind("mx-3 flex-auto")}>
              <ThemedText
                dark={tailwind("text-darkprimary-500")}
                light={tailwind("text-primary-500")}
                style={tailwind("text-sm font-normal")}
              >
                {translate(
                  "screens/AddressControlScreen",
                  "CREATE WALLET ADDRESS"
                )}
              </ThemedText>
            </View>
          </View>
        </ThemedTouchableOpacity>
      )}
    </>
  );
}

export function AddressItemRow({
  address,
  isActive,
  index,
  onPress,
}: {
  address: string;
  isActive: boolean;
  index: number;
  onPress: () => void;
}): JSX.Element {
  const hasPendingJob = useSelector((state: RootState) =>
    hasTxQueued(state.transactionQueue)
  );
  const hasPendingBroadcastJob = useSelector((state: RootState) =>
    hasOceanTXQueued(state.ocean)
  );

  return (
    <ThemedTouchableOpacity
      onPress={onPress}
      light={tailwind("bg-white border-gray-100")}
      dark={tailwind("bg-gray-900 border-gray-700")}
      style={tailwind("py-4 pl-4 pr-2 border-b")}
      testID={`address_row_${index}`}
      disabled={hasPendingJob || hasPendingBroadcastJob}
    >
      <View style={tailwind("flex-row items-center flex-grow")}>
        <RandomAvatar name={address} size={20} />
        <View style={tailwind("ml-3 flex-auto")}>
          <ThemedText
            light={tailwind("text-gray-900")}
            dark={tailwind("text-gray-100")}
            style={tailwind("text-sm w-full font-normal")}
            numberOfLines={1}
            testID={`address_row_text_${index}`}
            ellipsizeMode="middle"
          >
            {address}
          </ThemedText>
        </View>
        {isActive && (
          <ThemedView
            light={tailwind("bg-blue-100")}
            dark={tailwind("bg-darkblue-100")}
            style={tailwind("ml-1")}
            testID={`address_active_indicator_${address}`}
          >
            <ThemedText
              light={tailwind("text-blue-500")}
              dark={tailwind("text-darkblue-500")}
              style={tailwind("text-xs px-1 font-medium")}
            >
              {translate("screens/AddressControlScreen", "ACTIVE")}
            </ThemedText>
          </ThemedView>
        )}
        <View style={tailwind("ml-3 flex-row items-center")}>
          <ThemedIcon
            light={tailwind("text-gray-900")}
            dark={tailwind("text-gray-100")}
            iconType="MaterialIcons"
            name="chevron-right"
            size={24}
          />
        </View>
      </View>
    </ThemedTouchableOpacity>
  );
}

export function DiscoverWalletAddress({
  size = 24,
}: {
  size?: number;
}): JSX.Element {
  const { discoverWalletAddresses } = useWalletContext();
  return (
    <TouchableOpacity
      onPress={discoverWalletAddresses}
      testID="discover_wallet_addresses"
    >
      <ThemedIcon
        dark={tailwind("text-darkprimary-500")}
        iconType="MaterialIcons"
        light={tailwind("text-primary-500")}
        name="sync"
        size={size}
      />
    </TouchableOpacity>
  );
}