You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			252 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			252 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			TypeScript
		
	
import React, { MouseEvent, useEffect, useState } from 'react';
 | 
						|
 | 
						|
import { ToastUtils } from '../../session/utils';
 | 
						|
import { matchesHash } from '../../util/passwordUtils';
 | 
						|
import { Data } from '../../data/data';
 | 
						|
import { QRCode } from 'react-qr-svg';
 | 
						|
import { mn_decode } from '../../session/crypto/mnemonic';
 | 
						|
import { SpacerSM } from '../basic/Text';
 | 
						|
import { recoveryPhraseModal } from '../../state/ducks/modalDialog';
 | 
						|
import { useDispatch } from 'react-redux';
 | 
						|
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
 | 
						|
import { SessionWrapperModal } from '../SessionWrapperModal';
 | 
						|
import { getCurrentRecoveryPhrase } from '../../util/storage';
 | 
						|
import styled from 'styled-components';
 | 
						|
import { saveQRCode } from '../../util/saveQRCode';
 | 
						|
 | 
						|
interface PasswordProps {
 | 
						|
  setPasswordValid: (val: boolean) => any;
 | 
						|
  passwordHash: string;
 | 
						|
}
 | 
						|
 | 
						|
const Password = (props: PasswordProps) => {
 | 
						|
  const { setPasswordValid, passwordHash } = props;
 | 
						|
  const i18n = window.i18n;
 | 
						|
  const dispatch = useDispatch();
 | 
						|
 | 
						|
  const onClose = () => dispatch(recoveryPhraseModal(null));
 | 
						|
 | 
						|
  const confirmPassword = () => {
 | 
						|
    const passwordValue = (document.getElementById('seed-input-password') as any)?.value;
 | 
						|
    const isPasswordValid = matchesHash(passwordValue as string, passwordHash);
 | 
						|
 | 
						|
    if (!passwordValue) {
 | 
						|
      ToastUtils.pushToastError('enterPasswordErrorToast', i18n('noGivenPassword'));
 | 
						|
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    if (passwordHash && !isPasswordValid) {
 | 
						|
      ToastUtils.pushToastError('enterPasswordErrorToast', i18n('invalidPassword'));
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    setPasswordValid(true);
 | 
						|
 | 
						|
    window.removeEventListener('keyup', onEnter);
 | 
						|
    return true;
 | 
						|
  };
 | 
						|
 | 
						|
  const onEnter = (event: any) => {
 | 
						|
    if (event.key === 'Enter') {
 | 
						|
      confirmPassword();
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  return (
 | 
						|
    <>
 | 
						|
      <div className="session-modal__input-group">
 | 
						|
        <input
 | 
						|
          type="password"
 | 
						|
          id="seed-input-password"
 | 
						|
          placeholder={i18n('enterPassword')}
 | 
						|
          onKeyUp={onEnter}
 | 
						|
        />
 | 
						|
      </div>
 | 
						|
 | 
						|
      <SpacerSM />
 | 
						|
 | 
						|
      <div
 | 
						|
        className="session-modal__button-group"
 | 
						|
        style={{ justifyContent: 'center', width: '100%' }}
 | 
						|
      >
 | 
						|
        <SessionButton
 | 
						|
          text={i18n('done')}
 | 
						|
          buttonType={SessionButtonType.Simple}
 | 
						|
          onClick={confirmPassword}
 | 
						|
        />
 | 
						|
        <SessionButton
 | 
						|
          text={i18n('cancel')}
 | 
						|
          buttonType={SessionButtonType.Simple}
 | 
						|
          buttonColor={SessionButtonColor.Danger}
 | 
						|
          onClick={onClose}
 | 
						|
        />
 | 
						|
      </div>
 | 
						|
    </>
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
interface SeedProps {
 | 
						|
  recoveryPhrase: string;
 | 
						|
  onClickCopy?: () => any;
 | 
						|
}
 | 
						|
 | 
						|
const StyledRecoveryPhrase = styled.i``;
 | 
						|
 | 
						|
const StyledQRImage = styled.div`
 | 
						|
  width: fit-content;
 | 
						|
  margin: 0 auto var(--margins-lg);
 | 
						|
  cursor: pointer;
 | 
						|
`;
 | 
						|
 | 
						|
const handleSaveQRCode = (event: MouseEvent) => {
 | 
						|
  event.preventDefault();
 | 
						|
  saveQRCode(
 | 
						|
    'session-recovery-phrase',
 | 
						|
    '220px',
 | 
						|
    '220px',
 | 
						|
    'var(--white-color)',
 | 
						|
    'var(--black-color)'
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
const Seed = (props: SeedProps) => {
 | 
						|
  const { recoveryPhrase, onClickCopy } = props;
 | 
						|
  const i18n = window.i18n;
 | 
						|
  const bgColor = 'var(--white-color)';
 | 
						|
  const fgColor = 'var(--black-color)';
 | 
						|
  const dispatch = useDispatch();
 | 
						|
 | 
						|
  const hexEncodedSeed = mn_decode(recoveryPhrase, 'english');
 | 
						|
 | 
						|
  const copyRecoveryPhrase = (recoveryPhraseToCopy: string) => {
 | 
						|
    window.clipboard.writeText(recoveryPhraseToCopy);
 | 
						|
    ToastUtils.pushCopiedToClipBoard();
 | 
						|
    if (onClickCopy) {
 | 
						|
      onClickCopy();
 | 
						|
    }
 | 
						|
    dispatch(recoveryPhraseModal(null));
 | 
						|
  };
 | 
						|
 | 
						|
  return (
 | 
						|
    <>
 | 
						|
      <div className="session-modal__centered text-center">
 | 
						|
        <p
 | 
						|
          className="session-modal__description"
 | 
						|
          style={{
 | 
						|
            lineHeight: '1.3333',
 | 
						|
            marginTop: '0px',
 | 
						|
            marginBottom: 'var(--margins-md)',
 | 
						|
            maxWidth: '600px',
 | 
						|
          }}
 | 
						|
        >
 | 
						|
          {i18n('recoveryPhraseSavePromptMain')}
 | 
						|
        </p>
 | 
						|
 | 
						|
        <StyledQRImage
 | 
						|
          aria-label={window.i18n('clickToTrustContact')}
 | 
						|
          title={window.i18n('clickToTrustContact')}
 | 
						|
          className="qr-image"
 | 
						|
          onClick={handleSaveQRCode}
 | 
						|
        >
 | 
						|
          <QRCode value={hexEncodedSeed} bgColor={bgColor} fgColor={fgColor} level="L" />
 | 
						|
        </StyledQRImage>
 | 
						|
 | 
						|
        <StyledRecoveryPhrase
 | 
						|
          data-testid="recovery-phrase-seed-modal"
 | 
						|
          className="session-modal__text-highlight"
 | 
						|
        >
 | 
						|
          {recoveryPhrase}
 | 
						|
        </StyledRecoveryPhrase>
 | 
						|
      </div>
 | 
						|
      <div
 | 
						|
        className="session-modal__button-group"
 | 
						|
        style={{ justifyContent: 'center', width: '100%' }}
 | 
						|
      >
 | 
						|
        <SessionButton
 | 
						|
          text={i18n('editMenuCopy')}
 | 
						|
          buttonType={SessionButtonType.Simple}
 | 
						|
          onClick={() => {
 | 
						|
            copyRecoveryPhrase(recoveryPhrase);
 | 
						|
          }}
 | 
						|
        />
 | 
						|
      </div>
 | 
						|
    </>
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
const StyledSeedModalContainer = styled.div`
 | 
						|
  margin: var(--margins-md) var(--margins-sm);
 | 
						|
`;
 | 
						|
 | 
						|
interface ModalInnerProps {
 | 
						|
  onClickOk?: () => any;
 | 
						|
}
 | 
						|
 | 
						|
const SessionSeedModalInner = (props: ModalInnerProps) => {
 | 
						|
  const { onClickOk } = props;
 | 
						|
  const [loadingPassword, setLoadingPassword] = useState(true);
 | 
						|
  const [loadingSeed, setLoadingSeed] = useState(true);
 | 
						|
  const [recoveryPhrase, setRecoveryPhrase] = useState('');
 | 
						|
  const [hasPassword, setHasPassword] = useState<null | boolean>(null);
 | 
						|
  const [passwordValid, setPasswordValid] = useState(false);
 | 
						|
  const [passwordHash, setPasswordHash] = useState('');
 | 
						|
  const dispatch = useDispatch();
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    setTimeout(() => (document.getElementById('seed-input-password') as any)?.focus(), 100);
 | 
						|
    void checkHasPassword();
 | 
						|
    void getRecoveryPhrase();
 | 
						|
  }, []);
 | 
						|
 | 
						|
  const i18n = window.i18n;
 | 
						|
 | 
						|
  const onClose = () => dispatch(recoveryPhraseModal(null));
 | 
						|
 | 
						|
  const checkHasPassword = async () => {
 | 
						|
    if (!loadingPassword) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    const hash = await Data.getPasswordHash();
 | 
						|
    setHasPassword(!!hash);
 | 
						|
    setPasswordHash(hash || '');
 | 
						|
    setLoadingPassword(false);
 | 
						|
  };
 | 
						|
 | 
						|
  const getRecoveryPhrase = async () => {
 | 
						|
    if (recoveryPhrase) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    const newRecoveryPhrase = getCurrentRecoveryPhrase();
 | 
						|
    setRecoveryPhrase(newRecoveryPhrase);
 | 
						|
    setLoadingSeed(false);
 | 
						|
 | 
						|
    return true;
 | 
						|
  };
 | 
						|
 | 
						|
  return (
 | 
						|
    <>
 | 
						|
      {!loadingSeed && (
 | 
						|
        <SessionWrapperModal
 | 
						|
          title={i18n('showRecoveryPhrase')}
 | 
						|
          onClose={onClose}
 | 
						|
          showExitIcon={true}
 | 
						|
        >
 | 
						|
          <StyledSeedModalContainer>
 | 
						|
            <SpacerSM />
 | 
						|
 | 
						|
            {hasPassword && !passwordValid ? (
 | 
						|
              <Password passwordHash={passwordHash} setPasswordValid={setPasswordValid} />
 | 
						|
            ) : (
 | 
						|
              <Seed recoveryPhrase={recoveryPhrase} onClickCopy={onClickOk} />
 | 
						|
            )}
 | 
						|
          </StyledSeedModalContainer>
 | 
						|
        </SessionWrapperModal>
 | 
						|
      )}
 | 
						|
    </>
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
export const SessionSeedModal = SessionSeedModalInner;
 |