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

import { ClipboardEventHandler, useCallback, useEffect, useState } from 'react';
import { Button, Input } from 'src/components';
import Dropdown from 'src/components/Dropdown/Dropdown';
import Modal from 'src/components/modal/Modal';
import MnemonicInput from './MnemonicInput';

import * as styles from './ConnectWalletModal.style';

const columnsIndexes12 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
const columnsIndexes24 = [
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
  22, 23,
];
const columns = { '12': columnsIndexes12, '24': columnsIndexes24 };
const dropdownOptions = [
  { label: '12', value: '12' },
  { label: '24', value: '24' },
];

interface ConnectWalletModalProps {
  onAdd(name: string, mnemonic: string): void | Promise<void>;
  onCancel(): void;
}

export default function ConnectWalletModal({
  onAdd,
  onCancel,
}: ConnectWalletModalProps) {
  const [mnemonicsLength, setMnemonicsLength] =
    useState<keyof typeof columns>('12');
  const [columnsIndexes, setColumnsIndexes] = useState(columnsIndexes12);
  const [values, setValues] = useState<Record<number, string>>(
    columnsIndexes.reduce((acc, index) => ({ ...acc, [index]: '' }), {})
  );
  const [name, setName] = useState('');
  const [isTouched, setIsTouched] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [hidden, setHidden] = useState(false);
  const [showWords, setShowWords] = useState(false);

  // Best-effort cleanup on unmount โ€” setState during unmount is a no-op in React 18,
  // string values remain in fiber tree until GC. Same inherent JS limitation as MetaMask/Keplr.
  useEffect(() => {
    return () => {
      setValues({});
      setName('');
    };
  }, []);

  // Clear mnemonic inputs when app goes to background (prevents iOS app switcher screenshot)
  useEffect(() => {
    const onVisibility = () => {
      if (document.hidden) {
        setHidden(true);
        setValues({});
      } else {
        setHidden(false);
      }
    };
    document.addEventListener('visibilitychange', onVisibility);
    return () => document.removeEventListener('visibilitychange', onVisibility);
  }, []);

  const distributeWords = useCallback((words: string[], startIndex = 0) => {
    const targetLength: keyof typeof columns = words.length + startIndex > 12 ? '24' : '12';
    const targetCount = Number(targetLength);

    if (targetLength !== mnemonicsLength) {
      setMnemonicsLength(targetLength);
      setColumnsIndexes(columns[targetLength]);
    }

    setValues((prev) => {
      const newValues: Record<number, string> = {};
      for (let i = 0; i < targetCount; i++) {
        newValues[i] = i < startIndex ? (prev[i] || '') : '';
      }
      for (let i = 0; i < words.length && startIndex + i < targetCount; i++) {
        newValues[startIndex + i] = words[i].trim();
      }
      return newValues;
    });
  }, [mnemonicsLength]);

  const onMnemonicsPaste = useCallback<ClipboardEventHandler<HTMLDivElement>>(
    (event) => {
      event.preventDefault();

      const paste = event.clipboardData?.getData('text');
      if (!paste) return;

      const words = paste.trim().split(/\s+/);
      distributeWords(words);

      // Clear clipboard to prevent mnemonic leaking to clipboard managers
      navigator.clipboard.writeText('').catch(() => {});
    },
    [distributeWords]
  );

  useEffect(() => {
    setColumnsIndexes(columns[mnemonicsLength]);
  }, [mnemonicsLength]);

  const onSingleChange = useCallback((idx: number, val: string) => {
    setValues((prev) => ({ ...prev, [idx]: val }));
  }, []);

  const onInputBlurFunc = useCallback(() => {
    setIsTouched(true);
  }, []);

  const onAddClickWithValidation = useCallback(async () => {
    setIsTouched(true);
    setError(null);

    const filledCount = Object.values(values).filter(Boolean).length;
    const targetCount = Number(mnemonicsLength);

    if (!name) {
      return;
    }

    if (filledCount < targetCount) {
      return;
    }

    try {
      await onAdd(name, Object.values(values).join(' '));
    } catch {
      setError('Invalid seed phrase. Please check your words and try again.');
    }
  }, [onAdd, name, values, mnemonicsLength]);

  return (
    <Modal isOpen onPaste={onMnemonicsPaste} onClose={onCancel}>
      <div>
        <h3 style={styles.heading}>Enter your name</h3>
        <Input
          value={name}
          onChange={(e) => setName(e.target.value)}
          onBlurFnc={onInputBlurFunc}
          error={isTouched && !name ? 'Name is missing' : undefined}
        />
      </div>
      <div style={styles.wrapper}>
        <h3 style={styles.heading}>Enter or paste your seed phrase</h3>
        <button
          type="button"
          onClick={() => setShowWords((v) => !v)}
          style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#36d6ae', fontSize: '18px', padding: '15px 5px' }}
          title={showWords ? 'Hide seed words' : 'Show seed words'}
        >
          {showWords ? '๐Ÿ™ˆ' : '๐Ÿ‘'}
        </button>
        <div style={styles.dropdown}>
          <Dropdown
            value={mnemonicsLength}
            options={dropdownOptions}
            onChange={(v: string) => setMnemonicsLength(v as keyof typeof columns)}
          />
        </div>
      </div>
      <div style={styles.mnemonics}>
        {columnsIndexes.map((index) => (
          <MnemonicInput
            key={`mnemonic-input-${index}`}
            index={index}
            values={values}
            isTouched={isTouched}
            showWords={showWords}
            onBlurFunc={onInputBlurFunc}
            onWordsDetected={distributeWords}
            onSingleChange={onSingleChange}
          />
        ))}
      </div>
      {error && (
        <div style={{ color: '#ff4d4d', fontSize: '14px', marginTop: '8px', textAlign: 'center' }}>
          {error}
        </div>
      )}
      <div style={styles.buttons}>
        <Button onClick={onCancel}>Cancel</Button>
        <Button onClick={onAddClickWithValidation}>
          Add
        </Button>
      </div>
    </Modal>
  );
}

Neighbours