diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index 5b55965d1539..9cd620cf8f12 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -26,6 +26,7 @@ import Log from '@libs/Log'; import * as LoginUtils from '@libs/LoginUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; +import Visibility from '@libs/Visibility'; import styles from '@styles/styles'; import * as CloseAccount from '@userActions/CloseAccount'; import * as MemoryOnlyKeys from '@userActions/MemoryOnlyKeys/MemoryOnlyKeys'; @@ -98,18 +99,52 @@ function LoginForm(props) { const [login, setLogin] = useState(() => Str.removeSMSDomain(props.credentials.login || '')); const [formError, setFormError] = useState(false); const prevIsVisible = usePrevious(props.isVisible); + const firstBlurred = useRef(false); const {translate} = props; /** - * Handle text input and clear formError upon text change + * Validate the input value and set the error for formError + * + * @param {String} value + */ + const validate = useCallback( + (value) => { + const loginTrim = value.trim(); + if (!loginTrim) { + setFormError('common.pleaseEnterEmailOrPhoneNumber'); + return false; + } + + const phoneLogin = LoginUtils.appendCountryCode(LoginUtils.getPhoneNumberWithoutSpecialChars(loginTrim)); + const parsedPhoneNumber = parsePhoneNumber(phoneLogin); + + if (!Str.isValidEmail(loginTrim) && !parsedPhoneNumber.possible) { + if (ValidationUtils.isNumericWithSpecialChars(loginTrim)) { + setFormError('common.error.phoneNumber'); + } else { + setFormError('loginForm.error.invalidFormatEmailLogin'); + } + return false; + } + + setFormError(null); + return true; + }, + [setFormError], + ); + + /** + * Handle text input and validate the text input if it is blurred * * @param {String} text */ const onTextInput = useCallback( (text) => { setLogin(text); - setFormError(null); + if (firstBlurred.current) { + validate(text); + } if (props.account.errors || props.account.message) { Session.clearAccountMessages(); @@ -120,7 +155,7 @@ function LoginForm(props) { CloseAccount.setDefaultData(); } }, - [props.account, props.closeAccount, input, setFormError, setLogin], + [props.account, props.closeAccount, input, setLogin, validate], ); function getSignInWithStyles() { @@ -140,35 +175,30 @@ function LoginForm(props) { CloseAccount.setDefaultData(); } - const loginTrim = login.trim(); - if (!loginTrim) { - setFormError('common.pleaseEnterEmailOrPhoneNumber'); - return; + // For native, the single input doesn't lost focus when we click outside. + // So we need to change firstBlurred here to make the validate function is called whenever the text input is changed after the first validation. + if (!firstBlurred.current) { + firstBlurred.current = true; } - const phoneLogin = LoginUtils.appendCountryCode(LoginUtils.getPhoneNumberWithoutSpecialChars(loginTrim)); - const parsedPhoneNumber = parsePhoneNumber(phoneLogin); - - if (!Str.isValidEmail(loginTrim) && !parsedPhoneNumber.possible) { - if (ValidationUtils.isNumericWithSpecialChars(loginTrim)) { - setFormError('common.error.phoneNumber'); - } else { - setFormError('loginForm.error.invalidFormatEmailLogin'); - } + if (!validate(login)) { return; } + const loginTrim = login.trim(); + // If the user has entered a guide email, then we are going to enable an experimental Onyx mode to help with performance if (PolicyUtils.isExpensifyGuideTeam(loginTrim)) { Log.info('Detected guide email in login field, setting memory only keys.'); MemoryOnlyKeys.enable(); } - setFormError(null); + const phoneLogin = LoginUtils.appendCountryCode(LoginUtils.getPhoneNumberWithoutSpecialChars(loginTrim)); + const parsedPhoneNumber = parsePhoneNumber(phoneLogin); // Check if this login has an account associated with it or not Session.beginSignIn(parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : loginTrim); - }, [login, props.account, props.closeAccount, props.network, setFormError]); + }, [login, props.account, props.closeAccount, props.network, validate]); useEffect(() => { // Just call clearAccountMessages on the login page (home route), because when the user is in the transition route and not yet authenticated, @@ -227,6 +257,13 @@ function LoginForm(props) { textContentType="username" id="username" name="username" + onBlur={() => { + if (firstBlurred.current || !Visibility.isVisible() || !Visibility.hasFocus()) { + return; + } + firstBlurred.current = true; + validate(login); + }} onChangeText={onTextInput} onSubmitEditing={validateAndSubmitForm} autoCapitalize="none" @@ -276,8 +313,12 @@ function LoginForm(props) { - - + e.preventDefault()}> + + + e.preventDefault()}> + + )