cyb/src/pages/Keys/ActionBar/actionBarConnect.tsx

/* eslint-disable @typescript-eslint/no-explicit-any */

import { Pane } from '@cybercongress/gravity';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { ActionBar, ConnectAddress, Dots, Input, TransactionError } from 'src/components';
import { PATTERN_CYBER } from 'src/constants/patterns';
import { useSigningClient } from 'src/contexts/signerClient';
import { addAddressPocket } from 'src/redux/features/pocket';
import { AccountValue } from 'src/types/defaultAccount';
import { LEDGER } from 'src/utils/config';
import { toHex } from 'src/utils/encoding';
import { encryptMnemonic } from 'src/utils/mnemonicCrypto';
import { getOfflineSigner } from 'src/utils/offlineSigner';
import { setEncryptedMnemonic } from 'src/utils/utils';
import { useAdviser } from 'src/features/adviser/context';
import { AdviserColors } from 'src/features/adviser/Adviser/Adviser';
import { KEY_TYPE } from '../types';
import ActionBarSecrets from './actionBarSecrets';
import ConnectWalletModal from './ConnectWalletModal/ConnectWalletModal';
import { ConnectMethod } from './types';

const { STAGE_INIT, HDPATH, STAGE_ERROR } = LEDGER;

const STAGE_ADD_ADDRESS_USER = 2.1;
const STAGE_ADD_ADDRESS_OK = 2.2;
const STAGE_OPEN_MODAL = 2.5;
const STAGE_SET_PASSWORD = 2.6;
const STAGE_LEDGER_WAITING = 2.7;
const STAGE_ADD_SECRETS = 100;

const PASSWORD_HINT =
  'Password protects your seed phrase. ' +
  'Use 8+ chars with mixed case, digits & symbols (e.g. "Cyb3r!net"), ' +
  'or 12+ chars of any kind. Weak example: "password" โ€” don\'t do that';

