diff --git a/lib/selectors/account-selectors.js b/lib/selectors/account-selectors.js index 4b18040d1..0040963aa 100644 --- a/lib/selectors/account-selectors.js +++ b/lib/selectors/account-selectors.js @@ -1,96 +1,96 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { cookieSelector, sessionIDSelector, deviceTokensSelector, } from './keyserver-selectors.js'; import { currentCalendarQuery } from './nav-selectors.js'; import type { LogInExtraInfo } from '../types/account-types.js'; import type { CalendarQuery } from '../types/entry-types.js'; import type { KeyserverInfos } from '../types/keyserver-types.js'; import type { AppState } from '../types/redux-types.js'; import type { PreRequestUserState, PreRequestUserKeyserverSessionInfo, } from '../types/session-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; const logInExtraInfoSelector: ( state: AppState, ) => (calendarActive: boolean) => LogInExtraInfo = createSelector( deviceTokensSelector, currentCalendarQuery, ( deviceTokens: { +[keyserverID: string]: ?string }, calendarQuery: (calendarActive: boolean) => CalendarQuery, ) => { const deviceTokenUpdateRequest = {}; for (const keyserverID in deviceTokens) { if (deviceTokens[keyserverID]) { deviceTokenUpdateRequest[keyserverID] = { deviceToken: deviceTokens[keyserverID], }; } } // Return a function since we depend on the time of evaluation return (calendarActive: boolean): LogInExtraInfo => ({ calendarQuery: calendarQuery(calendarActive), deviceTokenUpdateRequest, }); }, ); const basePreRequestUserStateForSingleKeyserverSelector: ( keyserverID: string, ) => (state: AppState) => PreRequestUserState = keyserverID => createSelector( (state: AppState) => state.currentUserInfo, - cookieSelector, + cookieSelector(keyserverID), sessionIDSelector(keyserverID), ( currentUserInfo: ?CurrentUserInfo, cookie: ?string, sessionID: ?string, ) => ({ currentUserInfo, cookiesAndSessions: { [keyserverID]: { cookie, sessionID } }, }), ); const preRequestUserStateForSingleKeyserverSelector: ( keyserverID: string, ) => (state: AppState) => PreRequestUserState = _memoize( basePreRequestUserStateForSingleKeyserverSelector, ); const preRequestUserStateSelector: (state: AppState) => PreRequestUserState = createSelector( (state: AppState) => state.currentUserInfo, (state: AppState) => state.keyserverStore.keyserverInfos, (currentUserInfo: ?CurrentUserInfo, keyserverInfos: KeyserverInfos) => { const cookiesAndSessions: { [string]: PreRequestUserKeyserverSessionInfo, } = {}; for (const keyserverID in keyserverInfos) { cookiesAndSessions[keyserverID] = { cookie: keyserverInfos[keyserverID].cookie, sessionID: keyserverInfos[keyserverID].sessionID, }; } return { currentUserInfo, cookiesAndSessions, }; }, ); export { logInExtraInfoSelector, preRequestUserStateForSingleKeyserverSelector, preRequestUserStateSelector, }; diff --git a/lib/selectors/keyserver-selectors.js b/lib/selectors/keyserver-selectors.js index 6764b47c1..4811dd33b 100644 --- a/lib/selectors/keyserver-selectors.js +++ b/lib/selectors/keyserver-selectors.js @@ -1,147 +1,151 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import type { PlatformDetails } from '../types/device-types'; import type { KeyserverInfo, KeyserverInfos, SelectedKeyserverInfo, } from '../types/keyserver-types'; import type { AppState } from '../types/redux-types.js'; import type { ConnectionInfo } from '../types/socket-types.js'; import type { UserInfos } from '../types/user-types.js'; -import { ashoatKeyserverID } from '../utils/validation-utils.js'; -const cookieSelector: (state: AppState) => ?string = (state: AppState) => - state.keyserverStore.keyserverInfos[ashoatKeyserverID]?.cookie; +const baseCookieSelector: ( + keyserverID: string, +) => (state: AppState) => ?string = keyserverID => (state: AppState) => + state.keyserverStore.keyserverInfos[keyserverID]?.cookie; + +const cookieSelector: (keyserverID: string) => (state: AppState) => ?string = + _memoize(baseCookieSelector); const cookiesSelector: (state: AppState) => { +[keyserverID: string]: string, } = createSelector( (state: AppState) => state.keyserverStore.keyserverInfos, (infos: { +[key: string]: KeyserverInfo }) => { const cookies = {}; for (const keyserverID in infos) { cookies[keyserverID] = infos[keyserverID].cookie; } return cookies; }, ); const baseSessionIDSelector: ( keyserverID: string, ) => (state: AppState) => ?string = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.sessionID; const sessionIDSelector: (keyserverID: string) => (state: AppState) => ?string = _memoize(baseSessionIDSelector); const baseUpdatesCurrentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.updatesCurrentAsOf ?? 0; const updatesCurrentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = _memoize(baseUpdatesCurrentAsOfSelector); const baseCurrentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = keyserverID => (state: AppState) => state.messageStore.currentAsOf[keyserverID] ?? 0; const currentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = _memoize(baseCurrentAsOfSelector); const baseUrlPrefixSelector: ( keyserverID: string, ) => (state: AppState) => ?string = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.urlPrefix; const urlPrefixSelector: (keyserverID: string) => (state: AppState) => ?string = _memoize(baseUrlPrefixSelector); const baseConnectionSelector: ( keyserverID: string, ) => (state: AppState) => ?ConnectionInfo = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.connection; const connectionSelector: ( keyserverID: string, ) => (state: AppState) => ?ConnectionInfo = _memoize(baseConnectionSelector); const baseLastCommunicatedPlatformDetailsSelector: ( keyserverID: string, ) => (state: AppState) => ?PlatformDetails = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID] ?.lastCommunicatedPlatformDetails; const lastCommunicatedPlatformDetailsSelector: ( keyserverID: string, ) => (state: AppState) => ?PlatformDetails = _memoize( baseLastCommunicatedPlatformDetailsSelector, ); const selectedKeyserversSelector: ( state: AppState, ) => $ReadOnlyArray = createSelector( (state: AppState) => state.keyserverStore.keyserverInfos, (state: AppState) => state.userStore.userInfos, (keyserverInfos: KeyserverInfos, userInfos: UserInfos) => { const result = []; for (const key in keyserverInfos) { const keyserverInfo = keyserverInfos[key]; const keyserverAdminUsername = userInfos[key]?.username; if (!keyserverAdminUsername) { continue; } result.push({ keyserverAdminUsername, keyserverInfo, }); } return result; }, ); const deviceTokensSelector: (state: AppState) => { +[keyserverID: string]: ?string, } = createSelector( (state: AppState) => state.keyserverStore.keyserverInfos, (infos: { +[key: string]: KeyserverInfo }) => { const deviceTokens = {}; for (const keyserverID in infos) { deviceTokens[keyserverID] = infos[keyserverID].deviceToken; } return deviceTokens; }, ); const baseDeviceTokenSelector: ( keyserverID: string, ) => (state: AppState) => ?string = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.deviceToken; const deviceTokenSelector: ( keyserverID: string, ) => (state: AppState) => ?string = _memoize(baseDeviceTokenSelector); export { cookieSelector, cookiesSelector, sessionIDSelector, updatesCurrentAsOfSelector, currentAsOfSelector, urlPrefixSelector, connectionSelector, lastCommunicatedPlatformDetailsSelector, deviceTokensSelector, deviceTokenSelector, selectedKeyserversSelector, }; diff --git a/lib/selectors/server-calls.js b/lib/selectors/server-calls.js index e96c584cb..d7a630fe1 100644 --- a/lib/selectors/server-calls.js +++ b/lib/selectors/server-calls.js @@ -1,63 +1,63 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { cookieSelector, sessionIDSelector, urlPrefixSelector, connectionSelector, lastCommunicatedPlatformDetailsSelector, } from './keyserver-selectors.js'; import type { PlatformDetails } from '../types/device-types.js'; import type { AppState } from '../types/redux-types.js'; import type { ConnectionInfo, ConnectionStatus, } from '../types/socket-types.js'; import { type CurrentUserInfo } from '../types/user-types.js'; export type ServerCallState = { +cookie: ?string, +urlPrefix: ?string, +sessionID: ?string, +currentUserInfo: ?CurrentUserInfo, +connectionStatus: ?ConnectionStatus, +lastCommunicatedPlatformDetails: ?PlatformDetails, }; const baseServerCallStateSelector: ( keyserverID: string, ) => (state: AppState) => ServerCallState = keyserverID => createSelector( - cookieSelector, + cookieSelector(keyserverID), urlPrefixSelector(keyserverID), sessionIDSelector(keyserverID), (state: AppState) => state.currentUserInfo, connectionSelector(keyserverID), lastCommunicatedPlatformDetailsSelector(keyserverID), ( cookie: ?string, urlPrefix: ?string, sessionID: ?string, currentUserInfo: ?CurrentUserInfo, connectionInfo: ?ConnectionInfo, lastCommunicatedPlatformDetails: ?PlatformDetails, ) => ({ cookie, urlPrefix, sessionID, currentUserInfo, connectionStatus: connectionInfo?.status, lastCommunicatedPlatformDetails, }), ); const serverCallStateSelector: ( keyserverID: string, ) => (state: AppState) => ServerCallState = _memoize( baseServerCallStateSelector, ); export { serverCallStateSelector }; diff --git a/lib/shared/session-utils.js b/lib/shared/session-utils.js index e23f9ad53..89f7f3ea2 100644 --- a/lib/shared/session-utils.js +++ b/lib/shared/session-utils.js @@ -1,63 +1,63 @@ // @flow import { cookieSelector, sessionIDSelector, } from '../selectors/keyserver-selectors.js'; import { logInActionSources, type LogInActionSource, } from '../types/account-types.js'; import type { AppState } from '../types/redux-types.js'; import type { PreRequestUserState } from '../types/session-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; import { ashoatKeyserverID } from '../utils/validation-utils.js'; function invalidSessionDowngrade( currentReduxState: AppState, actionCurrentUserInfo: ?CurrentUserInfo, preRequestUserState: ?PreRequestUserState, ): boolean { // If this action represents a session downgrade - oldState has a loggedIn // currentUserInfo, but the action has an anonymous one - then it is only // valid if the currentUserInfo used for the request matches what oldState // currently has. If the currentUserInfo in Redux has changed since the // request, and is currently loggedIn, then the session downgrade does not // apply to it. In this case we will simply swallow the action. const currentCurrentUserInfo = currentReduxState.currentUserInfo; return !!( currentCurrentUserInfo && !currentCurrentUserInfo.anonymous && // Note that an undefined actionCurrentUserInfo represents an action that // doesn't affect currentUserInfo, whereas a null one represents an action // that sets it to null (actionCurrentUserInfo === null || (actionCurrentUserInfo && actionCurrentUserInfo.anonymous)) && preRequestUserState && (preRequestUserState.currentUserInfo?.id !== currentCurrentUserInfo.id || preRequestUserState.cookiesAndSessions[ashoatKeyserverID].cookie !== - cookieSelector(currentReduxState) || + cookieSelector(ashoatKeyserverID)(currentReduxState) || preRequestUserState.cookiesAndSessions[ashoatKeyserverID].sessionID !== sessionIDSelector(ashoatKeyserverID)(currentReduxState)) ); } function invalidSessionRecovery( currentReduxState: AppState, actionCurrentUserInfo: CurrentUserInfo, logInActionSource: ?LogInActionSource, ): boolean { if ( logInActionSource !== logInActionSources.cookieInvalidationResolutionAttempt && logInActionSource !== logInActionSources.socketAuthErrorResolutionAttempt ) { return false; } return ( !currentReduxState.dataLoaded || currentReduxState.currentUserInfo?.id !== actionCurrentUserInfo.id ); } export { invalidSessionDowngrade, invalidSessionRecovery }; diff --git a/native/account/logged-out-modal.react.js b/native/account/logged-out-modal.react.js index 0ba08e82f..d7c774d27 100644 --- a/native/account/logged-out-modal.react.js +++ b/native/account/logged-out-modal.react.js @@ -1,843 +1,843 @@ // @flow import Icon from '@expo/vector-icons/FontAwesome.js'; import invariant from 'invariant'; import _isEqual from 'lodash/fp/isEqual.js'; import * as React from 'react'; import { View, Text, TouchableOpacity, Image, Keyboard, Platform, BackHandler, ActivityIndicator, } from 'react-native'; import Animated, { EasingNode } from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useDispatch } from 'react-redux'; import { resetUserStateActionType } from 'lib/actions/user-actions.js'; import { cookieSelector, urlPrefixSelector, } from 'lib/selectors/keyserver-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { logInActionSources } from 'lib/types/account-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { fetchNewCookieFromNativeCredentials } from 'lib/utils/action-utils.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { splashBackgroundURI } from './background-info.js'; import FullscreenSIWEPanel from './fullscreen-siwe-panel.react.js'; import LogInPanel from './log-in-panel.react.js'; import type { LogInState } from './log-in-panel.react.js'; import LoggedOutStaffInfo from './logged-out-staff-info.react.js'; import RegisterPanel from './register-panel.react.js'; import type { RegisterState } from './register-panel.react.js'; import KeyboardAvoidingView from '../components/keyboard-avoiding-view.react.js'; import ConnectedStatusBar from '../connected-status-bar.react.js'; import { addKeyboardShowListener, addKeyboardDismissListener, removeKeyboardListener, } from '../keyboard/keyboard.js'; import { createIsForegroundSelector } from '../navigation/nav-selectors.js'; import { NavContext } from '../navigation/navigation-context.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import { type NavigationRoute, LoggedOutModalRouteName, RegistrationRouteName, QRCodeSignInNavigatorRouteName, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { usePersistedStateLoaded } from '../selectors/app-state-selectors.js'; import { type DerivedDimensionsInfo, derivedDimensionsInfoSelector, } from '../selectors/dimensions-selectors.js'; import { splashStyleSelector } from '../splash.js'; import { useStyles } from '../themes/colors.js'; import type { EventSubscription, KeyboardEvent, } from '../types/react-native.js'; import type { ImageStyle } from '../types/styles.js'; import { runTiming, ratchetAlongWithKeyboardHeight, } from '../utils/animation-utils.js'; import { useInitialNotificationsEncryptedMessage } from '../utils/crypto-utils.js'; import { type StateContainer, type StateChange, setStateForContainer, } from '../utils/state-container.js'; import EthereumLogo from '../vectors/ethereum-logo.react.js'; let initialAppLoad = true; const safeAreaEdges = ['top', 'bottom']; /* eslint-disable import/no-named-as-default-member */ const { Value, Node, Clock, block, set, call, cond, not, and, eq, neq, lessThan, greaterOrEq, add, sub, divide, max, stopClock, clockRunning, } = Animated; /* eslint-enable import/no-named-as-default-member */ export type LoggedOutMode = | 'loading' | 'prompt' | 'log-in' | 'register' | 'siwe'; const modeNumbers: { [LoggedOutMode]: number } = { 'loading': 0, 'prompt': 1, 'log-in': 2, 'register': 3, 'siwe': 4, }; function isPastPrompt(modeValue: Node) { return and( neq(modeValue, modeNumbers['loading']), neq(modeValue, modeNumbers['prompt']), ); } type BaseProps = { +navigation: RootNavigationProp<'LoggedOutModal'>, +route: NavigationRoute<'LoggedOutModal'>, }; type Props = { ...BaseProps, // Navigation state +isForeground: boolean, // Redux state +persistedStateLoaded: boolean, +rehydrateConcluded: boolean, +cookie: ?string, +urlPrefix: string, +loggedIn: boolean, +dimensions: DerivedDimensionsInfo, +splashStyle: ImageStyle, +styles: typeof unboundStyles, // Redux dispatch functions +dispatch: Dispatch, // Keyserver olm sessions functions +getInitialNotificationsEncryptedMessage: () => Promise, }; type State = { +mode: LoggedOutMode, +nextMode: LoggedOutMode, +logInState: StateContainer, +registerState: StateContainer, }; class LoggedOutModal extends React.PureComponent { keyboardShowListener: ?EventSubscription; keyboardHideListener: ?EventSubscription; mounted = false; nextMode: LoggedOutMode = 'loading'; activeAlert = false; contentHeight: Value; keyboardHeightValue = new Value(0); modeValue: Value; buttonOpacity: Value; panelPaddingTopValue: Node; panelOpacityValue: Node; constructor(props: Props) { super(props); // Man, this is a lot of boilerplate just to containerize some state. // Mostly due to Flow typing requirements... const setLogInState = setStateForContainer( this.guardedSetState, (change: $Shape) => (fullState: State) => ({ logInState: { ...fullState.logInState, state: { ...fullState.logInState.state, ...change }, }, }), ); const setRegisterState = setStateForContainer( this.guardedSetState, (change: $Shape) => (fullState: State) => ({ registerState: { ...fullState.registerState, state: { ...fullState.registerState.state, ...change }, }, }), ); const initialMode = props.persistedStateLoaded ? 'prompt' : 'loading'; this.state = { mode: initialMode, nextMode: initialMode, logInState: { state: { usernameInputText: null, passwordInputText: null, }, setState: setLogInState, }, registerState: { state: { usernameInputText: '', passwordInputText: '', confirmPasswordInputText: '', }, setState: setRegisterState, }, }; this.nextMode = initialMode; this.contentHeight = new Value(props.dimensions.safeAreaHeight); this.modeValue = new Value(modeNumbers[this.nextMode]); this.buttonOpacity = new Value(props.persistedStateLoaded ? 1 : 0); this.panelPaddingTopValue = this.panelPaddingTop(); this.panelOpacityValue = this.panelOpacity(); } guardedSetState = (change: StateChange, callback?: () => mixed) => { if (this.mounted) { this.setState(change, callback); } }; setMode(newMode: LoggedOutMode) { this.nextMode = newMode; this.guardedSetState({ mode: newMode, nextMode: newMode }); this.modeValue.setValue(modeNumbers[newMode]); } proceedToNextMode = () => { this.guardedSetState({ mode: this.nextMode }); }; componentDidMount() { this.mounted = true; if (this.props.rehydrateConcluded) { this.onInitialAppLoad(); } if (this.props.isForeground) { this.onForeground(); } } componentWillUnmount() { this.mounted = false; if (this.props.isForeground) { this.onBackground(); } } componentDidUpdate(prevProps: Props, prevState: State) { if (!prevProps.persistedStateLoaded && this.props.persistedStateLoaded) { this.setMode('prompt'); } if (!prevProps.rehydrateConcluded && this.props.rehydrateConcluded) { this.onInitialAppLoad(); } if (!prevProps.isForeground && this.props.isForeground) { this.onForeground(); } else if (prevProps.isForeground && !this.props.isForeground) { this.onBackground(); } if (this.state.mode === 'prompt' && prevState.mode !== 'prompt') { this.buttonOpacity.setValue(0); Animated.timing(this.buttonOpacity, { easing: EasingNode.out(EasingNode.ease), duration: 250, toValue: 1.0, }).start(); } const newContentHeight = this.props.dimensions.safeAreaHeight; const oldContentHeight = prevProps.dimensions.safeAreaHeight; if (newContentHeight !== oldContentHeight) { this.contentHeight.setValue(newContentHeight); } } onForeground() { this.keyboardShowListener = addKeyboardShowListener(this.keyboardShow); this.keyboardHideListener = addKeyboardDismissListener(this.keyboardHide); BackHandler.addEventListener('hardwareBackPress', this.hardwareBack); } onBackground() { if (this.keyboardShowListener) { removeKeyboardListener(this.keyboardShowListener); this.keyboardShowListener = null; } if (this.keyboardHideListener) { removeKeyboardListener(this.keyboardHideListener); this.keyboardHideListener = null; } BackHandler.removeEventListener('hardwareBackPress', this.hardwareBack); } // This gets triggered when an app is killed and restarted // Not when it is returned from being backgrounded async onInitialAppLoad() { if (!initialAppLoad) { return; } initialAppLoad = false; const { loggedIn, cookie, urlPrefix, dispatch } = this.props; const hasUserCookie = cookie && cookie.startsWith('user='); if (loggedIn === !!hasUserCookie) { return; } if (!__DEV__) { const actionSource = loggedIn ? logInActionSources.appStartReduxLoggedInButInvalidCookie : logInActionSources.appStartCookieLoggedInButInvalidRedux; const sessionChange = await fetchNewCookieFromNativeCredentials( dispatch, cookie, urlPrefix, actionSource, ashoatKeyserverID, this.props.getInitialNotificationsEncryptedMessage, ); if ( sessionChange && sessionChange.cookie && sessionChange.cookie.startsWith('user=') ) { // success! we can expect subsequent actions to fix up the state return; } } this.props.dispatch({ type: resetUserStateActionType }); } hardwareBack = () => { if (this.nextMode !== 'prompt') { this.goBackToPrompt(); return true; } return false; }; panelPaddingTop() { const headerHeight = Platform.OS === 'ios' ? 62.33 : 58.54; const promptButtonsSize = Platform.OS === 'ios' ? 40 : 61; const logInContainerSize = 140; const registerPanelSize = Platform.OS === 'ios' ? 181 : 180; const siwePanelSize = 250; const containerSize = add( headerHeight, cond(not(isPastPrompt(this.modeValue)), promptButtonsSize, 0), cond(eq(this.modeValue, modeNumbers['log-in']), logInContainerSize, 0), cond(eq(this.modeValue, modeNumbers['register']), registerPanelSize, 0), cond(eq(this.modeValue, modeNumbers['siwe']), siwePanelSize, 0), ); const potentialPanelPaddingTop = divide( max(sub(this.contentHeight, this.keyboardHeightValue, containerSize), 0), 2, ); const panelPaddingTop = new Value(-1); const targetPanelPaddingTop = new Value(-1); const prevModeValue = new Value(modeNumbers[this.nextMode]); const clock = new Clock(); const keyboardTimeoutClock = new Clock(); return block([ cond(lessThan(panelPaddingTop, 0), [ set(panelPaddingTop, potentialPanelPaddingTop), set(targetPanelPaddingTop, potentialPanelPaddingTop), ]), cond( lessThan(this.keyboardHeightValue, 0), [ runTiming(keyboardTimeoutClock, 0, 1, true, { duration: 500 }), cond( not(clockRunning(keyboardTimeoutClock)), set(this.keyboardHeightValue, 0), ), ], stopClock(keyboardTimeoutClock), ), cond( and( greaterOrEq(this.keyboardHeightValue, 0), neq(prevModeValue, this.modeValue), ), [ stopClock(clock), cond( neq(isPastPrompt(prevModeValue), isPastPrompt(this.modeValue)), set(targetPanelPaddingTop, potentialPanelPaddingTop), ), set(prevModeValue, this.modeValue), ], ), ratchetAlongWithKeyboardHeight(this.keyboardHeightValue, [ stopClock(clock), set(targetPanelPaddingTop, potentialPanelPaddingTop), ]), cond( neq(panelPaddingTop, targetPanelPaddingTop), set( panelPaddingTop, runTiming(clock, panelPaddingTop, targetPanelPaddingTop), ), ), panelPaddingTop, ]); } panelOpacity() { const targetPanelOpacity = isPastPrompt(this.modeValue); const panelOpacity = new Value(-1); const prevPanelOpacity = new Value(-1); const prevTargetPanelOpacity = new Value(-1); const clock = new Clock(); return block([ cond(lessThan(panelOpacity, 0), [ set(panelOpacity, targetPanelOpacity), set(prevPanelOpacity, targetPanelOpacity), set(prevTargetPanelOpacity, targetPanelOpacity), ]), cond(greaterOrEq(this.keyboardHeightValue, 0), [ cond(neq(targetPanelOpacity, prevTargetPanelOpacity), [ stopClock(clock), set(prevTargetPanelOpacity, targetPanelOpacity), ]), cond( neq(panelOpacity, targetPanelOpacity), set(panelOpacity, runTiming(clock, panelOpacity, targetPanelOpacity)), ), ]), cond( and(eq(panelOpacity, 0), neq(prevPanelOpacity, 0)), call([], this.proceedToNextMode), ), set(prevPanelOpacity, panelOpacity), panelOpacity, ]); } keyboardShow = (event: KeyboardEvent) => { if ( event.startCoordinates && _isEqual(event.startCoordinates)(event.endCoordinates) ) { return; } const keyboardHeight = Platform.select({ // Android doesn't include the bottomInset in this height measurement android: event.endCoordinates.height, default: Math.max( event.endCoordinates.height - this.props.dimensions.bottomInset, 0, ), }); this.keyboardHeightValue.setValue(keyboardHeight); }; keyboardHide = () => { if (!this.activeAlert) { this.keyboardHeightValue.setValue(0); } }; setActiveAlert = (activeAlert: boolean) => { this.activeAlert = activeAlert; }; goBackToPrompt = () => { this.nextMode = 'prompt'; this.guardedSetState({ nextMode: 'prompt' }); this.keyboardHeightValue.setValue(0); this.modeValue.setValue(modeNumbers['prompt']); Keyboard.dismiss(); }; render() { const { styles } = this.props; const siweButton = ( <> Sign in with Ethereum or ); let panel = null; let buttons = null; if (this.state.mode === 'log-in') { panel = ( ); } else if (this.state.mode === 'register') { panel = ( ); } else if (this.state.mode === 'prompt') { const opacityStyle = { opacity: this.buttonOpacity }; const registerButtons = []; registerButtons.push( Register , ); if (__DEV__) { registerButtons.push( Register (new) , ); } const signInButtons = []; signInButtons.push( Sign in , ); if (__DEV__) { signInButtons.push( Sign in (QR) , ); } buttons = ( {siweButton} {signInButtons} {registerButtons} ); } else if (this.state.mode === 'loading') { panel = ( ); } const windowWidth = this.props.dimensions.width; const buttonStyle = { opacity: this.panelOpacityValue, left: windowWidth < 360 ? 28 : 40, }; const padding = { paddingTop: this.panelPaddingTopValue }; const animatedContent = ( Comm {panel} ); let siwePanel; if (this.state.mode === 'siwe') { siwePanel = ( ); } const backgroundSource = { uri: splashBackgroundURI }; return ( {animatedContent} {buttons} {siwePanel} ); } onPressSIWE = () => { this.setMode('siwe'); }; onPressLogIn = () => { if (Platform.OS !== 'ios') { // For some strange reason, iOS's password management logic doesn't // realize that the username and password fields in LogInPanel are related // if the username field gets focused on mount. To avoid this issue we // need the username and password fields to both appear on-screen before // we focus the username field. However, when we set keyboardHeightValue // to -1 here, we are telling our Reanimated logic to wait until the // keyboard appears before showing LogInPanel. Since we need LogInPanel // to appear before the username field is focused, we need to avoid this // behavior on iOS. this.keyboardHeightValue.setValue(-1); } this.setMode('log-in'); }; onPressQRCodeSignIn = () => { this.props.navigation.navigate(QRCodeSignInNavigatorRouteName); }; onPressRegister = () => { this.keyboardHeightValue.setValue(-1); this.setMode('register'); }; onPressNewRegister = () => { this.props.navigation.navigate(RegistrationRouteName); }; } const unboundStyles = { animationContainer: { flex: 1, }, backButton: { position: 'absolute', top: 13, }, button: { borderRadius: 4, marginBottom: 4, marginTop: 4, marginLeft: 4, marginRight: 4, paddingBottom: 14, paddingLeft: 18, paddingRight: 18, paddingTop: 14, flex: 1, }, buttonContainer: { bottom: 0, left: 0, marginLeft: 26, marginRight: 26, paddingBottom: 20, position: 'absolute', right: 0, }, buttonText: { fontFamily: 'OpenSans-Semibold', fontSize: 17, textAlign: 'center', }, classicAuthButton: { backgroundColor: 'purpleButton', }, classicAuthButtonText: { color: 'whiteText', }, registerButtons: { flexDirection: 'row', }, signInButtons: { flexDirection: 'row', }, container: { backgroundColor: 'transparent', flex: 1, }, header: { color: 'white', fontFamily: Platform.OS === 'ios' ? 'IBMPlexSans' : 'IBMPlexSans-Medium', fontSize: 56, fontWeight: '500', lineHeight: 66, textAlign: 'center', }, loadingIndicator: { paddingTop: 15, }, modalBackground: { bottom: 0, left: 0, position: 'absolute', right: 0, top: 0, }, siweButton: { backgroundColor: 'siweButton', flex: 1, flexDirection: 'row', justifyContent: 'center', }, siweButtonText: { color: 'siweButtonText', }, siweOr: { flex: 1, flexDirection: 'row', marginBottom: 18, marginTop: 14, }, siweOrLeftHR: { borderColor: 'logInSpacer', borderTopWidth: 1, flex: 1, marginRight: 18, marginTop: 10, }, siweOrRightHR: { borderColor: 'logInSpacer', borderTopWidth: 1, flex: 1, marginLeft: 18, marginTop: 10, }, siweOrText: { color: 'whiteText', fontSize: 17, textAlign: 'center', }, siweIcon: { paddingRight: 10, }, }; const isForegroundSelector = createIsForegroundSelector( LoggedOutModalRouteName, ); const ConnectedLoggedOutModal: React.ComponentType = React.memo(function ConnectedLoggedOutModal(props: BaseProps) { const navContext = React.useContext(NavContext); const isForeground = isForegroundSelector(navContext); const rehydrateConcluded = useSelector( state => !!(state._persist && state._persist.rehydrated && navContext), ); const persistedStateLoaded = usePersistedStateLoaded(); - const cookie = useSelector(cookieSelector); + const cookie = useSelector(cookieSelector(ashoatKeyserverID)); const urlPrefix = useSelector(urlPrefixSelector(ashoatKeyserverID)); invariant(urlPrefix, "missing urlPrefix for ashoat's keyserver"); const loggedIn = useSelector(isLoggedIn); const dimensions = useSelector(derivedDimensionsInfoSelector); const splashStyle = useSelector(splashStyleSelector); const styles = useStyles(unboundStyles); const dispatch = useDispatch(); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(); return ( ); }); export default ConnectedLoggedOutModal; diff --git a/native/data/sqlite-data-handler.js b/native/data/sqlite-data-handler.js index 2aef4fcab..50bbaa8b8 100644 --- a/native/data/sqlite-data-handler.js +++ b/native/data/sqlite-data-handler.js @@ -1,225 +1,225 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import { MediaCacheContext } from 'lib/components/media-cache-provider.react.js'; import { reportStoreOpsHandlers } from 'lib/ops/report-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { cookieSelector, urlPrefixSelector, } from 'lib/selectors/keyserver-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { logInActionSources, type LogInActionSource, } from 'lib/types/account-types.js'; import { fetchNewCookieFromNativeCredentials } from 'lib/utils/action-utils.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { filesystemMediaCache } from '../media/media-cache.js'; import { commCoreModule } from '../native-modules.js'; import { setStoreLoadedActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; import { StaffContext } from '../staff/staff-context.js'; import Alert from '../utils/alert.js'; import { useInitialNotificationsEncryptedMessage } from '../utils/crypto-utils.js'; import { isTaskCancelledError } from '../utils/error-handling.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; async function clearSensitiveData() { await commCoreModule.clearSensitiveData(); try { await filesystemMediaCache.clearCache(); } catch { throw new Error('clear_media_cache_failed'); } } function SQLiteDataHandler(): React.Node { const storeLoaded = useSelector(state => state.storeLoaded); const dispatch = useDispatch(); const rehydrateConcluded = useSelector( state => !!(state._persist && state._persist.rehydrated), ); - const cookie = useSelector(cookieSelector); + const cookie = useSelector(cookieSelector(ashoatKeyserverID)); const urlPrefix = useSelector(urlPrefixSelector(ashoatKeyserverID)); invariant(urlPrefix, "missing urlPrefix for ashoat's keyserver"); const staffCanSee = useStaffCanSee(); const { staffUserHasBeenLoggedIn } = React.useContext(StaffContext); const loggedIn = useSelector(isLoggedIn); const currentLoggedInUserID = useSelector(state => state.currentUserInfo?.anonymous ? undefined : state.currentUserInfo?.id, ); const mediaCacheContext = React.useContext(MediaCacheContext); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(); const callFetchNewCookieFromNativeCredentials = React.useCallback( async (source: LogInActionSource) => { try { await fetchNewCookieFromNativeCredentials( dispatch, cookie, urlPrefix, source, ashoatKeyserverID, getInitialNotificationsEncryptedMessage, ); dispatch({ type: setStoreLoadedActionType }); } catch (fetchCookieException) { if (staffCanSee) { Alert.alert( `Error fetching new cookie from native credentials: ${ getMessageForException(fetchCookieException) ?? '{no exception message}' }. Please kill the app.`, ); } else { commCoreModule.terminate(); } } }, [ cookie, dispatch, staffCanSee, urlPrefix, getInitialNotificationsEncryptedMessage, ], ); const callClearSensitiveData = React.useCallback( async (triggeredBy: string) => { await clearSensitiveData(); console.log(`SQLite database deletion was triggered by ${triggeredBy}`); }, [], ); const handleSensitiveData = React.useCallback(async () => { try { const databaseCurrentUserInfoID = await commCoreModule.getCurrentUserID(); if ( databaseCurrentUserInfoID && databaseCurrentUserInfoID !== currentLoggedInUserID ) { await callClearSensitiveData('change in logged-in user credentials'); } if (currentLoggedInUserID) { await commCoreModule.setCurrentUserID(currentLoggedInUserID); } } catch (e) { if (isTaskCancelledError(e)) { return; } if (__DEV__) { throw e; } console.log(e); if (e.message !== 'clear_media_cache_failed') { commCoreModule.terminate(); } } }, [callClearSensitiveData, currentLoggedInUserID]); React.useEffect(() => { if (!rehydrateConcluded) { return; } const databaseNeedsDeletion = commCoreModule.checkIfDatabaseNeedsDeletion(); if (databaseNeedsDeletion) { (async () => { try { await callClearSensitiveData('detecting corrupted database'); } catch (e) { if (__DEV__) { throw e; } console.log(e); if (e.message !== 'clear_media_cache_failed') { commCoreModule.terminate(); } } await callFetchNewCookieFromNativeCredentials( logInActionSources.corruptedDatabaseDeletion, ); })(); return; } const sensitiveDataHandled = handleSensitiveData(); if (storeLoaded) { return; } if (!loggedIn) { dispatch({ type: setStoreLoadedActionType }); return; } (async () => { await Promise.all([ sensitiveDataHandled, mediaCacheContext?.evictCache(), ]); try { const { threads, messages, drafts, messageStoreThreads, reports } = await commCoreModule.getClientDBStore(); const threadInfosFromDB = threadStoreOpsHandlers.translateClientDBData(threads); const reportsFromDb = reportStoreOpsHandlers.translateClientDBData(reports); dispatch({ type: setClientDBStoreActionType, payload: { drafts, messages, threadStore: { threadInfos: threadInfosFromDB }, currentUserID: currentLoggedInUserID, messageStoreThreads, reports: reportsFromDb, }, }); } catch (setStoreException) { if (isTaskCancelledError(setStoreException)) { dispatch({ type: setStoreLoadedActionType }); return; } if (staffCanSee) { Alert.alert( 'Error setting threadStore or messageStore', getMessageForException(setStoreException) ?? '{no exception message}', ); } await callFetchNewCookieFromNativeCredentials( logInActionSources.sqliteLoadFailure, ); } })(); }, [ currentLoggedInUserID, handleSensitiveData, loggedIn, cookie, dispatch, rehydrateConcluded, staffCanSee, storeLoaded, urlPrefix, staffUserHasBeenLoggedIn, callFetchNewCookieFromNativeCredentials, callClearSensitiveData, mediaCacheContext, ]); return null; } export { SQLiteDataHandler, clearSensitiveData }; diff --git a/native/navigation/navigation-handler.react.js b/native/navigation/navigation-handler.react.js index 46e016530..4dcc7695f 100644 --- a/native/navigation/navigation-handler.react.js +++ b/native/navigation/navigation-handler.react.js @@ -1,86 +1,87 @@ // @flow import * as React from 'react'; import { cookieSelector } from 'lib/selectors/keyserver-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; +import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { logInActionType, logOutActionType } from './action-types.js'; import ModalPruner from './modal-pruner.react.js'; import NavFromReduxHandler from './nav-from-redux-handler.react.js'; import { useIsAppLoggedIn } from './nav-selectors.js'; import { NavContext, type NavAction } from './navigation-context.js'; import PolicyAcknowledgmentHandler from './policy-acknowledgment-handler.react.js'; import ThreadScreenTracker from './thread-screen-tracker.react.js'; import DevTools from '../redux/dev-tools.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { usePersistedStateLoaded } from '../selectors/app-state-selectors.js'; const NavigationHandler: React.ComponentType<{}> = React.memo<{}>( function NavigationHandler() { const navContext = React.useContext(NavContext); const persistedStateLoaded = usePersistedStateLoaded(); const devTools = __DEV__ ? : null; if (!navContext || !persistedStateLoaded) { if (__DEV__) { return ( <> {devTools} ); } else { return null; } } const { dispatch } = navContext; return ( <> {devTools} ); }, ); NavigationHandler.displayName = 'NavigationHandler'; type LogInHandlerProps = { +dispatch: (action: NavAction) => void, }; const LogInHandler = React.memo(function LogInHandler( props: LogInHandlerProps, ) { const { dispatch } = props; const hasCurrentUserInfo = useSelector(isLoggedIn); - const cookie = useSelector(cookieSelector); + const cookie = useSelector(cookieSelector(ashoatKeyserverID)); const hasUserCookie = !!(cookie && cookie.startsWith('user=')); const loggedIn = hasCurrentUserInfo && hasUserCookie; const navLoggedIn = useIsAppLoggedIn(); const prevLoggedInRef = React.useRef(); React.useEffect(() => { if (loggedIn === prevLoggedInRef.current) { return; } prevLoggedInRef.current = loggedIn; if (loggedIn && !navLoggedIn) { dispatch({ type: (logInActionType: 'LOG_IN') }); } else if (!loggedIn && navLoggedIn) { dispatch({ type: (logOutActionType: 'LOG_OUT') }); } }, [navLoggedIn, loggedIn, dispatch]); return null; }); LogInHandler.displayName = 'LogInHandler'; export default NavigationHandler; diff --git a/native/selectors/socket-selectors.js b/native/selectors/socket-selectors.js index 289e6a450..bef5428f7 100644 --- a/native/selectors/socket-selectors.js +++ b/native/selectors/socket-selectors.js @@ -1,144 +1,151 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { cookieSelector, urlPrefixSelector, } from 'lib/selectors/keyserver-selectors.js'; import { getClientResponsesSelector, sessionStateFuncSelector, } from 'lib/selectors/socket-selectors.js'; import { createOpenSocketFunction } from 'lib/shared/socket-utils.js'; import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js'; import type { ClientServerRequest, ClientClientResponse, } from 'lib/types/request-types.js'; import type { SessionIdentification, SessionState, } from 'lib/types/session-types.js'; import type { OneTimeKeyGenerator } from 'lib/types/socket-types.js'; import { commCoreModule } from '../native-modules.js'; import { calendarActiveSelector } from '../navigation/nav-selectors.js'; import type { AppState } from '../redux/state-types.js'; import type { NavPlusRedux } from '../types/selector-types.js'; const baseOpenSocketSelector: ( keyserverID: string, ) => (state: AppState) => ?() => WebSocket = keyserverID => createSelector( urlPrefixSelector(keyserverID), // We don't actually use the cookie in the socket open function, // but we do use it in the initial message, and when the cookie changes // the socket needs to be reopened. By including the cookie here, // whenever the cookie changes this function will change, // which tells the Socket component to restart the connection. - cookieSelector, + cookieSelector(keyserverID), (urlPrefix: ?string) => { if (!urlPrefix) { return null; } return createOpenSocketFunction(urlPrefix); }, ); const openSocketSelector: ( keyserverID: string, ) => (state: AppState) => ?() => WebSocket = _memoize(baseOpenSocketSelector); +const baseSessionIdentificationSelector: ( + keyserverID: string, +) => (state: AppState) => SessionIdentification = keyserverID => + createSelector( + cookieSelector(keyserverID), + (cookie: ?string): SessionIdentification => ({ cookie }), + ); + const sessionIdentificationSelector: ( - state: AppState, -) => SessionIdentification = createSelector( - cookieSelector, - (cookie: ?string): SessionIdentification => ({ cookie }), + keyserverID: string, +) => (state: AppState) => SessionIdentification = _memoize( + baseSessionIdentificationSelector, ); function oneTimeKeyGenerator(inc: number): string { // todo replace this hard code with something like // commCoreModule.generateOneTimeKeys() let str = Date.now().toString() + '_' + inc.toString() + '_'; while (str.length < 43) { str += Math.random().toString(36).substr(2, 5); } str = str.substr(0, 43); return str; } async function getSignedIdentityKeysBlob(): Promise { await commCoreModule.initializeCryptoAccount(); const { blobPayload, signature } = await commCoreModule.getUserPublicKey(); const signedIdentityKeysBlob: SignedIdentityKeysBlob = { payload: blobPayload, signature, }; return signedIdentityKeysBlob; } type NativeGetClientResponsesSelectorInputType = { ...NavPlusRedux, getInitialNotificationsEncryptedMessage: () => Promise, }; const nativeGetClientResponsesSelector: ( input: NativeGetClientResponsesSelectorInputType, ) => ( serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = createSelector( (input: NativeGetClientResponsesSelectorInputType) => getClientResponsesSelector(input.redux), (input: NativeGetClientResponsesSelectorInputType) => calendarActiveSelector(input.navContext), (input: NativeGetClientResponsesSelectorInputType) => input.getInitialNotificationsEncryptedMessage, ( getClientResponsesFunc: ( calendarActive: boolean, oneTimeKeyGenerator: ?OneTimeKeyGenerator, getSignedIdentityKeysBlob: ?() => Promise, getInitialNotificationsEncryptedMessage: ?() => Promise, serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>, calendarActive: boolean, getInitialNotificationsEncryptedMessage: () => Promise, ) => (serverRequests: $ReadOnlyArray) => getClientResponsesFunc( calendarActive, oneTimeKeyGenerator, getSignedIdentityKeysBlob, getInitialNotificationsEncryptedMessage, serverRequests, ), ); const baseNativeSessionStateFuncSelector: ( keyserverID: string, ) => (input: NavPlusRedux) => () => SessionState = keyserverID => createSelector( (input: NavPlusRedux) => sessionStateFuncSelector(keyserverID)(input.redux), (input: NavPlusRedux) => calendarActiveSelector(input.navContext), ( sessionStateFunc: (calendarActive: boolean) => SessionState, calendarActive: boolean, ) => () => sessionStateFunc(calendarActive), ); const nativeSessionStateFuncSelector: ( keyserverID: string, ) => (input: NavPlusRedux) => () => SessionState = _memoize( baseNativeSessionStateFuncSelector, ); export { openSocketSelector, sessionIdentificationSelector, nativeGetClientResponsesSelector, nativeSessionStateFuncSelector, }; diff --git a/native/socket.react.js b/native/socket.react.js index a41704c8d..ff4637826 100644 --- a/native/socket.react.js +++ b/native/socket.react.js @@ -1,177 +1,179 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { useLogOut, logOutActionTypes } from 'lib/actions/user-actions.js'; import { preRequestUserStateForSingleKeyserverSelector } from 'lib/selectors/account-selectors.js'; import { cookieSelector, urlPrefixSelector, connectionSelector, lastCommunicatedPlatformDetailsSelector, } from 'lib/selectors/keyserver-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import Socket, { type BaseSocketProps } from 'lib/socket/socket.react.js'; import { logInActionSources } from 'lib/types/account-types.js'; import { useDispatchActionPromise, fetchNewCookieFromNativeCredentials, } from 'lib/utils/action-utils.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { InputStateContext } from './input/input-state.js'; import { activeMessageListSelector, nativeCalendarQuery, } from './navigation/nav-selectors.js'; import { NavContext } from './navigation/navigation-context.js'; import { useSelector } from './redux/redux-utils.js'; import { noDataAfterPolicyAcknowledgmentSelector } from './selectors/account-selectors.js'; import { openSocketSelector, sessionIdentificationSelector, nativeGetClientResponsesSelector, nativeSessionStateFuncSelector, } from './selectors/socket-selectors.js'; import Alert from './utils/alert.js'; import { useInitialNotificationsEncryptedMessage } from './utils/crypto-utils.js'; import { decompressMessage } from './utils/decompress.js'; const NativeSocket: React.ComponentType = React.memo(function NativeSocket(props: BaseSocketProps) { const inputState = React.useContext(InputStateContext); const navContext = React.useContext(NavContext); - const cookie = useSelector(cookieSelector); + const cookie = useSelector(cookieSelector(ashoatKeyserverID)); const urlPrefix = useSelector(urlPrefixSelector(ashoatKeyserverID)); invariant(urlPrefix, 'missing urlPrefix for given keyserver id'); const connection = useSelector(connectionSelector(ashoatKeyserverID)); invariant(connection, 'keyserver missing from keyserverStore'); const frozen = useSelector(state => state.frozen); const active = useSelector( state => isLoggedIn(state) && state.lifecycleState !== 'background', ); const noDataAfterPolicyAcknowledgment = useSelector( noDataAfterPolicyAcknowledgmentSelector(ashoatKeyserverID), ); const currentUserInfo = useSelector(state => state.currentUserInfo); const openSocket = useSelector(openSocketSelector(ashoatKeyserverID)); invariant(openSocket, 'openSocket failed to be created'); - const sessionIdentification = useSelector(sessionIdentificationSelector); + const sessionIdentification = useSelector( + sessionIdentificationSelector(ashoatKeyserverID), + ); const preRequestUserState = useSelector( preRequestUserStateForSingleKeyserverSelector(ashoatKeyserverID), ); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(); const getClientResponses = useSelector(state => nativeGetClientResponsesSelector({ redux: state, navContext, getInitialNotificationsEncryptedMessage, }), ); const sessionStateFunc = useSelector(state => nativeSessionStateFuncSelector(ashoatKeyserverID)({ redux: state, navContext, }), ); const currentCalendarQuery = useSelector(state => nativeCalendarQuery({ redux: state, navContext, }), ); const canSendReports = useSelector( state => !state.frozen && state.connectivity.hasWiFi && (!inputState || !inputState.uploadInProgress()), ); const activeThread = React.useMemo(() => { if (!active) { return null; } return activeMessageListSelector(navContext); }, [active, navContext]); const lastCommunicatedPlatformDetails = useSelector( lastCommunicatedPlatformDetailsSelector(ashoatKeyserverID), ); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const callLogOut = useLogOut(); const socketCrashLoopRecovery = React.useCallback(async () => { if (!accountHasPassword(currentUserInfo)) { dispatchActionPromise( logOutActionTypes, callLogOut(preRequestUserState), ); Alert.alert( 'Log in needed', 'After acknowledging the policies, we need you to log in to your account again', [{ text: 'OK' }], ); return; } await fetchNewCookieFromNativeCredentials( dispatch, cookie, urlPrefix, logInActionSources.refetchUserDataAfterAcknowledgment, ashoatKeyserverID, getInitialNotificationsEncryptedMessage, ); }, [ callLogOut, cookie, currentUserInfo, dispatch, dispatchActionPromise, preRequestUserState, urlPrefix, getInitialNotificationsEncryptedMessage, ]); return ( ); }); export default NativeSocket; diff --git a/web/selectors/socket-selectors.js b/web/selectors/socket-selectors.js index 00b7360e8..672650aa4 100644 --- a/web/selectors/socket-selectors.js +++ b/web/selectors/socket-selectors.js @@ -1,162 +1,162 @@ // @flow import olm from '@commapp/olm'; import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { sessionIDSelector, urlPrefixSelector, cookieSelector, } from 'lib/selectors/keyserver-selectors.js'; import { getClientResponsesSelector, sessionStateFuncSelector, } from 'lib/selectors/socket-selectors.js'; import { createOpenSocketFunction } from 'lib/shared/socket-utils.js'; import type { OLMIdentityKeys, PickledOLMAccount, SignedIdentityKeysBlob, IdentityKeysBlob, } from 'lib/types/crypto-types.js'; import type { ClientServerRequest, ClientClientResponse, } from 'lib/types/request-types.js'; import type { SessionIdentification, SessionState, } from 'lib/types/session-types.js'; import type { OneTimeKeyGenerator } from 'lib/types/socket-types.js'; import { initOlm } from '../olm/olm-utils.js'; import type { AppState } from '../redux/redux-setup.js'; const baseOpenSocketSelector: ( keyserverID: string, ) => (state: AppState) => ?() => WebSocket = keyserverID => createSelector(urlPrefixSelector(keyserverID), (urlPrefix: ?string) => { if (!urlPrefix) { return null; } return createOpenSocketFunction(urlPrefix); }); const openSocketSelector: ( keyserverID: string, ) => (state: AppState) => ?() => WebSocket = _memoize(baseOpenSocketSelector); const baseSessionIdentificationSelector: ( keyserverID: string, ) => (state: AppState) => SessionIdentification = keyserverID => createSelector( - cookieSelector, + cookieSelector(keyserverID), sessionIDSelector(keyserverID), (cookie: ?string, sessionID: ?string): SessionIdentification => ({ cookie, sessionID, }), ); const sessionIdentificationSelector: ( keyserverID: string, ) => (state: AppState) => SessionIdentification = _memoize( baseSessionIdentificationSelector, ); const getSignedIdentityKeysBlobSelector: ( state: AppState, ) => ?() => Promise = createSelector( (state: AppState) => state.cryptoStore.primaryAccount, (state: AppState) => state.cryptoStore.primaryIdentityKeys, (state: AppState) => state.cryptoStore.notificationIdentityKeys, ( primaryAccount: ?PickledOLMAccount, primaryIdentityKeys: ?OLMIdentityKeys, notificationIdentityKeys: ?OLMIdentityKeys, ) => { if (!primaryAccount || !primaryIdentityKeys || !notificationIdentityKeys) { return null; } return async () => { 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; }; }, ); const webGetClientResponsesSelector: ( state: AppState, ) => ( serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = createSelector( getClientResponsesSelector, getSignedIdentityKeysBlobSelector, (state: AppState) => state.navInfo.tab === 'calendar', ( getClientResponsesFunc: ( calendarActive: boolean, oneTimeKeyGenerator: ?OneTimeKeyGenerator, getSignedIdentityKeysBlob: ?() => Promise, getInitialNotificationsEncryptedMessage: ?() => Promise, serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>, getSignedIdentityKeysBlob: ?() => Promise, calendarActive: boolean, ) => (serverRequests: $ReadOnlyArray) => getClientResponsesFunc( calendarActive, null, getSignedIdentityKeysBlob, null, serverRequests, ), ); const baseWebSessionStateFuncSelector: ( keyserverID: string, ) => (state: AppState) => () => SessionState = keyserverID => createSelector( sessionStateFuncSelector(keyserverID), (state: AppState) => state.navInfo.tab === 'calendar', ( sessionStateFunc: (calendarActive: boolean) => SessionState, calendarActive: boolean, ) => () => sessionStateFunc(calendarActive), ); const webSessionStateFuncSelector: ( keyserverID: string, ) => (state: AppState) => () => SessionState = _memoize( baseWebSessionStateFuncSelector, ); export { openSocketSelector, sessionIdentificationSelector, getSignedIdentityKeysBlobSelector, webGetClientResponsesSelector, webSessionStateFuncSelector, }; diff --git a/web/socket.react.js b/web/socket.react.js index 5af836137..fca5d6863 100644 --- a/web/socket.react.js +++ b/web/socket.react.js @@ -1,102 +1,102 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { useLogOut } from 'lib/actions/user-actions.js'; import { preRequestUserStateForSingleKeyserverSelector } from 'lib/selectors/account-selectors.js'; import { cookieSelector, urlPrefixSelector, connectionSelector, lastCommunicatedPlatformDetailsSelector, } from 'lib/selectors/keyserver-selectors.js'; import Socket, { type BaseSocketProps } from 'lib/socket/socket.react.js'; import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { useSelector } from './redux/redux-utils.js'; import { activeThreadSelector, webCalendarQuery, } from './selectors/nav-selectors.js'; import { openSocketSelector, sessionIdentificationSelector, webGetClientResponsesSelector, webSessionStateFuncSelector, } from './selectors/socket-selectors.js'; import { decompressMessage } from './utils/decompress.js'; const WebSocket: React.ComponentType = React.memo(function WebSocket(props) { - const cookie = useSelector(cookieSelector); + const cookie = useSelector(cookieSelector(ashoatKeyserverID)); const urlPrefix = useSelector(urlPrefixSelector(ashoatKeyserverID)); invariant(urlPrefix, 'missing urlPrefix for given keyserver id'); const connection = useSelector(connectionSelector(ashoatKeyserverID)); invariant(connection, 'keyserver missing from keyserverStore'); const active = useSelector( state => !!state.currentUserInfo && !state.currentUserInfo.anonymous && state.lifecycleState !== 'background', ); const openSocket = useSelector(openSocketSelector(ashoatKeyserverID)); invariant(openSocket, 'openSocket failed to be created'); const sessionIdentification = useSelector( sessionIdentificationSelector(ashoatKeyserverID), ); const preRequestUserState = useSelector( preRequestUserStateForSingleKeyserverSelector(ashoatKeyserverID), ); const getClientResponses = useSelector(webGetClientResponsesSelector); const sessionStateFunc = useSelector( webSessionStateFuncSelector(ashoatKeyserverID), ); const currentCalendarQuery = useSelector(webCalendarQuery); const reduxActiveThread = useSelector(activeThreadSelector); const windowActive = useSelector(state => state.windowActive); const activeThread = React.useMemo(() => { if (!active || !windowActive) { return null; } return reduxActiveThread; }, [active, windowActive, reduxActiveThread]); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const callLogOut = useLogOut(); const lastCommunicatedPlatformDetails = useSelector( lastCommunicatedPlatformDetailsSelector(ashoatKeyserverID), ); return ( ); }); export default WebSocket;