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 @@ -301,7 +301,7 @@ failed: 'DELETE_ACCOUNT_FAILED', }); -function useDeleteAccount(): () => Promise { +function useDeleteAccount(): (password: ?string) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; @@ -312,39 +312,48 @@ 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'); - } - if (!identityClient.deleteWalletUser) { - throw new Error('Delete wallet user method unimplemented'); - } - 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 React.useCallback( + async password => { + const identityPromise = (async () => { + if (!usingCommServicesAccessToken) { + return undefined; + } + if (!identityClient) { + throw new Error('Identity service client is not initialized'); + } + if ( + !identityClient.deleteWalletUser || + !identityClient.deletePasswordUser + ) { + throw new Error('Delete user method unimplemented'); + } + const deleteUserPromise = password + ? identityClient.deletePasswordUser(password) + : identityClient.deleteWalletUser(); + return await deleteUserPromise; + })(); + const [keyserverResult] = await Promise.all([ + callKeyserverDeleteAccount({ + preRequestUserState, + }), + identityPromise, + ]); + const { keyserverIDs: _, ...result } = keyserverResult; + return { + ...result, + preRequestUserState: { + ...result.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,17 @@ useDeleteAccount, } from 'lib/actions/user-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; +import { accountHasPassword } from 'lib/shared/account-utils.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 Alert from '../utils/alert.js'; const deleteAccountLoadingStatusSelector = createLoadingStatusSelector( @@ -34,6 +42,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 +63,72 @@ () => [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' }], + [{ 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}