diff --git a/native/account/fullscreen-siwe-panel.react.js b/native/account/fullscreen-siwe-panel.react.js
index 452bebc24..ddb583445 100644
--- a/native/account/fullscreen-siwe-panel.react.js
+++ b/native/account/fullscreen-siwe-panel.react.js
@@ -1,225 +1,226 @@
// @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,
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'),
+ onAlertAcknowledged: goBackToPrompt,
});
} 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);
} 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,
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/registration/registration-server-call.js b/native/account/registration/registration-server-call.js
index 147e0b703..61c70143a 100644
--- a/native/account/registration/registration-server-call.js
+++ b/native/account/registration/registration-server-call.js
@@ -1,483 +1,515 @@
// @flow
import * as React from 'react';
import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js';
import { setSyncedMetadataEntryActionType } from 'lib/actions/synced-metadata-actions.js';
import {
legacyKeyserverRegisterActionTypes,
legacyKeyserverRegister,
useIdentityPasswordRegister,
identityRegisterActionTypes,
deleteAccountActionTypes,
useDeleteDiscardedIdentityAccount,
} from 'lib/actions/user-actions.js';
import { useKeyserverAuthWithRetry } from 'lib/keyserver-conn/keyserver-auth.js';
import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js';
import { isLoggedInToKeyserver } from 'lib/selectors/user-selectors.js';
import {
type LegacyLogInStartingPayload,
logInActionSources,
} from 'lib/types/account-types.js';
import { syncedMetadataNames } from 'lib/types/synced-metadata-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 { setURLPrefix } from 'lib/utils/url-utils.js';
import { waitUntilDatabaseDeleted } from 'lib/utils/wait-until-db-deleted.js';
import type {
RegistrationServerCallInput,
UsernameAccountSelection,
AvatarData,
} from './registration-types.js';
import { authoritativeKeyserverID } from '../../authoritative-keyserver.js';
import {
useNativeSetUserAvatar,
useUploadSelectedMedia,
} from '../../avatars/avatar-hooks.js';
import { commCoreModule } from '../../native-modules.js';
import { useSelector } from '../../redux/redux-utils.js';
import { nativeLegacyLogInExtraInfoSelector } from '../../selectors/account-selectors.js';
import {
appOutOfDateAlertDetails,
usernameReservedAlertDetails,
usernameTakenAlertDetails,
unknownErrorAlertDetails,
} from '../../utils/alert-messages.js';
import Alert from '../../utils/alert.js';
import { defaultURLPrefix } from '../../utils/url-utils.js';
import { setNativeCredentials } from '../native-credentials.js';
import {
useLegacySIWEServerCall,
useIdentityWalletRegisterCall,
} 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: 'identity_registration_dispatched',
+clearCachedSelections: () => void,
+ +onAlertAcknowledged: ?() => mixed,
+avatarData: ?AvatarData,
+credentialsToSave: ?{ +username: string, +password: string },
+resolve: () => void,
+reject: Error => void,
}
| {
+step: 'authoritative_keyserver_registration_dispatched',
+clearCachedSelections: () => void,
+avatarData: ?AvatarData,
+credentialsToSave: ?{ +username: string, +password: string },
+resolve: () => void,
+reject: Error => void,
};
const inactiveStep = { step: 'inactive' };
function useRegistrationServerCall(): RegistrationServerCallInput => Promise {
const [currentStep, setCurrentStep] =
React.useState(inactiveStep);
// STEP 1: ACCOUNT REGISTRATION
const legacyLogInExtraInfo = useSelector(nativeLegacyLogInExtraInfoSelector);
const dispatchActionPromise = useDispatchActionPromise();
const callLegacyKeyserverRegister = useLegacyAshoatKeyserverCall(
legacyKeyserverRegister,
);
const callIdentityPasswordRegister = useIdentityPasswordRegister();
const identityRegisterUsernameAccount = React.useCallback(
async (
accountSelection: UsernameAccountSelection,
farcasterID: ?string,
+ onAlertAcknowledged: ?() => mixed,
) => {
const identityRegisterPromise = (async () => {
try {
return await callIdentityPasswordRegister(
accountSelection.username,
accountSelection.password,
farcasterID,
);
} catch (e) {
if (e.message === 'username reserved') {
Alert.alert(
usernameReservedAlertDetails.title,
usernameReservedAlertDetails.message,
+ [{ text: 'OK', onPress: onAlertAcknowledged }],
+ { cancelable: !onAlertAcknowledged },
);
} else if (e.message === 'username already exists') {
Alert.alert(
usernameTakenAlertDetails.title,
usernameTakenAlertDetails.message,
+ [{ text: 'OK', onPress: onAlertAcknowledged }],
+ { cancelable: !onAlertAcknowledged },
);
} else if (e.message === 'Unsupported version') {
Alert.alert(
appOutOfDateAlertDetails.title,
appOutOfDateAlertDetails.message,
+ [{ text: 'OK', onPress: onAlertAcknowledged }],
+ { cancelable: !onAlertAcknowledged },
);
} else {
Alert.alert(
unknownErrorAlertDetails.title,
unknownErrorAlertDetails.message,
+ [{ text: 'OK', onPress: onAlertAcknowledged }],
+ { cancelable: !onAlertAcknowledged },
);
}
throw e;
}
})();
void dispatchActionPromise(
identityRegisterActionTypes,
identityRegisterPromise,
);
await identityRegisterPromise;
},
[callIdentityPasswordRegister, dispatchActionPromise],
);
const legacyKeyserverRegisterUsernameAccount = React.useCallback(
async (
accountSelection: UsernameAccountSelection,
keyserverURL: string,
+ onAlertAcknowledged: ?() => mixed,
) => {
const extraInfo = await legacyLogInExtraInfo();
const legacyKeyserverRegisterPromise = (async () => {
try {
return await callLegacyKeyserverRegister(
{
...extraInfo,
username: accountSelection.username,
password: accountSelection.password,
},
{
urlPrefixOverride: keyserverURL,
},
);
} catch (e) {
if (e.message === 'username_reserved') {
Alert.alert(
usernameReservedAlertDetails.title,
usernameReservedAlertDetails.message,
+ [{ text: 'OK', onPress: onAlertAcknowledged }],
+ { cancelable: !onAlertAcknowledged },
);
} else if (e.message === 'username_taken') {
Alert.alert(
usernameTakenAlertDetails.title,
usernameTakenAlertDetails.message,
+ [{ text: 'OK', onPress: onAlertAcknowledged }],
+ { cancelable: !onAlertAcknowledged },
);
} else if (e.message === 'client_version_unsupported') {
Alert.alert(
appOutOfDateAlertDetails.title,
appOutOfDateAlertDetails.message,
+ [{ text: 'OK', onPress: onAlertAcknowledged }],
+ { cancelable: !onAlertAcknowledged },
);
} else {
Alert.alert(
unknownErrorAlertDetails.title,
unknownErrorAlertDetails.message,
+ [{ text: 'OK', onPress: onAlertAcknowledged }],
+ { cancelable: !onAlertAcknowledged },
);
}
throw e;
}
})();
void dispatchActionPromise(
legacyKeyserverRegisterActionTypes,
legacyKeyserverRegisterPromise,
undefined,
({
calendarQuery: extraInfo.calendarQuery,
}: LegacyLogInStartingPayload),
);
await legacyKeyserverRegisterPromise;
},
[legacyLogInExtraInfo, callLegacyKeyserverRegister, dispatchActionPromise],
);
const legacySiweServerCall = useLegacySIWEServerCall();
const identityWalletRegisterCall = useIdentityWalletRegisterCall();
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: passedKeyserverURL,
farcasterID,
siweBackupSecrets,
clearCachedSelections,
onNonceExpired,
+ onAlertAcknowledged,
} = input;
const keyserverURL = passedKeyserverURL ?? defaultURLPrefix;
if (
accountSelection.accountType === 'username' &&
!usingCommServicesAccessToken
) {
await legacyKeyserverRegisterUsernameAccount(
accountSelection,
keyserverURL,
+ onAlertAcknowledged,
);
} else if (accountSelection.accountType === 'username') {
await identityRegisterUsernameAccount(
accountSelection,
farcasterID,
+ onAlertAcknowledged,
);
} else if (!usingCommServicesAccessToken) {
try {
await legacySiweServerCall(accountSelection, {
urlPrefixOverride: keyserverURL,
});
} catch (e) {
Alert.alert(
unknownErrorAlertDetails.title,
unknownErrorAlertDetails.message,
+ [{ text: 'OK', onPress: onAlertAcknowledged }],
+ { cancelable: !onAlertAcknowledged },
);
throw e;
}
} else {
try {
await identityWalletRegisterCall({
address: accountSelection.address,
message: accountSelection.message,
signature: accountSelection.signature,
fid: farcasterID,
});
} catch (e) {
const messageForException = getMessageForException(e);
if (messageForException === 'nonce expired') {
onNonceExpired();
} else {
Alert.alert(
unknownErrorAlertDetails.title,
unknownErrorAlertDetails.message,
+ [{ text: 'OK', onPress: onAlertAcknowledged }],
+ { cancelable: !onAlertAcknowledged },
);
}
throw e;
}
}
if (passedKeyserverURL) {
dispatch({
type: setURLPrefix,
payload: passedKeyserverURL,
});
}
if (farcasterID) {
dispatch({
type: setSyncedMetadataEntryActionType,
payload: {
name: syncedMetadataNames.CURRENT_USER_FID,
data: farcasterID,
},
});
}
if (siweBackupSecrets) {
await commCoreModule.setSIWEBackupSecrets(siweBackupSecrets);
}
const credentialsToSave =
accountSelection.accountType === 'username'
? {
username: accountSelection.username,
password: accountSelection.password,
}
: null;
if (usingCommServicesAccessToken) {
setCurrentStep({
step: 'identity_registration_dispatched',
avatarData,
clearCachedSelections,
+ onAlertAcknowledged,
credentialsToSave,
resolve,
reject,
});
} else {
setCurrentStep({
step: 'authoritative_keyserver_registration_dispatched',
avatarData,
clearCachedSelections,
credentialsToSave,
resolve,
reject,
});
}
} catch (e) {
reject(e);
}
},
),
[
currentStep,
legacyKeyserverRegisterUsernameAccount,
identityRegisterUsernameAccount,
legacySiweServerCall,
dispatch,
identityWalletRegisterCall,
],
);
// STEP 2: REGISTERING ON AUTHORITATIVE KEYSERVER
const keyserverAuth = useKeyserverAuthWithRetry(authoritativeKeyserverID);
const isRegisteredOnIdentity = useSelector(
state =>
!!state.commServicesAccessToken &&
!!state.currentUserInfo &&
!state.currentUserInfo.anonymous,
);
// We call deleteDiscardedIdentityAccount in order to reset state if identity
// registration succeeds but authoritative keyserver auth fails
const deleteDiscardedIdentityAccount = useDeleteDiscardedIdentityAccount();
const registeringOnAuthoritativeKeyserverRef = React.useRef(false);
React.useEffect(() => {
if (
!isRegisteredOnIdentity ||
currentStep.step !== 'identity_registration_dispatched' ||
registeringOnAuthoritativeKeyserverRef.current
) {
return;
}
registeringOnAuthoritativeKeyserverRef.current = true;
const {
avatarData,
clearCachedSelections,
+ onAlertAcknowledged,
credentialsToSave,
resolve,
reject,
} = currentStep;
void (async () => {
try {
await keyserverAuth({
authActionSource: process.env.BROWSER
? logInActionSources.keyserverAuthFromWeb
: logInActionSources.keyserverAuthFromNative,
setInProgress: () => {},
hasBeenCancelled: () => false,
doNotRegister: false,
});
setCurrentStep({
step: 'authoritative_keyserver_registration_dispatched',
avatarData,
clearCachedSelections,
credentialsToSave,
resolve,
reject,
});
} catch (keyserverAuthException) {
const discardIdentityAccountPromise = (async () => {
try {
const deletionResult = await deleteDiscardedIdentityAccount();
Alert.alert(
unknownErrorAlertDetails.title,
unknownErrorAlertDetails.message,
+ [{ text: 'OK', onPress: onAlertAcknowledged }],
+ { cancelable: !onAlertAcknowledged },
);
return deletionResult;
} catch (deleteException) {
Alert.alert(
'Account created but login failed',
'We were able to create your account, but were unable to log ' +
'you in. Try going back to the login screen and logging in ' +
'with your new credentials.',
+ [{ text: 'OK', onPress: onAlertAcknowledged }],
+ { cancelable: !onAlertAcknowledged },
);
throw deleteException;
}
})();
void dispatchActionPromise(
deleteAccountActionTypes,
discardIdentityAccountPromise,
);
await waitUntilDatabaseDeleted();
reject(keyserverAuthException);
setCurrentStep(inactiveStep);
} finally {
registeringOnAuthoritativeKeyserverRef.current = false;
}
})();
}, [
currentStep,
isRegisteredOnIdentity,
keyserverAuth,
dispatchActionPromise,
deleteDiscardedIdentityAccount,
]);
// STEP 3: SETTING AVATAR
const uploadSelectedMedia = useUploadSelectedMedia();
const nativeSetUserAvatar = useNativeSetUserAvatar();
const isLoggedInToAuthoritativeKeyserver = useSelector(
isLoggedInToKeyserver(authoritativeKeyserverID),
);
const avatarBeingSetRef = React.useRef(false);
React.useEffect(() => {
if (
!isLoggedInToAuthoritativeKeyserver ||
currentStep.step !== 'authoritative_keyserver_registration_dispatched' ||
avatarBeingSetRef.current
) {
return;
}
avatarBeingSetRef.current = true;
const { avatarData, resolve, clearCachedSelections, credentialsToSave } =
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,
},
});
clearCachedSelections();
if (credentialsToSave) {
void setNativeCredentials(credentialsToSave);
}
setCurrentStep(inactiveStep);
avatarBeingSetRef.current = false;
resolve();
}
})();
}, [
currentStep,
isLoggedInToAuthoritativeKeyserver,
uploadSelectedMedia,
nativeSetUserAvatar,
dispatch,
]);
return returnedFunc;
}
export { useRegistrationServerCall };
diff --git a/native/account/registration/registration-types.js b/native/account/registration/registration-types.js
index de82500e6..0c2c6e791 100644
--- a/native/account/registration/registration-types.js
+++ b/native/account/registration/registration-types.js
@@ -1,70 +1,71 @@
// @flow
import type {
UpdateUserAvatarRequest,
ClientAvatar,
} from 'lib/types/avatar-types.js';
import type { NativeMediaSelection } from 'lib/types/media-types.js';
import type { SIWEResult, SIWEBackupSecrets } from 'lib/types/siwe-types.js';
export type CoolOrNerdMode = 'cool' | 'nerd';
export type EthereumAccountSelection = {
+accountType: 'ethereum',
...SIWEResult,
+avatarURI: ?string,
};
export type UsernameAccountSelection = {
+accountType: 'username',
+username: string,
+password: string,
};
export type AccountSelection =
| EthereumAccountSelection
| UsernameAccountSelection;
export type AvatarData =
| {
+needsUpload: true,
+mediaSelection: NativeMediaSelection,
+clientAvatar: ClientAvatar,
}
| {
+needsUpload: false,
+updateUserAvatarRequest: UpdateUserAvatarRequest,
+clientAvatar: ClientAvatar,
};
export type RegistrationServerCallInput = {
+coolOrNerdMode?: ?CoolOrNerdMode,
+keyserverURL?: ?string,
+farcasterID: ?string,
+accountSelection: AccountSelection,
+avatarData: ?AvatarData,
+siweBackupSecrets?: ?SIWEBackupSecrets,
+clearCachedSelections: () => void,
+onNonceExpired: () => mixed,
+ +onAlertAcknowledged?: () => mixed,
};
export type CachedUserSelections = {
+coolOrNerdMode?: CoolOrNerdMode,
+keyserverURL?: string,
+username?: string,
+password?: string,
+avatarData?: ?AvatarData,
+ethereumAccount?: ?EthereumAccountSelection,
+farcasterID?: string,
+siweBackupSecrets?: ?SIWEBackupSecrets,
};
export const ensAvatarSelection: AvatarData = {
needsUpload: false,
updateUserAvatarRequest: { type: 'ens' },
clientAvatar: { type: 'ens' },
};
export const enableNewRegistrationMode = __DEV__;
export const enableSIWEBackupCreation = __DEV__;