diff --git a/native/account/fullscreen-siwe-panel.react.js b/native/account/fullscreen-siwe-panel.react.js index b59387cc5..bf4f09702 100644 --- a/native/account/fullscreen-siwe-panel.react.js +++ b/native/account/fullscreen-siwe-panel.react.js @@ -1,169 +1,202 @@ // @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 } from 'lib/utils/errors.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 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) { - await walletLogIn(result.address, result.message, result.signature); + try { + await walletLogIn( + result.address, + result.message, + result.signature, + ); + } catch (e) { + const messageForException = getMessageForException(e); + if (messageForException === 'nonce expired') { + onNonceExpired('login'); + } else { + throw e; + } + } } else if (enableNewRegistrationMode) { await onAccountDoesNotExist(result); } else { - await registrationServerCall({ - farcasterID: null, - accountSelection: { - accountType: 'ethereum', - ...result, - avatarURI: null, - }, - avatarData: null, - clearCachedSelections: () => {}, - }); + 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 }, ); - throw e; } } else { try { await legacySiweServerCall({ ...result, doNotRegister: enableNewRegistrationMode, }); } catch (e) { if ( e instanceof ServerError && e.message === 'account_does_not_exist' ) { await onAccountDoesNotExist(result); return; } Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, [{ text: 'OK', onPress: goBackToPrompt }], { cancelable: false }, ); - throw e; + 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 caf4c85f5..e9b574052 100644 --- a/native/account/registration/registration-server-call.js +++ b/native/account/registration/registration-server-call.js @@ -1,476 +1,483 @@ // @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, +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, ) => { 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, ); } else if (e.message === 'username already exists') { Alert.alert( UsernameTakenAlertDetails.title, UsernameTakenAlertDetails.message, ); } else if (e.message === 'Unsupported version') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, ); } else { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, ); } throw e; } })(); void dispatchActionPromise( identityRegisterActionTypes, identityRegisterPromise, ); await identityRegisterPromise; }, [callIdentityPasswordRegister, dispatchActionPromise], ); const legacyKeyserverRegisterUsernameAccount = React.useCallback( async ( accountSelection: UsernameAccountSelection, keyserverURL: string, ) => { 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, ); } else if (e.message === 'username_taken') { Alert.alert( UsernameTakenAlertDetails.title, UsernameTakenAlertDetails.message, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, ); } else { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, ); } 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, } = input; const keyserverURL = passedKeyserverURL ?? defaultURLPrefix; if ( accountSelection.accountType === 'username' && !usingCommServicesAccessToken ) { await legacyKeyserverRegisterUsernameAccount( accountSelection, keyserverURL, ); } else if (accountSelection.accountType === 'username') { await identityRegisterUsernameAccount( accountSelection, farcasterID, ); } else if (!usingCommServicesAccessToken) { try { await legacySiweServerCall(accountSelection, { urlPrefixOverride: keyserverURL, }); } catch (e) { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, ); throw e; } } else { try { await identityWalletRegisterCall({ address: accountSelection.address, message: accountSelection.message, signature: accountSelection.signature, fid: farcasterID, }); } catch (e) { - Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, - ); + const messageForException = getMessageForException(e); + if (messageForException === 'nonce expired') { + onNonceExpired(); + } else { + Alert.alert( + UnknownErrorAlertDetails.title, + UnknownErrorAlertDetails.message, + ); + } 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, 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, 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, ); 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.', ); 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-terms.react.js b/native/account/registration/registration-terms.react.js index 5d0a51f86..0caf77d15 100644 --- a/native/account/registration/registration-terms.react.js +++ b/native/account/registration/registration-terms.react.js @@ -1,162 +1,198 @@ // @flow +import type { + StackNavigationEventMap, + StackNavigationState, + StackOptions, +} from '@react-navigation/core'; import invariant from 'invariant'; import * as React from 'react'; -import { Text, View, Image, Linking } from 'react-native'; +import { Text, View, Image, Linking, Alert } from 'react-native'; import type { SIWEBackupSecrets } from 'lib/types/siwe-types.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 { CoolOrNerdMode, AccountSelection, AvatarData, } from './registration-types.js'; import commSwooshSource from '../../img/comm-swoosh.png'; import { logInActionType } from '../../navigation/action-types.js'; -import type { NavigationRoute } from '../../navigation/route-names.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'; export type RegistrationTermsParams = { +userSelections: { +coolOrNerdMode?: CoolOrNerdMode, +keyserverURL?: string, +farcasterID: ?string, +accountSelection: AccountSelection, +avatarData: ?AvatarData, +siweBackupSecrets?: ?SIWEBackupSecrets, }, }; const onTermsOfUsePressed = () => { void Linking.openURL('https://comm.app/terms'); }; const onPrivacyPolicyPressed = () => { void Linking.openURL('https://comm.app/privacy'); }; type Props = { +navigation: RegistrationNavigationProp<'RegistrationTerms'>, +route: NavigationRoute<'RegistrationTerms'>, }; function RegistrationTerms(props: Props): React.Node { const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { register, setCachedSelections } = registrationContext; const [registrationInProgress, setRegistrationInProgress] = React.useState(false); const { userSelections } = props.route.params; const clearCachedSelections = React.useCallback(() => { setCachedSelections({}); }, [setCachedSelections]); + const { navigation } = props; + const goBackToHome = navigation.getParent< + ScreenParamList, + 'Registration', + StackNavigationState, + StackOptions, + StackNavigationEventMap, + RootNavigationProp<'Registration'>, + >()?.goBack; + const onNonceExpired = React.useCallback(() => { + setCachedSelections(oldUserSelections => ({ + ...oldUserSelections, + ethereumAccount: undefined, + })); + Alert.alert( + 'Registration attempt timed out', + 'Please try to connect your Ethereum wallet again', + [{ text: 'OK', onPress: goBackToHome }], + { + cancelable: false, + }, + ); + }, [goBackToHome, setCachedSelections]); + const onProceed = React.useCallback(async () => { setRegistrationInProgress(true); try { - await register({ ...userSelections, clearCachedSelections }); + await register({ + ...userSelections, + clearCachedSelections, + onNonceExpired, + }); } finally { setRegistrationInProgress(false); } - }, [register, userSelections, clearCachedSelections]); + }, [register, userSelections, clearCachedSelections, onNonceExpired]); - const { navigation } = props; React.useEffect(() => { if (!registrationInProgress) { return undefined; } navigation.setOptions({ gestureEnabled: false, headerLeft: null, }); const removeListener = navigation.addListener('beforeRemove', e => { if (e.data.action.type !== logInActionType) { e.preventDefault(); } }); return () => { navigation.setOptions({ gestureEnabled: true, headerLeft: undefined, }); removeListener(); }; }, [navigation, registrationInProgress]); const styles = useStyles(unboundStyles); const termsNotice = ( By registering, you are agreeing to our{' '} Terms of Use {' and '} Privacy Policy . ); return ( Finish registration {termsNotice} ); } const unboundStyles = { scrollViewContentContainer: { flexGrow: 1, }, header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, body: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 16, }, commSwooshContainer: { flexGrow: 1, flexShrink: 1, alignItems: 'center', justifyContent: 'center', }, commSwoosh: { resizeMode: 'center', width: '100%', height: '100%', }, hyperlinkText: { color: 'purpleLink', }, }; export default RegistrationTerms; diff --git a/native/account/registration/registration-types.js b/native/account/registration/registration-types.js index 9a0f82f13..1badb7c1f 100644 --- a/native/account/registration/registration-types.js +++ b/native/account/registration/registration-types.js @@ -1,69 +1,70 @@ // @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, }; 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__;