diff --git a/src/containers/Settings/ProfileSettings/DisplaySeed/DisplaySeed.js b/src/containers/DisplaySeed/DisplaySeed.js similarity index 69% rename from src/containers/Settings/ProfileSettings/DisplaySeed/DisplaySeed.js rename to src/containers/DisplaySeed/DisplaySeed.js index 1f8ce2ad..dd36a480 100644 --- a/src/containers/Settings/ProfileSettings/DisplaySeed/DisplaySeed.js +++ b/src/containers/DisplaySeed/DisplaySeed.js @@ -12,14 +12,14 @@ import { import { NavigationActions } from '@react-navigation/compat'; import { connect } from 'react-redux'; import QRCode from 'react-native-qrcode-svg'; -import Styles from '../../../../styles/index' -import Colors from "../../../../globals/colors"; +import Styles from '../../styles/index' +import Colors from "../../globals/colors"; import { CommonActions } from '@react-navigation/native'; -import { DLIGHT_PRIVATE, ELECTRUM, ETH, WYRE_SERVICE } from "../../../../utils/constants/intervalConstants"; +import { DLIGHT_PRIVATE, ELECTRUM, ETH, WYRE_SERVICE } from "../../utils/constants/intervalConstants"; import { Card, Paragraph, Title, Button } from 'react-native-paper' -import { deriveKeyPair, dlightSeedToBytes, isSeedPhrase } from "../../../../utils/keys"; -import { createAlert } from "../../../../actions/actions/alert/dispatchers/alert"; -import { coinsList } from "../../../../utils/CoinData/CoinsList"; +import { deriveKeyPair, dlightSeedToBytes, isSeedPhrase } from "../../utils/keys"; +import { createAlert } from "../../actions/actions/alert/dispatchers/alert"; +import { coinsList } from "../../utils/CoinData/CoinsList"; class DisplaySeed extends Component { constructor() { @@ -31,7 +31,8 @@ class DisplaySeed extends Component { dualSameSeed: false, derivedKeys: {}, toggleDerivedKey: {}, - fetchingDerivedKey: {} + fetchingDerivedKey: {}, + completeOnBack: false, }; this.SEED_NAMES = { @@ -65,6 +66,15 @@ class DisplaySeed extends Component { .fromDeleteAccount, }); } + + if ( + data && + data.completeOnBack + ) { + this.setState({ + completeOnBack: data.completeOnBack, + }); + } } resetToScreen = () => { @@ -114,8 +124,9 @@ class DisplaySeed extends Component { } } - // Method to derive the key from seed. Replace this with your actual implementation deriveKeyFromSeed = async (seed, key, coinObj) => { + const { data } = this.props.route.params; + switch (key) { case DLIGHT_PRIVATE: return Buffer.from(await dlightSeedToBytes(seed)).toString('hex'); @@ -124,14 +135,14 @@ class DisplaySeed extends Component { seed, coinsList.ETH, key, - this.props.activeAccount.keyDerivationVersion, + data.keyDerivationVersion, )).privKey; case ELECTRUM: return (await deriveKeyPair( seed, coinObj, key, - this.props.activeAccount.keyDerivationVersion, + data.keyDerivationVersion, )).privKey; default: return seed @@ -139,7 +150,8 @@ class DisplaySeed extends Component { } render() { - const { seeds, toggleDerivedKey, fetchingDerivedKey, derivedKeys } = this.state; + const { seeds, toggleDerivedKey, fetchingDerivedKey, derivedKeys, completeOnBack } = this.state; + const { data } = this.props.route.params; return ( @@ -163,28 +175,30 @@ class DisplaySeed extends Component { - { - ((key === DLIGHT_PRIVATE && isSeedPhrase(seeds[key])) || - key === ETH || - key === ELECTRUM) && ( - - ) - } - { - !isToggleOn && !fetchingDerivedKey[key] && key === ELECTRUM && ( - - ) - } + {data.showDerivedKeys && <> + { + ((key === DLIGHT_PRIVATE && isSeedPhrase(seeds[key])) || + key === ETH || + key === ELECTRUM) && ( + + ) + } + { + !isToggleOn && !fetchingDerivedKey[key] && key === ELECTRUM && ( + + ) + } + } @@ -194,12 +208,16 @@ class DisplaySeed extends Component { - - + {!completeOnBack && ( + + )} diff --git a/src/containers/Login/Login.js b/src/containers/Login/Login.js index 6ed17a24..0fd05681 100644 --- a/src/containers/Login/Login.js +++ b/src/containers/Login/Login.js @@ -22,6 +22,7 @@ import { } from '../../utils/constants/sendModal'; import {useSelector} from 'react-redux'; import TallButton from '../../components/LargerButton'; +import SignedOutDropdown from '../SignedOutDropdown/SignedOutDropdown'; const {height} = Dimensions.get('window'); @@ -32,6 +33,9 @@ const Login = props => { const authModalUsed = useSelector( state => state.authentication.authModalUsed, ); + const modalVisible = useSelector( + state => state.sendModal.visible, + ); const accounts = useSelector(state => state.authentication.accounts); openAuthModal = ignoreDefault => { @@ -67,12 +71,22 @@ const Login = props => { props.navigation.navigate('CreateProfile'); }; + handleRecoverSeed = () => { + props.navigation.navigate("RecoverSeeds"); + }; + return ( + + {!modalVisible && handleRecoverSeed()} + hasAccount={true} + />} + { + const accounts = useSelector(state => state.authentication.accounts); + const [numSeeds, setNumSeeds] = useState({}); + const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); + const [passwordDialogTitle, setPasswordDialogTitle] = useState(''); + const [passwordDialogAccount, setPasswordDialogAccount] = useState(null); + + useEffect(() => { + for (let account of accounts) { + setNumSeeds(prevState => ({ + ...prevState, + [account.accountHash]: Object.values(account.encryptedKeys).filter(x => x != null).length, + })); + } + }, [accounts]); + + const handleCardPress = async (account) => { + if (await canShowSeed()) { + setPasswordDialogAccount(account); + setPasswordDialogOpen(true); + setPasswordDialogTitle(`Enter your password for "${account.id}"`); + } + }; + + const onPasswordResult = async (result) => { + if (result.valid) { + setPasswordDialogOpen(false); + + try { + const seeds = await checkPinForUser(result.password, passwordDialogAccount.id); + + navigation.dispatch(CommonActions.reset({ + index: 0, + routes: [{ name: 'DisplaySeed', params: { data: { seeds, showDerivedKeys: true, keyDerivationVersion: passwordDialogAccount.keyDerivationVersion, completeOnBack: true } } }], + })); + } catch(e) { + createAlert("Error", "Failed to retrieve seeds"); + } + } else { + createAlert("Authentication Error", "Incorrect password"); + } + } + + const renderAccountCards = () => { + return accounts.map(account => ( + + handleCardPress(account)} + key={account.accountHash} + style={{ backgroundColor: Colors.primaryColor }} + > + 1 ? 's' : ''}`} + left={() => } + descriptionStyle={{ color: Colors.secondaryColor }} + right={() => ( + + )} + /> + + + )); + }; + + return ( + + {passwordDialogOpen && + setPasswordDialogOpen(false)} + submit={(result) => onPasswordResult(result)} + visible={passwordDialogOpen} + title={passwordDialogTitle} + userName={passwordDialogAccount ? passwordDialogAccount.id : ''} + account={passwordDialogAccount} + allowBiometry={true} + /> + } + {'Select a Profile'} + + {renderAccountCards()} + + navigation.goBack()} + mode="outlined" + style={{ marginHorizontal: 8 }} + > + {"Cancel"} + + + ); +}; + +export default RecoverSeedsSelectAccount; diff --git a/src/containers/RootStack/MainStackScreens/MainStackScreens.js b/src/containers/RootStack/MainStackScreens/MainStackScreens.js index 4a2fe261..2374d222 100644 --- a/src/containers/RootStack/MainStackScreens/MainStackScreens.js +++ b/src/containers/RootStack/MainStackScreens/MainStackScreens.js @@ -3,7 +3,7 @@ import { createStackNavigator } from "@react-navigation/stack"; import { defaultHeaderOptions } from '../../../utils/navigation/header'; import AddCoin from '../../AddCoin/AddCoin' import CoinDetails from '../../CoinDetails/CoinDetails' -import DisplaySeed from '../../Settings/ProfileSettings/DisplaySeed/DisplaySeed' +import DisplaySeed from '../../DisplaySeed/DisplaySeed' import SettingsMenus from '../../Settings/SettingsMenus' import CoinMenus from '../../Coin/CoinMenus' import VerusPay from '../../VerusPay/VerusPay' diff --git a/src/containers/RootStack/RecoverSeedsStackScreens/RecoverSeedsStackScreens.js b/src/containers/RootStack/RecoverSeedsStackScreens/RecoverSeedsStackScreens.js new file mode 100644 index 00000000..c4c24098 --- /dev/null +++ b/src/containers/RootStack/RecoverSeedsStackScreens/RecoverSeedsStackScreens.js @@ -0,0 +1,38 @@ +import React, { useState } from 'react'; +import { createStackNavigator } from "@react-navigation/stack"; +import { defaultHeaderOptions } from '../../../utils/navigation/header'; +import RecoverSeedsSelectAccount from '../../RecoverSeeds/RecoverSeedsSelectAccount'; +import DisplaySeed from '../../DisplaySeed/DisplaySeed'; + +const RecoverSeedsStack = createStackNavigator(); + +const RecoverSeedsStackScreens = props => { + return ( + + + {(_props) => ( + + )} + + + + + ); +}; + +export default RecoverSeedsStackScreens; \ No newline at end of file diff --git a/src/containers/RootStack/SignedOutStackScreens/SignedOutStackScreens.js b/src/containers/RootStack/SignedOutStackScreens/SignedOutStackScreens.js index fa6333ed..d790790c 100644 --- a/src/containers/RootStack/SignedOutStackScreens/SignedOutStackScreens.js +++ b/src/containers/RootStack/SignedOutStackScreens/SignedOutStackScreens.js @@ -1,6 +1,6 @@ import React, {useEffect, useState} from 'react'; import {createStackNavigator} from '@react-navigation/stack'; -import DisplaySeed from '../../Settings/ProfileSettings/DisplaySeed/DisplaySeed'; +import DisplaySeed from '../../DisplaySeed/DisplaySeed'; import RecoverSeed from '../../Settings/ProfileSettings/RecoverSeed/RecoverSeed'; import DeleteProfile from '../../Settings/ProfileSettings/DeleteProfile/DeleteProfile'; import SecureLoading from '../../SecureLoading/SecureLoading'; @@ -9,6 +9,7 @@ import SignUp from '../../SignUp/SignUp'; import {useDispatch, useSelector} from 'react-redux'; import {setDeeplinkUrl} from '../../../actions/actionCreators'; import CreateProfile from '../../Onboard/CreateProfile/CreateProfile'; +import RecoverSeedsStackScreens from '../RecoverSeedsStackScreens/RecoverSeedsStackScreens'; const SignedOutStack = createStackNavigator(); @@ -84,6 +85,14 @@ const SignedOutStackScreens = props => { headerShown: false, }} /> + + ); }; diff --git a/src/containers/Settings/ProfileSettings/ProfileSettings.js b/src/containers/Settings/ProfileSettings/ProfileSettings.js index 1a4ffc66..bd259000 100644 --- a/src/containers/Settings/ProfileSettings/ProfileSettings.js +++ b/src/containers/Settings/ProfileSettings/ProfileSettings.js @@ -276,7 +276,7 @@ class ProfileSettings extends Component { .then((seeds) => { this.setState({ password: null }, () => { this.props.navigation.navigate("DisplaySeed", { - data: { seeds }, + data: { seeds, showDerivedKeys: true, keyDerivationVersion: this.props.activeAccount.keyDerivationVersion }, }); }); }) diff --git a/src/containers/SignedOutDropdown/SignedOutDropdown.js b/src/containers/SignedOutDropdown/SignedOutDropdown.js new file mode 100644 index 00000000..c0bd91e6 --- /dev/null +++ b/src/containers/SignedOutDropdown/SignedOutDropdown.js @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { Divider, FAB, Menu, Portal, Button, IconButton } from 'react-native-paper'; +import { SafeAreaView, View } from 'react-native' +import Colors from '../../globals/colors'; + +const SignedOutDropdown = (props) => { + const { + handleRecoverSeed, + hasAccount + } = props; + const [visible, setVisible] = React.useState(false); + + const openMenu = () => setVisible(true); + + const closeMenu = () => setVisible(false); + + const actions = !hasAccount + ? [] + : [ + { + label: 'Recover account seeds', + onPress: handleRecoverSeed, + } + ]; + + return ( + + + }> + { + actions.map((action, index) => { + return ( + { + closeMenu(); + action.onPress(props); + }} title={action.label} /> + ); + }) + } + + + ); +}; + +export default SignedOutDropdown; \ No newline at end of file