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;