DeFiCh/wallet

View on GitHub
mobile-app/app/screens/AppNavigator/screens/Settings/SettingsScreen.tsx

Summary

Maintainability
B
5 hrs
Test Coverage
import {
  ThemedIcon,
  ThemedScrollViewV2,
  ThemedSectionTitleV2,
  ThemedTextV2,
  ThemedTouchableOpacityV2,
  ThemedViewV2,
} from "@components/themed";
import { Switch, View } from "@components";
import { WalletAlert } from "@components/WalletAlert";
import { usePrivacyLockContext } from "@contexts/LocalAuthContext";
import {
  useWalletPersistenceContext,
  useServiceProviderContext,
  useWalletNodeContext,
} from "@waveshq/walletkit-ui";
import { StackScreenProps } from "@react-navigation/stack";
import { authentication, Authentication } from "@store/authentication";
import { ocean } from "@waveshq/walletkit-ui/dist/store";
import { tailwind } from "@tailwind";
import { getAppLanguages, translate } from "@translations";
import { useCallback } from "react";
import { Text } from "react-native";
import { MnemonicStorage } from "@api/wallet/mnemonic_storage";
import { useLogger } from "@shared-contexts/NativeLoggingProvider";
import { useAddressBook } from "@hooks/useAddressBook";
import { useAppDispatch } from "@hooks/useAppDispatch";
import { useFeatureFlagContext } from "@contexts/FeatureFlagContext";
import { useLanguageContext } from "@shared-contexts/LanguageProvider";
import { useCustomServiceProviderContext } from "@contexts/CustomServiceProvider";
import { RowThemeItem } from "./components/RowThemeItem";
import { SettingsParamList } from "./SettingsNavigator";

type Props = StackScreenProps<SettingsParamList, "SettingsScreen">;

