diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -32,6 +32,10 @@ +notificationIdentityKeys: OLMIdentityKeys, }; +export type CryptoStoreContextType = { + +getInitializedCryptoStore: () => Promise, +}; + export type IdentityKeysBlob = { +primaryIdentityPublicKeys: OLMIdentityKeys, +notificationIdentityPublicKeys: OLMIdentityKeys, diff --git a/web/account/account-hooks.js b/web/account/account-hooks.js --- a/web/account/account-hooks.js +++ b/web/account/account-hooks.js @@ -1,35 +1,161 @@ // @flow +import olm from '@commapp/olm'; +import invariant from 'invariant'; import * as React from 'react'; +import { useDispatch } from 'react-redux'; +import uuid from 'uuid'; -import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js'; +import type { + SignedIdentityKeysBlob, + CryptoStore, + IdentityKeysBlob, + CryptoStoreContextType, +} from 'lib/types/crypto-types.js'; +import { initOlm } from '../olm/olm-utils.js'; +import { setCryptoStore } from '../redux/crypto-store-reducer.js'; import { useSelector } from '../redux/redux-utils.js'; -import { getSignedIdentityKeysBlobSelector } from '../selectors/socket-selectors.js'; -function useSignedIdentityKeysBlob(): ?SignedIdentityKeysBlob { - const getSignedIdentityKeysBlob: ?() => Promise = - useSelector(getSignedIdentityKeysBlobSelector); +const CryptoStoreContext: React.Context = + React.createContext(null); - const [signedIdentityKeysBlob, setSignedIdentityKeysBlob] = - React.useState(null); +type Props = { + +children: React.Node, +}; - React.useEffect(() => { - (async () => { - if ( - getSignedIdentityKeysBlob === null || - getSignedIdentityKeysBlob === undefined - ) { - setSignedIdentityKeysBlob(null); - return; +function GetOrCreateCryptoStoreProvider(props: Props): React.Node { + const dispatch = useDispatch(); + const createCryptoStore = React.useCallback(async () => { + await initOlm(); + + const identityAccount = new olm.Account(); + identityAccount.create(); + const { ed25519: identityED25519, curve25519: identityCurve25519 } = + JSON.parse(identityAccount.identity_keys()); + + const identityAccountPicklingKey = uuid.v4(); + const pickledIdentityAccount = identityAccount.pickle( + identityAccountPicklingKey, + ); + + const notificationAccount = new olm.Account(); + notificationAccount.create(); + const { ed25519: notificationED25519, curve25519: notificationCurve25519 } = + JSON.parse(notificationAccount.identity_keys()); + + const notificationAccountPicklingKey = uuid.v4(); + const pickledNotificationAccount = notificationAccount.pickle( + notificationAccountPicklingKey, + ); + + const newCryptoStore = { + primaryAccount: { + picklingKey: identityAccountPicklingKey, + pickledAccount: pickledIdentityAccount, + }, + primaryIdentityKeys: { + ed25519: identityED25519, + curve25519: identityCurve25519, + }, + notificationAccount: { + picklingKey: notificationAccountPicklingKey, + pickledAccount: pickledNotificationAccount, + }, + notificationIdentityKeys: { + ed25519: notificationED25519, + curve25519: notificationCurve25519, + }, + }; + + dispatch({ type: setCryptoStore, payload: newCryptoStore }); + return newCryptoStore; + }, [dispatch]); + + const currentCryptoStore = useSelector(state => state.cryptoStore); + const createCryptoStorePromiseRef = React.useRef>(null); + const getCryptoStorePromise = React.useCallback(() => { + if (currentCryptoStore) { + return Promise.resolve(currentCryptoStore); + } + + const currentCreateCryptoStorePromiseRef = + createCryptoStorePromiseRef.current; + if (currentCreateCryptoStorePromiseRef) { + return currentCreateCryptoStorePromiseRef; + } + + const newCreateCryptoStorePromise = (async () => { + try { + return await createCryptoStore(); + } catch (e) { + createCryptoStorePromiseRef.current = undefined; + throw e; } - const resolvedSignedIdentityKeysBlob: SignedIdentityKeysBlob = - await getSignedIdentityKeysBlob(); - setSignedIdentityKeysBlob(resolvedSignedIdentityKeysBlob); })(); - }, [getSignedIdentityKeysBlob]); - return signedIdentityKeysBlob; + createCryptoStorePromiseRef.current = newCreateCryptoStorePromise; + return newCreateCryptoStorePromise; + }, [createCryptoStore, currentCryptoStore]); + + const isCryptoStoreSet = !!currentCryptoStore; + React.useEffect(() => { + if (!isCryptoStoreSet) { + createCryptoStorePromiseRef.current = undefined; + } + }, [isCryptoStoreSet]); + + const contextValue = React.useMemo( + () => ({ + getInitializedCryptoStore: getCryptoStorePromise, + }), + [getCryptoStorePromise], + ); + + return ( + + {props.children} + + ); +} + +function useGetOrCreateCryptoStore(): () => Promise { + const context = React.useContext(CryptoStoreContext); + invariant(context, 'CryptoStoreContext not found'); + return context.getInitializedCryptoStore; +} + +function useGetSignedIdentityKeysBlob(): () => Promise { + const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); + + return React.useCallback(async () => { + const { primaryAccount, primaryIdentityKeys, notificationIdentityKeys } = + await getOrCreateCryptoStore(); + + await initOlm(); + const primaryOLMAccount = new olm.Account(); + primaryOLMAccount.unpickle( + primaryAccount.picklingKey, + primaryAccount.pickledAccount, + ); + + const identityKeysBlob: IdentityKeysBlob = { + primaryIdentityPublicKeys: primaryIdentityKeys, + notificationIdentityPublicKeys: notificationIdentityKeys, + }; + + const payloadToBeSigned: string = JSON.stringify(identityKeysBlob); + const signedIdentityKeysBlob: SignedIdentityKeysBlob = { + payload: payloadToBeSigned, + signature: primaryOLMAccount.sign(payloadToBeSigned), + }; + + return signedIdentityKeysBlob; + }, [getOrCreateCryptoStore]); } -export { useSignedIdentityKeysBlob }; +export { + useGetSignedIdentityKeysBlob, + useGetOrCreateCryptoStore, + GetOrCreateCryptoStoreProvider, +}; diff --git a/web/account/log-in-form.react.js b/web/account/log-in-form.react.js --- a/web/account/log-in-form.react.js +++ b/web/account/log-in-form.react.js @@ -1,84 +1,31 @@ // @flow -import olm from '@commapp/olm'; import { useConnectModal } from '@rainbow-me/rainbowkit'; import * as React from 'react'; import { useDispatch } from 'react-redux'; -import uuid from 'uuid'; import { useWalletClient } from 'wagmi'; import { isDev } from 'lib/utils/dev-utils.js'; +import { useGetOrCreateCryptoStore } from './account-hooks.js'; import css from './log-in-form.css'; import SIWEButton from './siwe-button.react.js'; import SIWELoginForm from './siwe-login-form.react.js'; import TraditionalLoginForm from './traditional-login-form.react.js'; import Button from '../components/button.react.js'; import OrBreak from '../components/or-break.react.js'; -import { initOlm } from '../olm/olm-utils.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; -import { setCryptoStore } from '../redux/crypto-store-reducer.js'; -import { useSelector } from '../redux/redux-utils.js'; function LoginForm(): React.Node { const { openConnectModal } = useConnectModal(); const { data: signer } = useWalletClient(); const dispatch = useDispatch(); - const cryptoStore = useSelector(state => state.cryptoStore); + const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); React.useEffect(() => { - (async () => { - if (cryptoStore !== null && cryptoStore !== undefined) { - return; - } - await initOlm(); - - const identityAccount = new olm.Account(); - identityAccount.create(); - const { ed25519: identityED25519, curve25519: identityCurve25519 } = - JSON.parse(identityAccount.identity_keys()); - - const identityAccountPicklingKey = uuid.v4(); - const pickledIdentityAccount = identityAccount.pickle( - identityAccountPicklingKey, - ); - - const notificationAccount = new olm.Account(); - notificationAccount.create(); - const { - ed25519: notificationED25519, - curve25519: notificationCurve25519, - } = JSON.parse(notificationAccount.identity_keys()); - - const notificationAccountPicklingKey = uuid.v4(); - const pickledNotificationAccount = notificationAccount.pickle( - notificationAccountPicklingKey, - ); - - dispatch({ - type: setCryptoStore, - payload: { - primaryAccount: { - picklingKey: identityAccountPicklingKey, - pickledAccount: pickledIdentityAccount, - }, - primaryIdentityKeys: { - ed25519: identityED25519, - curve25519: identityCurve25519, - }, - notificationAccount: { - picklingKey: notificationAccountPicklingKey, - pickledAccount: pickledNotificationAccount, - }, - notificationIdentityKeys: { - ed25519: notificationED25519, - curve25519: notificationCurve25519, - }, - }, - }); - })(); - }, [dispatch, cryptoStore]); + getOrCreateCryptoStore(); + }, [getOrCreateCryptoStore]); const onQRCodeLoginButtonClick = React.useCallback(() => { dispatch({ diff --git a/web/account/siwe-login-form.react.js b/web/account/siwe-login-form.react.js --- a/web/account/siwe-login-form.react.js +++ b/web/account/siwe-login-form.react.js @@ -19,10 +19,7 @@ import stores from 'lib/facts/stores.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { LogInStartingPayload } from 'lib/types/account-types.js'; -import type { - OLMIdentityKeys, - SignedIdentityKeysBlob, -} from 'lib/types/crypto-types.js'; +import type { OLMIdentityKeys } from 'lib/types/crypto-types.js'; import { useDispatchActionPromise, useServerCall, @@ -34,7 +31,7 @@ siweMessageSigningExplanationStatements, } from 'lib/utils/siwe-utils.js'; -import { useSignedIdentityKeysBlob } from './account-hooks.js'; +import { useGetSignedIdentityKeysBlob } from './account-hooks.js'; import HeaderSeparator from './header-separator.react.js'; import css from './siwe.css'; import Button from '../components/button.react.js'; @@ -88,11 +85,11 @@ state => state.cryptoStore?.primaryIdentityKeys, ); - const signedIdentityKeysBlob: ?SignedIdentityKeysBlob = - useSignedIdentityKeysBlob(); + const getSignedIdentityKeysBlob = useGetSignedIdentityKeysBlob(); const callSIWEAuthEndpoint = React.useCallback( async (message: string, signature: string, extraInfo) => { + const signedIdentityKeysBlob = await getSignedIdentityKeysBlob(); invariant( signedIdentityKeysBlob, 'signedIdentityKeysBlob must be set in attemptSIWEAuth', @@ -115,7 +112,7 @@ throw e; } }, - [signedIdentityKeysBlob, siweAuthCall], + [getSignedIdentityKeysBlob, siweAuthCall], ); const attemptSIWEAuth = React.useCallback( @@ -186,8 +183,7 @@ if ( siweAuthLoadingStatus === 'loading' || !siweNonce || - !primaryIdentityPublicKeys || - !signedIdentityKeysBlob + !primaryIdentityPublicKeys ) { return (
diff --git a/web/account/traditional-login-form.react.js b/web/account/traditional-login-form.react.js --- a/web/account/traditional-login-form.react.js +++ b/web/account/traditional-login-form.react.js @@ -15,10 +15,9 @@ LogInStartingPayload, } from 'lib/types/account-types.js'; import { logInActionSources } from 'lib/types/account-types.js'; -import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js'; import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; -import { useSignedIdentityKeysBlob } from './account-hooks.js'; +import { useGetSignedIdentityKeysBlob } from './account-hooks.js'; import HeaderSeparator from './header-separator.react.js'; import css from './log-in-form.css'; import PasswordInput from './password-input.react.js'; @@ -36,8 +35,7 @@ const dispatchActionPromise = useDispatchActionPromise(); const modalContext = useModalContext(); - const signedIdentityKeysBlob: ?SignedIdentityKeysBlob = - useSignedIdentityKeysBlob(); + const getSignedIdentityKeysBlob = useGetSignedIdentityKeysBlob(); const usernameInputRef = React.useRef(); React.useEffect(() => { @@ -64,6 +62,7 @@ const logInAction = React.useCallback( async (extraInfo: LogInExtraInfo) => { + const signedIdentityKeysBlob = await getSignedIdentityKeysBlob(); try { invariant( signedIdentityKeysBlob, @@ -91,7 +90,7 @@ throw e; } }, - [callLogIn, modalContext, password, signedIdentityKeysBlob, username], + [callLogIn, modalContext, password, getSignedIdentityKeysBlob, username], ); const onSubmit = React.useCallback( @@ -171,11 +170,7 @@