diff --git a/web/grpc/identity-service-client-wrapper.js b/web/grpc/identity-service-client-wrapper.js index 6da34b432..5b7805ad6 100644 --- a/web/grpc/identity-service-client-wrapper.js +++ b/web/grpc/identity-service-client-wrapper.js @@ -1,86 +1,90 @@ // @flow import identityServiceConfig from 'lib/facts/identity-service.js'; import type { IdentityServiceAuthLayer } from 'lib/types/identity-service-types.js'; import { VersionInterceptor, AuthInterceptor } from './interceptor.js'; import * as IdentityAuthClient from '../protobufs/identity-auth-client.cjs'; import { Empty } from '../protobufs/identity-unauth-structs.cjs'; import * as IdentityClient from '../protobufs/identity-unauth.cjs'; class IdentityServiceClientWrapper { authClient: ?IdentityAuthClient.IdentityClientServicePromiseClient; unauthorizedClient: ?IdentityClient.IdentityClientServicePromiseClient; constructor() { this.authClient = null; this.unauthorizedClient = null; } determineSocketAddr(): string { return process.env.IDENTITY_SOCKET_ADDR ?? identityServiceConfig.defaultURL; } async initAuthClient(authLayer: IdentityServiceAuthLayer): Promise { const { userID, deviceID, commServicesAccessToken } = authLayer; const identitySocketAddr = this.determineSocketAddr(); const versionInterceptor = new VersionInterceptor(); const authInterceptor = new AuthInterceptor( userID, deviceID, commServicesAccessToken, ); const authClientOpts = { unaryInterceptors: [versionInterceptor, authInterceptor], }; this.authClient = new IdentityAuthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, authClientOpts, ); } async initUnauthorizedClient(): Promise { const identitySocketAddr = this.determineSocketAddr(); const versionInterceptor = new VersionInterceptor(); const unauthorizedClientOpts = { unaryInterceptors: [versionInterceptor], }; this.unauthorizedClient = new IdentityClient.IdentityClientServicePromiseClient( identitySocketAddr, null, unauthorizedClientOpts, ); } - async deleteUser( + deleteUser: ( userID: string, deviceID: string, accessToken: string, - ): Promise { + ) => Promise = async ( + userID: string, + deviceID: string, + accessToken: string, + ): Promise => { if (!this.authClient) { const authLayer: IdentityServiceAuthLayer = { userID, deviceID, commServicesAccessToken: accessToken, }; await this.initAuthClient(authLayer); } if (this.authClient) { await this.authClient.deleteUser(new Empty()); } else { throw new Error('Identity service client is not initialized'); } - } + }; } export { IdentityServiceClientWrapper }; diff --git a/web/settings/account-delete-modal.react.js b/web/settings/account-delete-modal.react.js index 88cc83e1d..8ac0fc6e6 100644 --- a/web/settings/account-delete-modal.react.js +++ b/web/settings/account-delete-modal.react.js @@ -1,93 +1,160 @@ // @flow import * as React from 'react'; import { useDeleteKeyserverAccount, deleteKeyserverAccountActionTypes, + useDeleteIdentityAccount, + deleteIdentityAccountActionTypes, } from 'lib/actions/user-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { preRequestUserStateSelector } from 'lib/selectors/account-selectors.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; +import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import css from './account-delete-modal.css'; import Button, { buttonThemes } from '../components/button.react.js'; +import { IdentityServiceClientWrapper } from '../grpc/identity-service-client-wrapper.js'; import Modal from '../modals/modal.react.js'; import { useSelector } from '../redux/redux-utils.js'; -const deleteAccountLoadingStatusSelector = createLoadingStatusSelector( +const deleteKeyserverAccountLoadingStatusSelector = createLoadingStatusSelector( deleteKeyserverAccountActionTypes, ); +const deleteIdentityAccountLoadingStatusSelector = createLoadingStatusSelector( + deleteIdentityAccountActionTypes, +); const AccountDeleteModal: React.ComponentType<{}> = React.memo<{}>( function AccountDeleteModal(): React.Node { const preRequestUserState = useSelector(preRequestUserStateSelector); - const inputDisabled = useSelector( - state => deleteAccountLoadingStatusSelector(state) === 'loading', + const isDeleteKeyserverAccountLoading = useSelector( + state => deleteKeyserverAccountLoadingStatusSelector(state) === 'loading', + ); + const isDeleteIdentityAccountLoading = useSelector( + state => deleteIdentityAccountLoadingStatusSelector(state) === 'loading', ); - const callDeleteAccount = useDeleteKeyserverAccount(); + const inputDisabled = + isDeleteKeyserverAccountLoading || isDeleteIdentityAccountLoading; + + const deviceID = useSelector( + state => state.cryptoStore?.primaryIdentityKeys.ed25519, + ); + + const identityServiceClientWrapperRef = + React.useRef(null); + if (!identityServiceClientWrapperRef.current) { + identityServiceClientWrapperRef.current = + new IdentityServiceClientWrapper(); + } + const identityServiceClient = identityServiceClientWrapperRef.current; + const callDeleteIdentityAccount = useDeleteIdentityAccount(); + const callDeleteKeyserverAccount = useDeleteKeyserverAccount(); + const dispatchActionPromise = useDispatchActionPromise(); const { popModal } = useModalContext(); - const [errorMessage, setErrorMessage] = React.useState(''); + const [keyserverErrorMessage, setKeyserverErrorMessage] = + React.useState(''); + const [identityErrorMessage, setIdentityErrorMessage] = React.useState(''); - let errorMsg; - if (errorMessage) { - errorMsg =
{errorMessage}
; + const keyserverError = keyserverErrorMessage ? ( +

{keyserverErrorMessage}

+ ) : null; + const identityError = identityErrorMessage ? ( +

{identityErrorMessage}

+ ) : null; + let combinedErrorMessages; + if (keyserverError || identityError) { + combinedErrorMessages = ( +
+ {keyserverError} + {identityError} +
+ ); } - const deleteAction = React.useCallback(async () => { + const deleteKeyserverAction = React.useCallback(async () => { try { - setErrorMessage(''); - const response = await callDeleteAccount(preRequestUserState); + setKeyserverErrorMessage(''); + const response = await callDeleteKeyserverAccount(preRequestUserState); + // This check ensures that we don't call `popModal()` twice + if (!usingCommServicesAccessToken) { + popModal(); + } + return response; + } catch (e) { + setKeyserverErrorMessage( + 'unknown error deleting account from keyserver', + ); + throw e; + } + }, [callDeleteKeyserverAccount, preRequestUserState, popModal]); + + const deleteIdentityAction = React.useCallback(async () => { + try { + setIdentityErrorMessage(''); + const response = await callDeleteIdentityAccount( + identityServiceClient, + deviceID, + ); popModal(); return response; } catch (e) { - setErrorMessage('unknown error'); + setIdentityErrorMessage( + 'unknown error deleting account from identity service', + ); throw e; } - }, [callDeleteAccount, preRequestUserState, popModal]); + }, [callDeleteIdentityAccount, deviceID, identityServiceClient, popModal]); const onDelete = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); void dispatchActionPromise( deleteKeyserverAccountActionTypes, - deleteAction(), + deleteKeyserverAction(), ); + if (usingCommServicesAccessToken) { + void dispatchActionPromise( + deleteIdentityAccountActionTypes, + deleteIdentityAction(), + ); + } }, - [dispatchActionPromise, deleteAction], + [dispatchActionPromise, deleteKeyserverAction, deleteIdentityAction], ); return (

Your account will be permanently deleted. There is no way to reverse this.

- {errorMsg} + {combinedErrorMessages}
); }, ); export default AccountDeleteModal;