diff --git a/lib/hooks/login-hooks.js b/lib/hooks/login-hooks.js index f55f0fbe0..a96d03b5e 100644 --- a/lib/hooks/login-hooks.js +++ b/lib/hooks/login-hooks.js @@ -1,197 +1,197 @@ // @flow import * as React from 'react'; import { setDataLoadedActionType } from '../actions/client-db-store-actions.js'; import { identityLogInActionTypes, useIdentityPasswordLogIn, useIdentityWalletLogIn, useIdentitySecondaryDeviceLogIn, logOutActionTypes, useIdentityLogOut, } from '../actions/user-actions.js'; import { useKeyserverAuthWithRetry } from '../keyserver-conn/keyserver-auth.js'; import { logInActionSources } from '../types/account-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector, useDispatch } from '../utils/redux-utils.js'; import { waitUntilDatabaseDeleted } from '../utils/wait-until-db-deleted.js'; // We can't just do everything in one async callback, since the server calls // would get bound to Redux state from before the login. In order to pick up the // updated CSAT and currentUserInfo from Redux, we break the login into two // steps. type CurrentStep = | { +step: 'inactive' } | { +step: 'identity_login_dispatched', +resolve: () => void, +reject: Error => void, }; const inactiveStep = { step: 'inactive' }; function useLogIn(): (identityAuthPromise: Promise) => Promise { const [currentStep, setCurrentStep] = React.useState(inactiveStep); const returnedFunc = React.useCallback( (promise: Promise) => new Promise( // eslint-disable-next-line no-async-promise-executor async (resolve, reject) => { if (currentStep.step !== 'inactive') { return; } try { await promise; setCurrentStep({ step: 'identity_login_dispatched', resolve, reject, }); } catch (e) { reject(e); } }, ), [currentStep], ); const keyserverAuth = useKeyserverAuthWithRetry(authoritativeKeyserverID()); const isRegisteredOnIdentity = useSelector( state => !!state.commServicesAccessToken && !!state.currentUserInfo && !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(); const dispatchActionPromise = useDispatchActionPromise(); React.useEffect(() => { if ( !isRegisteredOnIdentity || currentStep.step !== 'identity_login_dispatched' || registeringOnAuthoritativeKeyserverRef.current ) { return; } registeringOnAuthoritativeKeyserverRef.current = true; const { resolve, reject } = currentStep; void (async () => { try { await keyserverAuth({ authActionSource: process.env.BROWSER ? logInActionSources.keyserverAuthFromWeb : logInActionSources.keyserverAuthFromNative, setInProgress: () => {}, hasBeenCancelled: () => false, doNotRegister: false, }); dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); resolve(); } catch (e) { void dispatchActionPromise(logOutActionTypes, identityLogOut()); await waitUntilDatabaseDeleted(); reject(e); } finally { setCurrentStep(inactiveStep); registeringOnAuthoritativeKeyserverRef.current = false; } })(); }, [ currentStep, isRegisteredOnIdentity, keyserverAuth, dispatch, dispatchActionPromise, identityLogOut, ]); return returnedFunc; } function usePasswordLogIn(): ( username: string, password: string, ) => Promise { const identityPasswordLogIn = useIdentityPasswordLogIn(); const dispatchActionPromise = useDispatchActionPromise(); const identityPasswordAuth = React.useCallback( (username: string, password: string) => { const promise = identityPasswordLogIn(username, password); void dispatchActionPromise(identityLogInActionTypes, promise); return promise; }, [identityPasswordLogIn, dispatchActionPromise], ); const logIn = useLogIn(); return React.useCallback( (username: string, password: string) => logIn(identityPasswordAuth(username, password)), [logIn, identityPasswordAuth], ); } function useWalletLogIn(): ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise { const identityWalletLogIn = useIdentityWalletLogIn(); const dispatchActionPromise = useDispatchActionPromise(); const identityWalletAuth = React.useCallback( (walletAddress: string, siweMessage: string, siweSignature: string) => { const promise = identityWalletLogIn( walletAddress, siweMessage, siweSignature, ); void dispatchActionPromise(identityLogInActionTypes, promise); return promise; }, [identityWalletLogIn, dispatchActionPromise], ); const logIn = useLogIn(); return React.useCallback( (walletAddress: string, siweMessage: string, siweSignature: string) => logIn(identityWalletAuth(walletAddress, siweMessage, siweSignature)), [logIn, identityWalletAuth], ); } function useSecondaryDeviceLogIn(): (userID: string) => Promise { const identitySecondaryDeviceLogIn = useIdentitySecondaryDeviceLogIn(); const dispatchActionPromise = useDispatchActionPromise(); const identitySecondaryDeviceAuth = React.useCallback( (userID: string) => { const promise = identitySecondaryDeviceLogIn(userID); void dispatchActionPromise(identityLogInActionTypes, promise); return promise; }, [identitySecondaryDeviceLogIn, dispatchActionPromise], ); const logIn = useLogIn(); return React.useCallback( (userID: string) => logIn(identitySecondaryDeviceAuth(userID)), [logIn, identitySecondaryDeviceAuth], ); } -export { usePasswordLogIn, useWalletLogIn, useSecondaryDeviceLogIn }; +export { usePasswordLogIn, useWalletLogIn, useSecondaryDeviceLogIn, useLogIn }; diff --git a/native/account/restore.js b/native/account/restore.js index a92ce2de4..6cab97af9 100644 --- a/native/account/restore.js +++ b/native/account/restore.js @@ -1,116 +1,158 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; +import { restoreUserActionTypes } from 'lib/actions/user-actions.js'; +import { useLogIn } from 'lib/hooks/login-hooks.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import type { IdentityAuthResult, SignedDeviceList, } from 'lib/types/identity-service-types.js'; import { getConfig } from 'lib/utils/config.js'; import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import { composeRawDeviceList } from 'lib/utils/device-list-utils.js'; import { getMessageForException } from 'lib/utils/errors.js'; +import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useSelector } from 'lib/utils/redux-utils.js'; import { useClientBackup } from '../backup/use-client-backup.js'; import { commCoreModule } from '../native-modules.js'; function useRestoreProtocol(): ( // username or wallet address userIdentifier: string, // password or SIWE signature secret: string, // social proof for SIWE restore siweMessage?: string, siweSignature?: string, ) => Promise { const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'identity context not set'); const { identityClient } = identityContext; const { restoreUser } = identityClient; invariant( restoreUser, 'restoreUser() should be defined on native. ' + 'Are you calling it on a non-primary device?', ); const preRequestUserState = useSelector(state => state.currentUserInfo); const { retrieveLatestBackupInfo, getBackupUserKeys } = useClientBackup(); return React.useCallback( async ( userIdentifier: string, secret: string, siweMessage?: string, siweSignature?: string, ) => { //1. Runs Key Generation const { olmAPI } = getConfig(); await olmAPI.initializeCryptoAccount(); //2. Retrieve User Keys Backup and `userID` const { userID, backupID } = await retrieveLatestBackupInfo(userIdentifier); const { pickledAccount, pickleKey } = await getBackupUserKeys( userIdentifier, secret, backupID, ); //3. Create signed singleton device list const primaryDeviceID = await getContentSigningKey(); const initialDeviceList = composeRawDeviceList([primaryDeviceID]); const rawDeviceList = JSON.stringify(initialDeviceList); const [curPrimarySignature, lastPrimarySignature] = await Promise.all([ olmAPI.signMessage(rawDeviceList), commCoreModule.signMessageUsingAccount( rawDeviceList, pickledAccount, pickleKey, ), ]); const signedDeviceList: SignedDeviceList = { rawDeviceList, curPrimarySignature, lastPrimarySignature, }; //4. Call single RPC, this: // - runs Key Upload // - send device list to Comm // - get new CSAT const result = await restoreUser( userID, signedDeviceList, siweMessage, siweSignature, ); //5. Mark keys as published try { await olmAPI.markPrekeysAsPublished(); } catch (e) { console.log( 'Failed to mark prekeys as published:', getMessageForException(e), ); } //6. Return IdentityAuthResult result return { ...result, preRequestUserState, }; }, [ getBackupUserKeys, preRequestUserState, restoreUser, retrieveLatestBackupInfo, ], ); } -export { useRestoreProtocol }; +function useRestore(): ( + userIdentifier: string, + secret: string, + siweMessage?: string, + siweSignature?: string, +) => Promise { + const restoreProtocol = useRestoreProtocol(); + const dispatchActionPromise = useDispatchActionPromise(); + const restoreAuth = React.useCallback( + ( + userIdentifier: string, + secret: string, + siweMessage?: string, + siweSignature?: string, + ) => { + const promise = restoreProtocol( + userIdentifier, + secret, + siweMessage, + siweSignature, + ); + void dispatchActionPromise(restoreUserActionTypes, promise); + return promise; + }, + [dispatchActionPromise, restoreProtocol], + ); + + const logIn = useLogIn(); + return React.useCallback( + ( + userIdentifier: string, + secret: string, + siweMessage?: string, + siweSignature?: string, + ) => logIn(restoreAuth(userIdentifier, secret, siweMessage, siweSignature)), + [logIn, restoreAuth], + ); +} + +export { useRestore };