export function SettingsScreen({ navigation }: Props): JSX.Element {
  const logger = useLogger();
  const dispatch = useAppDispatch();
  const walletContext = useWalletPersistenceContext();
  const localAuth = usePrivacyLockContext();
  const {
    data: { type },
  } = useWalletNodeContext();
  const isEncrypted = type === "MNEMONIC_ENCRYPTED";
  const { isCustomUrl: isCustomDvmUrl } = useServiceProviderContext();
  const { isCustomEvmUrl, isCustomEthRpcUrl } =
    useCustomServiceProviderContext();
  const { isFeatureAvailable } = useFeatureFlagContext();
  const { language } = useLanguageContext();
  const languages = getAppLanguages();

  const selectedLanguage = languages.find((languageItem) =>
    language?.startsWith(languageItem.locale),
  );

  const revealRecoveryWords = useCallback(() => {
    if (!isEncrypted) {
      return;
    }

    const auth: Authentication<string[]> = {
      consume: async (passphrase) => await MnemonicStorage.get(passphrase),
      onAuthenticated: async (words) => {
        navigation.navigate({
          name: "RecoveryWordsScreen",
          params: { words },
          merge: true,
        });
      },
      onError: (e) => logger.error(e),
      message: translate("screens/UnlockWallet", "Enter passcode to continue"),
      loading: translate(
        "screens/UnlockWallet",
        "It may take a few seconds to verify",
      ),
      title: translate(
        "screens/UnlockWallet",
        "Provide your passcode to view recovery words.",
      ),
      successMessage: translate("screens/UnlockWallet", "Passcode verified!"),
    };
    dispatch(authentication.actions.prompt(auth));
  }, [dispatch, isEncrypted, navigation]);

  const changePasscode = useCallback(() => {
    if (!isEncrypted) {
      return;
    }

    const auth: Authentication<string[]> = {
      consume: async (passphrase) => await MnemonicStorage.get(passphrase),
      onAuthenticated: async (words) => {
        navigation.navigate({
          name: "ChangePinScreen",
          params: {
            words,
            pinLength: 6,
          },
          merge: true,
        });
      },
      onError: (e) => {
        dispatch(ocean.actions.setError(e));
      },
      message: translate("screens/UnlockWallet", "Enter passcode to continue"),
      loading: translate(
        "screens/UnlockWallet",
        "It may take a few seconds to verify",
      ),
      title: translate(
        "screens/UnlockWallet",
        "Provide existing passcode to change passcode.",
      ),
      successMessage: translate("screens/UnlockWallet", "Passcode verified!"),
    };

    dispatch(authentication.actions.prompt(auth));
  }, [walletContext.wallets[0], dispatch, navigation]);

  return (
    <ThemedScrollViewV2
      contentContainerStyle={tailwind("px-5 pb-16")}
      style={tailwind("flex-1")}
      testID="setting_screen"
    >
      <ThemedSectionTitleV2
        testID="network_title"
        text={translate("screens/Settings", "GENERAL")}
      />

      <ThemedViewV2
        dark={tailwind("bg-mono-dark-v2-00")}
        light={tailwind("bg-mono-light-v2-00")}
        style={tailwind("rounded-lg-v2 pl-5 pr-4")}
      >
        <NavigateItemRow
          testID="setting_navigate_About"
          label="About"
          border
          onPress={() => navigation.navigate("AboutScreen")}
        />

        {isFeatureAvailable("service_provider") && (
          <NavigateItemRow
            testID="setting_navigate_service_provider"
            label="Provider"
            border
            value={
              isCustomDvmUrl || isCustomEvmUrl || isCustomEthRpcUrl
                ? "Custom (3rd-party)"
                : "Default"
            }
            onPress={() => navigation.navigate("ServiceProviderScreen", {})}
          />
        )}
        <NavigateItemRow
          testID="address_book_title"
          label="Address book"
          onPress={() => navigation.navigate("AddressBookScreen", {})}
        />
      </ThemedViewV2>

      {(isEncrypted || localAuth.isDeviceProtected) && (
        <>
          <ThemedSectionTitleV2
            testID="security_title"
            text={translate("screens/Settings", "SECURITY")}
          />
          <ThemedViewV2
            dark={tailwind("bg-mono-dark-v2-00")}
            light={tailwind("bg-mono-light-v2-00")}
            style={tailwind("rounded-lg-v2 pl-5 pr-4")}
          >
            {isEncrypted && (
              <>
                <NavigateItemRow
                  label="Recovery words"
                  onPress={revealRecoveryWords}
                  border
                  testID="view_recovery_words"
                />
                <NavigateItemRow
                  testID="setting_analytics"
                  label="Analytics"
                  border
                  onPress={() => navigation.navigate("AnalyticsScreen")}
                />
                <NavigateItemRow
                  label="Change passcode"
                  onPress={changePasscode}
                  border={localAuth.isDeviceProtected}
                  testID="view_change_passcode"
                />
              </>
            )}
            {localAuth.isDeviceProtected && (
              <PrivacyLockToggle
                value={localAuth.isEnabled}
                onToggle={async () => {
                  await localAuth.togglePrivacyLock();
                }}
                authenticationName={localAuth.getAuthenticationNaming()}
              />
            )}
          </ThemedViewV2>
          {localAuth.isDeviceProtected && (
            <ThemedTextV2
              dark={tailwind("text-mono-dark-v2-500")}
              light={tailwind("text-mono-light-v2-500")}
              style={tailwind("px-5 pt-2 text-xs font-normal-v2")}
            >
              {translate(
                "screens/Settings",
                "Auto-locks wallet if there is no activity for 1 min.",
              )}
            </ThemedTextV2>
          )}
        </>
      )}

      <ThemedSectionTitleV2
        testID="addtional_options_title"
        text={translate("screens/Settings", "DISPLAY & LANGUAGE")}
      />
      <ThemedViewV2
        dark={tailwind("bg-mono-dark-v2-00")}
        light={tailwind("bg-mono-light-v2-00")}
        style={tailwind("rounded-lg-v2 mb-6 pl-5 pr-4")}
      >
        <RowThemeItem border />
        <NavigateItemRow
          testID="setting_navigate_language_selection"
          label="Language"
          value={selectedLanguage?.displayName}
          onPress={() => navigation.navigate("LanguageSelectionScreen")}
        />
      </ThemedViewV2>
      <RowExitWalletItem />
      <ThemedTextV2
        dark={tailwind("text-mono-dark-v2-500")}
        light={tailwind("text-mono-light-v2-500")}
        style={tailwind("px-5 pt-2 text-xs font-normal-v2 text-center")}
      >
        {translate(
          "screens/Settings",
          "This will unlink your wallet from the app.",
        )}
      </ThemedTextV2>
    </ThemedScrollViewV2>
  );
}

