diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -384,7 +384,7 @@ failed: 'DELETE_ACCOUNT_FAILED', }); -function useDeleteAccount(): () => Promise { +function useDeleteAccount(): (password: ?string) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; @@ -395,39 +395,60 @@ state => state.commServicesAccessToken, ); - return React.useCallback(async () => { - const identityPromise = (async () => { - if (!usingCommServicesAccessToken) { - return undefined; - } - if (!identityClient) { - throw new Error('Identity service client is not initialized'); + return React.useCallback( + async password => { + if (usingCommServicesAccessToken) { + if (!identityClient) { + throw new Error('Identity service client is not initialized'); + } + if ( + !identityClient.deleteWalletUser || + !identityClient.deletePasswordUser + ) { + throw new Error('Delete user method unimplemented'); + } + if (password) { + await identityClient.deletePasswordUser(password); + } else { + await identityClient.deleteWalletUser(); + } } - if (!identityClient.deleteWalletUser) { - throw new Error('Delete wallet user method unimplemented'); + try { + const keyserverResult = await callKeyserverDeleteAccount({ + preRequestUserState, + }); + const { keyserverIDs: _, ...result } = keyserverResult; + return { + ...result, + preRequestUserState: { + ...result.preRequestUserState, + commServicesAccessToken, + }, + }; + } catch (e) { + if (!usingCommServicesAccessToken) { + throw e; + } + console.log( + 'Failed to delete account on keyserver:', + getMessageForException(e), + ); } - return await identityClient.deleteWalletUser(); - })(); - const [keyserverResult] = await Promise.all([ - callKeyserverDeleteAccount({ - preRequestUserState, - }), - identityPromise, - ]); - const { keyserverIDs: _, ...result } = keyserverResult; - return { - ...result, - preRequestUserState: { - ...result.preRequestUserState, - commServicesAccessToken, - }, - }; - }, [ - callKeyserverDeleteAccount, - commServicesAccessToken, - identityClient, - preRequestUserState, - ]); + return { + currentUserInfo: null, + preRequestUserState: { + ...preRequestUserState, + commServicesAccessToken, + }, + }; + }, + [ + callKeyserverDeleteAccount, + commServicesAccessToken, + identityClient, + preRequestUserState, + ], + ); } // Unlike useDeleteAccount, we always dispatch a success here (never throw). diff --git a/native/profile/delete-account.react.js b/native/profile/delete-account.react.js --- a/native/profile/delete-account.react.js +++ b/native/profile/delete-account.react.js @@ -1,7 +1,12 @@ // @flow import * as React from 'react'; -import { Text, View, ActivityIndicator } from 'react-native'; +import { + Text, + View, + ActivityIndicator, + TextInput as BaseTextInput, +} from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import { @@ -9,14 +14,19 @@ useDeleteAccount, } from 'lib/actions/user-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; +import { accountHasPassword } from 'lib/shared/account-utils.js'; +import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; +import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import { deleteNativeCredentialsFor } from '../account/native-credentials.js'; import Button from '../components/button.react.js'; +import TextInput from '../components/text-input.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; -import { useStyles } from '../themes/colors.js'; +import { useStyles, useColors } from '../themes/colors.js'; +import { unknownErrorAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; const deleteAccountLoadingStatusSelector = createLoadingStatusSelector( @@ -34,6 +44,11 @@ ); const styles = useStyles(unboundStyles); + const isAccountWithPassword = useSelector(state => + accountHasPassword(state.currentUserInfo), + ); + const { panelForegroundTertiaryLabel } = useColors(); + const [password, setPassword] = React.useState(''); const dispatchActionPromise = useDispatchActionPromise(); const callDeleteAccount = useDeleteAccount(); @@ -50,30 +65,79 @@ () => [styles.warningText, styles.lastWarningText], [styles.warningText, styles.lastWarningText], ); + const passwordInputRef = + React.useRef>(null); + + const onErrorAlertAcknowledged = React.useCallback(() => { + passwordInputRef.current?.focus(); + }, []); const deleteAccountAction = React.useCallback(async () => { try { await deleteNativeCredentialsFor(); - return await callDeleteAccount(); + return await callDeleteAccount(password); } catch (e) { - Alert.alert( - 'Unknown error deleting account', - 'Uhh... try again?', - [{ text: 'OK' }], - { - cancelable: false, - }, - ); + if (getMessageForException(e) === 'login failed') { + Alert.alert( + 'Incorrect password', + 'The password you entered is incorrect', + [{ text: 'OK', onPress: onErrorAlertAcknowledged }], + { cancelable: false }, + ); + } else { + Alert.alert( + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, + [{ text: 'OK', onPress: onErrorAlertAcknowledged }], + { cancelable: false }, + ); + } throw e; } - }, [callDeleteAccount]); + }, [callDeleteAccount, onErrorAlertAcknowledged, password]); const onDelete = React.useCallback(() => { + if (!password && isAccountWithPassword && usingCommServicesAccessToken) { + Alert.alert('Password required', 'Please enter your password.', [ + { text: 'OK', onPress: onErrorAlertAcknowledged }, + ]); + return; + } void dispatchActionPromise( deleteAccountActionTypes, deleteAccountAction(), ); - }, [dispatchActionPromise, deleteAccountAction]); + }, [ + password, + isAccountWithPassword, + dispatchActionPromise, + deleteAccountAction, + onErrorAlertAcknowledged, + ]); + + let inputPasswordPrompt; + if (isAccountWithPassword && usingCommServicesAccessToken) { + inputPasswordPrompt = ( + <> + PASSWORD + + + + + ); + } return ( + {inputPasswordPrompt}