function ActionBarConnect({ addAddress, updateAddress, updateFuncActionBar, onClickBack }) {
  const { activateWalletSigner, connectLedger } = useSigningClient();
  const { setAdviser } = useAdviser();
  const [stage, setStage] = useState(STAGE_INIT);
  const [valueInputAddres, setValueInputAddres] = useState('');
  const [connectMethod, setConnectMethod] = useState<ConnectMethod | ''>('');
  const selectNetwork = 'cyber';
  const [validAddressAddedUser, setValidAddressAddedUser] = useState(true);

  // Mnemonic flow state โ€” useRef to avoid React DevTools exposure
  const pendingNameRef = useRef('');
  const pendingMnemonicRef = useRef('');
  const [password, setPassword] = useState('');
  const [passwordConfirm, setPasswordConfirm] = useState('');
  const [passwordError, setPasswordError] = useState('');
  const [saving, setSaving] = useState(false);

  const dispatch = useDispatch();

  const clearState = () => {
    setStage(STAGE_INIT);
    setValueInputAddres('');
    setConnectMethod('');
    setValidAddressAddedUser(true);
    pendingNameRef.current = '';
    pendingMnemonicRef.current = '';
    setPassword('');
    setPasswordConfirm('');
    setPasswordError('');
    setSaving(false);
  };

  // Cleanup on unmount โ€” refs are cleared synchronously, setState is no-op in React 18
  useEffect(() => {
    return () => {
      pendingNameRef.current = '';
      pendingMnemonicRef.current = '';
    };
  }, []);

  useEffect(() => {
    if (addAddress === false && stage === STAGE_ADD_ADDRESS_OK) {
      clearState();
    }
  }, [stage, addAddress, clearState]);

  // Show password requirements in adviser when entering password stage
  useEffect(() => {
    if (stage === STAGE_SET_PASSWORD) {
      setAdviser(PASSWORD_HINT, AdviserColors.yellow);
    }
  }, [stage, setAdviser]);


  useEffect(() => {
    if (valueInputAddres.match(PATTERN_CYBER)) {
      setValidAddressAddedUser(false);
    } else {
      setValidAddressAddedUser(true);
    }
  }, [valueInputAddres]);

  const connectAddress = () => {
    switch (connectMethod) {
      case KEY_TYPE.secrets:
        onClickToggleSecrets();
        break;
      case KEY_TYPE.wallet:
        setStage(STAGE_OPEN_MODAL);
        break;
      case KEY_TYPE.ledger:
        onClickConnectLedger();
        break;
      default:
        onClickAddAddressUser();
        break;
    }
  };

  const onClickAddAddressUser = () => {
    setStage(STAGE_ADD_ADDRESS_USER);
  };

  const onClickToggleSecrets = () => {
    setStage(STAGE_ADD_SECRETS);
  };

  const onClickConnectLedger = async () => {
    setStage(STAGE_LEDGER_WAITING);
    try {
      const { address, pubkey } = await connectLedger();
      const pk = toHex(pubkey);

      const accounts: AccountValue = {
        pk,
        keys: 'ledger',
        path: HDPATH,
        bech32: address,
      };

      setStage(STAGE_ADD_ADDRESS_OK);
      setTimeout(() => {
        dispatch(addAddressPocket(accounts));
      }, 100);

      clearState();
      if (updateAddress) {
        updateAddress();
      }
      if (updateFuncActionBar) {
        updateFuncActionBar();
      }
    } catch (err: any) {
      setAdviser(
        `Could not connect to Ledger: ${err?.message || 'unknown error'}. Open the Cosmos app on your device and try again`,
        AdviserColors.red
      );
      setStage(STAGE_INIT);
    }
  };

  const onClickAddAddressUserToLocalStr = async () => {
    const accounts = { bech32: valueInputAddres, keys: 'read-only' };

    setTimeout(() => {
      dispatch(addAddressPocket(accounts));
    }, 100);

    setStage(STAGE_ADD_ADDRESS_OK);

    clearState();
    if (updateAddress) {
      updateAddress();
    }
    if (updateFuncActionBar) {
      updateFuncActionBar();
    }
  };

  // Step 1: mnemonic entered โ†’ ask for password
  const onMnemonicSubmit = (name: string, mnemonic: string) => {
    pendingNameRef.current = name;
    pendingMnemonicRef.current = mnemonic;
    setStage(STAGE_SET_PASSWORD);
  };

  // Step 2: password confirmed โ†’ encrypt & save
  const onPasswordSubmit = async () => {
    if (saving) return;
    setPasswordError('');

    if (password.length < 8) {
      setPasswordError('Password must be at least 8 characters');
      return;
    }

    // Require at least 3 of 4 character classes for passwords under 12 chars
    if (password.length < 12) {
      const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^a-zA-Z0-9]/];
      const matched = classes.filter((re) => re.test(password)).length;
      if (matched < 3) {
        setPasswordError(
          'Use uppercase, lowercase, digits and special characters (at least 3 of 4), or use 12+ characters'
        );
        return;
      }
    }

    if (password !== passwordConfirm) {
      setPasswordError('Passwords do not match');
      return;
    }

    setSaving(true);
    try {
      const mnemonic = pendingMnemonicRef.current;
      const offlineSigner = await getOfflineSigner(mnemonic);

      if (offlineSigner) {
        const [{ address, pubkey: pubKey }] = await offlineSigner.getAccounts();
        const pk = toHex(pubKey);

        // Persist encrypted mnemonic before setting signer โ€”
        // if localStorage write fails, don't activate a non-persisted wallet
        const encrypted = await encryptMnemonic(mnemonic, password);
        setEncryptedMnemonic(encrypted, address);
        activateWalletSigner(offlineSigner, mnemonic);

        const accounts: AccountValue = {
          pk,
          keys: 'wallet',
          path: HDPATH,
          name: pendingNameRef.current,
          bech32: address,
        };

        setStage(STAGE_ADD_ADDRESS_OK);
        setTimeout(() => {
          dispatch(addAddressPocket(accounts));
        }, 100);

        clearState();
        if (updateAddress) {
          updateAddress();
        }
        if (updateFuncActionBar) {
          updateFuncActionBar();
        }
      }
    } catch (err: any) {
      pendingMnemonicRef.current = '';
      setPassword('');
      setPasswordConfirm('');

      const isStorageError = err?.message?.includes('storage');
      setPasswordError(
        isStorageError
          ? 'Could not save wallet. Check browser storage settings'
          : 'Failed to import wallet. Check your seed phrase and try again'
      );
    } finally {
      setSaving(false);
    }
  };

  const selectMethodFunc = (method: ConnectMethod) => {
    if (method !== connectMethod) {
      setConnectMethod(method);
    } else {
      setConnectMethod('');
    }
  };

  if (stage === STAGE_OPEN_MODAL) {
    return (
      <ConnectWalletModal
        onAdd={onMnemonicSubmit}
        onCancel={() => clearState()}
      />
    );
  }

  if (stage === STAGE_SET_PASSWORD) {
    return (
      <ActionBar
        button={{
          disabled: !password || !passwordConfirm || saving,
          onClick: onPasswordSubmit,
          text: 'Encrypt & Save',
        }}
        onClickBack={() => setStage(STAGE_OPEN_MODAL)}
      >
        <Pane display="flex" alignItems="center" justifyContent="center" flex={1} gap="10px">
          <Input
            width="200px"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            placeholder="password"
            type="password"
            autoComplete="new-password"
            autoCorrect="off"
            autoCapitalize="off"
            spellCheck={false}
            autoFocus
          />
          <Input
            width="200px"
            value={passwordConfirm}
            onChange={(e) => setPasswordConfirm(e.target.value)}
            placeholder="confirm password"
            type="password"
            autoComplete="new-password"
            autoCorrect="off"
            autoCapitalize="off"
            spellCheck={false}
          />
          {passwordError && (
            <span style={{ color: '#ff4d4d', fontSize: '14px' }}>{passwordError}</span>
          )}
        </Pane>
      </ActionBar>
    );
  }

  if (stage === STAGE_INIT) {
    return (
      <ConnectAddress
        selectMethodFunc={selectMethodFunc}
        selectMethod={connectMethod}
        selectNetwork={selectNetwork}
        connectAddress={connectAddress}
        onClickBack={onClickBack}
      />
    );
  }

  if (stage === STAGE_ADD_ADDRESS_USER) {
    return (
      <ActionBar
        button={{
          disabled: validAddressAddedUser,
          onClick: onClickAddAddressUserToLocalStr,
          text: 'Add address',
        }}
        onClickBack={() => setStage(STAGE_INIT)}
      >
        <Pane flex={1} justifyContent="center" alignItems="center" fontSize="18px" display="flex">
          put {selectNetwork} address:
          <Input
            width="250px"
            value={valueInputAddres}
            onChange={(e) => setValueInputAddres(e.target.value)}
            placeholder="address"
            autoFocus
          />
        </Pane>
      </ActionBar>
    );
  }

  if (stage === STAGE_ADD_SECRETS) {
    return <ActionBarSecrets onClickBack={() => setStage(STAGE_INIT)} />;
  }

  if (stage === STAGE_LEDGER_WAITING) {
    return (
      <ActionBar onClickBack={() => setStage(STAGE_INIT)}>
        <Pane display="flex" alignItems="center">
          <Pane fontSize={20}>Connect your Ledger and open the Cosmos app</Pane>
          <Dots big />
        </Pane>
      </ActionBar>
    );
  }

  if (stage === STAGE_ADD_ADDRESS_OK) {
    return (
      <ActionBar>
        <Pane display="flex" alignItems="center">
          <Pane fontSize={20}>adding address</Pane>
          <Dots big />
        </Pane>
      </ActionBar>
    );
  }

  if (stage === STAGE_ERROR) {
    return (
      <TransactionError
        onClickBtn={() => clearState()}
        errorMessage="you have this address in your pocket"
      />
    );
  }

  return null;
}

export default ActionBarConnect;

Synonyms

pussy-ts/src/pages/Keys/ActionBar/actionBarConnect.tsx

Neighbours