function RowExitWalletItem(): JSX.Element {
  const { clearWallets } = useWalletPersistenceContext();
  const { clearAddressBook } = useAddressBook();

  async function onExitWallet(): Promise<void> {
    WalletAlert({
      title: translate(
        "screens/Settings",
        "Are you sure you want to unlink your wallet?",
      ),
      message: translate(
        "screens/Settings",
        "Once unlinked, you will need to enter your recovery words to restore your wallet.",
      ),
      buttons: [
        {
          text: translate("screens/Settings", "Cancel"),
          style: "cancel",
        },
        {
          text: translate("screens/Settings", "Unlink wallet"),
          onPress: async () => {
            clearAddressBook();
            await clearWallets();
          },
          style: "destructive",
        },
      ],
    });
  }

  return (
    <ThemedTouchableOpacityV2
      light={tailwind("bg-mono-light-v2-00")}
      dark={tailwind("bg-mono-dark-v2-00")}
      style={tailwind("border-0 p-4.5 flex-row justify-center rounded-lg-v2")}
      onPress={onExitWallet}
      testID="setting_exit_wallet"
    >
      <Text style={tailwind("font-normal-v2 text-sm text-red-v2")}>
        {translate("screens/Settings", "Unlink wallet")}
      </Text>
    </ThemedTouchableOpacityV2>
  );
}

function PrivacyLockToggle({
  value,
  onToggle,
  authenticationName,
}: {
  // eslint-disable-next-line react/no-unused-prop-types
  disabled?: boolean;
  value: boolean;
  onToggle: (newValue: boolean) => void;
  authenticationName?: string;
}): JSX.Element {
  return (
    <View style={tailwind("flex py-4.5 flex-row items-center justify-between")}>
      <ThemedTextV2
        light={tailwind("text-mono-light-v2-900")}
        dark={tailwind("text-mono-dark-v2-900")}
        style={tailwind("font-normal-v2 text-sm flex-1")}
        testID="text_privacy_lock"
      >
        {authenticationName !== undefined &&
          translate("screens/Settings", "Secure with {{option}}", {
            option: translate("screens/Settings", authenticationName),
          })}
      </ThemedTextV2>
      <Switch
        onValueChange={onToggle}
        value={value}
        testID="switch_privacy_lock"
      />
    </View>
  );
}

interface INavigateItemRow {
  testID: string;
  label: string;
  value?: string;
  onPress: () => void;
  border?: boolean;
}

function NavigateItemRow({
  testID,
  label,
  value,
  onPress,
  border,
}: INavigateItemRow): JSX.Element {
  return (
    <ThemedViewV2
      style={tailwind({ "border-b-0.5": border })}
      light={tailwind("border-mono-light-v2-300")}
      dark={tailwind("border-mono-dark-v2-300")}
    >
      <ThemedTouchableOpacityV2
        onPress={onPress}
        style={tailwind(
          "flex py-4.5 flex-row items-center justify-between border-0",
        )}
        testID={testID}
      >
        <ThemedTextV2
          light={tailwind("text-mono-light-v2-900")}
          dark={tailwind("text-mono-dark-v2-900")}
          style={tailwind("font-normal-v2 text-sm")}
        >
          {translate("screens/Settings", label)}
        </ThemedTextV2>

        <View style={tailwind("flex flex-row items-center")}>
          {value !== undefined && (
            <ThemedTextV2
              light={tailwind("text-mono-light-v2-700")}
              dark={tailwind("text-mono-dark-v2-700")}
              style={tailwind("font-normal-v2 text-sm mr-1")}
              testID={`${testID}_value`}
            >
              {translate("screens/Settings", value)}
            </ThemedTextV2>
          )}
          <ThemedIcon
            dark={tailwind("text-mono-dark-v2-900")}
            light={tailwind("text-mono-light-v2-900")}
            iconType="Feather"
            name="chevron-right"
            size={24}
          />
        </View>
      </ThemedTouchableOpacityV2>
    </ThemedViewV2>
  );
}