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 2 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
293 changes: 152 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,213 @@ 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 && password) {
clearPassword();
}
// We cannot add password to the dependency list because it will clear every time it updates
// eslint-disable-next-line react-hooks/exhaustive-deps
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@esh-g We shouldn’t disable eslint for react-hooks/exhaustive-deps . Actually, since password is not included in the deps array this hook will always run with the initial password's value. Finally, the autofill feature is broken, when you select a saved login with password , both login and password should be filled.

}, [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();
// eslint-disable-next-line react-hooks/exhaustive-deps -- We don't need to call this when the function changes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn’t disable eslint here as well ,

We don't need to call this when the function changes.

can you please explain this comment ? what happens if we call the hook when the function changes ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well it was more of an optimisation tweak to make sure that we are not running this on any unintentional changes, but I don't think it will matter if we include it or not. So anyway, I'll include it.

}, [twoFactorAuthCode]);

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