diff --git a/native/account/fullscreen-siwe-panel.react.js b/native/account/fullscreen-siwe-panel.react.js index 0f5b1c6c3..66e1f4ccd 100644 --- a/native/account/fullscreen-siwe-panel.react.js +++ b/native/account/fullscreen-siwe-panel.react.js @@ -1,207 +1,208 @@ // @flow import { useNavigation } from '@react-navigation/native'; import invariant from 'invariant'; import * as React from 'react'; import { ActivityIndicator, View } from 'react-native'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { useWalletLogIn } from 'lib/hooks/login-hooks.js'; import { type SIWEResult, SIWEMessageTypes } from 'lib/types/siwe-types.js'; import { ServerError, getMessageForException } from 'lib/utils/errors.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { useGetEthereumAccountFromSIWEResult } from './registration/ethereum-utils.js'; import { RegistrationContext } from './registration/registration-context.js'; import { useLegacySIWEServerCall } from './siwe-hooks.js'; import SIWEPanel from './siwe-panel.react.js'; import { commRustModule } from '../native-modules.js'; import { AccountDoesNotExistRouteName, RegistrationRouteName, } from '../navigation/route-names.js'; import { unknownErrorAlertDetails, appOutOfDateAlertDetails, } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; const siweSignatureRequestData = { messageType: SIWEMessageTypes.MSG_AUTH }; type Props = { +goBackToPrompt: () => mixed, +closing: boolean, }; function FullscreenSIWEPanel(props: Props): React.Node { const [loading, setLoading] = React.useState(true); const activity = loading ? : null; const activityContainer = React.useMemo( () => ({ flex: 1, }), [], ); const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { setSkipEthereumLoginOnce } = registrationContext; const getEthereumAccountFromSIWEResult = useGetEthereumAccountFromSIWEResult(); const { navigate } = useNavigation(); const { goBackToPrompt } = props; const onAccountDoesNotExist = React.useCallback( async (result: SIWEResult) => { await getEthereumAccountFromSIWEResult(result); setSkipEthereumLoginOnce(true); goBackToPrompt(); navigate<'Registration'>(RegistrationRouteName, { screen: AccountDoesNotExistRouteName, }); }, [ getEthereumAccountFromSIWEResult, navigate, goBackToPrompt, setSkipEthereumLoginOnce, ], ); const onNonceExpired = React.useCallback( (registrationOrLogin: 'registration' | 'login') => { Alert.alert( registrationOrLogin === 'registration' ? 'Registration attempt timed out' : 'Login attempt timed out', 'Please try again', [{ text: 'OK', onPress: goBackToPrompt }], { cancelable: false }, ); }, [goBackToPrompt], ); const legacySiweServerCall = useLegacySIWEServerCall(); const walletLogIn = useWalletLogIn(); const successRef = React.useRef(false); const dispatch = useDispatch(); const onSuccess = React.useCallback( async (result: SIWEResult) => { successRef.current = true; if (usingCommServicesAccessToken) { try { const findUserIDResponseString = await commRustModule.findUserIDForWalletAddress(result.address); const findUserIDResponse = JSON.parse(findUserIDResponseString); if (findUserIDResponse.userID || findUserIDResponse.isReserved) { try { await walletLogIn( result.address, result.message, result.signature, ); } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'nonce_expired') { onNonceExpired('login'); } else if ( messageForException === 'unsupported_version' || - messageForException === 'client_version_unsupported' + messageForException === 'client_version_unsupported' || + messageForException === 'use_new_flow' ) { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: goBackToPrompt }], { cancelable: false }, ); } else { throw e; } } } else { await onAccountDoesNotExist(result); } } catch (e) { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: goBackToPrompt }], { cancelable: false }, ); } } else { try { await legacySiweServerCall({ ...result, doNotRegister: true, }); } catch (e) { if ( e instanceof ServerError && e.message === 'account_does_not_exist' ) { await onAccountDoesNotExist(result); } else if ( e instanceof ServerError && e.message === 'client_version_unsupported' ) { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: goBackToPrompt }], { cancelable: false }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: goBackToPrompt }], { cancelable: false }, ); } return; } dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); } }, [ walletLogIn, goBackToPrompt, dispatch, legacySiweServerCall, onAccountDoesNotExist, onNonceExpired, ], ); const ifBeforeSuccessGoBackToPrompt = React.useCallback(() => { if (!successRef.current) { goBackToPrompt(); } }, [goBackToPrompt]); const { closing } = props; return ( <> {activity} ); } export default FullscreenSIWEPanel; diff --git a/native/account/log-in-panel.react.js b/native/account/log-in-panel.react.js index dea5042bd..69f7242fb 100644 --- a/native/account/log-in-panel.react.js +++ b/native/account/log-in-panel.react.js @@ -1,473 +1,474 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, StyleSheet, Keyboard, Platform } from 'react-native'; import { legacyLogInActionTypes, useLegacyLogIn, getOlmSessionInitializationDataActionTypes, } from 'lib/actions/user-actions.js'; import { usePasswordLogIn } from 'lib/hooks/login-hooks.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 LegacyLogInInfo, type LegacyLogInExtraInfo, type LegacyLogInResult, type LegacyLogInStartingPayload, logInActionSources, } from 'lib/types/account-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-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 { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLegacyLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import type { KeyPressEvent } from '../types/react-native.js'; import type { ViewStyle } from '../types/styles.js'; import { appOutOfDateAlertDetails, unknownErrorAlertDetails, userNotFoundAlertDetails, } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; import type { StateContainer } from '../utils/state-container.js'; export type LogInState = { +usernameInputText: ?string, +passwordInputText: ?string, }; type BaseProps = { +setActiveAlert: (activeAlert: boolean) => void, +opacityStyle: ViewStyle, +logInState: StateContainer, }; type Props = { ...BaseProps, +loadingStatus: LoadingStatus, +legacyLogInExtraInfo: () => Promise, +dispatchActionPromise: DispatchActionPromise, +legacyLogIn: (logInInfo: LegacyLogInInfo) => Promise, +identityPasswordLogIn: (username: string, password: string) => Promise, +getInitialNotificationsEncryptedMessage: () => Promise, }; type State = { +logInPending: boolean, }; class LogInPanel extends React.PureComponent { usernameInput: ?TextInput; passwordInput: ?PasswordInput; state: State = { logInPending: false }; 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 ( ); } getLoadingStatus(): LoadingStatus { if (this.props.loadingStatus === 'loading') { return 'loading'; } if (this.state.logInPending) { return 'loading'; } return 'inactive'; } 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(); if (usingCommServicesAccessToken) { await this.identityPasswordLogIn(); return; } const extraInfo = await this.props.legacyLogInExtraInfo(); const initialNotificationsEncryptedMessage = await this.props.getInitialNotificationsEncryptedMessage(); void this.props.dispatchActionPromise( legacyLogInActionTypes, this.legacyLogInAction({ ...extraInfo, initialNotificationsEncryptedMessage, }), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LegacyLogInStartingPayload), ); }; async legacyLogInAction( extraInfo: LegacyLogInExtraInfo, ): Promise { try { const result = await this.props.legacyLogIn({ ...extraInfo, username: this.usernameInputText, password: this.passwordInputText, authActionSource: 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( userNotFoundAlertDetails.title, userNotFoundAlertDetails.message, [{ text: 'OK', onPress: this.onUnsuccessfulLoginAlertAckowledged }], { cancelable: false }, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onOtherErrorAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onOtherErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } async identityPasswordLogIn(): Promise { if (this.state.logInPending) { return; } this.setState({ logInPending: true }); try { await this.props.identityPasswordLogIn( this.usernameInputText, this.passwordInputText, ); this.props.setActiveAlert(false); await setNativeCredentials({ username: this.usernameInputText, password: this.passwordInputText, }); } catch (e) { const messageForException = getMessageForException(e); if ( messageForException === 'user_not_found' || messageForException === 'login_failed' ) { Alert.alert( userNotFoundAlertDetails.title, userNotFoundAlertDetails.message, [{ text: 'OK', onPress: this.onUnsuccessfulLoginAlertAckowledged }], { cancelable: false }, ); } else if ( messageForException === 'unsupported_version' || - messageForException === 'client_version_unsupported' + messageForException === 'client_version_unsupported' || + messageForException === 'use_new_flow' ) { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onOtherErrorAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onOtherErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } finally { this.setState({ logInPending: false }); } } 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, ); }; onOtherErrorAlertAcknowledged: () => 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( legacyLogInActionTypes, ); 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 legacyLogInExtraInfo = useSelector( nativeLegacyLogInExtraInfoSelector, ); const dispatchActionPromise = useDispatchActionPromise(); const callLegacyLogIn = useLegacyLogIn(); const callIdentityPasswordLogIn = usePasswordLogIn(); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); return ( ); }); export default ConnectedLogInPanel; diff --git a/native/account/registration/existing-ethereum-account.react.js b/native/account/registration/existing-ethereum-account.react.js index 4279edaf9..5100f205e 100644 --- a/native/account/registration/existing-ethereum-account.react.js +++ b/native/account/registration/existing-ethereum-account.react.js @@ -1,212 +1,213 @@ // @flow import type { StackNavigationEventMap, StackNavigationState, StackOptions, } from '@react-navigation/core'; import invariant from 'invariant'; import * as React from 'react'; import { Text, View } from 'react-native'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { useENSName } from 'lib/hooks/ens-cache.js'; import { useWalletLogIn } from 'lib/hooks/login-hooks.js'; import type { SIWEResult } from 'lib/types/siwe-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import RegistrationButtonContainer from './registration-button-container.react.js'; import RegistrationContainer from './registration-container.react.js'; import RegistrationContentContainer from './registration-content-container.react.js'; import { RegistrationContext } from './registration-context.js'; import type { RegistrationNavigationProp } from './registration-navigator.react.js'; import PrimaryButton from '../../components/primary-button.react.js'; import type { RootNavigationProp } from '../../navigation/root-navigator.react.js'; import type { NavigationRoute, ScreenParamList, } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; import { unknownErrorAlertDetails, appOutOfDateAlertDetails, } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; import { useLegacySIWEServerCall } from '../siwe-hooks.js'; export type ExistingEthereumAccountParams = SIWEResult; type Props = { +navigation: RegistrationNavigationProp<'ExistingEthereumAccount'>, +route: NavigationRoute<'ExistingEthereumAccount'>, }; function ExistingEthereumAccount(props: Props): React.Node { const legacySiweServerCall = useLegacySIWEServerCall(); const walletLogIn = useWalletLogIn(); const [logInPending, setLogInPending] = React.useState(false); const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { setCachedSelections } = registrationContext; const { params } = props.route; const dispatch = useDispatch(); const { navigation } = props; const goBackToHome = navigation.getParent< ScreenParamList, 'Registration', StackNavigationState, StackOptions, StackNavigationEventMap, RootNavigationProp<'Registration'>, >()?.goBack; const onProceedToLogIn = React.useCallback(async () => { if (logInPending) { return; } setLogInPending(true); try { if (usingCommServicesAccessToken) { await walletLogIn(params.address, params.message, params.signature); } else { await legacySiweServerCall({ ...params, doNotRegister: true }); dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); } } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'nonce_expired') { setCachedSelections(oldUserSelections => ({ ...oldUserSelections, ethereumAccount: undefined, })); Alert.alert( 'Login attempt timed out', 'Try logging in from the main SIWE button on the home screen', [{ text: 'OK', onPress: goBackToHome }], { cancelable: false, }, ); } else if ( messageForException === 'unsupported_version' || - messageForException === 'client_version_unsupported' + messageForException === 'client_version_unsupported' || + messageForException === 'use_new_flow' ) { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: goBackToHome }], { cancelable: false }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK' }], { cancelable: false, }, ); } throw e; } finally { setLogInPending(false); } }, [ logInPending, legacySiweServerCall, walletLogIn, params, dispatch, goBackToHome, setCachedSelections, ]); const { address } = params; const walletIdentifier = useENSName(address); const walletIdentifierTitle = walletIdentifier === address ? 'Ethereum wallet' : 'ENS name'; const { goBack } = navigation; const styles = useStyles(unboundStyles); return ( Account already exists for wallet You can proceed to log in with this wallet, or go back and use a different wallet. {walletIdentifierTitle} {walletIdentifier} ); } const unboundStyles = { header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, body: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 40, }, walletTile: { backgroundColor: 'panelForeground', borderRadius: 8, padding: 24, alignItems: 'center', }, walletIdentifierTitleText: { fontSize: 17, color: 'panelForegroundLabel', textAlign: 'center', }, walletIdentifier: { backgroundColor: 'panelSecondaryForeground', paddingVertical: 8, paddingHorizontal: 24, borderRadius: 56, marginTop: 8, alignItems: 'center', }, walletIdentifierText: { fontSize: 15, color: 'panelForegroundLabel', }, }; export default ExistingEthereumAccount; diff --git a/web/account/siwe-login-form.react.js b/web/account/siwe-login-form.react.js index 3761707b5..fd6ddc704 100644 --- a/web/account/siwe-login-form.react.js +++ b/web/account/siwe-login-form.react.js @@ -1,364 +1,365 @@ // @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, legacySiweAuth, legacySiweAuthActionTypes, } from 'lib/actions/siwe-actions.js'; import { identityGenerateNonceActionTypes, useIdentityGenerateNonce, } from 'lib/actions/user-actions.js'; import ConnectedWalletInfo from 'lib/components/connected-wallet-info.react.js'; import SWMansionIcon from 'lib/components/swmansion-icon.react.js'; import stores from 'lib/facts/stores.js'; import { useWalletLogIn } from 'lib/hooks/login-hooks.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { legacyLogInExtraInfoSelector } from 'lib/selectors/account-selectors.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { LegacyLogInStartingPayload, LegacyLogInExtraInfo, } from 'lib/types/account-types.js'; import { SIWEMessageTypes } from 'lib/types/siwe-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { createSIWEMessage, getSIWEStatementForPublicKey, siweMessageSigningExplanationStatements, } from 'lib/utils/siwe-utils.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 { olmAPI } from '../crypto/olm-api.js'; import LoadingIndicator from '../loading-indicator.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { getVersionUnsupportedError } from '../utils/version-utils.js'; type SIWELogInError = | 'account_does_not_exist' | 'client_version_unsupported' | 'retry_from_native'; type SIWELoginFormProps = { +cancelSIWEAuthFlow: () => void, }; const legacyGetSIWENonceLoadingStatusSelector = createLoadingStatusSelector( getSIWENonceActionTypes, ); const identityGenerateNonceLoadingStatusSelector = createLoadingStatusSelector( identityGenerateNonceActionTypes, ); const legacySiweAuthLoadingStatusSelector = createLoadingStatusSelector( legacySiweAuthActionTypes, ); function SIWELoginForm(props: SIWELoginFormProps): React.Node { const { address } = useAccount(); const { data: signer } = useWalletClient(); const dispatchActionPromise = useDispatchActionPromise(); const legacyGetSIWENonceCall = useLegacyAshoatKeyserverCall(getSIWENonce); const legacyGetSIWENonceCallLoadingStatus = useSelector( legacyGetSIWENonceLoadingStatusSelector, ); const identityGenerateNonce = useIdentityGenerateNonce(); const identityGenerateNonceLoadingStatus = useSelector( identityGenerateNonceLoadingStatusSelector, ); const siweAuthLoadingStatus = useSelector( legacySiweAuthLoadingStatusSelector, ); const legacySiweAuthCall = useLegacyAshoatKeyserverCall(legacySiweAuth); const legacyLogInExtraInfo = useSelector(legacyLogInExtraInfoSelector); const walletLogIn = useWalletLogIn(); const [siweNonce, setSIWENonce] = React.useState(null); const siweNonceShouldBeFetched = !siweNonce && legacyGetSIWENonceCallLoadingStatus !== 'loading' && identityGenerateNonceLoadingStatus !== 'loading'; React.useEffect(() => { if (!siweNonceShouldBeFetched) { return; } if (usingCommServicesAccessToken) { void dispatchActionPromise( identityGenerateNonceActionTypes, (async () => { const response = await identityGenerateNonce(); setSIWENonce(response); })(), ); } else { void dispatchActionPromise( getSIWENonceActionTypes, (async () => { const response = await legacyGetSIWENonceCall(); setSIWENonce(response); })(), ); } }, [ dispatchActionPromise, identityGenerateNonce, legacyGetSIWENonceCall, siweNonceShouldBeFetched, ]); const callLegacySIWEAuthEndpoint = React.useCallback( async ( message: string, signature: string, extraInfo: LegacyLogInExtraInfo, ) => { await olmAPI.initializeCryptoAccount(); const userPublicKey = await olmAPI.getUserPublicKey(); try { return await legacySiweAuthCall({ message, signature, signedIdentityKeysBlob: { payload: userPublicKey.blobPayload, signature: userPublicKey.signature, }, doNotRegister: true, ...extraInfo, }); } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'account_does_not_exist') { setError('account_does_not_exist'); } else if (messageForException === 'client_version_unsupported') { setError('client_version_unsupported'); } throw e; } }, [legacySiweAuthCall], ); const attemptLegacySIWEAuth = React.useCallback( (message: string, signature: string) => { return dispatchActionPromise( legacySiweAuthActionTypes, callLegacySIWEAuthEndpoint(message, signature, legacyLogInExtraInfo), undefined, ({ calendarQuery: legacyLogInExtraInfo.calendarQuery, }: LegacyLogInStartingPayload), ); }, [callLegacySIWEAuthEndpoint, dispatchActionPromise, legacyLogInExtraInfo], ); const attemptWalletLogIn = React.useCallback( async ( walletAddress: string, siweMessage: string, siweSignature: string, ) => { try { await walletLogIn(walletAddress, siweMessage, siweSignature); } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'user_not_found') { setError('account_does_not_exist'); } else if ( messageForException === 'client_version_unsupported' || - messageForException === 'unsupported_version' + messageForException === 'unsupported_version' || + messageForException === 'use_new_flow' ) { setError('client_version_unsupported'); } else if (messageForException === 'retry_from_native') { setError('retry_from_native'); } } }, [walletLogIn], ); 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'); await olmAPI.initializeCryptoAccount(); const { primaryIdentityPublicKeys: { ed25519 }, } = await olmAPI.getUserPublicKey(); const statement = getSIWEStatementForPublicKey( ed25519, SIWEMessageTypes.MSG_AUTH, ); const message = createSIWEMessage(address, statement, siweNonce); let signature; try { signature = await signer.signMessage({ message }); } catch (e) { // If we fail to get the signature (e.g. user cancels the request), we // should return immediately return; } if (usingCommServicesAccessToken) { await attemptWalletLogIn(address, message, signature); } else { await attemptLegacySIWEAuth(message, signature); dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); } }, [ address, attemptLegacySIWEAuth, attemptWalletLogIn, 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) { 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 .

); } else if (error === 'client_version_unsupported') { errorText =

{getVersionUnsupportedError()}

; } else if (error === 'retry_from_native') { errorText = ( <>

No primary device found for that Ethereum wallet!

Please try logging in from a mobile device to establish your primary device. Comm relies on a primary device capable of scanning QR codes in order to authorize secondary devices. Once you’ve logged in from a mobile device, you will be able to log in from your browser.

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 e3f862c4e..755f35dfa 100644 --- a/web/account/traditional-login-form.react.js +++ b/web/account/traditional-login-form.react.js @@ -1,272 +1,273 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useLegacyLogIn, legacyLogInActionTypes, } from 'lib/actions/user-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { usePasswordLogIn } from 'lib/hooks/login-hooks.js'; import { legacyLogInExtraInfoSelector } 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 { LegacyLogInExtraInfo, LegacyLogInStartingPayload, } from 'lib/types/account-types.js'; import { logInActionSources } from 'lib/types/account-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.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 { olmAPI } from '../crypto/olm-api.js'; import LoadingIndicator from '../loading-indicator.react.js'; import Input from '../modals/input.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { getShortVersionUnsupportedError } from '../utils/version-utils.js'; const loadingStatusSelector = createLoadingStatusSelector( legacyLogInActionTypes, ); function TraditionalLoginForm(): React.Node { const legacyAuthInProgress = useSelector(loadingStatusSelector) === 'loading'; const [identityAuthInProgress, setIdentityAuthInProgress] = React.useState(false); const inputDisabled = legacyAuthInProgress || identityAuthInProgress; const legacyLoginExtraInfo = useSelector(legacyLogInExtraInfoSelector); const callLegacyLogIn = useLegacyLogIn(); const dispatchActionPromise = useDispatchActionPromise(); const modalContext = useModalContext(); 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 legacyLogInAction = React.useCallback( async (extraInfo: LegacyLogInExtraInfo) => { await olmAPI.initializeCryptoAccount(); const userPublicKey = await olmAPI.getUserPublicKey(); try { const result = await callLegacyLogIn({ ...extraInfo, username, password, authActionSource: logInActionSources.logInFromWebForm, signedIdentityKeysBlob: { payload: userPublicKey.blobPayload, signature: userPublicKey.signature, }, }); modalContext.popModal(); return result; } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'invalid_credentials') { setUsername(''); setPassword(''); setErrorMessage('incorrect username or password'); } else if (messageForException === 'client_version_unsupported') { setErrorMessage(getShortVersionUnsupportedError()); } else { setErrorMessage('unknown error'); } usernameInputRef.current?.focus(); throw e; } }, [callLegacyLogIn, modalContext, password, username], ); const callIdentityPasswordLogIn = usePasswordLogIn(); const identityPasswordLogInAction = React.useCallback(async () => { if (identityAuthInProgress) { return; } setIdentityAuthInProgress(true); try { await callIdentityPasswordLogIn(username, password); modalContext.popModal(); } catch (e) { const messageForException = getMessageForException(e); if ( messageForException === 'user_not_found' || messageForException === 'login_failed' ) { setUsername(''); setPassword(''); setErrorMessage('incorrect username or password'); } else if ( messageForException === 'client_version_unsupported' || - messageForException === 'unsupported_version' + messageForException === 'unsupported_version' || + messageForException === 'use_new_flow' ) { setErrorMessage(getShortVersionUnsupportedError()); } else if ( messageForException === 'need_keyserver_message_to_claim_username' ) { // We don't want to let users claim their reserved usernames from web // because we won't be able to establish a primary device for them. setErrorMessage('please log in from a mobile device then retry'); } else { setErrorMessage('unknown error'); } usernameInputRef.current?.focus(); } finally { setIdentityAuthInProgress(false); } }, [ identityAuthInProgress, callIdentityPasswordLogIn, modalContext, password, 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; } if (usingCommServicesAccessToken) { void identityPasswordLogInAction(); } else { void dispatchActionPromise( legacyLogInActionTypes, legacyLogInAction(legacyLoginExtraInfo), undefined, ({ calendarQuery: legacyLoginExtraInfo.calendarQuery, }: LegacyLogInStartingPayload), ); } }, [ dispatchActionPromise, identityPasswordLogInAction, legacyLogInAction, legacyLoginExtraInfo, username, password, ], ); const loadingIndicatorClassName = inputDisabled ? css.loadingIndicator : css.hiddenLoadingIndicator; const buttonTextClassName = inputDisabled ? css.invisibleButtonText : undefined; const loginButtonContent = React.useMemo( () => ( <>
Sign in
), [loadingIndicatorClassName, buttonTextClassName], ); const signInButtonColor = React.useMemo( () => ({ backgroundColor: '#6A20E3' }), [], ); return (

Sign in to Comm

Username
Password
{errorMessage}
); } export default TraditionalLoginForm;