diff --git a/src/components/FullNameInputRow.js b/src/components/FullNameInputRow.js index 6ed98c5be043..3aa4fff516a0 100644 --- a/src/components/FullNameInputRow.js +++ b/src/components/FullNameInputRow.js @@ -18,9 +18,15 @@ const propTypes = { /** Used to prefill the firstName input, can also be used to make it a controlled input */ firstName: PropTypes.string, + /** Error message to display below firstName input */ + firstNameError: PropTypes.string, + /** Used to prefill the lastName input, can also be used to make it a controlled input */ lastName: PropTypes.string, + /** Error message to display below lastName input */ + lastNameError: PropTypes.string, + /** Additional styles to add after local styles */ style: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.object), @@ -29,14 +35,17 @@ const propTypes = { }; const defaultProps = { firstName: '', + firstNameError: '', lastName: '', + lastNameError: '', style: {}, }; const FullNameInputRow = ({ translate, onChangeFirstName, onChangeLastName, - firstName, lastName, + firstName, firstNameError, + lastName, lastNameError, style, }) => { const additionalStyles = _.isArray(style) ? style : [style]; @@ -46,6 +55,7 @@ const FullNameInputRow = ({ `We've sent a magic sign in link to ${login}. Check your Inbox and your Spam folder and wait 5-10 minutes before trying again.`, diff --git a/src/languages/es.js b/src/languages/es.js index 56c3d288a566..670e45dd8a61 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -379,6 +379,12 @@ export default { invalidFormatEmailLogin: 'El email introducido no es válido. Corrígelo e inténtalo de nuevo.', }, }, + personalDetails: { + error: { + firstNameLength: 'El nombre no debe tener más de 50 caracteres', + lastNameLength: 'El apellido no debe tener más de 50 caracteres', + }, + }, resendValidationForm: { linkHasBeenResent: 'El enlace se ha reenviado', weSentYouMagicSignInLink: ({login}) => `Hemos enviado un enlace mágico de inicio de sesión a ${login}. Verifica tu bandeja de entrada y tu carpeta de correo no deseado y espera de 5 a 10 minutos antes de intentarlo de nuevo.`, diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js index 62bef31a9628..2de72b331023 100644 --- a/src/libs/ValidationUtils.js +++ b/src/libs/ValidationUtils.js @@ -230,6 +230,15 @@ function isNumericWithSpecialChars(input) { return /^\+?\d*$/.test(input.replace(/[()-]/g, '')); } +/** + * Checks whether a given first or last name is valid length + * @param {String} name + * @returns {Boolean} + */ +function isValidLengthForFirstOrLastName(name) { + return name.length <= 50; +} + export { meetsAgeRequirements, isValidAddress, @@ -245,5 +254,6 @@ export { isValidURL, validateIdentity, isNumericWithSpecialChars, + isValidLengthForFirstOrLastName, isValidPaypalUsername, }; diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 4af4da39780d..5e16192973f2 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -12,6 +12,7 @@ import {isDefaultRoom} from '../reportUtils'; import {getReportIcons, getDefaultAvatar} from '../OptionsListUtils'; import Growl from '../Growl'; import {translateLocal} from '../translate'; +import {isValidLengthForFirstOrLastName} from '../ValidationUtils'; let currentUserEmail = ''; Onyx.connect({ @@ -64,6 +65,21 @@ function getDisplayName(login, personalDetail) { return fullName || userLogin; } +/** + * Returns object with first and last name errors. If either are valid, + * those errors get returned as empty strings. + * + * @param {String} firstName + * @param {String} lastName + * @returns {Object} + */ +function getFirstAndLastNameErrors(firstName, lastName) { + return { + firstNameError: isValidLengthForFirstOrLastName(firstName) ? '' : translateLocal('personalDetails.error.firstNameLength'), + lastNameError: isValidLengthForFirstOrLastName(lastName) ? '' : translateLocal('personalDetails.error.lastNameLength'), + }; +} + /** * Format personal details * @@ -304,6 +320,7 @@ export { getFromReportParticipants, getDisplayName, getDefaultAvatar, + getFirstAndLastNameErrors, setPersonalDetails, setAvatar, deleteAvatar, diff --git a/src/pages/RequestCallPage.js b/src/pages/RequestCallPage.js index b4153f930d63..959cfc09a3f8 100644 --- a/src/pages/RequestCallPage.js +++ b/src/pages/RequestCallPage.js @@ -23,6 +23,7 @@ import ExpensiTextInput from '../components/ExpensiTextInput'; import Text from '../components/Text'; import KeyboardAvoidingView from '../components/KeyboardAvoidingView'; import RequestCallIcon from '../../assets/images/request-call.svg'; +import {getFirstAndLastNameErrors} from '../libs/actions/PersonalDetails'; const propTypes = { ...withLocalizePropTypes, @@ -69,7 +70,9 @@ class RequestCallPage extends Component { const {firstName, lastName} = this.getFirstAndLastName(props.myPersonalDetails); this.state = { firstName, + firstNameError: '', lastName, + lastNameError: '', phoneNumber: this.getPhoneNumber(props.user.loginList) ?? '', phoneNumberError: '', isLoading: false, @@ -77,26 +80,17 @@ class RequestCallPage extends Component { this.onSubmit = this.onSubmit.bind(this); this.getPhoneNumber = this.getPhoneNumber.bind(this); + this.getPhoneNumberError = this.getPhoneNumberError.bind(this); this.getFirstAndLastName = this.getFirstAndLastName.bind(this); + this.validateInputs = this.validateInputs.bind(this); this.validatePhoneInput = this.validatePhoneInput.bind(this); } onSubmit() { - const shouldNotSubmit = _.isEmpty(this.state.firstName.trim()) - || _.isEmpty(this.state.lastName.trim()) - || _.isEmpty(this.state.phoneNumber.trim()) - || !Str.isValidPhone(this.state.phoneNumber); - - if (_.isEmpty(this.state.firstName.trim()) || _.isEmpty(this.state.lastName.trim())) { - Growl.error(this.props.translate('requestCallPage.growlMessageEmptyName')); + if (!this.validateInputs()) { return; } - this.validatePhoneInput(); - - if (shouldNotSubmit) { - return; - } const personalPolicy = _.find(this.props.policies, policy => policy && policy.type === CONST.POLICY.TYPE.PERSONAL); if (!personalPolicy) { Growl.error(this.props.translate('requestCallPage.growlMessageNoPersonalPolicy'), 3000); @@ -129,6 +123,20 @@ class RequestCallPage extends Component { return secondaryLogin ? Str.removeSMSDomain(secondaryLogin.partnerUserID) : null; } + /** + * Gets proper phone number error message depending on phoneNumber input value. + * @returns {String} + */ + getPhoneNumberError() { + if (_.isEmpty(this.state.phoneNumber.trim())) { + return this.props.translate('messages.noPhoneNumber'); + } + if (!Str.isValidPhone(this.state.phoneNumber)) { + return this.props.translate('messages.errorMessageInvalidPhone'); + } + return ''; + } + /** * Gets the first and last name from the user's personal details. * If the login is the same as the displayName, then they don't exist, @@ -162,13 +170,28 @@ class RequestCallPage extends Component { } validatePhoneInput() { - if (_.isEmpty(this.state.phoneNumber.trim())) { - this.setState({phoneNumberError: this.props.translate('messages.noPhoneNumber')}); - } else if (!Str.isValidPhone(this.state.phoneNumber)) { - this.setState({phoneNumberError: this.props.translate('messages.errorMessageInvalidPhone')}); - } else { - this.setState({phoneNumberError: ''}); + this.setState({phoneNumberError: this.getPhoneNumberError()}); + } + + /** + * Checks for input errors, returns true if everything is valid, false otherwise. + * @returns {Boolean} + */ + validateInputs() { + const firstOrLastNameEmpty = _.isEmpty(this.state.firstName.trim()) || _.isEmpty(this.state.lastName.trim()); + if (firstOrLastNameEmpty) { + Growl.error(this.props.translate('requestCallPage.growlMessageEmptyName')); } + + const phoneNumberError = this.getPhoneNumberError(); + const {firstNameError, lastNameError} = getFirstAndLastNameErrors(this.state.firstName, this.state.lastName); + + this.setState({ + firstNameError, + lastNameError, + phoneNumberError, + }); + return !firstOrLastNameEmpty && _.isEmpty(phoneNumberError) && _.isEmpty(firstNameError) && _.isEmpty(lastNameError); } render() { @@ -194,7 +217,9 @@ class RequestCallPage extends Component { this.setState({firstName})} onChangeLastName={lastName => this.setState({lastName})} style={[styles.mv4]} diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index 1312416afbb8..e6dedf8ebf13 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -8,7 +8,12 @@ import _ from 'underscore'; import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton'; import Navigation from '../../../libs/Navigation/Navigation'; import ScreenWrapper from '../../../components/ScreenWrapper'; -import {setPersonalDetails, setAvatar, deleteAvatar} from '../../../libs/actions/PersonalDetails'; +import { + getFirstAndLastNameErrors, + setPersonalDetails, + setAvatar, + deleteAvatar, +} from '../../../libs/actions/PersonalDetails'; import ROUTES from '../../../ROUTES'; import ONYXKEYS from '../../../ONYXKEYS'; import CONST from '../../../CONST'; @@ -87,7 +92,9 @@ class ProfilePage extends Component { this.state = { firstName, + firstNameError: '', lastName, + lastNameError: '', pronouns: currentUserPronouns, selfSelectedPronouns: initialSelfSelectedPronouns, selectedTimezone: timezone.selected || CONST.DEFAULT_TIME_ZONE.selected, @@ -95,10 +102,11 @@ class ProfilePage extends Component { logins: this.getLogins(props.user.loginList), }; + this.getLogins = this.getLogins.bind(this); this.pronounDropdownValues = pronounsList.map(pronoun => ({value: pronoun, label: pronoun})); - this.updatePersonalDetails = this.updatePersonalDetails.bind(this); this.setAutomaticTimezone = this.setAutomaticTimezone.bind(this); - this.getLogins = this.getLogins.bind(this); + this.updatePersonalDetails = this.updatePersonalDetails.bind(this); + this.validateInputs = this.validateInputs.bind(this); } componentDidUpdate(prevProps) { @@ -166,6 +174,10 @@ class ProfilePage extends Component { isAutomaticTimezone, } = this.state; + if (!this.validateInputs()) { + return; + } + setPersonalDetails({ firstName: firstName.trim(), lastName: lastName.trim(), @@ -179,6 +191,16 @@ class ProfilePage extends Component { }, true); } + validateInputs() { + const {firstNameError, lastNameError} = getFirstAndLastNameErrors(this.state.firstName, this.state.lastName); + + this.setState({ + firstNameError, + lastNameError, + }); + return _.isEmpty(firstNameError) && _.isEmpty(lastNameError); + } + render() { // Determines if the pronouns/selected pronouns have changed const arePronounsUnchanged = this.props.myPersonalDetails.pronouns === this.state.pronouns @@ -217,7 +239,9 @@ class ProfilePage extends Component { this.setState({firstName})} onChangeLastName={lastName => this.setState({lastName})} style={[styles.mt4, styles.mb4]}