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')} - - -