diff --git a/lib/utils/action-utils.js b/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js similarity index 84% copy from lib/utils/action-utils.js copy to lib/keyserver-conn/call-keyserver-endpoint-provider.react.js index 03e735656..946039823 100644 --- a/lib/utils/action-utils.js +++ b/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js @@ -1,262 +1,227 @@ // @flow -import invariant from 'invariant'; import _memoize from 'lodash/memoize.js'; -import * as React from 'react'; import { createSelector } from 'reselect'; -import callServerEndpoint from './call-server-endpoint.js'; -import type { - CallServerEndpoint, - CallServerEndpointOptions, -} from './call-server-endpoint.js'; -import { useSelector, useDispatch } from './redux-utils.js'; -import { ashoatKeyserverID } from './validation-utils.js'; -import { setNewSession } from '../keyserver-conn/keyserver-conn-types.js'; +import { setNewSession } from './keyserver-conn-types.js'; import { canResolveKeyserverSessionInvalidation, resolveKeyserverSessionInvalidation, -} from '../keyserver-conn/recovery-utils.js'; -import { serverCallStateSelector } from '../selectors/server-calls.js'; +} from './recovery-utils.js'; import { logInActionSources } from '../types/account-types.js'; import type { PlatformDetails } from '../types/device-types.js'; import type { Endpoint, SocketAPIHandler } from '../types/endpoints.js'; import type { Dispatch } from '../types/redux-types.js'; import type { ClientSessionChange } from '../types/session-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; +import type { + CallServerEndpoint, + CallServerEndpointOptions, +} from '../utils/call-server-endpoint.js'; +import callServerEndpoint from '../utils/call-server-endpoint.js'; let currentlyWaitingForNewCookie = false; let serverEndpointCallsWaitingForNewCookie: (( callServerEndpoint: ?CallServerEndpoint, ) => void)[] = []; // Third param is optional and gets called with newCookie if we get a new cookie // Necessary to propagate cookie in cookieInvalidationRecovery below function bindCookieAndUtilsIntoCallServerEndpoint( params: BindServerCallsParams, ): CallServerEndpoint { const { dispatch, cookie, urlPrefix, sessionID, currentUserInfo, isSocketConnected, lastCommunicatedPlatformDetails, keyserverID, } = params; const loggedIn = !!(currentUserInfo && !currentUserInfo.anonymous && true); const boundSetNewSession = ( sessionChange: ClientSessionChange, error: ?string, ) => setNewSession( dispatch, sessionChange, { currentUserInfo, cookiesAndSessions: { [keyserverID]: { cookie, sessionID } }, }, error, undefined, keyserverID, ); const canResolveInvalidation = canResolveKeyserverSessionInvalidation(); // This function gets called before callServerEndpoint sends a request, // to make sure that we're not in the middle of trying to recover // an invalidated cookie const waitIfCookieInvalidated = () => { if (!canResolveInvalidation) { // If there is no way to resolve the session invalidation, // just let the caller callServerEndpoint instance continue return Promise.resolve(null); } if (!currentlyWaitingForNewCookie) { // Our cookie seems to be valid return Promise.resolve(null); } // Wait to run until we get our new cookie return new Promise(r => serverEndpointCallsWaitingForNewCookie.push(r), ); }; // This function is a helper for the next function defined below const attemptToResolveInvalidation = async ( sessionChange: ClientSessionChange, ) => { const newAnonymousCookie = sessionChange.cookie; const newSessionChange = await resolveKeyserverSessionInvalidation( dispatch, newAnonymousCookie, urlPrefix, logInActionSources.cookieInvalidationResolutionAttempt, keyserverID, ); currentlyWaitingForNewCookie = false; const currentWaitingCalls = serverEndpointCallsWaitingForNewCookie; serverEndpointCallsWaitingForNewCookie = []; const newCallServerEndpoint = newSessionChange ? bindCookieAndUtilsIntoCallServerEndpoint({ ...params, cookie: newSessionChange.cookie, sessionID: newSessionChange.sessionID, currentUserInfo: newSessionChange.currentUserInfo, }) : null; for (const func of currentWaitingCalls) { func(newCallServerEndpoint); } return newCallServerEndpoint; }; // If this function is called, callServerEndpoint got a response invalidating // its cookie, and is wondering if it should just like... give up? // Or if there's a chance at redemption const cookieInvalidationRecovery = (sessionChange: ClientSessionChange) => { if (!canResolveInvalidation) { // If there is no way to resolve the session invalidation, // just let the caller callServerEndpoint instance continue return Promise.resolve(null); } if (!loggedIn) { // We don't want to attempt any use native credentials of a logged out // user to log-in after a cookieInvalidation while logged out return Promise.resolve(null); } if (currentlyWaitingForNewCookie) { return new Promise(r => serverEndpointCallsWaitingForNewCookie.push(r), ); } currentlyWaitingForNewCookie = true; return attemptToResolveInvalidation(sessionChange); }; return ( endpoint: Endpoint, data: Object, options?: ?CallServerEndpointOptions, ) => callServerEndpoint( cookie, boundSetNewSession, waitIfCookieInvalidated, cookieInvalidationRecovery, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, socketAPIHandler, endpoint, data, dispatch, options, loggedIn, keyserverID, ); } export type ActionFunc = (callServerEndpoint: CallServerEndpoint) => F; -export type BindServerCall = (serverCall: ActionFunc) => F; export type BindServerCallsParams = { +dispatch: Dispatch, +cookie: ?string, +urlPrefix: string, +sessionID: ?string, +currentUserInfo: ?CurrentUserInfo, +isSocketConnected: boolean, +lastCommunicatedPlatformDetails: ?PlatformDetails, +keyserverID: string, }; // All server calls needs to include some information from the Redux state // (namely, the cookie). This information is used deep in the server call, // at the point where callServerEndpoint is called. We don't want to bother // propagating the cookie (and any future config info that callServerEndpoint // needs) through to the server calls so they can pass it to callServerEndpoint. // Instead, we "curry" the cookie onto callServerEndpoint within react-redux's // connect's mapStateToProps function, and then pass that "bound" // callServerEndpoint that no longer needs the cookie as a parameter on to // the server call. const baseCreateBoundServerCallsSelector = ( actionFunc: ActionFunc, ): (BindServerCallsParams => F) => createSelector( (state: BindServerCallsParams) => state.dispatch, (state: BindServerCallsParams) => state.cookie, (state: BindServerCallsParams) => state.urlPrefix, (state: BindServerCallsParams) => state.sessionID, (state: BindServerCallsParams) => state.currentUserInfo, (state: BindServerCallsParams) => state.isSocketConnected, (state: BindServerCallsParams) => state.lastCommunicatedPlatformDetails, (state: BindServerCallsParams) => state.keyserverID, ( dispatch: Dispatch, cookie: ?string, urlPrefix: string, sessionID: ?string, currentUserInfo: ?CurrentUserInfo, isSocketConnected: boolean, lastCommunicatedPlatformDetails: ?PlatformDetails, keyserverID: string, ) => { const boundCallServerEndpoint = bindCookieAndUtilsIntoCallServerEndpoint({ dispatch, cookie, urlPrefix, sessionID, currentUserInfo, isSocketConnected, lastCommunicatedPlatformDetails, keyserverID, }); return actionFunc(boundCallServerEndpoint); }, ); type CreateBoundServerCallsSelectorType = ( ActionFunc, ) => BindServerCallsParams => F; const createBoundServerCallsSelector: CreateBoundServerCallsSelectorType = (_memoize(baseCreateBoundServerCallsSelector): any); -function useServerCall( - serverCall: ActionFunc, - paramOverride?: ?Partial, -): F { - const dispatch = useDispatch(); - const serverCallState = useSelector( - serverCallStateSelector(ashoatKeyserverID), - ); - return React.useMemo(() => { - const { urlPrefix, isSocketConnected } = serverCallState; - invariant( - !!urlPrefix && - isSocketConnected !== undefined && - isSocketConnected !== null, - 'keyserver missing from keyserverStore', - ); - - return createBoundServerCallsSelector(serverCall)({ - ...serverCallState, - urlPrefix, - isSocketConnected, - dispatch, - ...paramOverride, - keyserverID: ashoatKeyserverID, - }); - }, [serverCall, serverCallState, dispatch, paramOverride]); -} - let socketAPIHandler: ?SocketAPIHandler = null; function registerActiveSocket(passedSocketAPIHandler: ?SocketAPIHandler) { socketAPIHandler = passedSocketAPIHandler; } export { createBoundServerCallsSelector, registerActiveSocket, - useServerCall, bindCookieAndUtilsIntoCallServerEndpoint, }; diff --git a/lib/socket/api-request-handler.react.js b/lib/socket/api-request-handler.react.js index c111e669d..f0c64f183 100644 --- a/lib/socket/api-request-handler.react.js +++ b/lib/socket/api-request-handler.react.js @@ -1,104 +1,104 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { InflightRequests } from './inflight-requests.js'; +import { registerActiveSocket } from '../keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { connectionSelector } from '../selectors/keyserver-selectors.js'; import type { APIRequest } from '../types/endpoints.js'; import { clientSocketMessageTypes, serverSocketMessageTypes, type ClientSocketMessageWithoutID, type ConnectionInfo, type APIResponseServerSocketMessage, } from '../types/socket-types.js'; -import { registerActiveSocket } from '../utils/action-utils.js'; import { SocketOffline } from '../utils/errors.js'; import { useSelector } from '../utils/redux-utils.js'; import { ashoatKeyserverID } from '../utils/validation-utils.js'; type BaseProps = { +inflightRequests: ?InflightRequests, +sendMessage: (message: ClientSocketMessageWithoutID) => number, }; type Props = { ...BaseProps, +connection: ConnectionInfo, }; class APIRequestHandler extends React.PureComponent { static isConnected(props: Props, request?: APIRequest): boolean { const { inflightRequests, connection } = props; if (!inflightRequests) { return false; } // This is a hack. We actually have a race condition between // ActivityHandler and Socket. Both of them respond to a backgrounding, but // we want ActivityHandler to go first. Once it sends its message, Socket // will wait for the response before shutting down. But if Socket starts // shutting down first, we'll have a problem. Note that this approach only // stops the race in fetchResponse below, and not in action-utils (which // happens earlier via the registerActiveSocket call below), but empirically // that hasn't been an issue. // The reason I didn't rewrite this to happen in a single component is // because I want to maintain separation of concerns. Upcoming React Hooks // will be a great way to rewrite them to be related but still separated. return ( connection.status === 'connected' || request?.endpoint === 'update_activity' ); } get registeredResponseFetcher(): ?(request: APIRequest) => Promise { return APIRequestHandler.isConnected(this.props) ? this.fetchResponse : null; } componentDidMount() { registerActiveSocket(this.registeredResponseFetcher); } componentWillUnmount() { registerActiveSocket(null); } componentDidUpdate(prevProps: Props) { const isConnected = APIRequestHandler.isConnected(this.props); const wasConnected = APIRequestHandler.isConnected(prevProps); if (isConnected !== wasConnected) { registerActiveSocket(this.registeredResponseFetcher); } } render(): React.Node { return null; } fetchResponse = async (request: APIRequest): Promise => { if (!APIRequestHandler.isConnected(this.props, request)) { throw new SocketOffline('socket_offline'); } const { inflightRequests } = this.props; invariant(inflightRequests, 'inflightRequests falsey inside fetchResponse'); const messageID = this.props.sendMessage({ type: clientSocketMessageTypes.API_REQUEST, payload: request, }); const response = await inflightRequests.fetchResponse( messageID, serverSocketMessageTypes.API_RESPONSE, ); return response.payload; }; } const ConnectedAPIRequestHandler: React.ComponentType = React.memo(function ConnectedAPIRequestHandler(props) { const connection = useSelector(connectionSelector(ashoatKeyserverID)); invariant(connection, 'keyserver missing from keyserverStore'); return ; }); export default ConnectedAPIRequestHandler; diff --git a/lib/utils/action-utils.js b/lib/utils/action-utils.js index 03e735656..1b7a370f0 100644 --- a/lib/utils/action-utils.js +++ b/lib/utils/action-utils.js @@ -1,262 +1,43 @@ // @flow import invariant from 'invariant'; -import _memoize from 'lodash/memoize.js'; import * as React from 'react'; -import { createSelector } from 'reselect'; -import callServerEndpoint from './call-server-endpoint.js'; -import type { - CallServerEndpoint, - CallServerEndpointOptions, -} from './call-server-endpoint.js'; import { useSelector, useDispatch } from './redux-utils.js'; import { ashoatKeyserverID } from './validation-utils.js'; -import { setNewSession } from '../keyserver-conn/keyserver-conn-types.js'; import { - canResolveKeyserverSessionInvalidation, - resolveKeyserverSessionInvalidation, -} from '../keyserver-conn/recovery-utils.js'; + type ActionFunc, + type BindServerCallsParams, + createBoundServerCallsSelector, +} from '../keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { serverCallStateSelector } from '../selectors/server-calls.js'; -import { logInActionSources } from '../types/account-types.js'; -import type { PlatformDetails } from '../types/device-types.js'; -import type { Endpoint, SocketAPIHandler } from '../types/endpoints.js'; -import type { Dispatch } from '../types/redux-types.js'; -import type { ClientSessionChange } from '../types/session-types.js'; -import type { CurrentUserInfo } from '../types/user-types.js'; - -let currentlyWaitingForNewCookie = false; -let serverEndpointCallsWaitingForNewCookie: (( - callServerEndpoint: ?CallServerEndpoint, -) => void)[] = []; - -// Third param is optional and gets called with newCookie if we get a new cookie -// Necessary to propagate cookie in cookieInvalidationRecovery below -function bindCookieAndUtilsIntoCallServerEndpoint( - params: BindServerCallsParams, -): CallServerEndpoint { - const { - dispatch, - cookie, - urlPrefix, - sessionID, - currentUserInfo, - isSocketConnected, - lastCommunicatedPlatformDetails, - keyserverID, - } = params; - const loggedIn = !!(currentUserInfo && !currentUserInfo.anonymous && true); - const boundSetNewSession = ( - sessionChange: ClientSessionChange, - error: ?string, - ) => - setNewSession( - dispatch, - sessionChange, - { - currentUserInfo, - cookiesAndSessions: { [keyserverID]: { cookie, sessionID } }, - }, - error, - undefined, - keyserverID, - ); - const canResolveInvalidation = canResolveKeyserverSessionInvalidation(); - // This function gets called before callServerEndpoint sends a request, - // to make sure that we're not in the middle of trying to recover - // an invalidated cookie - const waitIfCookieInvalidated = () => { - if (!canResolveInvalidation) { - // If there is no way to resolve the session invalidation, - // just let the caller callServerEndpoint instance continue - return Promise.resolve(null); - } - if (!currentlyWaitingForNewCookie) { - // Our cookie seems to be valid - return Promise.resolve(null); - } - // Wait to run until we get our new cookie - return new Promise(r => - serverEndpointCallsWaitingForNewCookie.push(r), - ); - }; - // This function is a helper for the next function defined below - const attemptToResolveInvalidation = async ( - sessionChange: ClientSessionChange, - ) => { - const newAnonymousCookie = sessionChange.cookie; - const newSessionChange = await resolveKeyserverSessionInvalidation( - dispatch, - newAnonymousCookie, - urlPrefix, - logInActionSources.cookieInvalidationResolutionAttempt, - keyserverID, - ); - - currentlyWaitingForNewCookie = false; - const currentWaitingCalls = serverEndpointCallsWaitingForNewCookie; - serverEndpointCallsWaitingForNewCookie = []; - - const newCallServerEndpoint = newSessionChange - ? bindCookieAndUtilsIntoCallServerEndpoint({ - ...params, - cookie: newSessionChange.cookie, - sessionID: newSessionChange.sessionID, - currentUserInfo: newSessionChange.currentUserInfo, - }) - : null; - for (const func of currentWaitingCalls) { - func(newCallServerEndpoint); - } - return newCallServerEndpoint; - }; - // If this function is called, callServerEndpoint got a response invalidating - // its cookie, and is wondering if it should just like... give up? - // Or if there's a chance at redemption - const cookieInvalidationRecovery = (sessionChange: ClientSessionChange) => { - if (!canResolveInvalidation) { - // If there is no way to resolve the session invalidation, - // just let the caller callServerEndpoint instance continue - return Promise.resolve(null); - } - if (!loggedIn) { - // We don't want to attempt any use native credentials of a logged out - // user to log-in after a cookieInvalidation while logged out - return Promise.resolve(null); - } - if (currentlyWaitingForNewCookie) { - return new Promise(r => - serverEndpointCallsWaitingForNewCookie.push(r), - ); - } - currentlyWaitingForNewCookie = true; - return attemptToResolveInvalidation(sessionChange); - }; - - return ( - endpoint: Endpoint, - data: Object, - options?: ?CallServerEndpointOptions, - ) => - callServerEndpoint( - cookie, - boundSetNewSession, - waitIfCookieInvalidated, - cookieInvalidationRecovery, - urlPrefix, - sessionID, - isSocketConnected, - lastCommunicatedPlatformDetails, - socketAPIHandler, - endpoint, - data, - dispatch, - options, - loggedIn, - keyserverID, - ); -} - -export type ActionFunc = (callServerEndpoint: CallServerEndpoint) => F; -export type BindServerCall = (serverCall: ActionFunc) => F; -export type BindServerCallsParams = { - +dispatch: Dispatch, - +cookie: ?string, - +urlPrefix: string, - +sessionID: ?string, - +currentUserInfo: ?CurrentUserInfo, - +isSocketConnected: boolean, - +lastCommunicatedPlatformDetails: ?PlatformDetails, - +keyserverID: string, -}; - -// All server calls needs to include some information from the Redux state -// (namely, the cookie). This information is used deep in the server call, -// at the point where callServerEndpoint is called. We don't want to bother -// propagating the cookie (and any future config info that callServerEndpoint -// needs) through to the server calls so they can pass it to callServerEndpoint. -// Instead, we "curry" the cookie onto callServerEndpoint within react-redux's -// connect's mapStateToProps function, and then pass that "bound" -// callServerEndpoint that no longer needs the cookie as a parameter on to -// the server call. -const baseCreateBoundServerCallsSelector = ( - actionFunc: ActionFunc, -): (BindServerCallsParams => F) => - createSelector( - (state: BindServerCallsParams) => state.dispatch, - (state: BindServerCallsParams) => state.cookie, - (state: BindServerCallsParams) => state.urlPrefix, - (state: BindServerCallsParams) => state.sessionID, - (state: BindServerCallsParams) => state.currentUserInfo, - (state: BindServerCallsParams) => state.isSocketConnected, - (state: BindServerCallsParams) => state.lastCommunicatedPlatformDetails, - (state: BindServerCallsParams) => state.keyserverID, - ( - dispatch: Dispatch, - cookie: ?string, - urlPrefix: string, - sessionID: ?string, - currentUserInfo: ?CurrentUserInfo, - isSocketConnected: boolean, - lastCommunicatedPlatformDetails: ?PlatformDetails, - keyserverID: string, - ) => { - const boundCallServerEndpoint = bindCookieAndUtilsIntoCallServerEndpoint({ - dispatch, - cookie, - urlPrefix, - sessionID, - currentUserInfo, - isSocketConnected, - lastCommunicatedPlatformDetails, - keyserverID, - }); - return actionFunc(boundCallServerEndpoint); - }, - ); - -type CreateBoundServerCallsSelectorType = ( - ActionFunc, -) => BindServerCallsParams => F; -const createBoundServerCallsSelector: CreateBoundServerCallsSelectorType = - (_memoize(baseCreateBoundServerCallsSelector): any); function useServerCall( serverCall: ActionFunc, paramOverride?: ?Partial, ): F { const dispatch = useDispatch(); const serverCallState = useSelector( serverCallStateSelector(ashoatKeyserverID), ); return React.useMemo(() => { const { urlPrefix, isSocketConnected } = serverCallState; invariant( !!urlPrefix && isSocketConnected !== undefined && isSocketConnected !== null, 'keyserver missing from keyserverStore', ); return createBoundServerCallsSelector(serverCall)({ ...serverCallState, urlPrefix, isSocketConnected, dispatch, ...paramOverride, keyserverID: ashoatKeyserverID, }); }, [serverCall, serverCallState, dispatch, paramOverride]); } -let socketAPIHandler: ?SocketAPIHandler = null; -function registerActiveSocket(passedSocketAPIHandler: ?SocketAPIHandler) { - socketAPIHandler = passedSocketAPIHandler; -} - -export { - createBoundServerCallsSelector, - registerActiveSocket, - useServerCall, - bindCookieAndUtilsIntoCallServerEndpoint, -}; +export { useServerCall }; diff --git a/lib/utils/keyserver-call.js b/lib/utils/keyserver-call.js index cac58aa7a..044530888 100644 --- a/lib/utils/keyserver-call.js +++ b/lib/utils/keyserver-call.js @@ -1,228 +1,230 @@ // @flow import _memoize from 'lodash/memoize.js'; import * as React from 'react'; import { createSelector } from 'reselect'; -import { bindCookieAndUtilsIntoCallServerEndpoint } from './action-utils.js'; -import type { BindServerCallsParams } from './action-utils.js'; import type { CallServerEndpoint, CallServerEndpointOptions, } from './call-server-endpoint.js'; import { promiseAll } from './promises.js'; import { useSelector, useDispatch } from './redux-utils.js'; import { useDerivedObject } from '../hooks/objects.js'; +import { + bindCookieAndUtilsIntoCallServerEndpoint, + type BindServerCallsParams, +} from '../keyserver-conn/call-keyserver-endpoint-provider.react.js'; import type { PlatformDetails } from '../types/device-types.js'; import type { Endpoint } from '../types/endpoints.js'; import type { KeyserverInfo } from '../types/keyserver-types.js'; import type { Dispatch } from '../types/redux-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; export type CallKeyserverEndpoint = ( endpoint: Endpoint, requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, options?: ?CallServerEndpointOptions, ) => Promise<{ +[keyserverID: string]: any }>; export type ActionFunc = ( callServerEndpoint: CallKeyserverEndpoint, // The second argument is only used in actions that call all keyservers, // and the request to all keyservers are exactly the same. // An example of such action is fetchEntries. allKeyserverIDs: $ReadOnlyArray, ) => Args => Promise; // _memoize memoizes the function by caching the result. // The first argument of the memoized function is used as the map cache key. const baseCreateBoundServerCallsSelector = ( keyserverID: string, ): (BindServerCallsParams => CallServerEndpoint) => createSelector( (state: BindServerCallsParams) => state.dispatch, (state: BindServerCallsParams) => state.cookie, (state: BindServerCallsParams) => state.urlPrefix, (state: BindServerCallsParams) => state.sessionID, (state: BindServerCallsParams) => state.currentUserInfo, (state: BindServerCallsParams) => state.isSocketConnected, (state: BindServerCallsParams) => state.lastCommunicatedPlatformDetails, ( dispatch: Dispatch, cookie: ?string, urlPrefix: string, sessionID: ?string, currentUserInfo: ?CurrentUserInfo, isSocketConnected: boolean, lastCommunicatedPlatformDetails: ?PlatformDetails, ) => bindCookieAndUtilsIntoCallServerEndpoint({ dispatch, cookie, urlPrefix, sessionID, currentUserInfo, isSocketConnected, lastCommunicatedPlatformDetails, keyserverID, }), ); type CreateBoundServerCallsSelectorType = ( keyserverID: string, ) => BindServerCallsParams => CallServerEndpoint; const createBoundServerCallsSelector: CreateBoundServerCallsSelectorType = _memoize(baseCreateBoundServerCallsSelector); type KeyserverInfoPartial = $ReadOnly<{ ...Partial, +urlPrefix: $PropertyType, }>; type KeyserverCallInfo = { +cookie: ?string, +urlPrefix: string, +sessionID: ?string, +isSocketConnected: boolean, +lastCommunicatedPlatformDetails: ?PlatformDetails, }; const createKeyserverCallSelector: () => KeyserverInfoPartial => KeyserverCallInfo = () => createSelector( (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.cookie, (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.urlPrefix, (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.sessionID, (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.connection?.status === 'connected', (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.lastCommunicatedPlatformDetails, ( cookie: ?string, urlPrefix: string, sessionID: ?string, isSocketConnected: boolean, lastCommunicatedPlatformDetails: ?PlatformDetails, ) => ({ cookie, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, }), ); function useKeyserverCallInfos(keyserverInfos: { +[keyserverID: string]: KeyserverInfoPartial, }): { +[keyserverID: string]: KeyserverCallInfo } { return useDerivedObject( keyserverInfos, createKeyserverCallSelector, ); } type BindCallKeyserverSelector = ( keyserverCall: ActionFunc, ) => Args => Promise; function useBindCallKeyserverEndpointSelector( dispatch: Dispatch, currentUserInfo: ?CurrentUserInfo, keyserverCallInfos: { +[keyserverID: string]: KeyserverCallInfo }, ): BindCallKeyserverSelector { return React.useMemo( () => _memoize( ( keyserverCall: ActionFunc, ): (Args => Promise) => { const callKeyserverEndpoint = ( endpoint: Endpoint, requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, options?: ?CallServerEndpointOptions, ) => { const bindCallKeyserverEndpoint = (keyserverID: string) => { const { cookie, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, } = keyserverCallInfos[keyserverID]; const boundCallServerEndpoint = createBoundServerCallsSelector( keyserverID, )({ dispatch, currentUserInfo, cookie, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, keyserverID, }); return boundCallServerEndpoint( endpoint, requests[keyserverID], options, ); }; const promises: { [string]: Promise } = {}; for (const keyserverID in requests) { promises[keyserverID] = bindCallKeyserverEndpoint(keyserverID); } return promiseAll(promises); }; const keyserverIDs = Object.keys(keyserverCallInfos); return keyserverCall(callKeyserverEndpoint, keyserverIDs); }, ), [dispatch, currentUserInfo, keyserverCallInfos], ); } export type KeyserverCallParamOverride = Partial<{ +dispatch: Dispatch, +currentUserInfo: ?CurrentUserInfo, +keyserverInfos: { +[keyserverID: string]: KeyserverInfoPartial }, }>; function useKeyserverCall( keyserverCall: ActionFunc, paramOverride?: ?KeyserverCallParamOverride, ): Args => Promise { const baseDispatch = useDispatch(); const baseCurrentUserInfo = useSelector(state => state.currentUserInfo); const keyserverInfos = useSelector( state => state.keyserverStore.keyserverInfos, ); const baseCombinedInfo = { dispatch: baseDispatch, currentUserInfo: baseCurrentUserInfo, keyserverInfos, ...paramOverride, }; const { dispatch, currentUserInfo, keyserverInfos: keyserverInfoPartials, } = baseCombinedInfo; const keyserverCallInfos = useKeyserverCallInfos(keyserverInfoPartials); const bindCallKeyserverEndpointToAction = useBindCallKeyserverEndpointSelector( dispatch, currentUserInfo, keyserverCallInfos, ); return React.useMemo( () => bindCallKeyserverEndpointToAction(keyserverCall), [bindCallKeyserverEndpointToAction, keyserverCall], ); } export { useKeyserverCall }; diff --git a/native/account/siwe-panel.react.js b/native/account/siwe-panel.react.js index dca1cb4bd..fe4fd412d 100644 --- a/native/account/siwe-panel.react.js +++ b/native/account/siwe-panel.react.js @@ -1,228 +1,226 @@ // @flow import BottomSheet from '@gorhom/bottom-sheet'; import * as React from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import WebView from 'react-native-webview'; import { getSIWENonce, getSIWENonceActionTypes, siweAuthActionTypes, } from 'lib/actions/siwe-actions.js'; +import type { BindServerCallsParams } from 'lib/keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { SIWEWebViewMessage, SIWEResult } from 'lib/types/siwe-types.js'; -import { - useServerCall, - type BindServerCallsParams, -} from 'lib/utils/action-utils.js'; +import { useServerCall } from 'lib/utils/action-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useKeyboardHeight } from '../keyboard/keyboard-hooks.js'; import { useSelector } from '../redux/redux-utils.js'; import type { BottomSheetRef } from '../types/bottom-sheet.js'; import Alert from '../utils/alert.js'; import { getContentSigningKey } from '../utils/crypto-utils.js'; import { defaultLandingURLPrefix } from '../utils/url-utils.js'; const commSIWE = `${defaultLandingURLPrefix}/siwe`; const getSIWENonceLoadingStatusSelector = createLoadingStatusSelector( getSIWENonceActionTypes, ); const siweAuthLoadingStatusSelector = createLoadingStatusSelector(siweAuthActionTypes); type WebViewMessageEvent = { +nativeEvent: { +data: string, ... }, ... }; type Props = { +onClosed: () => mixed, +onClosing: () => mixed, +onSuccessfulWalletSignature: SIWEResult => mixed, +closing: boolean, +setLoading: boolean => mixed, +keyserverCallParamOverride?: Partial, }; function SIWEPanel(props: Props): React.Node { const dispatchActionPromise = useDispatchActionPromise(); const getSIWENonceCall = useServerCall( getSIWENonce, props.keyserverCallParamOverride, ); const getSIWENonceCallFailed = useSelector( state => getSIWENonceLoadingStatusSelector(state) === 'error', ); const { onClosing } = props; React.useEffect(() => { if (getSIWENonceCallFailed) { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: onClosing }], { cancelable: false }, ); } }, [getSIWENonceCallFailed, onClosing]); const siweAuthCallLoading = useSelector( state => siweAuthLoadingStatusSelector(state) === 'loading', ); const [nonce, setNonce] = React.useState(null); const [primaryIdentityPublicKey, setPrimaryIdentityPublicKey] = React.useState(null); React.useEffect(() => { void (async () => { void dispatchActionPromise( getSIWENonceActionTypes, (async () => { const response = await getSIWENonceCall(); setNonce(response); })(), ); const ed25519 = await getContentSigningKey(); setPrimaryIdentityPublicKey(ed25519); })(); }, [dispatchActionPromise, getSIWENonceCall]); const [isLoading, setLoading] = React.useState(true); const [walletConnectModalHeight, setWalletConnectModalHeight] = React.useState(0); const insets = useSafeAreaInsets(); const keyboardHeight = useKeyboardHeight(); const bottomInset = insets.bottom; const snapPoints = React.useMemo(() => { if (isLoading) { return [1]; } else if (walletConnectModalHeight) { const baseHeight = bottomInset + walletConnectModalHeight + keyboardHeight; if (baseHeight < 400) { return [baseHeight - 10]; } else { return [baseHeight + 5]; } } else { const baseHeight = bottomInset + keyboardHeight; return [baseHeight + 435, baseHeight + 600]; } }, [isLoading, walletConnectModalHeight, bottomInset, keyboardHeight]); const bottomSheetRef = React.useRef(); const snapToIndex = bottomSheetRef.current?.snapToIndex; React.useEffect(() => { // When the snapPoints change, always reset to the first one // Without this, when we close the WalletConnect modal we don't resize snapToIndex?.(0); }, [snapToIndex, snapPoints]); const closeBottomSheet = bottomSheetRef.current?.close; const { closing, onSuccessfulWalletSignature } = props; const handleMessage = React.useCallback( async (event: WebViewMessageEvent) => { const data: SIWEWebViewMessage = JSON.parse(event.nativeEvent.data); if (data.type === 'siwe_success') { const { address, message, signature } = data; if (address && signature) { closeBottomSheet?.(); await onSuccessfulWalletSignature({ address, message, signature }); } } else if (data.type === 'siwe_closed') { onClosing(); closeBottomSheet?.(); } else if (data.type === 'walletconnect_modal_update') { const height = data.state === 'open' ? data.height : 0; setWalletConnectModalHeight(height); } }, [onSuccessfulWalletSignature, onClosing, closeBottomSheet], ); const prevClosingRef = React.useRef(); React.useEffect(() => { if (closing && !prevClosingRef.current) { closeBottomSheet?.(); } prevClosingRef.current = closing; }, [closing, closeBottomSheet]); const source = React.useMemo( () => ({ uri: commSIWE, headers: { 'siwe-nonce': nonce, 'siwe-primary-identity-public-key': primaryIdentityPublicKey, }, }), [nonce, primaryIdentityPublicKey], ); const onWebViewLoaded = React.useCallback(() => { setLoading(false); }, []); const walletConnectModalOpen = walletConnectModalHeight !== 0; const backgroundStyle = React.useMemo( () => ({ backgroundColor: walletConnectModalOpen ? '#3396ff' : '#242529', }), [walletConnectModalOpen], ); const bottomSheetHandleIndicatorStyle = React.useMemo( () => ({ backgroundColor: 'white', }), [], ); const { onClosed } = props; const onBottomSheetChange = React.useCallback( (index: number) => { if (index === -1) { onClosed(); } }, [onClosed], ); let bottomSheet; if (nonce && primaryIdentityPublicKey) { bottomSheet = ( ); } const setLoadingProp = props.setLoading; const loading = !getSIWENonceCallFailed && (isLoading || siweAuthCallLoading); React.useEffect(() => { setLoadingProp(loading); }, [setLoadingProp, loading]); return bottomSheet; } export default SIWEPanel;