diff --git a/lib/selectors/account-selectors.js b/lib/selectors/account-selectors.js index 6557ab015..b28d13176 100644 --- a/lib/selectors/account-selectors.js +++ b/lib/selectors/account-selectors.js @@ -1,99 +1,104 @@ // @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, DeviceTokenUpdateRequest, } from '../types/account-types.js'; -import type { CalendarQuery } from '../types/entry-types.js'; +import type { CalendarFilter } from '../types/filter-types.js'; import type { KeyserverInfos } from '../types/keyserver-types.js'; -import type { AppState } from '../types/redux-types.js'; +import type { BaseNavInfo } from '../types/nav-types.js'; +import type { AppState, BaseAppState } 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: { [string]: DeviceTokenUpdateRequest } = {}; +const logInExtraInfoSelector: (state: AppState) => LogInExtraInfo = + createSelector( + (state: BaseAppState<>) => state.navInfo, + (state: BaseAppState<>) => state.calendarFilters, + deviceTokensSelector, + ( + navInfo: BaseNavInfo, + calendarFilters: $ReadOnlyArray, + deviceTokens: { +[keyserverID: string]: ?string }, + ) => { + const deviceTokenUpdateRequest: { [string]: DeviceTokenUpdateRequest } = + {}; - for (const keyserverID in deviceTokens) { - if (deviceTokens[keyserverID]) { - deviceTokenUpdateRequest[keyserverID] = { - deviceToken: deviceTokens[keyserverID], - }; + 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, - }); - }, -); + return { + calendarQuery: { + startDate: navInfo.startDate, + endDate: navInfo.endDate, + filters: calendarFilters, + }, + deviceTokenUpdateRequest, + }; + }, + ); const basePreRequestUserStateForSingleKeyserverSelector: ( keyserverID: string, ) => (state: AppState) => PreRequestUserState = keyserverID => createSelector( (state: AppState) => state.currentUserInfo, 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/native/account/log-in-panel.react.js b/native/account/log-in-panel.react.js index d7485e90c..5708532cc 100644 --- a/native/account/log-in-panel.react.js +++ b/native/account/log-in-panel.react.js @@ -1,405 +1,398 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, StyleSheet, Keyboard, Platform } from 'react-native'; import Animated from 'react-native-reanimated'; import { logInActionTypes, useLogIn, getOlmSessionInitializationDataActionTypes, } from 'lib/actions/user-actions.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { validEmailRegex, oldValidUsernameRegex, } from 'lib/shared/account-utils.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import { type LogInInfo, type LogInExtraInfo, type LogInResult, type LogInStartingPayload, logInActionSources, } from 'lib/types/account-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/action-utils.js'; import { TextInput } from './modal-components.react.js'; import { fetchNativeCredentials, setNativeCredentials, } from './native-credentials.js'; import { PanelButton, Panel } from './panel-components.react.js'; import PasswordInput from './password-input.react.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; -import { NavContext } from '../navigation/navigation-context.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import type { KeyPressEvent } from '../types/react-native.js'; import { AppOutOfDateAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; import { nativeNotificationsSessionCreator } from '../utils/crypto-utils.js'; import type { StateContainer } from '../utils/state-container.js'; export type LogInState = { +usernameInputText: ?string, +passwordInputText: ?string, }; type BaseProps = { +setActiveAlert: (activeAlert: boolean) => void, +opacityValue: Animated.Node, +logInState: StateContainer, }; type Props = { ...BaseProps, +loadingStatus: LoadingStatus, +logInExtraInfo: () => Promise, +dispatchActionPromise: DispatchActionPromise, +logIn: (logInInfo: LogInInfo) => Promise, +getInitialNotificationsEncryptedMessage: () => Promise, }; class LogInPanel extends React.PureComponent { usernameInput: ?TextInput; passwordInput: ?PasswordInput; componentDidMount() { void this.attemptToFetchCredentials(); } get usernameInputText(): string { return this.props.logInState.state.usernameInputText || ''; } get passwordInputText(): string { return this.props.logInState.state.passwordInputText || ''; } async attemptToFetchCredentials() { if ( this.props.logInState.state.usernameInputText !== null && this.props.logInState.state.usernameInputText !== undefined ) { return; } const credentials = await fetchNativeCredentials(); if (!credentials) { return; } if ( this.props.logInState.state.usernameInputText !== null && this.props.logInState.state.usernameInputText !== undefined ) { return; } this.props.logInState.setState({ usernameInputText: credentials.username, passwordInputText: credentials.password, }); } render(): React.Node { return ( ); } usernameInputRef: (usernameInput: ?TextInput) => void = usernameInput => { this.usernameInput = usernameInput; if (Platform.OS === 'ios' && usernameInput) { setTimeout(() => usernameInput.focus()); } }; focusUsernameInput: () => void = () => { invariant(this.usernameInput, 'ref should be set'); this.usernameInput.focus(); }; passwordInputRef: (passwordInput: ?PasswordInput) => void = passwordInput => { this.passwordInput = passwordInput; }; focusPasswordInput: () => void = () => { invariant(this.passwordInput, 'ref should be set'); this.passwordInput.focus(); }; onChangeUsernameInputText: (text: string) => void = text => { this.props.logInState.setState({ usernameInputText: text.trim() }); }; onUsernameKeyPress: (event: KeyPressEvent) => void = event => { const { key } = event.nativeEvent; if ( key.length > 1 && key !== 'Backspace' && key !== 'Enter' && this.passwordInputText.length === 0 ) { this.focusPasswordInput(); } }; onChangePasswordInputText: (text: string) => void = text => { this.props.logInState.setState({ passwordInputText: text }); }; onSubmit: () => Promise = async () => { this.props.setActiveAlert(true); if (this.usernameInputText.search(validEmailRegex) > -1) { Alert.alert( 'Can’t log in with email', 'You need to log in with your username now', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); return; } else if (this.usernameInputText.search(oldValidUsernameRegex) === -1) { Alert.alert( 'Invalid username', 'Alphanumeric usernames only', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); return; } else if (this.passwordInputText === '') { Alert.alert( 'Empty password', 'Password cannot be empty', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); return; } Keyboard.dismiss(); const extraInfo = await this.props.logInExtraInfo(); const initialNotificationsEncryptedMessage = await this.props.getInitialNotificationsEncryptedMessage(); void this.props.dispatchActionPromise( logInActionTypes, this.logInAction({ ...extraInfo, initialNotificationsEncryptedMessage }), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); }; async logInAction(extraInfo: LogInExtraInfo): Promise { try { const result = await this.props.logIn({ ...extraInfo, username: this.usernameInputText, password: this.passwordInputText, logInActionSource: logInActionSources.logInFromNativeForm, }); this.props.setActiveAlert(false); await setNativeCredentials({ username: result.currentUserInfo.username, password: this.passwordInputText, }); return result; } catch (e) { if (e.message === 'invalid_credentials') { Alert.alert( 'Incorrect username or password', "Either that user doesn't exist, or the password is incorrect", [{ text: 'OK', onPress: this.onUnsuccessfulLoginAlertAckowledged }], { cancelable: false }, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } onUnsuccessfulLoginAlertAckowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', passwordInputText: '', }, this.focusUsernameInput, ); }; onUsernameAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', }, this.focusUsernameInput, ); }; onPasswordAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { passwordInputText: '', }, this.focusPasswordInput, ); }; onUnknownErrorAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', passwordInputText: '', }, this.focusUsernameInput, ); }; onAppOutOfDateAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); }; } export type InnerLogInPanel = LogInPanel; const styles = StyleSheet.create({ footer: { flexDirection: 'row', justifyContent: 'flex-end', }, icon: { bottom: 10, left: 4, position: 'absolute', }, input: { paddingLeft: 35, }, row: { marginHorizontal: 24, }, }); const logInLoadingStatusSelector = createLoadingStatusSelector(logInActionTypes); const olmSessionInitializationDataLoadingStatusSelector = createLoadingStatusSelector(getOlmSessionInitializationDataActionTypes); const ConnectedLogInPanel: React.ComponentType = React.memo(function ConnectedLogInPanel(props: BaseProps) { const logInLoadingStatus = useSelector(logInLoadingStatusSelector); const olmSessionInitializationDataLoadingStatus = useSelector( olmSessionInitializationDataLoadingStatusSelector, ); const loadingStatus = combineLoadingStatuses( logInLoadingStatus, olmSessionInitializationDataLoadingStatus, ); - const navContext = React.useContext(NavContext); - const logInExtraInfo = useSelector(state => - nativeLogInExtraInfoSelector({ - redux: state, - navContext, - }), - ); + const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); const dispatchActionPromise = useDispatchActionPromise(); const callLogIn = useLogIn(); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage( nativeNotificationsSessionCreator, ); return ( ); }); export default ConnectedLogInPanel; diff --git a/native/account/register-panel.react.js b/native/account/register-panel.react.js index 03225ed39..c83944787 100644 --- a/native/account/register-panel.react.js +++ b/native/account/register-panel.react.js @@ -1,511 +1,504 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View, StyleSheet, Platform, Keyboard, Linking, } from 'react-native'; import Animated from 'react-native-reanimated'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { registerActionTypes, register, getOlmSessionInitializationDataActionTypes, } from 'lib/actions/user-actions.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { validUsernameRegex } from 'lib/shared/account-utils.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import type { RegisterInfo, LogInExtraInfo, RegisterResult, LogInStartingPayload, } from 'lib/types/account-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { useServerCall, useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/action-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { TextInput } from './modal-components.react.js'; import { setNativeCredentials } from './native-credentials.js'; import { PanelButton, Panel } from './panel-components.react.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; -import { NavContext } from '../navigation/navigation-context.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import type { KeyPressEvent } from '../types/react-native.js'; import { AppOutOfDateAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; import { nativeNotificationsSessionCreator } from '../utils/crypto-utils.js'; import { type StateContainer } from '../utils/state-container.js'; type WritableRegisterState = { usernameInputText: string, passwordInputText: string, confirmPasswordInputText: string, }; export type RegisterState = $ReadOnly; type BaseProps = { +setActiveAlert: (activeAlert: boolean) => void, +opacityValue: Animated.Node, +registerState: StateContainer, }; type Props = { ...BaseProps, +loadingStatus: LoadingStatus, +logInExtraInfo: () => Promise, +dispatch: Dispatch, +dispatchActionPromise: DispatchActionPromise, +register: (registerInfo: RegisterInfo) => Promise, +getInitialNotificationsEncryptedMessage: () => Promise, }; type State = { +confirmPasswordFocused: boolean, }; class RegisterPanel extends React.PureComponent { state: State = { confirmPasswordFocused: false, }; usernameInput: ?TextInput; passwordInput: ?TextInput; confirmPasswordInput: ?TextInput; passwordBeingAutoFilled = false; render(): React.Node { let confirmPasswordTextInputExtraProps; if ( Platform.OS !== 'ios' || this.state.confirmPasswordFocused || this.props.registerState.state.confirmPasswordInputText.length > 0 ) { confirmPasswordTextInputExtraProps = { secureTextEntry: true, textContentType: 'password', }; } let onPasswordKeyPress; if (Platform.OS === 'ios') { onPasswordKeyPress = this.onPasswordKeyPress; } const privatePolicyNotice = ( By signing up, you agree to our{' '} Terms {' & '} Privacy Policy . ); return ( {privatePolicyNotice} ); } usernameInputRef = (usernameInput: ?TextInput) => { this.usernameInput = usernameInput; }; passwordInputRef = (passwordInput: ?TextInput) => { this.passwordInput = passwordInput; }; confirmPasswordInputRef = (confirmPasswordInput: ?TextInput) => { this.confirmPasswordInput = confirmPasswordInput; }; focusUsernameInput = () => { invariant(this.usernameInput, 'ref should be set'); this.usernameInput.focus(); }; focusPasswordInput = () => { invariant(this.passwordInput, 'ref should be set'); this.passwordInput.focus(); }; focusConfirmPasswordInput = () => { invariant(this.confirmPasswordInput, 'ref should be set'); this.confirmPasswordInput.focus(); }; onTermsOfUsePressed = () => { void Linking.openURL('https://comm.app/terms'); }; onPrivacyPolicyPressed = () => { void Linking.openURL('https://comm.app/privacy'); }; onChangeUsernameInputText = (text: string) => { this.props.registerState.setState({ usernameInputText: text }); }; onChangePasswordInputText = (text: string) => { const stateUpdate: Partial = {}; stateUpdate.passwordInputText = text; if (this.passwordBeingAutoFilled) { this.passwordBeingAutoFilled = false; stateUpdate.confirmPasswordInputText = text; } this.props.registerState.setState(stateUpdate); }; onPasswordKeyPress = (event: KeyPressEvent) => { const { key } = event.nativeEvent; if ( key.length > 1 && key !== 'Backspace' && key !== 'Enter' && this.props.registerState.state.confirmPasswordInputText.length === 0 ) { this.passwordBeingAutoFilled = true; } }; onChangeConfirmPasswordInputText = (text: string) => { this.props.registerState.setState({ confirmPasswordInputText: text }); }; onConfirmPasswordFocus = () => { this.setState({ confirmPasswordFocused: true }); }; onSubmit = async () => { this.props.setActiveAlert(true); if (this.props.registerState.state.passwordInputText === '') { Alert.alert( 'Empty password', 'Password cannot be empty', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); } else if ( this.props.registerState.state.passwordInputText !== this.props.registerState.state.confirmPasswordInputText ) { Alert.alert( 'Passwords don’t match', 'Password fields must contain the same password', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); } else if ( this.props.registerState.state.usernameInputText.search( validUsernameRegex, ) === -1 ) { Alert.alert( 'Invalid username', 'Usernames must be at least six characters long, start with either a ' + 'letter or a number, and may contain only letters, numbers, or the ' + 'characters “-” and “_”', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else { Keyboard.dismiss(); const extraInfo = await this.props.logInExtraInfo(); const initialNotificationsEncryptedMessage = await this.props.getInitialNotificationsEncryptedMessage(); void this.props.dispatchActionPromise( registerActionTypes, this.registerAction({ ...extraInfo, initialNotificationsEncryptedMessage, }), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); } }; onPasswordAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.registerState.setState( { passwordInputText: '', confirmPasswordInputText: '', }, () => { invariant(this.passwordInput, 'ref should exist'); this.passwordInput.focus(); }, ); }; onUsernameAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.registerState.setState( { usernameInputText: '', }, () => { invariant(this.usernameInput, 'ref should exist'); this.usernameInput.focus(); }, ); }; async registerAction(extraInfo: LogInExtraInfo): Promise { try { const result = await this.props.register({ ...extraInfo, username: this.props.registerState.state.usernameInputText, password: this.props.registerState.state.passwordInputText, }); this.props.setActiveAlert(false); this.props.dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); await setNativeCredentials({ username: result.currentUserInfo.username, password: this.props.registerState.state.passwordInputText, }); return result; } catch (e) { if (e.message === 'username_reserved') { Alert.alert( 'Username reserved', 'This username is currently reserved. Please contact support@' + 'comm.app if you would like to claim this account.', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else if (e.message === 'username_taken') { Alert.alert( 'Username taken', 'An account with that username already exists', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } onUnknownErrorAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.registerState.setState( { usernameInputText: '', passwordInputText: '', confirmPasswordInputText: '', }, () => { invariant(this.usernameInput, 'ref should exist'); this.usernameInput.focus(); }, ); }; onAppOutOfDateAlertAcknowledged = () => { this.props.setActiveAlert(false); }; } const styles = StyleSheet.create({ container: { zIndex: 2, }, footer: { alignItems: 'stretch', flexDirection: 'row', flexShrink: 1, justifyContent: 'space-between', paddingLeft: 24, }, hyperlinkText: { color: '#036AFF', fontWeight: 'bold', }, icon: { bottom: 10, left: 4, position: 'absolute', }, input: { paddingLeft: 35, }, notice: { alignSelf: 'center', display: 'flex', flexShrink: 1, maxWidth: 190, paddingBottom: 18, paddingRight: 8, paddingTop: 12, }, noticeText: { color: '#444', fontSize: 13, lineHeight: 20, textAlign: 'center', }, row: { marginHorizontal: 24, }, }); const registerLoadingStatusSelector = createLoadingStatusSelector(registerActionTypes); const olmSessionInitializationDataLoadingStatusSelector = createLoadingStatusSelector(getOlmSessionInitializationDataActionTypes); const ConnectedRegisterPanel: React.ComponentType = React.memo(function ConnectedRegisterPanel(props: BaseProps) { const registerLoadingStatus = useSelector(registerLoadingStatusSelector); const olmSessionInitializationDataLoadingStatus = useSelector( olmSessionInitializationDataLoadingStatusSelector, ); const loadingStatus = combineLoadingStatuses( registerLoadingStatus, olmSessionInitializationDataLoadingStatus, ); - const navContext = React.useContext(NavContext); - const logInExtraInfo = useSelector(state => - nativeLogInExtraInfoSelector({ - redux: state, - navContext, - }), - ); + const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const callRegister = useServerCall(register); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage( nativeNotificationsSessionCreator, ); return ( ); }); export default ConnectedRegisterPanel; diff --git a/native/account/registration/registration-server-call.js b/native/account/registration/registration-server-call.js index 4532f80f3..cbc6d43a7 100644 --- a/native/account/registration/registration-server-call.js +++ b/native/account/registration/registration-server-call.js @@ -1,228 +1,221 @@ // @flow import * as React from 'react'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { registerActionTypes, register } from 'lib/actions/user-actions.js'; import type { LogInStartingPayload } from 'lib/types/account-types.js'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { setURLPrefix } from 'lib/utils/url-utils.js'; import type { RegistrationServerCallInput, UsernameAccountSelection, AvatarData, } from './registration-types.js'; import { useNativeSetUserAvatar, useUploadSelectedMedia, } from '../../avatars/avatar-hooks.js'; -import { NavContext } from '../../navigation/navigation-context.js'; import { useSelector } from '../../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../../selectors/account-selectors.js'; import { AppOutOfDateAlertDetails } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; import { setNativeCredentials } from '../native-credentials.js'; import { useSIWEServerCall } from '../siwe-hooks.js'; // We can't just do everything in one async callback, since the server calls // would get bound to Redux state from before the registration. The registration // flow has multiple steps where critical Redux state is changed, where // subsequent steps depend on accessing the updated Redux state. // To address this, we break the registration process up into multiple steps. // When each step completes we update the currentStep state, and we have Redux // selectors that trigger useEffects for subsequent steps when relevant data // starts to appear in Redux. type CurrentStep = | { +step: 'inactive' } | { +step: 'waiting_for_registration_call', +avatarData: ?AvatarData, +resolve: () => void, +reject: Error => void, }; const inactiveStep = { step: 'inactive' }; function useRegistrationServerCall(): RegistrationServerCallInput => Promise { const [currentStep, setCurrentStep] = React.useState(inactiveStep); // STEP 1: ACCOUNT REGISTRATION - const navContext = React.useContext(NavContext); - const logInExtraInfo = useSelector(state => - nativeLogInExtraInfoSelector({ - redux: state, - navContext, - }), - ); + const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); const dispatchActionPromise = useDispatchActionPromise(); const callRegister = useServerCall(register); const registerUsernameAccount = React.useCallback( async ( accountSelection: UsernameAccountSelection, keyserverURL: string, ) => { const extraInfo = await logInExtraInfo(); const registerPromise = (async () => { try { const result = await callRegister( { ...extraInfo, username: accountSelection.username, password: accountSelection.password, }, { urlPrefixOverride: keyserverURL, }, ); await setNativeCredentials({ username: result.currentUserInfo.username, password: accountSelection.password, }); return result; } catch (e) { if (e.message === 'username_reserved') { Alert.alert( 'Username reserved', 'This username is currently reserved. Please contact support@' + 'comm.app if you would like to claim this account.', ); } else if (e.message === 'username_taken') { Alert.alert( 'Username taken', 'An account with that username already exists', ); } else if (e.message === 'client_version_unsupported') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, ); } else { Alert.alert('Unknown error', 'Uhh... try again?'); } throw e; } })(); void dispatchActionPromise( registerActionTypes, registerPromise, undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); await registerPromise; }, [logInExtraInfo, callRegister, dispatchActionPromise], ); const siweServerCall = useSIWEServerCall(); const dispatch = useDispatch(); const returnedFunc = React.useCallback( (input: RegistrationServerCallInput) => new Promise( // eslint-disable-next-line no-async-promise-executor async (resolve, reject) => { try { if (currentStep.step !== 'inactive') { return; } const { accountSelection, avatarData, keyserverURL } = input; if (accountSelection.accountType === 'username') { await registerUsernameAccount(accountSelection, keyserverURL); } else { try { await siweServerCall(accountSelection, { urlPrefixOverride: keyserverURL, }); } catch (e) { Alert.alert('Unknown error', 'Uhh... try again?'); throw e; } } dispatch({ type: setURLPrefix, payload: keyserverURL, }); setCurrentStep({ step: 'waiting_for_registration_call', avatarData, resolve, reject, }); } catch (e) { reject(e); } }, ), [currentStep, registerUsernameAccount, siweServerCall, dispatch], ); // STEP 2: SETTING AVATAR const uploadSelectedMedia = useUploadSelectedMedia(); const nativeSetUserAvatar = useNativeSetUserAvatar(); const hasCurrentUserInfo = useSelector( state => !!state.currentUserInfo && !state.currentUserInfo.anonymous, ); const avatarBeingSetRef = React.useRef(false); React.useEffect(() => { if ( !hasCurrentUserInfo || currentStep.step !== 'waiting_for_registration_call' || avatarBeingSetRef.current ) { return; } avatarBeingSetRef.current = true; const { avatarData, resolve } = currentStep; void (async () => { try { if (!avatarData) { return; } let updateUserAvatarRequest; if (!avatarData.needsUpload) { ({ updateUserAvatarRequest } = avatarData); } else { const { mediaSelection } = avatarData; updateUserAvatarRequest = await uploadSelectedMedia(mediaSelection); if (!updateUserAvatarRequest) { return; } } await nativeSetUserAvatar(updateUserAvatarRequest); } finally { dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); setCurrentStep(inactiveStep); avatarBeingSetRef.current = false; resolve(); } })(); }, [ currentStep, hasCurrentUserInfo, uploadSelectedMedia, nativeSetUserAvatar, dispatch, ]); return returnedFunc; } export { useRegistrationServerCall }; diff --git a/native/account/resolve-invalidated-cookie.js b/native/account/resolve-invalidated-cookie.js index a28cb33be..9bc432acf 100644 --- a/native/account/resolve-invalidated-cookie.js +++ b/native/account/resolve-invalidated-cookie.js @@ -1,53 +1,49 @@ // @flow import { logInActionTypes, logInRawAction } from 'lib/actions/user-actions.js'; import type { InitialNotifMessageOptions } from 'lib/shared/crypto-utils.js'; import type { LogInActionSource } from 'lib/types/account-types.js'; import type { DispatchRecoveryAttempt } from 'lib/utils/action-utils.js'; import type { CallServerEndpoint } from 'lib/utils/call-server-endpoint.js'; import type { CallKeyserverEndpoint } from 'lib/utils/keyserver-call.js'; import { fetchNativeKeychainCredentials } from './native-credentials.js'; -import { getGlobalNavContext } from '../navigation/icky-global.js'; import { store } from '../redux/redux-setup.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; async function resolveInvalidatedCookie( callServerEndpoint: CallServerEndpoint, callKeyserverEndpoint: CallKeyserverEndpoint, dispatchRecoveryAttempt: DispatchRecoveryAttempt, logInActionSource: LogInActionSource, keyserverID: string, getInitialNotificationsEncryptedMessage?: ( ?InitialNotifMessageOptions, ) => Promise, ) { const keychainCredentials = await fetchNativeKeychainCredentials(); if (!keychainCredentials) { return; } - let extraInfo = await nativeLogInExtraInfoSelector({ - redux: store.getState(), - navContext: getGlobalNavContext(), - })(); + let extraInfo = await nativeLogInExtraInfoSelector(store.getState())(); if (getInitialNotificationsEncryptedMessage) { const initialNotificationsEncryptedMessage = await getInitialNotificationsEncryptedMessage({ callServerEndpoint }); extraInfo = { ...extraInfo, initialNotificationsEncryptedMessage }; } const { calendarQuery } = extraInfo; await dispatchRecoveryAttempt( logInActionTypes, logInRawAction(callKeyserverEndpoint)({ ...keychainCredentials, ...extraInfo, logInActionSource, keyserverIDs: [keyserverID], }), { calendarQuery }, ); } export { resolveInvalidatedCookie }; diff --git a/native/account/siwe-hooks.js b/native/account/siwe-hooks.js index 9d6954c63..65a4c446b 100644 --- a/native/account/siwe-hooks.js +++ b/native/account/siwe-hooks.js @@ -1,104 +1,97 @@ // @flow import * as React from 'react'; import { siweAuth, siweAuthActionTypes } from 'lib/actions/siwe-actions.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import type { LogInStartingPayload, LogInExtraInfo, } from 'lib/types/account-types.js'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; import type { CallServerEndpointOptions } from 'lib/utils/call-server-endpoint.js'; -import { NavContext } from '../navigation/navigation-context.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import { nativeNotificationsSessionCreator } from '../utils/crypto-utils.js'; type SIWEServerCallParams = { +message: string, +signature: string, +doNotRegister?: boolean, ... }; function useSIWEServerCall(): ( SIWEServerCallParams, ?CallServerEndpointOptions, ) => Promise { const siweAuthCall = useServerCall(siweAuth); const callSIWE = React.useCallback( ( message: string, signature: string, extraInfo: $ReadOnly<{ ...LogInExtraInfo, +doNotRegister?: boolean }>, callServerEndpointOptions: ?CallServerEndpointOptions, ) => siweAuthCall( { message, signature, ...extraInfo, }, callServerEndpointOptions, ), [siweAuthCall], ); - const navContext = React.useContext(NavContext); - const logInExtraInfo = useSelector(state => - nativeLogInExtraInfoSelector({ - redux: state, - navContext, - }), - ); + const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(nativeNotificationsSessionCreator); const dispatchActionPromise = useDispatchActionPromise(); return React.useCallback( async ( { message, signature, doNotRegister }, callServerEndpointOptions, ) => { const extraInfo = await logInExtraInfo(); const initialNotificationsEncryptedMessage = await getInitialNotificationsEncryptedMessage({ callServerEndpointOptions, }); const siwePromise = callSIWE( message, signature, { ...extraInfo, initialNotificationsEncryptedMessage, doNotRegister, }, callServerEndpointOptions, ); void dispatchActionPromise( siweAuthActionTypes, siwePromise, undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); await siwePromise; }, [ logInExtraInfo, dispatchActionPromise, callSIWE, getInitialNotificationsEncryptedMessage, ], ); } export { useSIWEServerCall }; diff --git a/native/selectors/account-selectors.js b/native/selectors/account-selectors.js index 3b885b108..370784f5c 100644 --- a/native/selectors/account-selectors.js +++ b/native/selectors/account-selectors.js @@ -1,72 +1,66 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { logInExtraInfoSelector } from 'lib/selectors/account-selectors.js'; import { currentAsOfSelector } from 'lib/selectors/keyserver-selectors.js'; import type { LogInExtraInfo } from 'lib/types/account-types.js'; import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js'; import type { UserPolicies } from 'lib/types/policy-types.js'; import { values } from 'lib/utils/objects.js'; import { commCoreModule } from '../native-modules.js'; -import { calendarActiveSelector } from '../navigation/nav-selectors.js'; import type { AppState } from '../redux/state-types.js'; import type { ConnectivityInfo } from '../types/connectivity.js'; -import type { NavPlusRedux } from '../types/selector-types.js'; const nativeLogInExtraInfoSelector: ( - input: NavPlusRedux, + state: AppState, ) => () => Promise = createSelector( - (input: NavPlusRedux) => logInExtraInfoSelector(input.redux), - (input: NavPlusRedux) => calendarActiveSelector(input.navContext), - ( - logInExtraInfoFunc: (calendarActive: boolean) => LogInExtraInfo, - calendarActive: boolean, - ) => { + logInExtraInfoSelector, + (logInExtraInfo: LogInExtraInfo) => { const loginExtraFuncWithIdentityKey = async () => { await commCoreModule.initializeCryptoAccount(); const { blobPayload, signature } = await commCoreModule.getUserPublicKey(); const signedIdentityKeysBlob: SignedIdentityKeysBlob = { payload: blobPayload, signature, }; return { - ...logInExtraInfoFunc(calendarActive), + ...logInExtraInfo, signedIdentityKeysBlob, }; }; return loginExtraFuncWithIdentityKey; }, ); const baseNoDataAfterPolicyAcknowledgmentSelector: ( keyserverID: string, ) => (state: AppState) => boolean = keyserverID => createSelector( (state: AppState) => state.connectivity, currentAsOfSelector(keyserverID), (state: AppState) => state.userPolicies, ( connectivity: ConnectivityInfo, currentAsOf: number, userPolicies: UserPolicies, ) => connectivity.connected && currentAsOf === 0 && values(userPolicies).length > 0 && values(userPolicies).every(policy => policy.isAcknowledged), ); const noDataAfterPolicyAcknowledgmentSelector: ( keyserverID: string, ) => (state: AppState) => boolean = _memoize( baseNoDataAfterPolicyAcknowledgmentSelector, ); export { nativeLogInExtraInfoSelector, noDataAfterPolicyAcknowledgmentSelector, }; diff --git a/web/account/siwe-login-form.react.js b/web/account/siwe-login-form.react.js index 5164280ca..cb0c65f96 100644 --- a/web/account/siwe-login-form.react.js +++ b/web/account/siwe-login-form.react.js @@ -1,268 +1,267 @@ // @flow import '@rainbow-me/rainbowkit/styles.css'; import classNames from 'classnames'; import invariant from 'invariant'; import * as React from 'react'; import { useAccount, useWalletClient } from 'wagmi'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { getSIWENonce, getSIWENonceActionTypes, siweAuth, siweAuthActionTypes, } from 'lib/actions/siwe-actions.js'; import ConnectedWalletInfo from 'lib/components/connected-wallet-info.react.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import stores from 'lib/facts/stores.js'; +import { logInExtraInfoSelector } from 'lib/selectors/account-selectors.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { LogInStartingPayload, LogInExtraInfo, } from 'lib/types/account-types.js'; import type { OLMIdentityKeys } from 'lib/types/crypto-types.js'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils.js'; import { ServerError } from 'lib/utils/errors.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { createSIWEMessage, getSIWEStatementForPublicKey, siweMessageSigningExplanationStatements, } from 'lib/utils/siwe-utils.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'; import OrBreak from '../components/or-break.react.js'; import LoadingIndicator from '../loading-indicator.react.js'; import { useSelector } from '../redux/redux-utils.js'; -import { webLogInExtraInfoSelector } from '../selectors/account-selectors.js'; type SIWELogInError = 'account_does_not_exist'; type SIWELoginFormProps = { +cancelSIWEAuthFlow: () => void, }; const getSIWENonceLoadingStatusSelector = createLoadingStatusSelector( getSIWENonceActionTypes, ); const siweAuthLoadingStatusSelector = createLoadingStatusSelector(siweAuthActionTypes); function SIWELoginForm(props: SIWELoginFormProps): React.Node { const { address } = useAccount(); const { data: signer } = useWalletClient(); const dispatchActionPromise = useDispatchActionPromise(); const getSIWENonceCall = useServerCall(getSIWENonce); const getSIWENonceCallLoadingStatus = useSelector( getSIWENonceLoadingStatusSelector, ); const siweAuthLoadingStatus = useSelector(siweAuthLoadingStatusSelector); const siweAuthCall = useServerCall(siweAuth); - const logInExtraInfo = useSelector(webLogInExtraInfoSelector); + const logInExtraInfo = useSelector(logInExtraInfoSelector); const [siweNonce, setSIWENonce] = React.useState(null); const siweNonceShouldBeFetched = !siweNonce && getSIWENonceCallLoadingStatus !== 'loading'; React.useEffect(() => { if (!siweNonceShouldBeFetched) { return; } void dispatchActionPromise( getSIWENonceActionTypes, (async () => { const response = await getSIWENonceCall(); setSIWENonce(response); })(), ); }, [dispatchActionPromise, getSIWENonceCall, siweNonceShouldBeFetched]); const primaryIdentityPublicKeys: ?OLMIdentityKeys = useSelector( state => state.cryptoStore?.primaryIdentityKeys, ); const getSignedIdentityKeysBlob = useGetSignedIdentityKeysBlob(); const callSIWEAuthEndpoint = React.useCallback( async (message: string, signature: string, extraInfo: LogInExtraInfo) => { const signedIdentityKeysBlob = await getSignedIdentityKeysBlob(); invariant( signedIdentityKeysBlob, 'signedIdentityKeysBlob must be set in attemptSIWEAuth', ); try { return await siweAuthCall({ message, signature, signedIdentityKeysBlob, doNotRegister: true, ...extraInfo, }); } catch (e) { if ( e instanceof ServerError && e.message === 'account_does_not_exist' ) { setError('account_does_not_exist'); } throw e; } }, [getSignedIdentityKeysBlob, siweAuthCall], ); const attemptSIWEAuth = React.useCallback( (message: string, signature: string) => { - const extraInfo = logInExtraInfo(); return dispatchActionPromise( siweAuthActionTypes, - callSIWEAuthEndpoint(message, signature, extraInfo), + callSIWEAuthEndpoint(message, signature, logInExtraInfo), undefined, - ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), + ({ calendarQuery: logInExtraInfo.calendarQuery }: LogInStartingPayload), ); }, [callSIWEAuthEndpoint, dispatchActionPromise, logInExtraInfo], ); const dispatch = useDispatch(); const onSignInButtonClick = React.useCallback(async () => { invariant(signer, 'signer must be present during SIWE attempt'); invariant(siweNonce, 'nonce must be present during SIWE attempt'); invariant( primaryIdentityPublicKeys, 'primaryIdentityPublicKeys must be present during SIWE attempt', ); const statement = getSIWEStatementForPublicKey( primaryIdentityPublicKeys.ed25519, ); const message = createSIWEMessage(address, statement, siweNonce); const signature = await signer.signMessage({ message }); await attemptSIWEAuth(message, signature); dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); }, [ address, attemptSIWEAuth, primaryIdentityPublicKeys, signer, siweNonce, dispatch, ]); const { cancelSIWEAuthFlow } = props; const backButtonColor = React.useMemo( () => ({ backgroundColor: '#211E2D' }), [], ); const signInButtonColor = React.useMemo( () => ({ backgroundColor: '#6A20E3' }), [], ); const [error, setError] = React.useState(); const mainMiddleAreaClassName = classNames({ [css.mainMiddleArea]: true, [css.hidden]: !!error, }); const errorOverlayClassNames = classNames({ [css.errorOverlay]: true, [css.hidden]: !error, }); if ( siweAuthLoadingStatus === 'loading' || !siweNonce || !primaryIdentityPublicKeys ) { return (
); } let errorText; if (error === 'account_does_not_exist') { errorText = ( <>

No Comm account found for that Ethereum wallet!

We require that users register on their mobile devices. Comm relies on a primary device capable of scanning QR codes in order to authorize secondary devices.

You can install our iOS app  here , or our Android app  here .

); } return (

Sign in with Ethereum

Wallet Connected

{siweMessageSigningExplanationStatements}

By signing up, you agree to our{' '} Terms of Use &{' '} Privacy Policy.

{errorText}
); } export default SIWELoginForm; diff --git a/web/account/traditional-login-form.react.js b/web/account/traditional-login-form.react.js index aa9ad2f18..ccb59f72f 100644 --- a/web/account/traditional-login-form.react.js +++ b/web/account/traditional-login-form.react.js @@ -1,191 +1,189 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useLogIn, logInActionTypes } from 'lib/actions/user-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; +import { logInExtraInfoSelector } from 'lib/selectors/account-selectors.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { oldValidUsernameRegex, validEmailRegex, } from 'lib/shared/account-utils.js'; import type { LogInExtraInfo, LogInStartingPayload, } from 'lib/types/account-types.js'; import { logInActionSources } from 'lib/types/account-types.js'; import { useDispatchActionPromise } from 'lib/utils/action-utils.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'; import Button from '../components/button.react.js'; import LoadingIndicator from '../loading-indicator.react.js'; import Input from '../modals/input.react.js'; import { useSelector } from '../redux/redux-utils.js'; -import { webLogInExtraInfoSelector } from '../selectors/account-selectors.js'; const loadingStatusSelector = createLoadingStatusSelector(logInActionTypes); function TraditionalLoginForm(): React.Node { const inputDisabled = useSelector(loadingStatusSelector) === 'loading'; - const loginExtraInfo = useSelector(webLogInExtraInfoSelector); + const loginExtraInfo = useSelector(logInExtraInfoSelector); const callLogIn = useLogIn(); const dispatchActionPromise = useDispatchActionPromise(); const modalContext = useModalContext(); const getSignedIdentityKeysBlob = useGetSignedIdentityKeysBlob(); const usernameInputRef = React.useRef(); React.useEffect(() => { usernameInputRef.current?.focus(); }, []); const [username, setUsername] = React.useState(''); const onUsernameChange = React.useCallback( (e: SyntheticEvent) => { invariant(e.target instanceof HTMLInputElement, 'target not input'); setUsername(e.target.value); }, [], ); const onUsernameBlur = React.useCallback(() => { setUsername(untrimmedUsername => untrimmedUsername.trim()); }, []); const [password, setPassword] = React.useState(''); const onPasswordChange = React.useCallback( (e: SyntheticEvent) => { invariant(e.target instanceof HTMLInputElement, 'target not input'); setPassword(e.target.value); }, [], ); const [errorMessage, setErrorMessage] = React.useState(''); const logInAction = React.useCallback( async (extraInfo: LogInExtraInfo) => { const signedIdentityKeysBlob = await getSignedIdentityKeysBlob(); try { invariant( signedIdentityKeysBlob, 'signedIdentityKeysBlob must be set in logInAction', ); const result = await callLogIn({ ...extraInfo, username, password, logInActionSource: logInActionSources.logInFromWebForm, signedIdentityKeysBlob, }); modalContext.popModal(); return result; } catch (e) { setUsername(''); setPassword(''); if (e.message === 'invalid_credentials') { setErrorMessage('incorrect username or password'); } else { setErrorMessage('unknown error'); } usernameInputRef.current?.focus(); throw e; } }, [callLogIn, modalContext, password, getSignedIdentityKeysBlob, username], ); const onSubmit = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); if (username.search(validEmailRegex) > -1) { setUsername(''); setErrorMessage('usernames only, not emails'); usernameInputRef.current?.focus(); return; } else if (username.search(oldValidUsernameRegex) === -1) { setUsername(''); setErrorMessage('alphanumeric usernames only'); usernameInputRef.current?.focus(); return; } else if (password === '') { setErrorMessage('password is empty'); usernameInputRef.current?.focus(); return; } - - const extraInfo = loginExtraInfo(); void dispatchActionPromise( logInActionTypes, - logInAction(extraInfo), + logInAction(loginExtraInfo), undefined, - ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), + ({ calendarQuery: loginExtraInfo.calendarQuery }: LogInStartingPayload), ); }, [dispatchActionPromise, logInAction, loginExtraInfo, username, password], ); const loginButtonContent = React.useMemo(() => { if (inputDisabled) { return ; } return 'Sign in'; }, [inputDisabled]); const signInButtonColor = React.useMemo( () => ({ backgroundColor: '#6A20E3' }), [], ); return (

Sign in to Comm

Username
Password
{errorMessage}
); } export default TraditionalLoginForm; diff --git a/web/selectors/account-selectors.js b/web/selectors/account-selectors.js deleted file mode 100644 index 0a9dfbb46..000000000 --- a/web/selectors/account-selectors.js +++ /dev/null @@ -1,22 +0,0 @@ -// @flow - -import { createSelector } from 'reselect'; - -import { logInExtraInfoSelector } from 'lib/selectors/account-selectors.js'; -import type { LogInExtraInfo } from 'lib/types/account-types.js'; - -import type { AppState } from '../redux/redux-setup.js'; - -const webLogInExtraInfoSelector: (state: AppState) => () => LogInExtraInfo = - createSelector( - logInExtraInfoSelector, - (state: AppState) => state.navInfo.tab === 'calendar', - ( - logInExtraInfoFunc: (calendarActive: boolean) => LogInExtraInfo, - calendarActive: boolean, - ) => - () => - logInExtraInfoFunc(calendarActive), - ); - -export { webLogInExtraInfoSelector };