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 @@ -21,6 +21,7 @@ UpdateUserAvatarResponse, } from '../types/avatar-types.js'; import type { RawEntryInfo, CalendarQuery } from '../types/entry-types.js'; +import type { IdentityServiceClient } from '../types/identity-service-types'; import type { RawMessageInfo, MessageTruncationStatuses, @@ -159,6 +160,31 @@ return useKeyserverCall(deleteKeyserverAccount); } +const deleteIdentityAccountActionTypes = Object.freeze({ + started: 'DELETE_IDENTITY_ACCOUNT_STARTED', + success: 'DELETE_IDENTITY_ACCOUNT_SUCCESS', + failed: 'DELETE_IDENTITY_ACCOUNT_FAILED', +}); + +function useDeleteIdentityAccount(): ( + client: IdentityServiceClient, + deviceID: ?string, +) => Promise { + const userID = useSelector(state => state.currentUserInfo?.id); + const accessToken = useSelector(state => state.commServicesAccessToken); + const deleteIdentityAccount = React.useCallback( + async (client: IdentityServiceClient, deviceID: ?string) => { + if (!userID || !accessToken || !deviceID) { + throw new Error('missing identity service auth metadata'); + } + await client.deleteUser(userID, deviceID, accessToken); + }, + [userID, accessToken], + ); + + return deleteIdentityAccount; +} + const registerActionTypes = Object.freeze({ started: 'REGISTER_STARTED', success: 'REGISTER_SUCCESS', @@ -536,4 +562,6 @@ updateUserAvatar, resetUserStateActionType, setAccessTokenActionType, + deleteIdentityAccountActionTypes, + useDeleteIdentityAccount, }; diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -19,6 +19,14 @@ +oneTimeNotifPrekey: ?string, }; +export interface IdentityServiceClient { + +deleteUser: ( + userID: string, + deviceID: string, + accessToken: string, + ) => Promise; +} + export type IdentityServiceAuthLayer = { +userID: string, +deviceID: string, diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -230,6 +230,22 @@ +payload: LogOutResult, +loadingInfo: LoadingInfo, } + | { + +type: 'DELETE_IDENTITY_ACCOUNT_STARTED', + +payload?: void, + +loadingInfo: LoadingInfo, + } + | { + +type: 'DELETE_IDENTITY_ACCOUNT_FAILED', + +error: true, + +payload: Error, + +loadingInfo: LoadingInfo, + } + | { + +type: 'DELETE_IDENTITY_ACCOUNT_SUCCESS', + +payload: LogOutResult, + +loadingInfo: LoadingInfo, + } | { +type: 'CREATE_LOCAL_ENTRY', +payload: RawEntryInfo, 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 @@ -5,24 +5,35 @@ import { ScrollView } from 'react-native-gesture-handler'; import { + deleteIdentityAccountActionTypes, deleteKeyserverAccountActionTypes, + useDeleteIdentityAccount, useDeleteKeyserverAccount, } from 'lib/actions/user-actions.js'; import { preRequestUserStateSelector } from 'lib/selectors/account-selectors.js'; -import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; +import { + createLoadingStatusSelector, + combineLoadingStatuses, +} from 'lib/selectors/loading-selectors.js'; import { useDispatchActionPromise } from 'lib/utils/action-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 { commRustModule } from '../native-modules.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; import Alert from '../utils/alert.js'; +import { getContentSigningKey } from '../utils/crypto-utils.js'; -const loadingStatusSelector = createLoadingStatusSelector( +const keyserverLoadingStatusSelector = createLoadingStatusSelector( deleteKeyserverAccountActionTypes, ); +const identityLoadingStatusSelector = createLoadingStatusSelector( + deleteIdentityAccountActionTypes, +); type Props = { +navigation: ProfileNavigationProp<'DeleteAccount'>, @@ -30,43 +41,79 @@ }; const DeleteAccount: React.ComponentType = React.memo( function DeleteAccount() { - const loadingStatus = useSelector(loadingStatusSelector); + const keyserverLoadingStatus = useSelector(keyserverLoadingStatusSelector); + const identityLoadingStatus = useSelector(identityLoadingStatusSelector); + const combinedLoadingStatuses = combineLoadingStatuses( + keyserverLoadingStatus, + identityLoadingStatus, + ); const preRequestUserState = useSelector(preRequestUserStateSelector); + const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); - const callDeleteAccount = useDeleteKeyserverAccount(); + const callDeleteKeyserverAccount = useDeleteKeyserverAccount(); + const callDeleteIdentityAccount = useDeleteIdentityAccount(); - const buttonContent = - loadingStatus === 'loading' ? ( - - ) : ( - Delete account - ); + const isButtonDisabled = combinedLoadingStatuses === 'loading'; + + const buttonContent = isButtonDisabled ? ( + + ) : ( + Delete account + ); const noWayToReverseThisStyles = React.useMemo( () => [styles.warningText, styles.lastWarningText], [styles.warningText, styles.lastWarningText], ); - const deleteAction = React.useCallback(async () => { + const deleteKeyserverAction = React.useCallback(async () => { try { await deleteNativeCredentialsFor(); - return await callDeleteAccount(preRequestUserState); + return await callDeleteKeyserverAccount(preRequestUserState); } catch (e) { - Alert.alert('Unknown error', 'Uhh... try again?', [{ text: 'OK' }], { - cancelable: false, - }); + Alert.alert( + 'Unknown error deleting keyserver account', + 'Uhh... try again?', + [{ text: 'OK' }], + { + cancelable: false, + }, + ); throw e; } - }, [callDeleteAccount, preRequestUserState]); + }, [callDeleteKeyserverAccount, preRequestUserState]); + + const deleteIdentityAction = React.useCallback(async () => { + try { + const deviceID = await getContentSigningKey(); + return await callDeleteIdentityAccount(commRustModule, deviceID); + } catch (e) { + Alert.alert( + 'Unknown error deleting account', + 'Uhh... try again?', + [{ text: 'OK' }], + { + cancelable: false, + }, + ); + throw e; + } + }, [callDeleteIdentityAccount]); const onDelete = React.useCallback(() => { void dispatchActionPromise( deleteKeyserverAccountActionTypes, - deleteAction(), + deleteKeyserverAction(), ); - }, [dispatchActionPromise, deleteAction]); + if (usingCommServicesAccessToken) { + void dispatchActionPromise( + deleteIdentityAccountActionTypes, + deleteIdentityAction(), + ); + } + }, [dispatchActionPromise, deleteKeyserverAction, deleteIdentityAction]); return ( -