Changeset View
Standalone View
native/account/registration/username-selection.react.js
// @flow | // @flow | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { Text } from 'react-native'; | 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 RegistrationButtonContainer from './registration-button-container.react.js'; | ||||
import RegistrationButton from './registration-button.react.js'; | import RegistrationButton from './registration-button.react.js'; | ||||
import RegistrationContainer from './registration-container.react.js'; | import RegistrationContainer from './registration-container.react.js'; | ||||
import RegistrationContentContainer from './registration-content-container.react.js'; | import RegistrationContentContainer from './registration-content-container.react.js'; | ||||
import type { RegistrationNavigationProp } from './registration-navigator.react.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 { CoolOrNerdMode } from './registration-types.js'; | ||||
import type { NavigationRoute } from '../../navigation/route-names.js'; | import type { NavigationRoute } from '../../navigation/route-names.js'; | ||||
import { useSelector } from '../../redux/redux-utils.js'; | |||||
import { useStyles } from '../../themes/colors.js'; | import { useStyles } from '../../themes/colors.js'; | ||||
const exactSearchUserLoadingStatusSelector = createLoadingStatusSelector( | |||||
exactSearchUserActionTypes, | |||||
); | |||||
export type UsernameSelectionParams = { | export type UsernameSelectionParams = { | ||||
+userSelections: { | +userSelections: { | ||||
+coolOrNerdMode: CoolOrNerdMode, | +coolOrNerdMode: CoolOrNerdMode, | ||||
+keyserverUsername: string, | +keyserverUsername: string, | ||||
}, | }, | ||||
}; | }; | ||||
type Props = { | type Props = { | ||||
+navigation: RegistrationNavigationProp<'UsernameSelection'>, | +navigation: RegistrationNavigationProp<'UsernameSelection'>, | ||||
+route: NavigationRoute<'UsernameSelection'>, | +route: NavigationRoute<'UsernameSelection'>, | ||||
}; | }; | ||||
// eslint-disable-next-line no-unused-vars | // eslint-disable-next-line no-unused-vars | ||||
function UsernameSelection(props: Props): React.Node { | function UsernameSelection(props: Props): React.Node { | ||||
const onProceed = React.useCallback(() => {}, []); | const [username, setUsername] = React.useState(''); | ||||
const validUsername = username.search(validUsernameRegex) > -1; | |||||
const [errorText, setErrorText] = React.useState<React.Node>(null); | |||||
const styles = useStyles(unboundStyles); | const styles = useStyles(unboundStyles); | ||||
const checkUsernameValidity = React.useCallback(() => { | |||||
if (validUsername) { | |||||
return true; | |||||
} | |||||
setErrorText( | |||||
<> | |||||
<Text style={styles.errorText}>Usernames must:</Text> | |||||
<View style={styles.listItem}> | |||||
<Text style={[styles.errorText, styles.listItemNumber]}>{'1. '}</Text> | |||||
<Text style={[styles.errorText, styles.listItemContent]}> | |||||
Be at least one character long. | |||||
</Text> | |||||
</View> | |||||
<View style={styles.listItem}> | |||||
<Text style={[styles.errorText, styles.listItemNumber]}>{'2. '}</Text> | |||||
<Text style={[styles.errorText, styles.listItemContent]}> | |||||
Start with either a letter or a number. | |||||
</Text> | |||||
</View> | |||||
<View style={styles.listItem}> | |||||
<Text style={[styles.errorText, styles.listItemNumber]}>{'3. '}</Text> | |||||
<Text style={[styles.errorText, styles.listItemContent]}> | |||||
Contain only letters, numbers, or the characters “-” and “_”. | |||||
</Text> | |||||
</View> | |||||
</>, | |||||
ashoat: Arguably it's a bit weird to have this JSX in React state. If preferred, I'm open to updating… | |||||
atulUnsubmitted Not Done Inline ActionsAh yeah, I think either enum or having errorText be some sort of array of strings would be better. Not sure on best practices or whatever, but JSX in state seems unusual atul: Ah yeah, I think either enum or having `errorText` be some sort of array of strings would be… | |||||
); | |||||
return false; | |||||
}, [ | |||||
validUsername, | |||||
styles.errorText, | |||||
styles.listItem, | |||||
styles.listItemNumber, | |||||
styles.listItemContent, | |||||
]); | |||||
const exactSearchUserCall = useServerCall(exactSearchUser); | |||||
const dispatchActionPromise = useDispatchActionPromise(); | |||||
const onProceed = React.useCallback(async () => { | |||||
if (!checkUsernameValidity()) { | |||||
return; | |||||
} | |||||
const searchPromise = exactSearchUserCall(username); | |||||
dispatchActionPromise(exactSearchUserActionTypes, searchPromise); | |||||
const { userInfo } = await searchPromise; | |||||
if (userInfo) { | |||||
setErrorText( | |||||
<Text style={styles.errorText}> | |||||
Username taken. Please try another one | |||||
</Text>, | |||||
); | |||||
return; | |||||
} | |||||
setErrorText(''); | |||||
}, [ | |||||
checkUsernameValidity, | |||||
username, | |||||
exactSearchUserCall, | |||||
dispatchActionPromise, | |||||
styles.errorText, | |||||
]); | |||||
const exactSearchUserCallLoading = useSelector( | |||||
state => exactSearchUserLoadingStatusSelector(state) === 'loading', | |||||
); | |||||
let buttonVariant = 'disabled'; | |||||
if (exactSearchUserCallLoading) { | |||||
buttonVariant = 'loading'; | |||||
} else if (validUsername) { | |||||
buttonVariant = 'enabled'; | |||||
} | |||||
return ( | return ( | ||||
<RegistrationContainer> | <RegistrationContainer> | ||||
<RegistrationContentContainer> | <RegistrationContentContainer> | ||||
<Text style={styles.header}>Pick a username</Text> | <Text style={styles.header}>Pick a username</Text> | ||||
<RegistrationTextInput | |||||
value={username} | |||||
onChangeText={setUsername} | |||||
placeholder="Username" | |||||
autoFocus={true} | |||||
autoCorrect={false} | |||||
autoCapitalize="none" | |||||
keyboardType="ascii-capable" | |||||
textContentType="username" | |||||
autoComplete="username-new" | |||||
returnKeyType="next" | |||||
onSubmitEditing={onProceed} | |||||
editable={!exactSearchUserCallLoading} | |||||
onBlur={checkUsernameValidity} | |||||
ashoatAuthorUnsubmitted Done Inline ActionsI added this onBlur because I figured that if the user dismisses the keyboard, they might see a gray button and not be clear on why it's gray, and not have any action they can take to figure out what is wrong. In contrast, if the keyboard is up, the gray button isn't visible, and the "Next" button on the keyboard is always pressable (and will yield an error message when pressed if appropriate) ashoat: I added this `onBlur` because I figured that if the user dismisses the keyboard, they might see… | |||||
/> | |||||
ashoatAuthorUnsubmitted Done Inline ActionsI got a lot of these props from RegisterPanel here ashoat: I got a lot of these props from `RegisterPanel` [here](https://github. | |||||
<View style={styles.error}>{errorText}</View> | |||||
</RegistrationContentContainer> | </RegistrationContentContainer> | ||||
<RegistrationButtonContainer> | <RegistrationButtonContainer> | ||||
<RegistrationButton | <RegistrationButton | ||||
onPress={onProceed} | onPress={onProceed} | ||||
label="Next" | label="Next" | ||||
variant="disabled" | variant={buttonVariant} | ||||
/> | /> | ||||
</RegistrationButtonContainer> | </RegistrationButtonContainer> | ||||
</RegistrationContainer> | </RegistrationContainer> | ||||
); | ); | ||||
} | } | ||||
const unboundStyles = { | const unboundStyles = { | ||||
header: { | header: { | ||||
fontSize: 24, | fontSize: 24, | ||||
color: 'panelForegroundLabel', | color: 'panelForegroundLabel', | ||||
paddingBottom: 16, | paddingBottom: 16, | ||||
}, | }, | ||||
error: { | |||||
marginTop: 16, | |||||
}, | |||||
errorText: { | |||||
fontFamily: 'Arial', | |||||
ashoatAuthorUnsubmitted Done Inline ActionsThis is necessary to make sure that the width of "1." is the same as the width of "2.". Otherwise the list for the error state on line 54 looks a bit clunky In a later diff I'll update all body text to use Arial for consistency ashoat: This is necessary to make sure that the width of "1." is the same as the width of "2.". | |||||
fontSize: 15, | |||||
lineHeight: 20, | |||||
color: 'redText', | |||||
}, | |||||
listItem: { | |||||
flexDirection: 'row', | |||||
}, | |||||
listItemNumber: { | |||||
fontWeight: 'bold', | |||||
}, | |||||
listItemContent: { | |||||
flexShrink: 1, | |||||
ashoatAuthorUnsubmitted Done Inline ActionsThis is pulled from how we render Markdown lists, and is also used in ConnectEthereum ashoat: This is pulled from how we render Markdown lists, and is also used in `ConnectEthereum` | |||||
}, | |||||
}; | }; | ||||
export default UsernameSelection; | export default UsernameSelection; |
Arguably it's a bit weird to have this JSX in React state. If preferred, I'm open to updating the React state to an enum, and rendering this conditionally based on the enum