Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PasswordForm Function Migration #20515

Merged
merged 5 commits into from
Jun 15, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
290 changes: 149 additions & 141 deletions src/pages/signin/PasswordForm.js
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {useState, useEffect, useCallback, useRef} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
Expand Down Expand Up @@ -52,202 +52,210 @@ const defaultProps = {
preferredLocale: CONST.LOCALES.DEFAULT,
};

class PasswordForm extends React.Component {
constructor(props) {
super(props);
this.validateAndSubmitForm = this.validateAndSubmitForm.bind(this);
this.resetPassword = this.resetPassword.bind(this);
this.clearSignInData = this.clearSignInData.bind(this);

this.state = {
formError: {},
password: '',
twoFactorAuthCode: '',
};
}

componentDidMount() {
if (!canFocusInputOnScreenFocus() || !this.inputPassword || !this.props.isVisible) {
return;
}
this.inputPassword.focus();
}
function PasswordForm(props) {
const [formError, setFormError] = useState({});
const [password, setPassword] = useState('');
const [twoFactorAuthCode, setTwoFactorAuthCode] = useState('');

componentDidUpdate(prevProps, prevState) {
if (!prevProps.isVisible && this.props.isVisible) {
this.inputPassword.focus();
}
if (prevProps.isVisible && !this.props.isVisible && this.state.password) {
this.clearPassword();
}
if (!prevProps.account.requiresTwoFactorAuth && this.props.account.requiresTwoFactorAuth) {
this.input2FA.focus();
}
if (prevState.twoFactorAuthCode !== this.state.twoFactorAuthCode && this.state.twoFactorAuthCode.length === CONST.TFA_CODE_LENGTH) {
this.validateAndSubmitForm();
}
}
const inputPasswordRef = useRef(null);
const input2FA = useRef(null);

/**
* Handle text input and clear formError upon text change
*
* @param {String} text
* @param {String} key
*/
onTextInput(text, key) {
this.setState({
[key]: text,
formError: {[key]: ''},
});
const onTextInput = (text, key) => {
if (key === 'password') {
setPassword(text);
}
if (key === 'twoFactorAuthCode') {
setTwoFactorAuthCode(text);
}
setFormError({[key]: ''});

if (this.props.account.errors) {
if (props.account.errors) {
Session.clearAccountMessages();
}
}
};

/**
* Clear Password from the state
*/
clearPassword() {
this.setState({password: ''}, this.inputPassword.clear);
}
const clearPassword = () => {
setPassword('');
inputPasswordRef.current.clear();
};

/**
* Trigger the reset password flow and ensure the 2FA input field is reset to avoid it being permanently hidden
*/
resetPassword() {
if (this.input2FA) {
this.setState({twoFactorAuthCode: ''}, this.input2FA.clear);
const resetPassword = () => {
if (input2FA.current) {
setTwoFactorAuthCode('');
input2FA.current.clear();
}
this.setState({formError: {}});
setFormError({});
Session.resetPassword();
}
};

/**
* Clears local and Onyx sign in states
*/
clearSignInData() {
this.setState({twoFactorAuthCode: '', formError: {}});
const clearSignInData = () => {
setTwoFactorAuthCode('');
setFormError({});
Session.clearSignInData();
}
};

/**
* Check that all the form fields are valid, then trigger the submit callback
*/
validateAndSubmitForm() {
const password = this.state.password.trim();
const twoFactorCode = this.state.twoFactorAuthCode.trim();
const requiresTwoFactorAuth = this.props.account.requiresTwoFactorAuth;
const validateAndSubmitForm = useCallback(() => {
const passwordTrimmed = password.trim();
const twoFactorCodeTrimmed = twoFactorAuthCode.trim();
const requiresTwoFactorAuth = props.account.requiresTwoFactorAuth;

if (!passwordTrimmed) {
setFormError({password: 'passwordForm.pleaseFillPassword'});
return;
}

if (!ValidationUtils.isValidPassword(passwordTrimmed)) {
setFormError({password: 'passwordForm.error.incorrectPassword'});
return;
}

if (!password) {
this.setState({formError: {password: 'passwordForm.pleaseFillPassword'}});
if (requiresTwoFactorAuth && !twoFactorCodeTrimmed) {
setFormError({twoFactorAuthCode: 'passwordForm.pleaseFillTwoFactorAuth'});
return;
}

if (!ValidationUtils.isValidPassword(password)) {
this.setState({formError: {password: 'passwordForm.error.incorrectPassword'}});
if (requiresTwoFactorAuth && !ValidationUtils.isValidTwoFactorCode(twoFactorCodeTrimmed)) {
setFormError({twoFactorAuthCode: 'passwordForm.error.incorrect2fa'});
return;
}

if (requiresTwoFactorAuth && !twoFactorCode) {
this.setState({formError: {twoFactorAuthCode: 'passwordForm.pleaseFillTwoFactorAuth'}});
setFormError({});

Session.signIn(passwordTrimmed, '', twoFactorCodeTrimmed, props.preferredLocale);
}, [password, twoFactorAuthCode, props.account.requiresTwoFactorAuth, props.preferredLocale]);

useEffect(() => {
if (!canFocusInputOnScreenFocus() || !inputPasswordRef.current || !props.isVisible) {
return;
}
inputPasswordRef.current.focus();
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want this effect to run again
}, []);

useEffect(() => {
if (props.isVisible) {
inputPasswordRef.current.focus();
}
if (!props.isVisible) {
clearPassword();
}
}, [props.isVisible]);

useEffect(() => {
if (!props.account.requiresTwoFactorAuth) {
return;
}
input2FA.current.focus();
}, [props.account.requiresTwoFactorAuth]);

if (requiresTwoFactorAuth && !ValidationUtils.isValidTwoFactorCode(twoFactorCode)) {
this.setState({formError: {twoFactorAuthCode: 'passwordForm.error.incorrect2fa'}});
useEffect(() => {
if (twoFactorAuthCode.length !== CONST.TFA_CODE_LENGTH) {
return;
}
validateAndSubmitForm();
}, [twoFactorAuthCode, validateAndSubmitForm]);

this.setState({
formError: {},
});
const isTwoFactorAuthRequired = Boolean(props.account.requiresTwoFactorAuth);
const hasServerError = Boolean(props.account) && !_.isEmpty(props.account.errors);

Session.signIn(password, '', twoFactorCode, this.props.preferredLocale);
}
// When the 2FA required flag is set, user has already passed/completed the password field
const passwordFieldHasError = !isTwoFactorAuthRequired && hasServerError;
const twoFactorFieldHasError = isTwoFactorAuthRequired && hasServerError;

render() {
const isTwoFactorAuthRequired = Boolean(this.props.account.requiresTwoFactorAuth);
const hasServerError = Boolean(this.props.account) && !_.isEmpty(this.props.account.errors);
return (
<>
<View style={[styles.mv3]}>
<TextInput
ref={inputPasswordRef}
label={props.translate('common.password')}
secureTextEntry
autoCompleteType={ComponentUtils.PASSWORD_AUTOCOMPLETE_TYPE}
textContentType="password"
nativeID="password"
name="password"
value={password}
onChangeText={(text) => onTextInput(text, 'password')}
onSubmitEditing={validateAndSubmitForm}
blurOnSubmit={false}
errorText={formError.password ? props.translate(formError.password) : ''}
hasError={passwordFieldHasError}
/>
<View style={[styles.changeExpensifyLoginLinkContainer]}>
<PressableWithFeedback
style={[styles.mt2]}
onPress={resetPassword}
accessibilityRole="link"
accessibilityLabel={props.translate('passwordForm.forgot')}
hoverDimmingValue={1}
>
<Text style={[styles.link]}>{props.translate('passwordForm.forgot')}</Text>
</PressableWithFeedback>
</View>
</View>

// When the 2FA required flag is set, user has already passed/completed the password field
const passwordFieldHasError = !isTwoFactorAuthRequired && hasServerError;
const twoFactorFieldHasError = isTwoFactorAuthRequired && hasServerError;
return (
<>
{isTwoFactorAuthRequired && (
<View style={[styles.mv3]}>
<TextInput
ref={(el) => (this.inputPassword = el)}
label={this.props.translate('common.password')}
secureTextEntry
autoCompleteType={ComponentUtils.PASSWORD_AUTOCOMPLETE_TYPE}
textContentType="password"
nativeID="password"
name="password"
value={this.state.password}
onChangeText={(text) => this.onTextInput(text, 'password')}
onSubmitEditing={this.validateAndSubmitForm}
ref={input2FA}
label={props.translate('common.twoFactorCode')}
value={twoFactorAuthCode}
placeholder={props.translate('passwordForm.requiredWhen2FAEnabled')}
placeholderTextColor={themeColors.placeholderText}
onChangeText={(text) => onTextInput(text, 'twoFactorAuthCode')}
onSubmitEditing={validateAndSubmitForm}
keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD}
blurOnSubmit={false}
errorText={this.state.formError.password ? this.props.translate(this.state.formError.password) : ''}
hasError={passwordFieldHasError}
maxLength={CONST.TFA_CODE_LENGTH}
errorText={formError.twoFactorAuthCode ? props.translate(formError.twoFactorAuthCode) : ''}
hasError={twoFactorFieldHasError}
/>
<View style={[styles.changeExpensifyLoginLinkContainer]}>
<PressableWithFeedback
style={[styles.mt2]}
onPress={this.resetPassword}
accessibilityRole="link"
accessibilityLabel={this.props.translate('passwordForm.forgot')}
hoverDimmingValue={1}
>
<Text style={[styles.link]}>{this.props.translate('passwordForm.forgot')}</Text>
</PressableWithFeedback>
</View>
</View>
)}

{isTwoFactorAuthRequired && (
<View style={[styles.mv3]}>
<TextInput
ref={(el) => (this.input2FA = el)}
label={this.props.translate('common.twoFactorCode')}
value={this.state.twoFactorAuthCode}
placeholder={this.props.translate('passwordForm.requiredWhen2FAEnabled')}
placeholderTextColor={themeColors.placeholderText}
onChangeText={(text) => this.onTextInput(text, 'twoFactorAuthCode')}
onSubmitEditing={this.validateAndSubmitForm}
keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD}
blurOnSubmit={false}
maxLength={CONST.TFA_CODE_LENGTH}
errorText={this.state.formError.twoFactorAuthCode ? this.props.translate(this.state.formError.twoFactorAuthCode) : ''}
hasError={twoFactorFieldHasError}
/>
</View>
)}

{hasServerError && <FormHelpMessage message={ErrorUtils.getLatestErrorMessage(this.props.account)} />}
<View>
<Button
isDisabled={this.props.network.isOffline}
success
style={[styles.mv3]}
text={this.props.translate('common.signIn')}
isLoading={
this.props.account.isLoading &&
this.props.account.loadingForm === (this.props.account.requiresTwoFactorAuth ? CONST.FORMS.VALIDATE_TFA_CODE_FORM : CONST.FORMS.VALIDATE_CODE_FORM)
}
onPress={this.validateAndSubmitForm}
/>
<ChangeExpensifyLoginLink onPress={this.clearSignInData} />
</View>
<View style={[styles.mt5, styles.signInPageWelcomeTextContainer]}>
<Terms />
</View>
</>
);
}
{hasServerError && <FormHelpMessage message={ErrorUtils.getLatestErrorMessage(props.account)} />}

<View>
<Button
isDisabled={props.network.isOffline}
success
style={[styles.mv3]}
text={props.translate('common.signIn')}
isLoading={
props.account.isLoading && props.account.loadingForm === (props.account.requiresTwoFactorAuth ? CONST.FORMS.VALIDATE_TFA_CODE_FORM : CONST.FORMS.VALIDATE_CODE_FORM)
}
onPress={validateAndSubmitForm}
/>
<ChangeExpensifyLoginLink onPress={clearSignInData} />
</View>

<View style={[styles.mt5, styles.signInPageWelcomeTextContainer]}>
<Terms />
</View>
</>
);
}

PasswordForm.propTypes = propTypes;
PasswordForm.defaultProps = defaultProps;
PasswordForm.displayName = 'PasswordForm';

export default compose(
withLocalize,
Expand Down