diff --git a/src/components/DotIndicatorMessage.js b/src/components/DotIndicatorMessage.js
new file mode 100644
index 000000000000..2b073627ef51
--- /dev/null
+++ b/src/components/DotIndicatorMessage.js
@@ -0,0 +1,68 @@
+import React from 'react';
+import _ from 'underscore';
+import PropTypes from 'prop-types';
+import {View} from 'react-native';
+import styles from '../styles/styles';
+import Icon from './Icon';
+import * as Expensicons from './Icon/Expensicons';
+import colors from '../styles/colors';
+import variables from '../styles/variables';
+import Text from './Text';
+
+const propTypes = {
+ /**
+ * In most cases this should just be errors from onxyData
+ * if you are not passing that data then this needs to be in a similar shape like
+ * {
+ * timestamp: 'message',
+ * }
+ */
+ messages: PropTypes.objectOf(PropTypes.string),
+
+ // The type of message, 'error' shows a red dot, 'success' shows a green dot
+ type: PropTypes.oneOf(['error', 'success']).isRequired,
+
+ // Additional styles to apply to the container */
+ // eslint-disable-next-line react/forbid-prop-types
+ style: PropTypes.arrayOf(PropTypes.object),
+};
+
+const defaultProps = {
+ messages: {},
+ style: [],
+};
+
+const DotIndicatorMessage = (props) => {
+ if (_.isEmpty(props.messages)) {
+ return null;
+ }
+
+ // To ensure messages are presented in order we are sort of destroying the data we are given
+ // and rebuilding as an array so we can render the messages in order. We don't really care about
+ // the microtime timestamps anyways so isn't the end of the world that we sort of lose them here.
+ // BEWARE: if you decide to refactor this and keep the microtime keys it could cause performance issues
+ const sortedMessages = _.chain(props.messages)
+ .keys()
+ .sortBy()
+ .map(key => props.messages[key])
+ .value();
+
+ return (
+
+
+
+
+
+ {_.map(sortedMessages, (message, i) => (
+ {message}
+ ))}
+
+
+ );
+};
+
+DotIndicatorMessage.propTypes = propTypes;
+DotIndicatorMessage.defaultProps = defaultProps;
+
+export default DotIndicatorMessage;
+
diff --git a/src/components/OfflineWithFeedback.js b/src/components/OfflineWithFeedback.js
index bd0eb8a8b6f7..54d6bc0ab9be 100644
--- a/src/components/OfflineWithFeedback.js
+++ b/src/components/OfflineWithFeedback.js
@@ -7,14 +7,12 @@ import withLocalize, {withLocalizePropTypes} from './withLocalize';
import {withNetwork} from './OnyxProvider';
import networkPropTypes from './networkPropTypes';
import stylePropTypes from '../styles/stylePropTypes';
-import Text from './Text';
import styles from '../styles/styles';
import Tooltip from './Tooltip';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import * as StyleUtils from '../styles/StyleUtils';
-import colors from '../styles/colors';
-import variables from '../styles/variables';
+import DotIndicatorMessage from './DotIndicatorMessage';
/**
* This component should be used when we are using the offline pattern B (offline with feedback).
@@ -83,11 +81,6 @@ const OfflineWithFeedback = (props) => {
const needsStrikeThrough = props.network.isOffline && props.pendingAction === 'delete';
const hideChildren = !props.network.isOffline && props.pendingAction === 'delete' && !hasErrors;
let children = props.children;
- const sortedErrors = _.chain(props.errors)
- .keys()
- .sortBy()
- .map(key => props.errors[key])
- .value();
// Apply strikethrough to children if needed, but skip it if we are not going to render them
if (needsStrikeThrough && !hideChildren) {
@@ -102,14 +95,7 @@ const OfflineWithFeedback = (props) => {
)}
{hasErrors && (
-
-
-
-
- {_.map(sortedErrors, (error, i) => (
- {error}
- ))}
-
+
onyxData.errors[key])
+ .first()
+ .value();
+}
+
export {
// eslint-disable-next-line import/prefer-default-export
getAuthenticateErrorMessage,
+ getLatestErrorMessage,
};
diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js
index 8e191c164fe6..6342b3d05ebf 100644
--- a/src/libs/actions/Session/index.js
+++ b/src/libs/actions/Session/index.js
@@ -90,11 +90,33 @@ function signOutAndRedirectToSignIn() {
* @param {String} [login]
*/
function resendValidationLink(login = credentials.login) {
- Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: true});
- DeprecatedAPI.ResendValidateCode({email: login})
- .finally(() => {
- Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false});
- });
+ const optimisticData = [{
+ onyxMethod: CONST.ONYX.METHOD.MERGE,
+ key: ONYXKEYS.ACCOUNT,
+ value: {
+ isLoading: true,
+ errors: null,
+ message: null,
+ },
+ }];
+ const successData = [{
+ onyxMethod: CONST.ONYX.METHOD.MERGE,
+ key: ONYXKEYS.ACCOUNT,
+ value: {
+ isLoading: false,
+ message: Localize.translateLocal('resendValidationForm.linkHasBeenResent'),
+ },
+ }];
+ const failureData = [{
+ onyxMethod: CONST.ONYX.METHOD.MERGE,
+ key: ONYXKEYS.ACCOUNT,
+ value: {
+ isLoading: false,
+ message: null,
+ },
+ }];
+
+ API.write('RequestAccountValidationLink', {email: login}, {optimisticData, successData, failureData});
}
/**
diff --git a/src/pages/signin/LoginForm.js b/src/pages/signin/LoginForm.js
index f80488fb3f4a..2490103b97e3 100755
--- a/src/pages/signin/LoginForm.js
+++ b/src/pages/signin/LoginForm.js
@@ -21,6 +21,7 @@ import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButto
import OfflineIndicator from '../../components/OfflineIndicator';
import {withNetwork} from '../../components/OnyxProvider';
import networkPropTypes from '../../components/networkPropTypes';
+import * as ErrorUtils from '../../libs/ErrorUtils';
const propTypes = {
/** Should we dismiss the keyboard when transitioning away from the page? */
@@ -151,13 +152,7 @@ class LoginForm extends React.Component {
render() {
const formErrorTranslated = this.state.formError && this.props.translate(this.state.formError);
- const error = formErrorTranslated || _.chain(this.props.account.errors || [])
- .keys()
- .sortBy()
- .reverse()
- .map(key => this.props.account.errors[key])
- .first()
- .value();
+ const error = formErrorTranslated || ErrorUtils.getLatestErrorMessage(this.props.account);
return (
<>
diff --git a/src/pages/signin/ResendValidationForm.js b/src/pages/signin/ResendValidationForm.js
index e643e94eed73..00f1c1d629a1 100755
--- a/src/pages/signin/ResendValidationForm.js
+++ b/src/pages/signin/ResendValidationForm.js
@@ -1,8 +1,8 @@
import React from 'react';
+import _ from 'underscore';
import {TouchableOpacity, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
-import _ from 'underscore';
import Str from 'expensify-common/lib/str';
import styles from '../../styles/styles';
import Button from '../../components/Button';
@@ -17,6 +17,7 @@ import * as ReportUtils from '../../libs/ReportUtils';
import OfflineIndicator from '../../components/OfflineIndicator';
import networkPropTypes from '../../components/networkPropTypes';
import {withNetwork} from '../../components/OnyxProvider';
+import DotIndicatorMessage from '../../components/DotIndicatorMessage';
const propTypes = {
/* Onyx Props */
@@ -46,92 +47,56 @@ const defaultProps = {
account: {},
};
-class ResendValidationForm extends React.Component {
- constructor(props) {
- super(props);
-
- this.validateAndSubmitForm = this.validateAndSubmitForm.bind(this);
-
- this.state = {
- formSuccess: '',
- };
- }
-
- componentWillUnmount() {
- if (!this.successMessageTimer) {
- return;
- }
-
- clearTimeout(this.successMessageTimer);
- }
-
- /**
- * Check that all the form fields are valid, then trigger the submit callback
- */
- validateAndSubmitForm() {
- this.setState({
- formSuccess: this.props.translate('resendValidationForm.linkHasBeenResent'),
- });
-
- if (!this.props.account.validated) {
- Session.resendValidationLink();
- } else {
- Session.resetPassword();
- }
-
- this.successMessageTimer = setTimeout(() => {
- this.setState({formSuccess: ''});
- }, 5000);
- }
-
- render() {
- const isSMSLogin = Str.isSMSLogin(this.props.credentials.login);
- const login = isSMSLogin ? this.props.toLocalPhone(Str.removeSMSDomain(this.props.credentials.login)) : this.props.credentials.login;
- const loginType = (isSMSLogin ? this.props.translate('common.phone') : this.props.translate('common.email')).toLowerCase();
-
- return (
- <>
-
-
-
-
- {login}
-
-
-
-
-
- {this.props.translate('resendValidationForm.weSentYouMagicSignInLink', {login, loginType})}
+const ResendValidationForm = (props) => {
+ const isSMSLogin = Str.isSMSLogin(props.credentials.login);
+ const login = isSMSLogin ? props.toLocalPhone(Str.removeSMSDomain(props.credentials.login)) : props.credentials.login;
+ const loginType = (isSMSLogin ? props.translate('common.phone') : props.translate('common.email')).toLowerCase();
+
+ return (
+ <>
+
+
+
+
+ {login}
- {!_.isEmpty(this.state.formSuccess) && (
-
- {this.state.formSuccess}
+
+
+
+ {props.translate('resendValidationForm.weSentYouMagicSignInLink', {login, loginType})}
+
+
+ {!_.isEmpty(props.account.message) && (
+
+ // DotIndicatorMessage mostly expects onyxData errors so we need to mock an object so that the messages looks similar to prop.account.errors
+
+ )}
+ {!_.isEmpty(props.account.errors) && (
+
+ )}
+
+ redirectToSignIn()}>
+
+ {props.translate('common.back')}
- )}
-
- redirectToSignIn()}>
-
- {this.props.translate('common.back')}
-
-
-
-
-
- >
- );
- }
-}
+
+
+
+ >
+ );
+};
ResendValidationForm.propTypes = propTypes;
ResendValidationForm.defaultProps = defaultProps;
diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js
index eca3a2bcbe5f..f687d964ab36 100644
--- a/src/pages/workspace/WorkspaceMembersPage.js
+++ b/src/pages/workspace/WorkspaceMembersPage.js
@@ -219,10 +219,10 @@ class WorkspaceMembersPage extends React.Component {
}) {
const canBeRemoved = this.props.policy.owner !== item.login && this.props.session.email !== item.login;
return (
- this.dismissError(item)} pendingAction={item.pendingAction} errors={item.errors}>
+ this.dismissError(item)} pendingAction={item.pendingAction} errors={item.errors}>
this.willTooltipShowForLogin(item.login, true)} onHoverOut={() => this.setState({showTooltipForLogin: ''})}>
this.toggleUser(item.login)}
activeOpacity={0.7}
>
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 8317acfa847d..51ab27573a8b 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -2352,9 +2352,13 @@ const styles = {
width: '100%',
flexDirection: 'row',
justifyContent: 'space-between',
- borderBottomWidth: 1,
+ ...spacing.pt2,
+ },
+
+ peopleRowBorderBottom: {
borderColor: themeColors.border,
- ...spacing.pv2,
+ borderBottomWidth: 1,
+ ...spacing.pb2,
},
peopleRowCell: {
@@ -2402,6 +2406,13 @@ const styles = {
},
},
+ dotIndicatorMessage: {
+ display: 'flex',
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+
sidebarPopover: {
width: variables.sideBarWidth - 68,
},