diff --git a/native/account/fullscreen-siwe-panel.react.js b/native/account/fullscreen-siwe-panel.react.js
index e7ce96df4..b59387cc5 100644
--- a/native/account/fullscreen-siwe-panel.react.js
+++ b/native/account/fullscreen-siwe-panel.react.js
@@ -1,172 +1,169 @@
// @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 { 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';
-import { defaultURLPrefix } from '../utils/url-utils.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 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);
} else if (enableNewRegistrationMode) {
await onAccountDoesNotExist(result);
} else {
await registrationServerCall({
- coolOrNerdMode: 'cool',
- keyserverURL: defaultURLPrefix,
farcasterID: null,
accountSelection: {
accountType: 'ethereum',
...result,
avatarURI: null,
},
avatarData: null,
clearCachedSelections: () => {},
});
}
} 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;
}
dispatch({
type: setDataLoadedActionType,
payload: {
dataLoaded: true,
},
});
}
},
[
walletLogIn,
registrationServerCall,
goBackToPrompt,
dispatch,
legacySiweServerCall,
onAccountDoesNotExist,
],
);
const ifBeforeSuccessGoBackToPrompt = React.useCallback(() => {
if (!successRef.current) {
goBackToPrompt();
}
}, [goBackToPrompt]);
const { closing } = props;
return (
<>
{activity}
>
);
}
export default FullscreenSIWEPanel;
diff --git a/native/account/registration/account-does-not-exist.react.js b/native/account/registration/account-does-not-exist.react.js
index a21a84aad..fc98b54e9 100644
--- a/native/account/registration/account-does-not-exist.react.js
+++ b/native/account/registration/account-does-not-exist.react.js
@@ -1,79 +1,79 @@
// @flow
import * as React from 'react';
import { Text, View, Image } from 'react-native';
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 type { RegistrationNavigationProp } from './registration-navigator.react.js';
import commSwooshSource from '../../img/comm-swoosh.png';
import {
type NavigationRoute,
- CoolOrNerdModeSelectionRouteName,
+ ConnectEthereumRouteName,
} from '../../navigation/route-names.js';
import { useStyles } from '../../themes/colors.js';
type Props = {
+navigation: RegistrationNavigationProp<'AccountDoesNotExist'>,
+route: NavigationRoute<'AccountDoesNotExist'>,
};
function AccountDoesNotExist(props: Props): React.Node {
const { navigate } = props.navigation;
const onSubmit = React.useCallback(() => {
- navigate(CoolOrNerdModeSelectionRouteName);
+ navigate(ConnectEthereumRouteName);
}, [navigate]);
const styles = useStyles(unboundStyles);
return (
New Comm account
It looks like this is your first time logging into Comm.
Let’s get started with registering your Ethereum account!
);
}
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%',
},
};
export default AccountDoesNotExist;
diff --git a/native/account/registration/avatar-selection.react.js b/native/account/registration/avatar-selection.react.js
index fb2b13121..8a5bfe333 100644
--- a/native/account/registration/avatar-selection.react.js
+++ b/native/account/registration/avatar-selection.react.js
@@ -1,217 +1,217 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { Text, View } from 'react-native';
import {
EditUserAvatarContext,
type UserAvatarSelection,
} from 'lib/components/edit-user-avatar-provider.react.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,
type AccountSelection,
type AvatarData,
ensAvatarSelection,
} from './registration-types.js';
import { enableSIWEBackupCreation } from './registration-types.js';
import EditUserAvatar from '../../avatars/edit-user-avatar.react.js';
import { useCurrentLeafRouteName } from '../../navigation/nav-selectors.js';
import {
type NavigationRoute,
RegistrationTermsRouteName,
CreateSIWEBackupMessageRouteName,
AvatarSelectionRouteName,
EmojiAvatarSelectionRouteName,
RegistrationUserAvatarCameraModalRouteName,
} from '../../navigation/route-names.js';
import { useStyles } from '../../themes/colors.js';
export type AvatarSelectionParams = {
+userSelections: {
- +coolOrNerdMode: CoolOrNerdMode,
- +keyserverURL: string,
+ +coolOrNerdMode?: CoolOrNerdMode,
+ +keyserverURL?: string,
+accountSelection: AccountSelection,
+farcasterID: ?string,
},
};
type Props = {
+navigation: RegistrationNavigationProp<'AvatarSelection'>,
+route: NavigationRoute<'AvatarSelection'>,
};
function AvatarSelection(props: Props): React.Node {
const { userSelections } = props.route.params;
const { accountSelection } = userSelections;
const usernameOrETHAddress =
accountSelection.accountType === 'username'
? accountSelection.username
: accountSelection.address;
const registrationContext = React.useContext(RegistrationContext);
invariant(registrationContext, 'registrationContext should be set');
const { cachedSelections, setCachedSelections } = registrationContext;
const editUserAvatarContext = React.useContext(EditUserAvatarContext);
invariant(editUserAvatarContext, 'editUserAvatarContext should be set');
const { setRegistrationMode } = editUserAvatarContext;
const prefetchedAvatarURI =
accountSelection.accountType === 'ethereum'
? accountSelection.avatarURI
: undefined;
let initialAvatarData = cachedSelections.avatarData;
if (!initialAvatarData && prefetchedAvatarURI) {
initialAvatarData = ensAvatarSelection;
}
const [avatarData, setAvatarData] =
React.useState(initialAvatarData);
const setClientAvatarFromSelection = React.useCallback(
(selection: UserAvatarSelection) => {
if (selection.needsUpload) {
const newAvatarData = {
...selection,
clientAvatar: {
type: 'image',
uri: selection.mediaSelection.uri,
},
};
setAvatarData(newAvatarData);
setCachedSelections(oldUserSelections => ({
...oldUserSelections,
avatarData: newAvatarData,
}));
} else if (selection.updateUserAvatarRequest.type !== 'remove') {
const clientRequest = selection.updateUserAvatarRequest;
invariant(
clientRequest.type !== 'image' &&
clientRequest.type !== 'encrypted_image',
'image avatars need to be uploaded',
);
const newAvatarData = {
...selection,
clientAvatar: clientRequest,
};
setAvatarData(newAvatarData);
setCachedSelections(oldUserSelections => ({
...oldUserSelections,
avatarData: newAvatarData,
}));
} else {
setAvatarData(undefined);
setCachedSelections(oldUserSelections => ({
...oldUserSelections,
avatarData: undefined,
}));
}
},
[setCachedSelections],
);
const currentRouteName = useCurrentLeafRouteName();
const avatarSelectionHappening =
currentRouteName === AvatarSelectionRouteName ||
currentRouteName === EmojiAvatarSelectionRouteName ||
currentRouteName === RegistrationUserAvatarCameraModalRouteName;
React.useEffect(() => {
if (!avatarSelectionHappening) {
return undefined;
}
setRegistrationMode({
registrationMode: 'on',
successCallback: setClientAvatarFromSelection,
});
return () => {
setRegistrationMode({ registrationMode: 'off' });
};
}, [
avatarSelectionHappening,
setRegistrationMode,
setClientAvatarFromSelection,
]);
const { navigate } = props.navigation;
const onProceed = React.useCallback(async () => {
const newUserSelections = {
...userSelections,
avatarData,
};
if (
userSelections.accountSelection.accountType === 'ethereum' &&
enableSIWEBackupCreation
) {
navigate<'CreateSIWEBackupMessage'>({
name: CreateSIWEBackupMessageRouteName,
params: { userSelections: newUserSelections },
});
return;
}
navigate<'RegistrationTerms'>({
name: RegistrationTermsRouteName,
params: { userSelections: newUserSelections },
});
}, [userSelections, avatarData, navigate]);
const clientAvatar = avatarData?.clientAvatar;
const userInfoOverride = React.useMemo(
() => ({
username: usernameOrETHAddress,
avatar: clientAvatar,
}),
[usernameOrETHAddress, clientAvatar],
);
const styles = useStyles(unboundStyles);
return (
Pick an avatar
);
}
const unboundStyles = {
scrollViewContentContainer: {
paddingHorizontal: 0,
},
header: {
fontSize: 24,
color: 'panelForegroundLabel',
paddingBottom: 16,
paddingHorizontal: 16,
},
stagedAvatarSection: {
marginTop: 16,
backgroundColor: 'panelForeground',
paddingVertical: 24,
alignItems: 'center',
},
editUserAvatar: {
alignItems: 'center',
justifyContent: 'center',
},
};
export default AvatarSelection;
diff --git a/native/account/registration/connect-ethereum.react.js b/native/account/registration/connect-ethereum.react.js
index cf420c23e..1a7f6d315 100644
--- a/native/account/registration/connect-ethereum.react.js
+++ b/native/account/registration/connect-ethereum.react.js
@@ -1,327 +1,332 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { Text, View } from 'react-native';
import {
exactSearchUser,
exactSearchUserActionTypes,
} from 'lib/actions/user-actions.js';
import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import { type SIWEResult, SIWEMessageTypes } from 'lib/types/siwe-types.js';
import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js';
import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js';
import { useGetEthereumAccountFromSIWEResult } from './ethereum-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 { CoolOrNerdMode } from './registration-types.js';
import { commRustModule } from '../../native-modules.js';
import {
type NavigationRoute,
ExistingEthereumAccountRouteName,
ConnectFarcasterRouteName,
} from '../../navigation/route-names.js';
import { useSelector } from '../../redux/redux-utils.js';
import { useStyles } from '../../themes/colors.js';
+import { defaultURLPrefix } from '../../utils/url-utils.js';
import EthereumLogoDark from '../../vectors/ethereum-logo-dark.react.js';
import SIWEPanel from '../siwe-panel.react.js';
const exactSearchUserLoadingStatusSelector = createLoadingStatusSelector(
exactSearchUserActionTypes,
);
-export type ConnectEthereumParams = {
- +userSelections: {
+export type ConnectEthereumParams = ?{
+ +userSelections?: {
+coolOrNerdMode: CoolOrNerdMode,
+keyserverURL: string,
},
};
type PanelState = 'closed' | 'opening' | 'open' | 'closing';
type Props = {
+navigation: RegistrationNavigationProp<'ConnectEthereum'>,
+route: NavigationRoute<'ConnectEthereum'>,
};
function ConnectEthereum(props: Props): React.Node {
const { params } = props.route;
- const { userSelections } = params;
const registrationContext = React.useContext(RegistrationContext);
invariant(registrationContext, 'registrationContext should be set');
const { cachedSelections } = registrationContext;
- const isNerdMode = userSelections.coolOrNerdMode === 'nerd';
+ const userSelections = params?.userSelections;
+ const isNerdMode = userSelections?.coolOrNerdMode === 'nerd';
const styles = useStyles(unboundStyles);
let body;
if (!isNerdMode) {
body = (
Connecting your Ethereum wallet allows you to use your ENS name and
avatar in the app. You’ll also be able to log in with your wallet
instead of a password.
);
} else {
body = (
<>
Connecting your Ethereum wallet has three benefits:
{'1. '}
Your peers will be able to cryptographically verify that your Comm
account is associated with your Ethereum wallet.
{'2. '}
You’ll be able to use your ENS name and avatar in the app.
{'3. '}
You can choose to skip setting a password, and to log in with your
Ethereum wallet instead.
>
);
}
const [panelState, setPanelState] = React.useState('closed');
const openPanel = React.useCallback(() => {
setPanelState('opening');
}, []);
const onPanelClosed = React.useCallback(() => {
setPanelState('closed');
}, []);
const onPanelClosing = React.useCallback(() => {
setPanelState('closing');
}, []);
const siwePanelSetLoading = React.useCallback(
(loading: boolean) => {
if (panelState === 'closing' || panelState === 'closed') {
return;
}
setPanelState(loading ? 'opening' : 'open');
},
[panelState],
);
const { navigate } = props.navigation;
const onSkip = React.useCallback(() => {
navigate<'ConnectFarcaster'>({
name: ConnectFarcasterRouteName,
- params,
+ params: {
+ userSelections: {
+ ...userSelections,
+ },
+ },
});
- }, [navigate, params]);
+ }, [navigate, userSelections]);
- const { keyserverURL } = userSelections;
+ const keyserverURL = userSelections?.keyserverURL ?? defaultURLPrefix;
const serverCallParamOverride = React.useMemo(
() => ({
urlPrefix: keyserverURL,
}),
[keyserverURL],
);
const exactSearchUserCall = useLegacyAshoatKeyserverCall(
exactSearchUser,
serverCallParamOverride,
);
const dispatchActionPromise = useDispatchActionPromise();
const getEthereumAccountFromSIWEResult =
useGetEthereumAccountFromSIWEResult();
const onSuccessfulWalletSignature = React.useCallback(
async (result: SIWEResult) => {
let userAlreadyExists;
if (usingCommServicesAccessToken) {
const findUserIDResponseString =
await commRustModule.findUserIDForWalletAddress(result.address);
const findUserIDResponse = JSON.parse(findUserIDResponseString);
userAlreadyExists =
!!findUserIDResponse.userID || findUserIDResponse.isReserved;
} else {
const searchPromise = exactSearchUserCall(result.address);
void dispatchActionPromise(exactSearchUserActionTypes, searchPromise);
const { userInfo } = await searchPromise;
userAlreadyExists = !!userInfo;
}
if (userAlreadyExists) {
navigate<'ExistingEthereumAccount'>({
name: ExistingEthereumAccountRouteName,
params: result,
});
return;
}
const ethereumAccount = await getEthereumAccountFromSIWEResult(result);
const newUserSelections = {
...userSelections,
ethereumAccount,
};
navigate<'ConnectFarcaster'>({
name: ConnectFarcasterRouteName,
params: { userSelections: newUserSelections },
});
},
[
userSelections,
exactSearchUserCall,
dispatchActionPromise,
navigate,
getEthereumAccountFromSIWEResult,
],
);
let siwePanel;
if (panelState !== 'closed') {
siwePanel = (
);
}
const { ethereumAccount } = cachedSelections;
const alreadyHasConnected = !!ethereumAccount;
const exactSearchUserCallLoading = useSelector(
state => exactSearchUserLoadingStatusSelector(state) === 'loading',
);
const defaultConnectButtonVariant = alreadyHasConnected
? 'outline'
: 'enabled';
const connectButtonVariant =
exactSearchUserCallLoading || panelState === 'opening'
? 'loading'
: defaultConnectButtonVariant;
const connectButtonText = alreadyHasConnected
? 'Connect new Ethereum wallet'
: 'Connect Ethereum wallet';
const onUseAlreadyConnectedWallet = React.useCallback(() => {
invariant(
ethereumAccount,
'ethereumAccount should be set in onUseAlreadyConnectedWallet',
);
const newUserSelections = {
...userSelections,
ethereumAccount,
};
navigate<'ConnectFarcaster'>({
name: ConnectFarcasterRouteName,
params: { userSelections: newUserSelections },
});
}, [ethereumAccount, userSelections, navigate]);
let alreadyConnectedButton;
if (alreadyHasConnected) {
alreadyConnectedButton = (
);
}
return (
<>
Do you want to connect an Ethereum wallet?
{body}
{alreadyConnectedButton}
{siwePanel}
>
);
}
const unboundStyles = {
scrollViewContentContainer: {
flexGrow: 1,
},
header: {
fontSize: 24,
color: 'panelForegroundLabel',
paddingBottom: 16,
},
body: {
fontFamily: 'Arial',
fontSize: 15,
lineHeight: 20,
color: 'panelForegroundSecondaryLabel',
paddingBottom: 16,
},
ethereumLogoContainer: {
flexGrow: 1,
alignItems: 'center',
justifyContent: 'center',
},
list: {
paddingBottom: 16,
},
listItem: {
flexDirection: 'row',
},
listItemNumber: {
fontFamily: 'Arial',
fontWeight: 'bold',
fontSize: 15,
lineHeight: 20,
color: 'panelForegroundSecondaryLabel',
},
listItemContent: {
fontFamily: 'Arial',
flexShrink: 1,
fontSize: 15,
lineHeight: 20,
color: 'panelForegroundSecondaryLabel',
},
};
export default ConnectEthereum;
diff --git a/native/account/registration/connect-farcaster.react.js b/native/account/registration/connect-farcaster.react.js
index ab31d31b9..aa04cff5e 100644
--- a/native/account/registration/connect-farcaster.react.js
+++ b/native/account/registration/connect-farcaster.react.js
@@ -1,223 +1,223 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { Alert } from 'react-native';
import { IdentityClientContext } from 'lib/shared/identity-client-context.js';
import { useIsAppForegrounded } from 'lib/shared/lifecycle-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 {
CoolOrNerdMode,
EthereumAccountSelection,
} from './registration-types.js';
import FarcasterPrompt from '../../components/farcaster-prompt.react.js';
import FarcasterWebView from '../../components/farcaster-web-view.react.js';
import type { FarcasterWebViewState } from '../../components/farcaster-web-view.react.js';
import {
type NavigationRoute,
UsernameSelectionRouteName,
AvatarSelectionRouteName,
} from '../../navigation/route-names.js';
export type ConnectFarcasterParams = {
+userSelections: {
- +coolOrNerdMode: CoolOrNerdMode,
- +keyserverURL: string,
+ +coolOrNerdMode?: CoolOrNerdMode,
+ +keyserverURL?: string,
+ethereumAccount?: EthereumAccountSelection,
},
};
type Props = {
+navigation: RegistrationNavigationProp<'ConnectFarcaster'>,
+route: NavigationRoute<'ConnectFarcaster'>,
};
function ConnectFarcaster(prop: Props): React.Node {
const { navigation, route } = prop;
const { navigate } = navigation;
const { params } = route;
const registrationContext = React.useContext(RegistrationContext);
invariant(registrationContext, 'registrationContext should be set');
const { cachedSelections, setCachedSelections } = registrationContext;
const [webViewState, setWebViewState] =
React.useState('closed');
const goToNextStep = React.useCallback(
(fid?: ?string) => {
setWebViewState('closed');
const { ethereumAccount, ...restUserSelections } = params.userSelections;
if (ethereumAccount) {
navigate<'AvatarSelection'>({
name: AvatarSelectionRouteName,
params: {
...params,
userSelections: {
...restUserSelections,
accountSelection: ethereumAccount,
farcasterID: fid,
},
},
});
} else {
navigate<'UsernameSelection'>({
name: UsernameSelectionRouteName,
params: {
...params,
userSelections: {
...restUserSelections,
farcasterID: fid,
},
},
});
}
},
[navigate, params],
);
const onSkip = React.useCallback(() => goToNextStep(), [goToNextStep]);
const identityServiceClient = React.useContext(IdentityClientContext);
const getFarcasterUsers =
identityServiceClient?.identityClient.getFarcasterUsers;
invariant(getFarcasterUsers, 'Could not get getFarcasterUsers');
const [queuedAlert, setQueuedAlert] = React.useState{
+title: string,
+body: string,
}>();
const onSuccess = React.useCallback(
async (fid: string) => {
try {
const commFCUsers = await getFarcasterUsers([fid]);
if (commFCUsers.length > 0 && commFCUsers[0].farcasterID === fid) {
const commUsername = commFCUsers[0].username;
setQueuedAlert({
title: 'Farcaster account already linked',
body: `That Farcaster account is already linked to ${commUsername}`,
});
setWebViewState('closed');
} else {
goToNextStep(fid);
setCachedSelections(oldUserSelections => ({
...oldUserSelections,
farcasterID: fid,
}));
}
} catch (e) {
setQueuedAlert({
title: 'Failed to query Comm',
body:
'We failed to query Comm to see if that Farcaster account is ' +
'already linked',
});
setWebViewState('closed');
}
},
[goToNextStep, setCachedSelections, getFarcasterUsers],
);
const isAppForegrounded = useIsAppForegrounded();
React.useEffect(() => {
if (!queuedAlert || !isAppForegrounded) {
return;
}
Alert.alert(queuedAlert.title, queuedAlert.body);
setQueuedAlert(null);
}, [queuedAlert, isAppForegrounded]);
const { farcasterID } = cachedSelections;
const alreadyHasConnected = !!farcasterID;
const onPressConnectFarcaster = React.useCallback(() => {
setWebViewState('opening');
}, []);
const defaultConnectButtonVariant = alreadyHasConnected
? 'outline'
: 'enabled';
const connectButtonVariant =
webViewState === 'opening' ? 'loading' : defaultConnectButtonVariant;
const connectButtonText = alreadyHasConnected
? 'Connect new Farcaster account'
: 'Connect Farcaster account';
const onUseAlreadyConnectedAccount = React.useCallback(() => {
invariant(
farcasterID,
'farcasterID should be set in onUseAlreadyConnectedAccount',
);
goToNextStep(farcasterID);
}, [farcasterID, goToNextStep]);
const alreadyConnectedButton = React.useMemo(() => {
if (!alreadyHasConnected) {
return null;
}
return (
);
}, [alreadyHasConnected, onUseAlreadyConnectedAccount]);
const connectFarcaster = React.useMemo(
() => (
{alreadyConnectedButton}
),
[
alreadyConnectedButton,
connectButtonText,
connectButtonVariant,
onPressConnectFarcaster,
onSkip,
onSuccess,
webViewState,
],
);
return connectFarcaster;
}
const styles = {
scrollViewContentContainer: {
flexGrow: 1,
},
};
export default ConnectFarcaster;
diff --git a/native/account/registration/password-selection.react.js b/native/account/registration/password-selection.react.js
index 953155030..40514eb27 100644
--- a/native/account/registration/password-selection.react.js
+++ b/native/account/registration/password-selection.react.js
@@ -1,251 +1,251 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { View, Text, Platform, TextInput } from 'react-native';
import sleep from 'lib/utils/sleep.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 RegistrationTextInput from './registration-text-input.react.js';
import type { CoolOrNerdMode } from './registration-types.js';
import {
type NavigationRoute,
AvatarSelectionRouteName,
} from '../../navigation/route-names.js';
import { useStyles } from '../../themes/colors.js';
import type { KeyPressEvent } from '../../types/react-native.js';
export type PasswordSelectionParams = {
+userSelections: {
- +coolOrNerdMode: CoolOrNerdMode,
- +keyserverURL: string,
+ +coolOrNerdMode?: CoolOrNerdMode,
+ +keyserverURL?: string,
+farcasterID: ?string,
+username: string,
},
};
type PasswordError = 'passwords_dont_match' | 'empty_password';
type Props = {
+navigation: RegistrationNavigationProp<'PasswordSelection'>,
+route: NavigationRoute<'PasswordSelection'>,
};
function PasswordSelection(props: Props): React.Node {
const registrationContext = React.useContext(RegistrationContext);
invariant(registrationContext, 'registrationContext should be set');
const { cachedSelections, setCachedSelections } = registrationContext;
const [password, setPassword] = React.useState(
cachedSelections.password ?? '',
);
const [confirmPassword, setConfirmPassword] = React.useState(
cachedSelections.password ?? '',
);
const passwordsMatch = password === confirmPassword;
const passwordIsEmpty = password === '';
const [passwordError, setPasswordError] = React.useState();
const potentiallyClearErrors = React.useCallback(() => {
if (!passwordsMatch || passwordIsEmpty) {
return false;
}
setPasswordError(null);
return true;
}, [passwordsMatch, passwordIsEmpty]);
const checkPasswordValidity = React.useCallback(() => {
if (!passwordsMatch) {
setPasswordError('passwords_dont_match');
return false;
} else if (passwordIsEmpty) {
setPasswordError('empty_password');
return false;
}
return potentiallyClearErrors();
}, [passwordsMatch, passwordIsEmpty, potentiallyClearErrors]);
const { userSelections } = props.route.params;
const { navigate } = props.navigation;
const onProceed = React.useCallback(() => {
if (!checkPasswordValidity()) {
return;
}
const { username, ...rest } = userSelections;
const newUserSelections = {
...rest,
accountSelection: {
accountType: 'username',
username,
password,
},
};
setCachedSelections(oldUserSelections => ({
...oldUserSelections,
password,
}));
navigate<'AvatarSelection'>({
name: AvatarSelectionRouteName,
params: { userSelections: newUserSelections },
});
}, [
checkPasswordValidity,
userSelections,
password,
setCachedSelections,
navigate,
]);
const styles = useStyles(unboundStyles);
let errorText;
if (passwordError === 'passwords_dont_match') {
errorText = (
Passwords don’t match
);
} else if (passwordError === 'empty_password') {
errorText = Password cannot be empty;
}
const confirmPasswordInputRef =
React.useRef>();
const focusConfirmPasswordInput = React.useCallback(() => {
confirmPasswordInputRef.current?.focus();
}, []);
const iosPasswordBeingAutoFilled = React.useRef(false);
const confirmPasswordEmpty = confirmPassword.length === 0;
const onPasswordKeyPress = React.useCallback(
(event: KeyPressEvent) => {
const { key } = event.nativeEvent;
// On iOS, paste doesn't trigger onKeyPress, but password autofill does
// Password autofill calls onKeyPress with `key` set to the whole password
if (
key.length > 1 &&
key !== 'Backspace' &&
key !== 'Enter' &&
confirmPasswordEmpty
) {
iosPasswordBeingAutoFilled.current = true;
}
},
[confirmPasswordEmpty],
);
const passwordInputRef = React.useRef>();
const passwordLength = password.length;
const onChangePasswordInput = React.useCallback(
(input: string) => {
setPassword(input);
if (iosPasswordBeingAutoFilled.current) {
// On iOS, paste doesn't trigger onKeyPress, but password autofill does
iosPasswordBeingAutoFilled.current = false;
setConfirmPassword(input);
passwordInputRef.current?.blur();
} else if (
Platform.OS === 'android' &&
input.length - passwordLength > 1 &&
confirmPasswordEmpty
) {
// On Android, password autofill doesn't trigger onKeyPress. Instead we
// rely on observing when the password field changes by more than one
// character at a time. This means we treat paste the same way as
// password autofill
setConfirmPassword(input);
passwordInputRef.current?.blur();
}
},
[passwordLength, confirmPasswordEmpty],
);
const shouldAutoFocus = React.useRef(!cachedSelections.password);
/* eslint-disable react-hooks/rules-of-hooks */
if (Platform.OS === 'android') {
// It's okay to call this hook conditionally because
// the condition is guaranteed to never change
React.useEffect(() => {
void (async () => {
await sleep(250);
if (shouldAutoFocus.current) {
passwordInputRef.current?.focus();
}
})();
}, []);
}
/* eslint-enable react-hooks/rules-of-hooks */
const autoFocus = Platform.OS !== 'android' && shouldAutoFocus.current;
return (
Pick a password
{errorText}
);
}
const unboundStyles = {
header: {
fontSize: 24,
color: 'panelForegroundLabel',
paddingBottom: 16,
},
error: {
marginTop: 16,
},
errorText: {
fontFamily: 'Arial',
fontSize: 15,
lineHeight: 20,
color: 'redText',
},
confirmPassword: {
marginTop: 16,
},
};
export default PasswordSelection;
diff --git a/native/account/registration/registration-navigator.react.js b/native/account/registration/registration-navigator.react.js
index b57d4881c..92a4d0270 100644
--- a/native/account/registration/registration-navigator.react.js
+++ b/native/account/registration/registration-navigator.react.js
@@ -1,131 +1,134 @@
// @flow
import type {
StackNavigationProp,
StackNavigationHelpers,
} from '@react-navigation/core';
import { createStackNavigator } from '@react-navigation/stack';
import * as React from 'react';
import AccountDoesNotExist from './account-does-not-exist.react.js';
import AvatarSelection from './avatar-selection.react.js';
import ConnectEthereum from './connect-ethereum.react.js';
import ConnectFarcaster from './connect-farcaster.react.js';
import CoolOrNerdModeSelection from './cool-or-nerd-mode-selection.react.js';
import EmojiAvatarSelection from './emoji-avatar-selection.react.js';
import ExistingEthereumAccount from './existing-ethereum-account.react.js';
import KeyserverSelection from './keyserver-selection.react.js';
import PasswordSelection from './password-selection.react.js';
import RegistrationTerms from './registration-terms.react.js';
import { CreateSIWEBackupMessage } from './siwe-backup-message-creation.react.js';
import UsernameSelection from './username-selection.react.js';
import RegistrationUserAvatarCameraModal from '../../media/registration-user-avatar-camera-modal.react.js';
import type { RootNavigationProp } from '../../navigation/root-navigator.react.js';
import {
KeyserverSelectionRouteName,
CoolOrNerdModeSelectionRouteName,
ConnectEthereumRouteName,
CreateSIWEBackupMessageRouteName,
ExistingEthereumAccountRouteName,
UsernameSelectionRouteName,
ConnectFarcasterRouteName,
PasswordSelectionRouteName,
AvatarSelectionRouteName,
EmojiAvatarSelectionRouteName,
RegistrationUserAvatarCameraModalRouteName,
RegistrationTermsRouteName,
AccountDoesNotExistRouteName,
type ScreenParamList,
type RegistrationParamList,
} from '../../navigation/route-names.js';
export type RegistrationNavigationProp<
RouteName: $Keys = $Keys,
> = StackNavigationProp;
const Registration = createStackNavigator<
ScreenParamList,
RegistrationParamList,
StackNavigationHelpers,
>();
const screenOptions = {
headerTransparent: true,
headerBackTitleVisible: false,
headerTitle: '',
headerTintColor: 'white',
headerLeftContainerStyle: {
paddingLeft: 12,
},
};
const cameraScreenOptions = {
headerShown: false,
};
type Props = {
+navigation: RootNavigationProp<'Registration'>,
...
};
// eslint-disable-next-line no-unused-vars
function RegistrationNavigator(props: Props): React.Node {
return (
-
+
);
}
export default RegistrationNavigator;
diff --git a/native/account/registration/registration-server-call.js b/native/account/registration/registration-server-call.js
index d795ce186..caf4c85f5 100644
--- a/native/account/registration/registration-server-call.js
+++ b/native/account/registration/registration-server-call.js
@@ -1,472 +1,476 @@
// @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 { 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,
+ keyserverURL: passedKeyserverURL,
farcasterID,
siweBackupSecrets,
clearCachedSelections,
} = 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,
);
throw e;
}
}
- dispatch({
- type: setURLPrefix,
- payload: keyserverURL,
- });
+ 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 126c495cb..5d0a51f86 100644
--- a/native/account/registration/registration-terms.react.js
+++ b/native/account/registration/registration-terms.react.js
@@ -1,162 +1,162 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { Text, View, Image, Linking } 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 { useStyles } from '../../themes/colors.js';
export type RegistrationTermsParams = {
+userSelections: {
- +coolOrNerdMode: CoolOrNerdMode,
- +keyserverURL: string,
+ +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 onProceed = React.useCallback(async () => {
setRegistrationInProgress(true);
try {
await register({ ...userSelections, clearCachedSelections });
} finally {
setRegistrationInProgress(false);
}
}, [register, userSelections, clearCachedSelections]);
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 8db7816ef..a99aab85b 100644
--- a/native/account/registration/registration-types.js
+++ b/native/account/registration/registration-types.js
@@ -1,69 +1,69 @@
// @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,
+ +coolOrNerdMode?: CoolOrNerdMode,
+ +keyserverURL?: string,
+farcasterID: ?string,
+accountSelection: AccountSelection,
+avatarData: ?AvatarData,
+siweBackupSecrets?: ?SIWEBackupSecrets,
+clearCachedSelections: () => void,
};
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__;
diff --git a/native/account/registration/siwe-backup-message-creation.react.js b/native/account/registration/siwe-backup-message-creation.react.js
index f5ab969d8..baf92121d 100644
--- a/native/account/registration/siwe-backup-message-creation.react.js
+++ b/native/account/registration/siwe-backup-message-creation.react.js
@@ -1,240 +1,240 @@
// @flow
import Icon from '@expo/vector-icons/MaterialIcons.js';
import invariant from 'invariant';
import * as React from 'react';
import { View, Text } from 'react-native';
import { type SIWEResult, SIWEMessageTypes } 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 {
type NavigationRoute,
RegistrationTermsRouteName,
} from '../../navigation/route-names.js';
import { useStyles } from '../../themes/colors.js';
import SIWEPanel from '../siwe-panel.react.js';
type PanelState = 'closed' | 'opening' | 'open' | 'closing';
type CreateSIWEBackupMessageBaseProps = {
+onSuccessfulWalletSignature: (result: SIWEResult) => void,
+onExistingWalletSignature?: () => void,
+onSkip?: () => void,
};
const CreateSIWEBackupMessageBase: React.ComponentType =
React.memo(
function CreateSIWEBackupMessageBase(
props: CreateSIWEBackupMessageBaseProps,
): React.Node {
const { onSuccessfulWalletSignature, onExistingWalletSignature, onSkip } =
props;
const styles = useStyles(unboundStyles);
const [panelState, setPanelState] = React.useState('closed');
const openPanel = React.useCallback(() => {
setPanelState('opening');
}, []);
const onPanelClosed = React.useCallback(() => {
setPanelState('closed');
}, []);
const onPanelClosing = React.useCallback(() => {
setPanelState('closing');
}, []);
const siwePanelSetLoading = React.useCallback(
(loading: boolean) => {
if (panelState === 'closing' || panelState === 'closed') {
return;
}
setPanelState(loading ? 'opening' : 'open');
},
[panelState],
);
let siwePanel;
if (panelState !== 'closed') {
siwePanel = (
);
}
const newSignatureButtonText = onExistingWalletSignature
? 'Encrypt with new signature'
: 'Encrypt with Ethereum signature';
const newSignatureButtonVariant = onExistingWalletSignature
? 'outline'
: 'enabled';
let useExistingSignatureButton;
if (onExistingWalletSignature) {
useExistingSignatureButton = (
);
}
let onSkipButton;
if (onSkip) {
onSkipButton = (
);
}
const body = (
Comm encrypts user backups so that our backend is not able to see user
data.
);
return (
<>
Encrypting your Comm Backup
{body}
{useExistingSignatureButton}
{onSkipButton}
{siwePanel}
>
);
},
);
export type CreateSIWEBackupMessageParams = {
+userSelections: {
- +coolOrNerdMode: CoolOrNerdMode,
- +keyserverURL: string,
+ +coolOrNerdMode?: CoolOrNerdMode,
+ +keyserverURL?: string,
+farcasterID: ?string,
+accountSelection: AccountSelection,
+avatarData: ?AvatarData,
},
};
type Props = {
+navigation: RegistrationNavigationProp<'CreateSIWEBackupMessage'>,
+route: NavigationRoute<'CreateSIWEBackupMessage'>,
};
function CreateSIWEBackupMessage(props: Props): React.Node {
const { navigate } = props.navigation;
const { params } = props.route;
const { userSelections } = params;
const registrationContext = React.useContext(RegistrationContext);
invariant(registrationContext, 'registrationContext should be set');
const { cachedSelections, setCachedSelections } = registrationContext;
const onSuccessfulWalletSignature = React.useCallback(
(result: SIWEResult) => {
const { message, signature } = result;
const newUserSelections = {
...userSelections,
siweBackupSecrets: { message, signature },
};
setCachedSelections(oldUserSelections => ({
...oldUserSelections,
siweBackupSecrets: { message, signature },
}));
navigate<'RegistrationTerms'>({
name: RegistrationTermsRouteName,
params: { userSelections: newUserSelections },
});
},
[navigate, setCachedSelections, userSelections],
);
const { siweBackupSecrets } = cachedSelections;
const onExistingWalletSignature = React.useCallback(() => {
const registrationTermsParams = {
userSelections: {
...userSelections,
siweBackupSecrets,
},
};
navigate<'RegistrationTerms'>({
name: RegistrationTermsRouteName,
params: registrationTermsParams,
});
}, [navigate, siweBackupSecrets, userSelections]);
if (siweBackupSecrets) {
return (
);
}
return (
);
}
const unboundStyles = {
scrollViewContentContainer: {
flexGrow: 1,
},
header: {
fontSize: 24,
color: 'panelForegroundLabel',
paddingBottom: 16,
},
body: {
fontFamily: 'Arial',
fontSize: 15,
lineHeight: 20,
color: 'panelForegroundSecondaryLabel',
paddingBottom: 16,
},
siweBackupIcon: {
color: 'panelForegroundIcon',
},
siweBackupIconContainer: {
flexGrow: 1,
alignItems: 'center',
justifyContent: 'center',
},
};
export { CreateSIWEBackupMessageBase, CreateSIWEBackupMessage };
diff --git a/native/account/registration/username-selection.react.js b/native/account/registration/username-selection.react.js
index ee2617acb..25e11889a 100644
--- a/native/account/registration/username-selection.react.js
+++ b/native/account/registration/username-selection.react.js
@@ -1,240 +1,240 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { View, Text } from 'react-native';
import {
exactSearchUser,
exactSearchUserActionTypes,
} from 'lib/actions/user-actions.js';
import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import { validUsernameRegex } from 'lib/shared/account-utils.js';
import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js';
import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js';
import { isValidEthereumAddress } from 'lib/utils/siwe-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 RegistrationTextInput from './registration-text-input.react.js';
import type { CoolOrNerdMode } from './registration-types.js';
import { commRustModule } from '../../native-modules.js';
import {
type NavigationRoute,
PasswordSelectionRouteName,
} from '../../navigation/route-names.js';
import { useSelector } from '../../redux/redux-utils.js';
import { useStyles } from '../../themes/colors.js';
const exactSearchUserLoadingStatusSelector = createLoadingStatusSelector(
exactSearchUserActionTypes,
);
export type UsernameSelectionParams = {
+userSelections: {
- +coolOrNerdMode: CoolOrNerdMode,
- +keyserverURL: string,
+ +coolOrNerdMode?: CoolOrNerdMode,
+ +keyserverURL?: string,
+farcasterID: ?string,
},
};
type UsernameError = 'username_invalid' | 'username_taken';
type Props = {
+navigation: RegistrationNavigationProp<'UsernameSelection'>,
+route: NavigationRoute<'UsernameSelection'>,
};
function UsernameSelection(props: Props): React.Node {
const registrationContext = React.useContext(RegistrationContext);
invariant(registrationContext, 'registrationContext should be set');
const { cachedSelections, setCachedSelections } = registrationContext;
const [username, setUsername] = React.useState(
cachedSelections.username ?? '',
);
const validUsername =
username.search(validUsernameRegex) > -1 &&
!isValidEthereumAddress(username.toLowerCase());
const [usernameError, setUsernameError] = React.useState();
const checkUsernameValidity = React.useCallback(() => {
if (!validUsername) {
setUsernameError('username_invalid');
return false;
}
setUsernameError(null);
return true;
}, [validUsername]);
const { userSelections } = props.route.params;
const { keyserverURL } = userSelections;
const serverCallParamOverride = React.useMemo(
() => ({
urlPrefix: keyserverURL,
}),
[keyserverURL],
);
const exactSearchUserCall = useLegacyAshoatKeyserverCall(
exactSearchUser,
serverCallParamOverride,
);
const dispatchActionPromise = useDispatchActionPromise();
const { navigate } = props.navigation;
const onProceed = React.useCallback(async () => {
if (!checkUsernameValidity()) {
return;
}
let userAlreadyExists;
if (usingCommServicesAccessToken) {
const findUserIDResponseString =
await commRustModule.findUserIDForUsername(username);
const findUserIDResponse = JSON.parse(findUserIDResponseString);
userAlreadyExists =
!!findUserIDResponse.userID || findUserIDResponse.isReserved;
} else {
const searchPromise = exactSearchUserCall(username);
void dispatchActionPromise(exactSearchUserActionTypes, searchPromise);
const { userInfo } = await searchPromise;
userAlreadyExists = !!userInfo;
}
if (userAlreadyExists) {
setUsernameError('username_taken');
return;
}
setUsernameError(undefined);
setCachedSelections(oldUserSelections => ({
...oldUserSelections,
username,
}));
navigate<'PasswordSelection'>({
name: PasswordSelectionRouteName,
params: {
userSelections: {
...userSelections,
username,
},
},
});
}, [
checkUsernameValidity,
username,
exactSearchUserCall,
dispatchActionPromise,
setCachedSelections,
navigate,
userSelections,
]);
const exactSearchUserCallLoading = useSelector(
state => exactSearchUserLoadingStatusSelector(state) === 'loading',
);
let buttonVariant = 'disabled';
if (exactSearchUserCallLoading) {
buttonVariant = 'loading';
} else if (validUsername) {
buttonVariant = 'enabled';
}
const styles = useStyles(unboundStyles);
let errorText;
if (usernameError === 'username_invalid') {
errorText = (
<>
Usernames must:
{'1. '}
Be at least one character long.
{'2. '}
Start with either a letter or a number.
{'3. '}
Contain only letters, numbers, or the characters “-” and “_”.
>
);
} else if (usernameError === 'username_taken') {
errorText = (
Username taken. Please try another one
);
}
const shouldAutoFocus = React.useRef(!cachedSelections.username);
return (
Pick a username
{errorText}
);
}
const unboundStyles = {
header: {
fontSize: 24,
color: 'panelForegroundLabel',
paddingBottom: 16,
},
error: {
marginTop: 16,
},
errorText: {
fontFamily: 'Arial',
fontSize: 15,
lineHeight: 20,
color: 'redText',
},
listItem: {
flexDirection: 'row',
},
listItemNumber: {
fontWeight: 'bold',
},
listItemContent: {
flexShrink: 1,
},
};
export default UsernameSelection;