diff --git a/native/account/registration/avatar-selection.react.js b/native/account/registration/avatar-selection.react.js
index f0bde1c3c..dd9012774 100644
--- a/native/account/registration/avatar-selection.react.js
+++ b/native/account/registration/avatar-selection.react.js
@@ -1,202 +1,202 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { Text, View } 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 { 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 {
EditUserAvatarContext,
type UserAvatarSelection,
} from '../../avatars/edit-user-avatar-provider.react.js';
import EditUserAvatar from '../../avatars/edit-user-avatar.react.js';
import { useCurrentLeafRouteName } from '../../navigation/nav-selectors.js';
import {
type NavigationRoute,
RegistrationTermsRouteName,
AvatarSelectionRouteName,
EmojiAvatarSelectionRouteName,
RegistrationUserAvatarCameraModalRouteName,
} from '../../navigation/route-names.js';
import { useStyles } from '../../themes/colors.js';
export type AvatarSelectionParams = {
+userSelections: {
+coolOrNerdMode: CoolOrNerdMode,
- +keyserverUsername: string,
+ +keyserverURL: string,
+accountSelection: AccountSelection,
},
};
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',
'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,
};
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 f072884f4..eb2571815 100644
--- a/native/account/registration/connect-ethereum.react.js
+++ b/native/account/registration/connect-ethereum.react.js
@@ -1,303 +1,303 @@
// @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 { ENSCacheContext } from 'lib/components/ens-cache-provider.react.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import type { SIWEResult } from 'lib/types/siwe-types.js';
import {
useServerCall,
useDispatchActionPromise,
} from 'lib/utils/action-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,
ensAvatarSelection,
} from './registration-types.js';
import {
type NavigationRoute,
ExistingEthereumAccountRouteName,
UsernameSelectionRouteName,
AvatarSelectionRouteName,
} from '../../navigation/route-names.js';
import { useSelector } from '../../redux/redux-utils.js';
import { useStyles } from '../../themes/colors.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: {
+coolOrNerdMode: CoolOrNerdMode,
- +keyserverUsername: string,
+ +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 } = props.route.params;
const registrationContext = React.useContext(RegistrationContext);
invariant(registrationContext, 'registrationContext should be set');
const { setCachedSelections } = registrationContext;
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<'UsernameSelection'>({
name: UsernameSelectionRouteName,
params,
});
}, [navigate, params]);
const exactSearchUserCall = useServerCall(exactSearchUser);
const dispatchActionPromise = useDispatchActionPromise();
const cacheContext = React.useContext(ENSCacheContext);
const { ensCache } = cacheContext;
const onSuccessfulWalletSignature = React.useCallback(
async (result: SIWEResult) => {
const searchPromise = exactSearchUserCall(result.address);
dispatchActionPromise(exactSearchUserActionTypes, searchPromise);
// We want to figure out if the user has an ENS avatar now
// so that we can default to the ENS avatar in AvatarSelection
const avatarURIPromise = (async () => {
if (!ensCache) {
return null;
}
return await ensCache.getAvatarURIForAddress(result.address);
})();
const { userInfo } = await searchPromise;
if (userInfo) {
navigate<'ExistingEthereumAccount'>({
name: ExistingEthereumAccountRouteName,
params: result,
});
return;
}
const avatarURI = await avatarURIPromise;
const ethereumAccount = {
accountType: 'ethereum',
...result,
avatarURI,
};
setCachedSelections(oldUserSelections => {
const base = {
...oldUserSelections,
ethereumAccount,
};
if (base.avatarData || !avatarURI) {
return base;
}
return {
...base,
avatarData: ensAvatarSelection,
};
});
const newUserSelections = {
...userSelections,
accountSelection: ethereumAccount,
};
navigate<'AvatarSelection'>({
name: AvatarSelectionRouteName,
params: { userSelections: newUserSelections },
});
},
[
userSelections,
exactSearchUserCall,
dispatchActionPromise,
setCachedSelections,
navigate,
ensCache,
],
);
let siwePanel;
if (panelState !== 'closed') {
siwePanel = (
);
}
const exactSearchUserCallLoading = useSelector(
state => exactSearchUserLoadingStatusSelector(state) === 'loading',
);
const connectButtonVariant =
exactSearchUserCallLoading || panelState === 'opening'
? 'loading'
: 'enabled';
return (
<>
Do you want to connect an Ethereum wallet?
{body}
{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/keyserver-selection.react.js b/native/account/registration/keyserver-selection.react.js
index ffb8326b8..140c0ee3c 100644
--- a/native/account/registration/keyserver-selection.react.js
+++ b/native/account/registration/keyserver-selection.react.js
@@ -1,232 +1,230 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { Text } from 'react-native';
import {
getVersion,
getVersionActionTypes,
} from 'lib/actions/device-actions.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import {
useServerCall,
useDispatchActionPromise,
} from 'lib/utils/action-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 {
RegistrationTile,
RegistrationTileHeader,
} from './registration-tile.react.js';
import type { CoolOrNerdMode } from './registration-types.js';
import CommIcon from '../../components/comm-icon.react.js';
import {
type NavigationRoute,
ConnectEthereumRouteName,
} from '../../navigation/route-names.js';
import { useSelector } from '../../redux/redux-utils.js';
import { useStyles, useColors } from '../../themes/colors.js';
import { defaultURLPrefix } from '../../utils/url-utils.js';
type Selection = 'ashoat' | 'custom';
export type KeyserverSelectionParams = {
+userSelections: {
+coolOrNerdMode: CoolOrNerdMode,
},
};
const getVersionLoadingStatusSelector = createLoadingStatusSelector(
getVersionActionTypes,
);
type Props = {
+navigation: RegistrationNavigationProp<'KeyserverSelection'>,
+route: NavigationRoute<'KeyserverSelection'>,
};
function KeyserverSelection(props: Props): React.Node {
const registrationContext = React.useContext(RegistrationContext);
invariant(registrationContext, 'registrationContext should be set');
const { cachedSelections, setCachedSelections } = registrationContext;
- const initialKeyserverUsername = cachedSelections.keyserverUsername;
+ const initialKeyserverURL = cachedSelections.keyserverURL;
const [customKeyserver, setCustomKeyserver] = React.useState(
- initialKeyserverUsername === defaultURLPrefix
- ? ''
- : initialKeyserverUsername,
+ initialKeyserverURL === defaultURLPrefix ? '' : initialKeyserverURL,
);
const customKeyserverTextInputRef = React.useRef();
let initialSelection;
- if (initialKeyserverUsername === defaultURLPrefix) {
+ if (initialKeyserverURL === defaultURLPrefix) {
initialSelection = 'ashoat';
- } else if (initialKeyserverUsername) {
+ } else if (initialKeyserverURL) {
initialSelection = 'custom';
}
const [currentSelection, setCurrentSelection] =
React.useState(initialSelection);
const selectAshoat = React.useCallback(() => {
setCurrentSelection('ashoat');
customKeyserverTextInputRef.current?.blur();
}, []);
const customKeyserverEmpty = !customKeyserver;
const selectCustom = React.useCallback(() => {
setCurrentSelection('custom');
if (customKeyserverEmpty) {
customKeyserverTextInputRef.current?.focus();
}
}, [customKeyserverEmpty]);
const onCustomKeyserverFocus = React.useCallback(() => {
setCurrentSelection('custom');
}, []);
- let keyserverUsername;
+ let keyserverURL;
if (currentSelection === 'ashoat') {
- keyserverUsername = defaultURLPrefix;
+ keyserverURL = defaultURLPrefix;
} else if (currentSelection === 'custom' && customKeyserver) {
- keyserverUsername = customKeyserver;
+ keyserverURL = customKeyserver;
}
const versionLoadingStatus = useSelector(getVersionLoadingStatusSelector);
- let buttonState = keyserverUsername ? 'enabled' : 'disabled';
+ let buttonState = keyserverURL ? 'enabled' : 'disabled';
if (versionLoadingStatus === 'loading') {
buttonState = 'loading';
}
const serverCallParamOverride = React.useMemo(
() => ({
- urlPrefix: keyserverUsername,
+ urlPrefix: keyserverURL,
}),
- [keyserverUsername],
+ [keyserverURL],
);
const getVersionCall = useServerCall(getVersion, serverCallParamOverride);
const dispatchActionPromise = useDispatchActionPromise();
const { navigate } = props.navigation;
const { coolOrNerdMode } = props.route.params.userSelections;
const onSubmit = React.useCallback(async () => {
- if (!keyserverUsername) {
+ if (!keyserverURL) {
return;
}
const getVersionPromise = getVersionCall();
dispatchActionPromise(getVersionActionTypes, getVersionPromise);
// We don't care about the result; just need to make sure this doesn't throw
await getVersionPromise;
setCachedSelections(oldUserSelections => ({
...oldUserSelections,
- keyserverUsername,
+ keyserverURL,
}));
navigate<'ConnectEthereum'>({
name: ConnectEthereumRouteName,
- params: { userSelections: { coolOrNerdMode, keyserverUsername } },
+ params: { userSelections: { coolOrNerdMode, keyserverURL } },
});
}, [
navigate,
coolOrNerdMode,
- keyserverUsername,
+ keyserverURL,
setCachedSelections,
dispatchActionPromise,
getVersionCall,
]);
const styles = useStyles(unboundStyles);
const colors = useColors();
return (
Select a keyserver to join
Chat communities on Comm are hosted on keyservers, which are
user-operated backends.
Keyservers allow Comm to offer strong privacy guarantees without
sacrificing functionality.
ashoat
Ashoat is Comm’s founder, and his keyserver currently hosts most of
the communities on Comm.
Enter a keyserver
);
}
const unboundStyles = {
header: {
fontSize: 24,
color: 'panelForegroundLabel',
paddingBottom: 16,
},
body: {
fontFamily: 'Arial',
fontSize: 15,
lineHeight: 20,
color: 'panelForegroundSecondaryLabel',
paddingBottom: 16,
},
tileTitleText: {
flex: 1,
fontSize: 18,
color: 'panelForegroundLabel',
},
tileBody: {
fontFamily: 'Arial',
fontSize: 13,
color: 'panelForegroundSecondaryLabel',
},
cloud: {
marginRight: 8,
},
};
export default KeyserverSelection;
diff --git a/native/account/registration/password-selection.react.js b/native/account/registration/password-selection.react.js
index 4f9fb5502..62e37bfb9 100644
--- a/native/account/registration/password-selection.react.js
+++ b/native/account/registration/password-selection.react.js
@@ -1,248 +1,248 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { View, Text, Platform } 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,
- +keyserverUsername: string,
+ +keyserverURL: 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 { coolOrNerdMode, keyserverUsername, username } = userSelections;
+ const { coolOrNerdMode, keyserverURL, username } = userSelections;
const newUserSelections = {
coolOrNerdMode,
- keyserverUsername,
+ keyserverURL,
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(() => {
(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-terms.react.js b/native/account/registration/registration-terms.react.js
index d7ab45f41..c2def8e41 100644
--- a/native/account/registration/registration-terms.react.js
+++ b/native/account/registration/registration-terms.react.js
@@ -1,131 +1,131 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { Text, View, Image, Linking } 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 { 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 type { NavigationRoute } from '../../navigation/route-names.js';
import { useStyles } from '../../themes/colors.js';
export type RegistrationTermsParams = {
+userSelections: {
+coolOrNerdMode: CoolOrNerdMode,
- +keyserverUsername: string,
+ +keyserverURL: string,
+accountSelection: AccountSelection,
+avatarData: ?AvatarData,
},
};
const onTermsOfUsePressed = () => {
Linking.openURL('https://comm.app/terms');
};
const onPrivacyPolicyPressed = () => {
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 } = registrationContext;
const [registrationInProgress, setRegistrationInProgress] =
React.useState(false);
const { userSelections } = props.route.params;
const onProceed = React.useCallback(async () => {
setRegistrationInProgress(true);
try {
await register(userSelections);
} finally {
setRegistrationInProgress(false);
}
}, [register, userSelections]);
const styles = useStyles(unboundStyles);
/* eslint-disable react-native/no-raw-text */
const termsNotice = (
By registering, you are agreeing to our{' '}
Terms of Use
{' and '}
Privacy Policy
.
);
/* eslint-enable react-native/no-raw-text */
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 5d07932e9..3106a51fe 100644
--- a/native/account/registration/registration-types.js
+++ b/native/account/registration/registration-types.js
@@ -1,60 +1,60 @@
// @flow
import type {
UpdateUserAvatarRequest,
ClientAvatar,
} from 'lib/types/avatar-types.js';
import type { NativeMediaSelection } from 'lib/types/media-types.js';
import type { SIWEResult } 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,
- +keyserverUsername: string,
+ +keyserverURL: string,
+accountSelection: AccountSelection,
+avatarData: ?AvatarData,
};
export type CachedUserSelections = {
+coolOrNerdMode?: CoolOrNerdMode,
- +keyserverUsername?: string,
+ +keyserverURL?: string,
+username?: string,
+password?: string,
+avatarData?: ?AvatarData,
+ethereumAccount?: EthereumAccountSelection,
};
export const ensAvatarSelection: AvatarData = {
needsUpload: false,
updateUserAvatarRequest: { type: 'ens' },
clientAvatar: { type: 'ens' },
};
diff --git a/native/account/registration/username-selection.react.js b/native/account/registration/username-selection.react.js
index 8b08a2612..6560e5a1b 100644
--- a/native/account/registration/username-selection.react.js
+++ b/native/account/registration/username-selection.react.js
@@ -1,215 +1,215 @@
// @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 { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import { validUsernameRegex } from 'lib/shared/account-utils.js';
import {
useServerCall,
useDispatchActionPromise,
} from 'lib/utils/action-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 {
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,
- +keyserverUsername: string,
+ +keyserverURL: 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;
const [usernameError, setUsernameError] = React.useState();
const checkUsernameValidity = React.useCallback(() => {
if (!validUsername) {
setUsernameError('username_invalid');
return false;
}
setUsernameError(null);
return true;
}, [validUsername]);
const exactSearchUserCall = useServerCall(exactSearchUser);
const dispatchActionPromise = useDispatchActionPromise();
const { navigate } = props.navigation;
const { userSelections } = props.route.params;
const onProceed = React.useCallback(async () => {
if (!checkUsernameValidity()) {
return;
}
const searchPromise = exactSearchUserCall(username);
dispatchActionPromise(exactSearchUserActionTypes, searchPromise);
const { userInfo } = await searchPromise;
if (userInfo) {
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;