diff --git a/native/account/native-credentials.js b/native/account/native-credentials.js index a2a979635..eb88379c6 100644 --- a/native/account/native-credentials.js +++ b/native/account/native-credentials.js @@ -1,101 +1,101 @@ // @flow import { getInternetCredentials, setInternetCredentials, resetInternetCredentials, } from 'react-native-keychain'; -type UserCredentials = { +export type UserCredentials = { +username: string, +password: string, }; type StoredCredentials = { state: 'undetermined' | 'determined' | 'unsupported', credentials: ?UserCredentials, }; let storedNativeKeychainCredentials: StoredCredentials = { state: 'undetermined', credentials: null, }; async function fetchNativeKeychainCredentials(): Promise { if (storedNativeKeychainCredentials.state === 'determined') { return storedNativeKeychainCredentials.credentials; } try { const result = await getInternetCredentials('comm.app'); const credentials = result ? { username: result.username, password: result.password } : undefined; storedNativeKeychainCredentials = { state: 'determined', credentials }; return credentials; } catch (e) { const credentials = null; storedNativeKeychainCredentials = { state: 'unsupported', credentials }; return credentials; } } async function fetchNativeCredentials(): Promise { const keychainCredentials = await fetchNativeKeychainCredentials(); if (keychainCredentials) { return keychainCredentials; } return null; } async function setNativeKeychainCredentials(credentials: UserCredentials) { const current = await fetchNativeKeychainCredentials(); if ( current && credentials.username === current.username && credentials.password === current.password ) { return; } try { await setInternetCredentials( 'comm.app', credentials.username, credentials.password, ); storedNativeKeychainCredentials = { state: 'determined', credentials }; } catch (e) { storedNativeKeychainCredentials = { state: 'unsupported', credentials: null, }; } } function setNativeCredentials(credentials: UserCredentials): Promise { return setNativeKeychainCredentials(credentials); } async function deleteNativeKeychainCredentials() { try { await resetInternetCredentials('comm.app'); storedNativeKeychainCredentials = { state: 'determined', credentials: undefined, }; } catch (e) { storedNativeKeychainCredentials = { state: 'unsupported', credentials: null, }; } } function deleteNativeCredentialsFor(): Promise { return deleteNativeKeychainCredentials(); } export { fetchNativeKeychainCredentials, fetchNativeCredentials, setNativeCredentials, deleteNativeCredentialsFor, }; diff --git a/native/account/restore-password-account-screen.react.js b/native/account/restore-password-account-screen.react.js index 6ff880cd1..37bfd424b 100644 --- a/native/account/restore-password-account-screen.react.js +++ b/native/account/restore-password-account-screen.react.js @@ -1,179 +1,214 @@ // @flow import * as React from 'react'; import { Text, TextInput, View } from 'react-native'; import { usePasswordLogIn } from 'lib/hooks/login-hooks.js'; import { getMessageForException } from 'lib/utils/errors.js'; -import { setNativeCredentials } from './native-credentials.js'; +import { + fetchNativeCredentials, + setNativeCredentials, +} from './native-credentials.js'; +import type { UserCredentials } from './native-credentials.js'; import PromptButton from './prompt-button.react.js'; import RegistrationButtonContainer from './registration/registration-button-container.react.js'; import RegistrationContainer from './registration/registration-container.react.js'; import RegistrationContentContainer from './registration/registration-content-container.react.js'; import RegistrationTextInput from './registration/registration-text-input.react.js'; import type { SignInNavigationProp } from './sign-in-navigator.react.js'; import { useClientBackup } from '../backup/use-client-backup.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { RestoreBackupScreenRouteName } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; import { appOutOfDateAlertDetails, unknownErrorAlertDetails, userNotFoundAlertDetails, } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; type Props = { +navigation: SignInNavigationProp<'RestorePasswordAccountScreen'>, +route: NavigationRoute<'RestorePasswordAccountScreen'>, }; function RestorePasswordAccountScreen(props: Props): React.Node { - const [username, setUsername] = React.useState(''); - const [password, setPassword] = React.useState(''); + const [credentials, setCredentials] = React.useState({ + username: '', + password: '', + }); + const setUsername = React.useCallback( + (username: string) => + setCredentials(prevCredentials => ({ + ...prevCredentials, + username, + })), + [], + ); + const setPassword = React.useCallback( + (password: string) => + setCredentials(prevCredentials => ({ + ...prevCredentials, + password, + })), + [], + ); + + React.useEffect(() => { + void (async () => { + const nativeCredentials = await fetchNativeCredentials(); + if (!nativeCredentials) { + return; + } + setCredentials(prevCredentials => { + if (!prevCredentials.username && !prevCredentials.password) { + return nativeCredentials; + } + return prevCredentials; + }); + })(); + }, []); const passwordInputRef = React.useRef>(); const focusPasswordInput = React.useCallback(() => { passwordInputRef.current?.focus(); }, []); const usernameInputRef = React.useRef>(); const focusUsernameInput = React.useCallback(() => { usernameInputRef.current?.focus(); }, []); const onUnsuccessfulLoginAlertAcknowledged = React.useCallback(() => { - setUsername(''); - setPassword(''); + setCredentials({ username: '', password: '' }); focusUsernameInput(); }, [focusUsernameInput]); const identityPasswordLogIn = usePasswordLogIn(); const { retrieveLatestBackupInfo } = useClientBackup(); - const areCredentialsPresent = !!username && !!password; + const areCredentialsPresent = + !!credentials.username && !!credentials.password; const [isProcessing, setIsProcessing] = React.useState(false); const onProceed = React.useCallback(async () => { if (!areCredentialsPresent) { return; } setIsProcessing(true); try { - const latestBackupInfo = await retrieveLatestBackupInfo(username); + const latestBackupInfo = await retrieveLatestBackupInfo( + credentials.username, + ); if (!latestBackupInfo) { - await identityPasswordLogIn(username, password); - await setNativeCredentials({ - username, - password, - }); + await identityPasswordLogIn(credentials.username, credentials.password); + await setNativeCredentials(credentials); return; } props.navigation.navigate(RestoreBackupScreenRouteName, { - userIdentifier: username, + userIdentifier: credentials.username, credentials: { type: 'password', - password, + password: credentials.password, }, }); } catch (e) { const messageForException = getMessageForException(e); let alertMessage = unknownErrorAlertDetails; let onPress = null; if ( messageForException === 'user_not_found' || messageForException === 'login_failed' ) { alertMessage = userNotFoundAlertDetails; onPress = onUnsuccessfulLoginAlertAcknowledged; } else if ( messageForException === 'unsupported_version' || messageForException === 'client_version_unsupported' || messageForException === 'use_new_flow' ) { alertMessage = appOutOfDateAlertDetails; } Alert.alert( alertMessage.title, alertMessage.message, [{ text: 'OK', onPress }], { cancelable: false }, ); } finally { setIsProcessing(false); } }, [ areCredentialsPresent, + credentials, identityPasswordLogIn, onUnsuccessfulLoginAlertAcknowledged, - password, props.navigation, retrieveLatestBackupInfo, - username, ]); let restoreButtonVariant = 'loading'; if (!isProcessing) { restoreButtonVariant = areCredentialsPresent ? 'enabled' : 'disabled'; } const styles = useStyles(unboundStyles); return ( Restore with password ); } const unboundStyles = { header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, buttonContainer: { flexDirection: 'row', }, password: { marginTop: 16, }, }; export default RestorePasswordAccountScreen;