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 @@ -132,7 +132,9 @@ if (!usingCommServicesAccessToken) { return; } - invariant(identityClient, 'Identity client should be set'); + if (!identityClient) { + throw new Error('Identity service client is not initialized'); + } try { await Promise.race([ identityClient.logOut(), @@ -168,6 +170,42 @@ ); } +function useIdentityLogOut(): () => Promise { + const client = React.useContext(IdentityClientContext); + const identityClient = client?.identityClient; + + const preRequestUserState = usePreRequestUserState(); + const commServicesAccessToken = useSelector( + state => state.commServicesAccessToken, + ); + + return React.useCallback(async () => { + invariant( + usingCommServicesAccessToken, + 'identityLogOut can only be called when usingCommServicesAccessToken', + ); + if (!identityClient) { + throw new Error('Identity service client is not initialized'); + } + try { + await Promise.race([ + identityClient.logOut(), + (async () => { + await sleep(500); + throw new Error('identity log_out took more than 500ms'); + })(), + ]); + } catch {} + return { + currentUserInfo: null, + preRequestUserState: { + ...preRequestUserState, + commServicesAccessToken, + }, + }; + }, [commServicesAccessToken, identityClient, preRequestUserState]); +} + const claimUsernameActionTypes = Object.freeze({ started: 'CLAIM_USERNAME_STARTED', success: 'CLAIM_USERNAME_SUCCESS', @@ -285,6 +323,54 @@ ]); } +// Unlike useDeleteAccount, we always dispatch a success here (never throw). +// That's because useDeleteAccount is used in a scenario where the user is +// visibly logged-in, and we don't want to log them out unless we succeeded in +// deleting their account. On the other hand, useDeleteDiscardedIdentityAccount +// is used in a scenario where the user is visibly logged-out, and it's only +// used to reset state (eg. Redux, SQLite) to a logged-out state. The state +// reset only occurs when a success action is dispatched, so we always dispatch +// a success. +function useDeleteDiscardedIdentityAccount(): () => Promise { + const client = React.useContext(IdentityClientContext); + const identityClient = client?.identityClient; + + const preRequestUserState = usePreRequestUserState(); + const commServicesAccessToken = useSelector( + state => state.commServicesAccessToken, + ); + + return React.useCallback(async () => { + invariant( + usingCommServicesAccessToken, + 'deleteDiscardedIdentityAccount can only be called when ' + + 'usingCommServicesAccessToken', + ); + if (!identityClient) { + throw new Error('Identity service client is not initialized'); + } + if (!identityClient.deleteWalletUser) { + throw new Error('Delete wallet user method unimplemented'); + } + try { + await Promise.race([ + identityClient.deleteWalletUser(), + (async () => { + await sleep(500); + throw new Error('identity delete_wallet_user took more than 500ms'); + })(), + ]); + } catch {} + return { + currentUserInfo: null, + preRequestUserState: { + ...preRequestUserState, + commServicesAccessToken, + }, + }; + }, [commServicesAccessToken, identityClient, preRequestUserState]); +} + const legacyKeyserverRegisterActionTypes = Object.freeze({ started: 'LEGACY_KEYSERVER_REGISTER_STARTED', success: 'LEGACY_KEYSERVER_REGISTER_SUCCESS', @@ -871,6 +957,7 @@ useLegacyLogIn, legacyLogInActionTypes, useLogOut, + useIdentityLogOut, logOutActionTypes, legacyKeyserverRegister, legacyKeyserverRegisterActionTypes, @@ -888,6 +975,7 @@ updateUserAvatar, deleteAccountActionTypes, useDeleteAccount, + useDeleteDiscardedIdentityAccount, keyserverAuthActionTypes, keyserverAuth as keyserverAuthRawAction, identityRegisterActionTypes, diff --git a/lib/hooks/login-hooks.js b/lib/hooks/login-hooks.js --- a/lib/hooks/login-hooks.js +++ b/lib/hooks/login-hooks.js @@ -7,6 +7,8 @@ identityLogInActionTypes, useIdentityPasswordLogIn, useIdentityWalletLogIn, + logOutActionTypes, + useIdentityLogOut, } from '../actions/user-actions.js'; import { useKeyserverAuth } from '../keyserver-conn/keyserver-auth.js'; import { logInActionSources } from '../types/account-types.js'; @@ -98,6 +100,10 @@ !state.currentUserInfo.anonymous, ); + // We call identityLogOut in order to reset state if identity auth succeeds + // but authoritative keyserver auth fails + const identityLogOut = useIdentityLogOut(); + const registeringOnAuthoritativeKeyserverRef = React.useRef(false); const dispatch = useDispatch(); React.useEffect(() => { @@ -128,13 +134,21 @@ }); resolve(); } catch (e) { + void dispatchActionPromise(logOutActionTypes, identityLogOut()); reject(e); } finally { setCurrentStep(inactiveStep); registeringOnAuthoritativeKeyserverRef.current = false; } })(); - }, [currentStep, isRegisteredOnIdentity, keyserverAuth, dispatch]); + }, [ + currentStep, + isRegisteredOnIdentity, + keyserverAuth, + dispatch, + dispatchActionPromise, + identityLogOut, + ]); return returnedFunc; } diff --git a/native/account/registration/registration-server-call.js b/native/account/registration/registration-server-call.js --- a/native/account/registration/registration-server-call.js +++ b/native/account/registration/registration-server-call.js @@ -9,6 +9,8 @@ legacyKeyserverRegister, useIdentityPasswordRegister, identityRegisterActionTypes, + deleteAccountActionTypes, + useDeleteDiscardedIdentityAccount, } from 'lib/actions/user-actions.js'; import { useKeyserverAuth } from 'lib/keyserver-conn/keyserver-auth.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; @@ -314,6 +316,10 @@ !state.currentUserInfo.anonymous, ); + // We call deleteDiscardedIdentityAccount in order to reset state if identity + // registration succeeds but authoritative keyserver auth fails + const deleteDiscardedIdentityAccount = useDeleteDiscardedIdentityAccount(); + const registeringOnAuthoritativeKeyserverRef = React.useRef(false); React.useEffect(() => { if ( @@ -349,14 +355,37 @@ resolve, reject, }); - } catch (e) { - reject(e); + } catch (keyserverAuthException) { + const discardIdentityAccountPromise = (async () => { + try { + return await deleteDiscardedIdentityAccount(); + } catch (deleteException) { + Alert.alert( + 'Account created but login failed', + 'We were able to create your account, but were unable to log ' + + 'you in. Try going back to the login screen and logging in ' + + 'with your new credentials.', + ); + throw deleteException; + } + })(); + void dispatchActionPromise( + deleteAccountActionTypes, + discardIdentityAccountPromise, + ); + reject(keyserverAuthException); setCurrentStep(inactiveStep); } finally { registeringOnAuthoritativeKeyserverRef.current = false; } })(); - }, [currentStep, isRegisteredOnIdentity, keyserverAuth]); + }, [ + currentStep, + isRegisteredOnIdentity, + keyserverAuth, + dispatchActionPromise, + deleteDiscardedIdentityAccount, + ]); // STEP 3: SETTING AVATAR