diff --git a/src/components/FormAlertWithSubmitButton.js b/src/components/FormAlertWithSubmitButton.js
new file mode 100644
index 000000000000..b85c77486740
--- /dev/null
+++ b/src/components/FormAlertWithSubmitButton.js
@@ -0,0 +1,101 @@
+import _ from 'underscore';
+import React from 'react';
+import {View} from 'react-native';
+import PropTypes from 'prop-types';
+import styles from '../styles/styles';
+import Icon from './Icon';
+import {Exclamation} from './Icon/Expensicons';
+import colors from '../styles/colors';
+import Button from './Button';
+import withLocalize, {withLocalizePropTypes} from './withLocalize';
+import TextLink from './TextLink';
+import Text from './Text';
+
+const propTypes = {
+ /** Whether to show the alert text */
+ isAlertVisible: PropTypes.bool.isRequired,
+
+ /** Submit function */
+ onSubmit: PropTypes.func.isRequired,
+
+ /** Text for the button */
+ buttonText: PropTypes.string.isRequired,
+
+ /** Callback fired when the "fix the errors" link is pressed */
+ onFixTheErrorsLinkPressed: PropTypes.func.isRequired,
+
+ /** Error message to display above button */
+ message: PropTypes.string,
+
+ ...withLocalizePropTypes,
+};
+
+const defaultProps = {
+ message: '',
+};
+
+const FormAlertWithSubmitButton = ({
+ isAlertVisible,
+ onSubmit,
+ buttonText,
+ translate,
+ onFixTheErrorsLinkPressed,
+ message,
+}) => {
+ /**
+ * @returns {React.Component}
+ */
+ function getAlertPrompt() {
+ let error = '';
+
+ if (!_.isEmpty(message)) {
+ error = (
+ {message}
+ );
+ } else {
+ error = (
+ <>
+
+ {`${translate('common.please')} `}
+
+
+ {translate('common.fixTheErrors')}
+
+
+ {` ${translate('common.inTheFormBeforeContinuing')}.`}
+
+ >
+ );
+ }
+
+ return (
+
+ {error}
+
+ );
+ }
+
+ return (
+
+ {isAlertVisible && (
+
+
+ {getAlertPrompt()}
+
+ )}
+
+
+ );
+};
+
+FormAlertWithSubmitButton.propTypes = propTypes;
+FormAlertWithSubmitButton.defaultProps = defaultProps;
+export default withLocalize(FormAlertWithSubmitButton);
diff --git a/src/languages/en.js b/src/languages/en.js
index c85b20fd44ca..91edec4ac266 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -78,6 +78,9 @@ export default {
},
please: 'Please',
contactUs: 'contact us',
+ pleaseEnterEmailOrPhoneNumber: 'Please enter an email or phone number',
+ fixTheErrors: 'fix the errors',
+ inTheFormBeforeContinuing: 'in the form before continuing',
confirm: 'Confirm',
reset: 'Reset',
done: 'Done',
@@ -369,7 +372,6 @@ export default {
},
},
loginForm: {
- pleaseEnterEmailOrPhoneNumber: 'Please enter an email or phone number',
phoneOrEmail: 'Phone or email',
error: {
invalidFormatLogin: 'The email or phone number entered is invalid. Please fix the format and try again.',
@@ -444,8 +446,6 @@ export default {
firstName: 'Please enter valid first name',
lastName: 'Please enter valid last name',
noDefaultDepositAccountOrDebitCardAvailable: 'Please add a default deposit bank account or debit card',
- fixTheErrors: 'fix the errors',
- inTheFormBeforeContinuing: 'in the form before continuing',
validationAmounts: 'The validation amounts you entered are incorrect. Please double-check your bank statement and try again.',
},
},
diff --git a/src/languages/es.js b/src/languages/es.js
index ce7c3f8f66c9..b36d18250e7f 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -78,6 +78,9 @@ export default {
},
please: 'Por favor',
contactUs: 'contáctenos',
+ pleaseEnterEmailOrPhoneNumber: 'Por favor escribe un email o número de teléfono',
+ fixTheErrors: 'corrige los errores',
+ inTheFormBeforeContinuing: 'en el formulario antes de continuar',
confirm: 'Confirmar',
reset: 'Restablecer',
done: 'Listo',
@@ -369,7 +372,6 @@ export default {
},
},
loginForm: {
- pleaseEnterEmailOrPhoneNumber: 'Por favor escribe un email o número de teléfono',
phoneOrEmail: 'Número de teléfono o email',
error: {
invalidFormatLogin: 'El email o número de teléfono que has introducido no es válido. Corrígelo e inténtalo de nuevo.',
@@ -444,8 +446,6 @@ export default {
firstName: 'Ingresa un nombre válido',
lastName: 'Ingresa un apellido válido',
noDefaultDepositAccountOrDebitCardAvailable: 'Por favor agrega una cuenta bancaria para depósitos o una tarjeta de débito',
- fixTheErrors: 'corrige los errores',
- inTheFormBeforeContinuing: 'en el formulario antes de continuar',
validationAmounts: 'Los montos de validación que ingresaste son incorrectos. Verifica tu cuenta de banco e intenta de nuevo.',
},
},
diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js
index 2c7625a877c9..83ae72680dd7 100644
--- a/src/libs/actions/BankAccounts.js
+++ b/src/libs/actions/BankAccounts.js
@@ -528,7 +528,7 @@ function setFreePlanVerifiedBankAccountID(bankAccountID) {
* @param {String} errorModalMessage The error message to be displayed in the modal's body.
*/
function showBankAccountErrorModal(errorModalMessage = null) {
- Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isErrorModalVisible: true, errorModalMessage});
+ Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errorModalMessage});
}
/**
@@ -582,13 +582,6 @@ function validateBankAccount(bankAccountID, validateCode) {
});
}
-/**
- * Hide error modal
- */
-function hideBankAccountErrorModal() {
- Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isErrorModalVisible: false});
-}
-
/**
* Set the current fields with errors.
*
@@ -806,7 +799,6 @@ export {
setupWithdrawalAccount,
validateBankAccount,
hideBankAccountErrors,
- hideBankAccountErrorModal,
showBankAccountErrorModal,
showBankAccountFormValidationError,
setBankAccountFormValidationErrors,
diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js
index b2d7b12f1534..de08b01caa72 100644
--- a/src/libs/actions/Policy.js
+++ b/src/libs/actions/Policy.js
@@ -73,7 +73,7 @@ function updateAllPolicies(policyCollection) {
// Set all the policies
_.each(policyCollection, (policyData, key) => {
- Onyx.merge(key, policyData);
+ Onyx.merge(key, {...policyData, alertMessage: '', errors: null});
});
}
@@ -179,6 +179,7 @@ function invite(logins, welcomeNote, policyID) {
// Make a shallow copy to preserve original data, and concat the login
const policy = _.clone(allPolicies[key]);
policy.employeeList = [...policy.employeeList, ...newEmployeeList];
+ policy.alertMessage = '';
// Optimistically add the user to the policy
Onyx.set(key, policy);
@@ -193,21 +194,21 @@ function invite(logins, welcomeNote, policyID) {
// Save the personalDetails for the invited user in Onyx
if (data.jsonCode === 200) {
Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, formatPersonalDetails(data.personalDetails));
+ Navigation.goBack();
return;
}
// If the operation failed, undo the optimistic addition
const policyDataWithoutLogin = _.clone(allPolicies[key]);
policyDataWithoutLogin.employeeList = _.without(allPolicies[key].employeeList, ...newEmployeeList);
- Onyx.set(key, policyDataWithoutLogin);
// Show the user feedback that the addition failed
- let errorMessage = translateLocal('workspace.invite.genericFailureMessage');
+ policyDataWithoutLogin.alertMessage = translateLocal('workspace.invite.genericFailureMessage');
if (data.jsonCode === 402) {
- errorMessage += ` ${translateLocal('workspace.invite.pleaseEnterValidLogin')}`;
+ policyDataWithoutLogin.alertMessage += ` ${translateLocal('workspace.invite.pleaseEnterValidLogin')}`;
}
- Growl.error(errorMessage, 5000);
+ Onyx.set(key, policyDataWithoutLogin);
});
}
@@ -294,6 +295,22 @@ function updateLocalPolicyValues(policyID, values) {
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, values);
}
+/**
+ * @param {String} policyID
+ * @param {Object} errors
+ */
+function setWorkspaceErrors(policyID, errors) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errors: null});
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errors});
+}
+
+/**
+ * @param {String} policyID
+ */
+function hideWorkspaceAlertMessage(policyID) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {alertMessage: ''});
+}
+
export {
getPolicySummaries,
getPolicyList,
@@ -304,4 +321,6 @@ export {
uploadAvatar,
update,
updateLocalPolicyValues,
+ setWorkspaceErrors,
+ hideWorkspaceAlertMessage,
};
diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js
index 347c032c45d3..3b1d5babd435 100644
--- a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js
+++ b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js
@@ -5,17 +5,12 @@ import PropTypes from 'prop-types';
import {ScrollView, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import TextLink from '../../components/TextLink';
-import Text from '../../components/Text';
-import Button from '../../components/Button';
import styles from '../../styles/styles';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
-import Icon from '../../components/Icon';
-import {Exclamation} from '../../components/Icon/Expensicons';
-import colors from '../../styles/colors';
import reimbursementAccountPropTypes from './reimbursementAccountPropTypes';
import compose from '../../libs/compose';
import ONYXKEYS from '../../ONYXKEYS';
+import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton';
const propTypes = {
/** ACH data for the withdrawal account actively being set up */
@@ -32,44 +27,6 @@ const defaultProps = {
};
class ReimbursementAccountForm extends React.Component {
- /**
- * @returns {React.Component|String}
- */
- getAlertPrompt() {
- let error = '';
-
- if (!_.isEmpty(this.props.reimbursementAccount.errorModalMessage)) {
- error = (
- {this.props.reimbursementAccount.errorModalMessage}
- );
- } else {
- error = (
- <>
-
- {`${this.props.translate('common.please')} `}
-
- {
- this.form.scrollTo({y: 0, animated: true});
- }}
- >
- {this.props.translate('bankAccount.error.fixTheErrors')}
-
-
- {` ${this.props.translate('bankAccount.error.inTheFormBeforeContinuing')}.`}
-
- >
- );
- }
-
- return (
-
- {error}
-
- );
- }
-
render() {
const isErrorVisible = _.size(lodashGet(this.props, 'reimbursementAccount.errors', {})) > 0
|| lodashGet(this.props, 'reimbursementAccount.errorModalMessage', '').length > 0
@@ -88,19 +45,15 @@ class ReimbursementAccountForm extends React.Component {
{this.props.children}
-
- {isErrorVisible && (
-
-
- {this.getAlertPrompt()}
-
- )}
-
-
+ {
+ this.form.scrollTo({y: 0, animated: true});
+ }}
+ message={this.props.reimbursementAccount.errorModalMessage}
+ />
);
}
diff --git a/src/pages/signin/LoginForm.js b/src/pages/signin/LoginForm.js
index 6c08bd5034fb..434b41b878f4 100755
--- a/src/pages/signin/LoginForm.js
+++ b/src/pages/signin/LoginForm.js
@@ -70,7 +70,7 @@ class LoginForm extends React.Component {
*/
validateAndSubmitForm() {
if (!this.state.login.trim()) {
- this.setState({formError: 'loginForm.pleaseEnterEmailOrPhoneNumber'});
+ this.setState({formError: 'common.pleaseEnterEmailOrPhoneNumber'});
return;
}
diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js
index e0c3d972f485..2ad3862a36c4 100644
--- a/src/pages/workspace/WorkspaceInvitePage.js
+++ b/src/pages/workspace/WorkspaceInvitePage.js
@@ -13,13 +13,10 @@ import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
import Navigation from '../../libs/Navigation/Navigation';
import styles from '../../styles/styles';
import Text from '../../components/Text';
-import Button from '../../components/Button';
import compose from '../../libs/compose';
import ONYXKEYS from '../../ONYXKEYS';
-import {invite} from '../../libs/actions/Policy';
-import Growl from '../../libs/Growl';
+import {hideWorkspaceAlertMessage, invite, setWorkspaceErrors} from '../../libs/actions/Policy';
import ExpensiTextInput from '../../components/ExpensiTextInput';
-import FixedFooter from '../../components/FixedFooter';
import KeyboardAvoidingView from '../../components/KeyboardAvoidingView';
import {isSystemUser} from '../../libs/userUtils';
import {addSMSDomainIfPhoneNumber} from '../../libs/OptionsListUtils';
@@ -27,6 +24,7 @@ import Icon from '../../components/Icon';
import {NewWindow} from '../../components/Icon/Expensicons';
import variables from '../../styles/variables';
import CONST from '../../CONST';
+import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton';
const propTypes = {
...withLocalizePropTypes,
@@ -60,13 +58,19 @@ class WorkspaceInvitePage extends React.Component {
this.state = {
userLogins: '',
welcomeNote: this.getWelcomeNotePlaceholder(),
+ foundSystemLogin: '',
};
this.focusEmailOrPhoneInput = this.focusEmailOrPhoneInput.bind(this);
this.inviteUser = this.inviteUser.bind(this);
+ this.clearErrors = this.clearErrors.bind(this);
this.emailOrPhoneInputRef = null;
}
+ componentDidMount() {
+ this.clearErrors();
+ }
+
/**
* Gets the welcome note default text
*
@@ -78,6 +82,39 @@ class WorkspaceInvitePage extends React.Component {
});
}
+ /**
+ * @returns {String}
+ */
+ getErrorText() {
+ const errors = lodashGet(this.props.policy, 'errors', {});
+
+ if (errors.invalidLogin) {
+ return this.props.translate('workspace.invite.pleaseEnterValidLogin');
+ }
+
+ if (errors.systemUserError) {
+ return this.props.translate('workspace.invite.systemUserError', {email: this.state.foundSystemLogin});
+ }
+
+ if (errors.duplicateLogin) {
+ return this.props.translate('workspace.invite.pleaseEnterUniqueLogin');
+ }
+
+ return '';
+ }
+
+ /**
+ * @returns {Boolean}
+ */
+ getShouldShowAlertPrompt() {
+ return _.size(lodashGet(this.props.policy, 'errors', {})) > 0 || this.props.policy.alertMessage.length > 0;
+ }
+
+ clearErrors() {
+ setWorkspaceErrors(this.props.route.params.policyID, {});
+ hideWorkspaceAlertMessage(this.props.route.params.policyID);
+ }
+
focusEmailOrPhoneInput() {
if (!this.emailOrPhoneInputRef) {
return;
@@ -89,29 +126,40 @@ class WorkspaceInvitePage extends React.Component {
* Handle the invite button click
*/
inviteUser() {
+ if (!this.validate()) {
+ return;
+ }
+
+ const logins = _.map(_.compact(this.state.userLogins.split(',')), login => login.trim());
+ invite(logins, this.state.welcomeNote || this.getWelcomeNotePlaceholder(),
+ this.props.route.params.policyID);
+ }
+
+ /**
+ * @returns {Boolean}
+ */
+ validate() {
const logins = _.map(_.compact(this.state.userLogins.split(',')), login => login.trim());
const isEnteredLoginsvalid = _.every(logins, login => Str.isValidEmail(login) || Str.isValidPhone(login));
- if (!isEnteredLoginsvalid) {
- Growl.error(this.props.translate('workspace.invite.pleaseEnterValidLogin'), 5000);
- return;
+ const errors = {};
+ let foundSystemLogin = '';
+ if (logins.length <= 0 || !isEnteredLoginsvalid) {
+ errors.invalidLogin = true;
}
- const foundSystemLogin = _.find(logins, login => isSystemUser(login));
+ foundSystemLogin = _.find(logins, login => isSystemUser(login));
if (foundSystemLogin) {
- Growl.error(this.props.translate('workspace.invite.systemUserError', {email: foundSystemLogin}), 5000);
- return;
+ errors.systemUserError = true;
}
const policyEmployeeList = lodashGet(this.props, 'policy.employeeList', []);
const areLoginsDuplicate = _.some(logins, login => _.contains(policyEmployeeList, addSMSDomainIfPhoneNumber(login)));
if (areLoginsDuplicate) {
- Growl.error(this.props.translate('workspace.invite.pleaseEnterUniqueLogin'), 5000);
- return;
+ errors.duplicateLogin = true;
}
- invite(logins, this.state.welcomeNote || this.getWelcomeNotePlaceholder(),
- this.props.route.params.policyID);
- Navigation.goBack();
+ this.setState({foundSystemLogin}, () => setWorkspaceErrors(this.props.route.params.policyID, errors));
+ return _.size(errors) <= 0;
}
render() {
@@ -120,80 +168,95 @@ class WorkspaceInvitePage extends React.Component {
{
+ this.clearErrors();
+ Navigation.dismissModal();
+ }}
/>
-
-
- {this.props.translate('workspace.invite.invitePeoplePrompt')}
-
-
- this.emailOrPhoneInputRef = el}
- label={this.props.translate('workspace.invite.enterEmailOrPhone')}
- placeholder={this.props.translate('workspace.invite.EmailOrPhonePlaceholder')}
- autoCompleteType="email"
- autoCorrect={false}
- autoCapitalize="none"
- multiline
- numberOfLines={2}
- value={this.state.userLogins}
- onChangeText={text => this.setState({userLogins: text})}
- />
-
-
- this.setState({welcomeNote: text})}
- />
-
- {
- e.preventDefault();
- Linking.openURL(CONST.PRIVACY_URL);
+ this.form = el}
+ contentContainerStyle={styles.flexGrow1}
+ keyboardShouldPersistTaps="handled"
+ >
+ {/* Form elements */}
+
+
+ {this.props.translate('workspace.invite.invitePeoplePrompt')}
+
+
+ this.emailOrPhoneInputRef = el}
+ label={this.props.translate('workspace.invite.enterEmailOrPhone')}
+ placeholder={this.props.translate('workspace.invite.EmailOrPhonePlaceholder')}
+ autoCompleteType="email"
+ autoCorrect={false}
+ autoCapitalize="none"
+ multiline
+ numberOfLines={2}
+ value={this.state.userLogins}
+ onChangeText={(text) => {
+ this.clearErrors();
+ this.setState({userLogins: text, foundSystemLogin: ''});
}}
- accessibilityRole="link"
- href={CONST.PRIVACY_URL}
- >
- {({hovered, pressed}) => (
-
-
- {this.props.translate('common.privacyPolicy')}
-
-
-
+ errorText={this.getErrorText()}
+ />
+
+
+ this.setState({welcomeNote: text})}
+ />
+
+ {
+ e.preventDefault();
+ Linking.openURL(CONST.PRIVACY_URL);
+ }}
+ accessibilityRole="link"
+ href={CONST.PRIVACY_URL}
+ >
+ {({hovered, pressed}) => (
+
+
+ {this.props.translate('common.privacyPolicy')}
+
+
+
+
-
- )}
-
+ )}
+
+
-
-
-
+
);