Skip to content

Commit

Permalink
Fix broken Import sheet paste button (#552)
Browse files Browse the repository at this point in the history
* Fix broken Import sheet paste button

Fixes RAI-477 https://linear.app/issue/RAI-477/import-sheet-paste-button-is-broken

* Fix incorrect privateKey length check
  • Loading branch information
mikedemarais committed Apr 15, 2020
1 parent 24a641d commit 6baa078
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 110 deletions.
131 changes: 64 additions & 67 deletions src/components/settings-menu/BackupSection.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import analytics from '@segment/analytics-react-native';
import PropTypes from 'prop-types';
import React from 'react';
import React, { useCallback, useState } from 'react';
import FastImage from 'react-native-fast-image';
import { compose, withHandlers, withState } from 'recompact';
import styled from 'styled-components';
import SeedPhraseImageSource from '../../assets/seed-phrase-icon.png';
import { loadSeedPhrase as loadSeedPhraseFromKeychain } from '../../model/wallet';
Expand All @@ -25,75 +24,73 @@ const ToggleSeedPhraseButton = styled(Button)`
width: 235;
`;

const BackupSection = ({ navigation, seedPhrase, toggleSeedPhrase }) => (
<Column align="center" css={padding(80, 40, 0)} flex={1}>
<FastImage
source={SeedPhraseImageSource}
style={position.sizeAsObject(70)}
/>
<Text lineHeight="loosest" size="larger" weight="semibold">
Your Private Key
</Text>
<Content flex={0} seedPhrase={seedPhrase}>
{seedPhrase ? (
<CopyTooltip
navigation={navigation}
textToCopy={seedPhrase}
tooltipText="Copy Private Key"
>
<Monospace
const BackupSection = ({ navigation }) => {
const [seedPhrase, setSeedPhrase] = useState(null);

const hideSeedPhrase = () => setSeedPhrase(null);

const handlePressToggleSeedPhrase = useCallback(() => {
if (!seedPhrase) {
loadSeedPhraseFromKeychain()
.then(keychainValue => {
setSeedPhrase(keychainValue);
analytics.track('Viewed backup seed phrase text');
})
.catch(hideSeedPhrase);
} else {
hideSeedPhrase();
}
}, [seedPhrase]);

return (
<Column align="center" css={padding(80, 40, 0)} flex={1}>
<FastImage
source={SeedPhraseImageSource}
style={position.sizeAsObject(70)}
/>
<Text lineHeight="loosest" size="larger" weight="semibold">
Your Private Key
</Text>
<Content flex={0} seedPhrase={seedPhrase}>
{seedPhrase ? (
<CopyTooltip
navigation={navigation}
textToCopy={seedPhrase}
tooltipText="Copy Private Key"
>
<Monospace
align="center"
lineHeight="looser"
size="large"
weight="regular"
>
{seedPhrase}
</Monospace>
</CopyTooltip>
) : (
<Text
align="center"
lineHeight="looser"
size="large"
weight="regular"
color={colors.alpha(colors.blueGreyDark, 0.6)}
lineHeight="loose"
size="lmedium"
>
{seedPhrase}
</Monospace>
</CopyTooltip>
) : (
<Text
align="center"
color={colors.alpha(colors.blueGreyDark, 0.6)}
lineHeight="loose"
size="lmedium"
>
If you lose access to your device, the only way to restore your funds
is with your private key.
<Br />
<Br />
Please store it in a safe place.
</Text>
)}
</Content>
<ToggleSeedPhraseButton onPress={toggleSeedPhrase}>
{seedPhrase ? 'Hide' : 'Show'} Private Key
</ToggleSeedPhraseButton>
</Column>
);
If you lose access to your device, the only way to restore your
funds is with your private key.
<Br />
<Br />
Please store it in a safe place.
</Text>
)}
</Content>
<ToggleSeedPhraseButton onPress={handlePressToggleSeedPhrase}>
{seedPhrase ? 'Hide' : 'Show'} Private Key
</ToggleSeedPhraseButton>
</Column>
);
};

BackupSection.propTypes = {
navigation: PropTypes.object,
seedPhrase: PropTypes.string,
toggleSeedPhrase: PropTypes.func.isRequired,
};

export default compose(
withState('seedPhrase', 'setSeedPhrase', null),
withHandlers({
hideSeedPhrase: ({ setSeedPhrase }) => () => setSeedPhrase(null),
}),
withHandlers({
toggleSeedPhrase: ({ hideSeedPhrase, seedPhrase, setSeedPhrase }) => () => {
if (!seedPhrase) {
loadSeedPhraseFromKeychain()
.then(keychainValue => {
setSeedPhrase(keychainValue);
analytics.track('Viewed backup seed phrase text');
})
.catch(hideSeedPhrase);
} else {
hideSeedPhrase();
}
},
})
)(BackupSection);
export default BackupSection;
11 changes: 10 additions & 1 deletion src/helpers/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,19 @@ const isValidSeedPhrase = seedPhrase => {
return phrases >= 12 && isValidMnemonic(seedPhrase);
};

/**
* @desc validate private key string
* @param {String} private key string
* @return {Boolean}
*/
const isValidPrivateKey = key => {
return key.length >= 64 && isHexStringIgnorePrefix(key);
};

/**
* @desc validate seed phrase mnemonic or private key
* @param {String} seed phrase mnemonic or private key
* @return {Boolean}
*/
export const isValidSeed = seed =>
isHexStringIgnorePrefix(seed) || isValidSeedPhrase(seed);
seed && (isValidPrivateKey(seed) || isValidSeedPhrase(seed));
34 changes: 24 additions & 10 deletions src/hooks/useClipboard.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
import { useCallback, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { Clipboard } from 'react-native';
import useAppState from './useAppState';

const listeners = new Set();

function setString(content) {
Clipboard.setString(content);
listeners.forEach(listener => listener(content));
}

export default function useClipboard() {
const { justBecameActive } = useAppState();
const [data, updateClipboardData] = useState('');

const setString = useCallback(content => {
Clipboard.setString(content);
updateClipboardData(content);
}, []);

async function updateClipboard() {
const content = await Clipboard.getString();
updateClipboardData(content);
function getClipboardData() {
Clipboard.getString().then(updateClipboardData);
}

// Get initial data
useEffect(() => getClipboardData(), []);

// Get data when app just became foregrounded
useEffect(() => {
if (justBecameActive) {
updateClipboard();
getClipboardData();
}
}, [justBecameActive]);

// Listen for updates
useEffect(() => {
listeners.add(updateClipboardData);

return () => {
listeners.delete(updateClipboardData);
};
}, []);

return {
clipboard: data,
setClipboard: setString,
Expand Down
79 changes: 47 additions & 32 deletions src/screens/ImportSeedPhraseSheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ import { Input } from '../components/inputs';
import { Centered, Column, Row, RowWithMargins } from '../components/layout';
import { LoadingOverlay } from '../components/modal';
import { Text } from '../components/text';
import { useClipboard, useInitializeWallet } from '../hooks';
import {
useAccountData,
useClipboard,
useInitializeWallet,
useTimeout,
} from '../hooks';
import { sheetVerticalOffset } from '../navigation/transitions/effects';
import { colors, padding, shadow, borders } from '../styles';
import { isValidSeed as validateSeed } from '../helpers/validators';
Expand Down Expand Up @@ -105,34 +110,48 @@ const ImportButton = ({ disabled, onPress, seedPhrase }) => (
);

const ImportSeedPhraseSheet = ({ isEmpty, setAppearListener }) => {
const { accountAddress } = useAccountData();
const { clipboard } = useClipboard();
const { navigate, setParams } = useNavigation();
const initializeWallet = useInitializeWallet();
const [isImporting, setImporting] = useState(false);
const [seedPhrase, setSeedPhrase] = useState('');
const [startFocusTimeout] = useTimeout();
const [startAnalyticsTimeout] = useTimeout();

const isClipboardValidSecret = useMemo(() => {
return clipboard !== accountAddress && validateSeed(clipboard);
}, [accountAddress, clipboard]);

const isSecretValid = useMemo(() => {
return seedPhrase !== accountAddress && validateSeed(seedPhrase);
}, [accountAddress, seedPhrase]);

const inputRef = useRef(null);
const focusListener = useCallback(() => {
inputRef.current && inputRef.current.focus();
}, []);

const inputRefListener = useCallback(value => {
value && setTimeout(value.focus, 100);
inputRef.current = value;
}, []);
const inputRefListener = useCallback(
value => {
value && startFocusTimeout(value.focus, 100);
inputRef.current = value;
},
[startFocusTimeout]
);

useEffect(() => {
setAppearListener && setAppearListener(focusListener);
return () => setAppearListener && setAppearListener(null);
});

const isClipboardValidSeedPhrase = useMemo(() => validateSeed(clipboard), [
clipboard,
]);

const isSeedPhraseValid = useMemo(() => validateSeed(seedPhrase), [
seedPhrase,
]);
const handleSetSeedPhrase = useCallback(
text => {
if (isImporting) return null;
return setSeedPhrase(text);
},
[isImporting]
);

const toggleImporting = useCallback(
newImportingState => {
Expand All @@ -142,27 +161,26 @@ const ImportSeedPhraseSheet = ({ isEmpty, setAppearListener }) => {
[setParams]
);

const handleSetSeedPhrase = useCallback(
text => {
if (isImporting) return null;
return setSeedPhrase(text);
},
[isImporting]
);

const onPressImportButton = () => {
if (isSeedPhraseValid && seedPhrase) {
const onPressImportButton = useCallback(() => {
if (isSecretValid && seedPhrase) {
return ConfirmImportAlert(() => toggleImporting(true));
}

if (isClipboardValidSeedPhrase && clipboard) {
if (isClipboardValidSecret && clipboard) {
return handleSetSeedPhrase(clipboard);
}
};
}, [
clipboard,
handleSetSeedPhrase,
isClipboardValidSecret,
isSecretValid,
seedPhrase,
toggleImporting,
]);

useEffect(() => {
if (isImporting) {
const id = setTimeout(() => {
startAnalyticsTimeout(() => {
initializeWallet(seedPhrase.trim())
.then(success => {
if (success) {
Expand All @@ -180,15 +198,14 @@ const ImportSeedPhraseSheet = ({ isEmpty, setAppearListener }) => {
logger.error('error importing seed phrase: ', error);
});
}, 50);

return () => clearTimeout(id);
}
}, [
initializeWallet,
isEmpty,
isImporting,
navigate,
seedPhrase,
startAnalyticsTimeout,
toggleImporting,
]);

Expand All @@ -206,20 +223,20 @@ const ImportSeedPhraseSheet = ({ isEmpty, setAppearListener }) => {
<Centered css={padding(0, 50)} flex={1}>
<StyledInput
align="center"
autoFocus
autoCapitalize="none"
autoCorrect={false}
autoFocus
enablesReturnKeyAutomatically
keyboardType={
Platform.OS === 'android' ? 'visible-password' : 'default'
}
lineHeight="looser"
multiline
numberOfLines={7}
ref={isNativeStackAvailable ? inputRef : inputRefListener}
onChangeText={handleSetSeedPhrase}
onSubmitEditing={onPressImportButton}
placeholder="Seed phrase or private key"
ref={isNativeStackAvailable ? inputRef : inputRefListener}
returnKeyType="done"
size="large"
value={seedPhrase}
Expand All @@ -229,9 +246,7 @@ const ImportSeedPhraseSheet = ({ isEmpty, setAppearListener }) => {
</Centered>
<Row align="start" justify="end">
<ImportButton
disabled={
seedPhrase ? !isSeedPhraseValid : !isClipboardValidSeedPhrase
}
disabled={seedPhrase ? !isSecretValid : !isClipboardValidSecret}
onPress={onPressImportButton}
seedPhrase={seedPhrase}
/>
Expand Down

0 comments on commit 6baa078

Please sign in to comment.