diff --git a/native/account/fullscreen-siwe-panel.react.js b/native/account/fullscreen-siwe-panel.react.js index deef997b3..452bebc24 100644 --- a/native/account/fullscreen-siwe-panel.react.js +++ b/native/account/fullscreen-siwe-panel.react.js @@ -1,202 +1,225 @@ // @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 { enableNewRegistrationMode } from './registration/registration-types.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 } from '../utils/alert-messages.js'; +import { + unknownErrorAlertDetails, + appOutOfDateAlertDetails, +} from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; 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, register: registrationServerCall } = 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' + ) { + Alert.alert( + appOutOfDateAlertDetails.title, + appOutOfDateAlertDetails.message, + [{ text: 'OK', onPress: goBackToPrompt }], + { cancelable: false }, + ); } else { throw e; } } } else if (enableNewRegistrationMode) { await onAccountDoesNotExist(result); } else { try { await registrationServerCall({ farcasterID: null, accountSelection: { accountType: 'ethereum', ...result, avatarURI: null, }, avatarData: null, clearCachedSelections: () => {}, onNonceExpired: () => onNonceExpired('registration'), }); } catch { // We swallow exceptions here because registrationServerCall // already handles showing Alerts, and we don't want to show two } } } catch (e) { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: goBackToPrompt }], { cancelable: false }, ); } } else { try { await legacySiweServerCall({ ...result, doNotRegister: enableNewRegistrationMode, }); } catch (e) { if ( e instanceof ServerError && e.message === 'account_does_not_exist' ) { await onAccountDoesNotExist(result); - return; + } 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 }, + ); } - Alert.alert( - unknownErrorAlertDetails.title, - unknownErrorAlertDetails.message, - [{ text: 'OK', onPress: goBackToPrompt }], - { cancelable: false }, - ); return; } dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); } }, [ walletLogIn, registrationServerCall, 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 85ac84f7e..26e9ba7c3 100644 --- a/native/account/log-in-panel.react.js +++ b/native/account/log-in-panel.react.js @@ -1,481 +1,484 @@ // @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 { 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 { 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, +opacityValue: Animated.Node, +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.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { 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') { + } else if ( + messageForException === 'Unsupported version' || + messageForException === 'client_version_unsupported' + ) { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { 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, ); }; 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( 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 8e6dd6199..e937e8963 100644 --- a/native/account/registration/existing-ethereum-account.react.js +++ b/native/account/registration/existing-ethereum-account.react.js @@ -1,199 +1,212 @@ // @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 RegistrationButton from './registration-button.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 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 } from '../../utils/alert-messages.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' + ) { + 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;