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

Show first & last name length errors on front end #5963

Merged
merged 12 commits into from
Oct 22, 2021
13 changes: 12 additions & 1 deletion src/components/FullNameInputRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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];
Expand All @@ -46,6 +55,7 @@ const FullNameInputRow = ({
<ExpensiTextInput
label={translate('common.firstName')}
value={firstName}
errorText={firstNameError}
onChangeText={onChangeFirstName}
placeholder={translate('profilePage.john')}
translateX={-10}
Expand All @@ -55,6 +65,7 @@ const FullNameInputRow = ({
<ExpensiTextInput
label={translate('common.lastName')}
value={lastName}
errorText={lastNameError}
onChangeText={onChangeLastName}
placeholder={translate('profilePage.doe')}
translateX={-10}
Expand Down
6 changes: 6 additions & 0 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,12 @@ export default {
invalidFormatEmailLogin: 'The email entered is invalid. Please fix the format and try again.',
},
},
personalDetails: {
error: {
firstNameLength: 'First name shouldn\'t be longer than 50 characters',
lastNameLength: 'Last name shouldn\'t be longer than 50 characters',
},
},
resendValidationForm: {
linkHasBeenResent: 'Link has been re-sent',
weSentYouMagicSignInLink: ({login}) => `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.`,
Expand Down
6 changes: 6 additions & 0 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.`,
Expand Down
10 changes: 10 additions & 0 deletions src/libs/ValidationUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,15 @@ function isNumericWithSpecialChars(input) {
return /^\+?\d*$/.test(input.replace(/[()-]/g, ''));
}

/**
* Checks weather a given first or last name is valid
* @param {String} name
* @returns {Boolean}
*/
function isValidFirstOrLastName(name) {
return name.length <= 50;
}

export {
meetsAgeRequirements,
isValidAddress,
Expand All @@ -245,5 +254,6 @@ export {
isValidURL,
validateIdentity,
isNumericWithSpecialChars,
isValidFirstOrLastName,
isValidPaypalUsername,
};
17 changes: 17 additions & 0 deletions src/libs/actions/PersonalDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {isDefaultRoom} from '../reportUtils';
import {getReportIcons, getDefaultAvatar} from '../OptionsListUtils';
import Growl from '../Growl';
import {translateLocal} from '../translate';
import {isValidFirstOrLastName} from '../ValidationUtils';

let currentUserEmail = '';
Onyx.connect({
Expand Down Expand Up @@ -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 {
firstName: isValidFirstOrLastName(firstName) ? '' : translateLocal('personalDetails.error.firstNameLength'),
lastName: isValidFirstOrLastName(lastName) ? '' : translateLocal('personalDetails.error.lastNameLength'),
};
}

/**
* Format personal details
*
Expand Down Expand Up @@ -304,6 +320,7 @@ export {
getFromReportParticipants,
getDisplayName,
getDefaultAvatar,
getFirstAndLastNameErrors,
setPersonalDetails,
setAvatar,
deleteAvatar,
Expand Down
64 changes: 46 additions & 18 deletions src/pages/RequestCallPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -69,34 +70,27 @@ 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,
};

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);
Expand Down Expand Up @@ -129,6 +123,21 @@ class RequestCallPage extends Component {
return secondaryLogin ? Str.removeSMSDomain(secondaryLogin.partnerUserID) : null;
}

/**
* Gets proper phone number error message depending on phoneNumber input value.
* @param {String} phoneNumber
* @returns {String}
*/
getPhoneNumberError(phoneNumber) {
if (_.isEmpty(phoneNumber.trim())) {
return this.props.translate('messages.noPhoneNumber');
}
if (!Str.isValidPhone(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,
Expand Down Expand Up @@ -162,13 +171,30 @@ 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((prevState) => ({phoneNumberError: this.getPhoneNumberError(prevState.phoneNumber)}));
}

/**
* 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'));
}

this.setState((prevState) => {
const phoneNumberError = this.getPhoneNumberError(prevState.phoneNumber);
const nameErrors = getFirstAndLastNameErrors(prevState.firstName, prevState.lastName);

return {
firstNameError: nameErrors.firstName,
lastNameError: nameErrors.lastName,
phoneNumberError,
};
});
return !firstOrLastNameEmpty && _.isEmpty(phoneNumberError) && _.isEmpty(nameErrors.firstName) && _.isEmpty(nameErrors.lastName);
}

render() {
Expand All @@ -194,7 +220,9 @@ class RequestCallPage extends Component {
</Text>
<FullNameInputRow
firstName={this.state.firstName}
firstNameError={this.state.firstNameError}
lastName={this.state.lastName}
lastNameError={this.state.lastNameError}
onChangeFirstName={firstName => this.setState({firstName})}
onChangeLastName={lastName => this.setState({lastName})}
style={[styles.mv4]}
Expand Down
32 changes: 29 additions & 3 deletions src/pages/settings/Profile/ProfilePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -87,18 +92,21 @@ class ProfilePage extends Component {

this.state = {
firstName,
firstNameError: '',
lastName,
lastNameError: '',
pronouns: currentUserPronouns,
selfSelectedPronouns: initialSelfSelectedPronouns,
selectedTimezone: timezone.selected || CONST.DEFAULT_TIME_ZONE.selected,
isAutomaticTimezone: timezone.automatic ?? CONST.DEFAULT_TIME_ZONE.automatic,
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) {
Expand Down Expand Up @@ -166,6 +174,10 @@ class ProfilePage extends Component {
isAutomaticTimezone,
} = this.state;

if (!this.validateInputs()) {
return;
}

setPersonalDetails({
firstName: firstName.trim(),
lastName: lastName.trim(),
Expand All @@ -179,6 +191,18 @@ class ProfilePage extends Component {
}, true);
}

validateInputs() {
this.setState((prevState) => {
const nameErrors = getFirstAndLastNameErrors(prevState.firstName, prevState.lastName);

return {
firstNameError: nameErrors.firstName,
lastNameError: nameErrors.lastName,
};
});
return _.isEmpty(nameErrors.firstName) && _.isEmpty(nameErrors.lastName);
}

render() {
// Determines if the pronouns/selected pronouns have changed
const arePronounsUnchanged = this.props.myPersonalDetails.pronouns === this.state.pronouns
Expand Down Expand Up @@ -217,7 +241,9 @@ class ProfilePage extends Component {
</Text>
<FullNameInputRow
firstName={this.state.firstName}
firstNameError={this.state.firstNameError}
lastName={this.state.lastName}
lastNameError={this.state.lastNameError}
onChangeFirstName={firstName => this.setState({firstName})}
onChangeLastName={lastName => this.setState({lastName})}
style={[styles.mt4, styles.mb4]}
Expand Down