Skip to content

Commit

Permalink
Merge pull request #10556 from Expensify/jasper-resendValidateCodeRef…
Browse files Browse the repository at this point in the history
…actor2

Refactor `Session.resendValidationLink` and the `ResendValidationForm` to use the new API
  • Loading branch information
bondydaa committed Sep 1, 2022
2 parents ea5b4c7 + be3212d commit 8b510f8
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 116 deletions.
68 changes: 68 additions & 0 deletions src/components/DotIndicatorMessage.js
Original file line number Diff line number Diff line change
@@ -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 (
<View style={[styles.dotIndicatorMessage, ...props.style]}>
<View style={styles.offlineFeedback.errorDot}>
<Icon src={Expensicons.DotIndicator} fill={props.type === 'error' ? colors.red : colors.green} height={variables.iconSizeSmall} width={variables.iconSizeSmall} />
</View>
<View style={styles.offlineFeedback.textContainer}>
{_.map(sortedMessages, (message, i) => (
<Text key={i} style={styles.offlineFeedback.text}>{message}</Text>
))}
</View>
</View>
);
};

DotIndicatorMessage.propTypes = propTypes;
DotIndicatorMessage.defaultProps = defaultProps;

export default DotIndicatorMessage;

18 changes: 2 additions & 16 deletions src/components/OfflineWithFeedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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) {
Expand All @@ -102,14 +95,7 @@ const OfflineWithFeedback = (props) => {
)}
{hasErrors && (
<View style={StyleUtils.combineStyles(styles.offlineFeedback.error, props.errorRowStyles)}>
<View style={styles.offlineFeedback.errorDot}>
<Icon src={Expensicons.DotIndicator} fill={colors.red} height={variables.iconSizeSmall} width={variables.iconSizeSmall} />
</View>
<View style={styles.offlineFeedback.textContainer}>
{_.map(sortedErrors, (error, i) => (
<Text key={i} style={styles.offlineFeedback.text}>{error}</Text>
))}
</View>
<DotIndicatorMessage messages={props.errors} type="error" />
<Tooltip text={props.translate('common.close')}>
<Pressable
onPress={props.onClose}
Expand Down
17 changes: 17 additions & 0 deletions src/libs/ErrorUtils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import _ from 'underscore';
import CONST from '../CONST';

/**
Expand Down Expand Up @@ -35,7 +36,23 @@ function getAuthenticateErrorMessage(response) {
}
}

/**
* @param {Object} onyxData
* @param {Object} onyxData.errors
* @returns {String}
*/
function getLatestErrorMessage(onyxData) {
return _.chain(onyxData.errors || [])
.keys()
.sortBy()
.reverse()
.map(key => onyxData.errors[key])
.first()
.value();
}

export {
// eslint-disable-next-line import/prefer-default-export
getAuthenticateErrorMessage,
getLatestErrorMessage,
};
32 changes: 27 additions & 5 deletions src/libs/actions/Session/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}

/**
Expand Down
9 changes: 2 additions & 7 deletions src/pages/signin/LoginForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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? */
Expand Down Expand Up @@ -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 (
<>
<View style={[styles.mt3]}>
Expand Down
133 changes: 49 additions & 84 deletions src/pages/signin/ResendValidationForm.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 */
Expand Down Expand Up @@ -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 (
<>
<View style={[styles.mt3, styles.flexRow, styles.alignItemsCenter, styles.justifyContentStart]}>
<Avatar
source={ReportUtils.getDefaultAvatar(this.props.credentials.login)}
imageStyles={[styles.mr2]}
/>
<View style={[styles.flex1]}>
<Text style={[styles.textStrong]}>
{login}
</Text>
</View>
</View>
<View style={[styles.mv5]}>
<Text>
{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 (
<>
<View style={[styles.mt3, styles.flexRow, styles.alignItemsCenter, styles.justifyContentStart]}>
<Avatar
source={ReportUtils.getDefaultAvatar(props.credentials.login)}
imageStyles={[styles.mr2]}
/>
<View style={[styles.flex1]}>
<Text style={[styles.textStrong]}>
{login}
</Text>
</View>
{!_.isEmpty(this.state.formSuccess) && (
<Text style={[styles.formSuccess]}>
{this.state.formSuccess}
</View>
<View style={[styles.mv5]}>
<Text>
{props.translate('resendValidationForm.weSentYouMagicSignInLink', {login, loginType})}
</Text>
</View>
{!_.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
<DotIndicatorMessage style={[styles.mb5]} type="success" messages={{0: props.account.message}} />
)}
{!_.isEmpty(props.account.errors) && (
<DotIndicatorMessage style={[styles.mb5]} type="error" messages={props.account.errors} />
)}
<View style={[styles.mb4, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter]}>
<TouchableOpacity onPress={() => redirectToSignIn()}>
<Text>
{props.translate('common.back')}
</Text>
)}
<View style={[styles.mb4, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter]}>
<TouchableOpacity onPress={() => redirectToSignIn()}>
<Text>
{this.props.translate('common.back')}
</Text>
</TouchableOpacity>
<Button
medium
success
text={this.props.translate('resendValidationForm.resendLink')}
isLoading={this.props.account.loading}
onPress={this.validateAndSubmitForm}
isDisabled={this.props.network.isOffline}
/>
</View>
<OfflineIndicator containerStyles={[styles.mv1]} />
</>
);
}
}
</TouchableOpacity>
<Button
medium
success
text={props.translate('resendValidationForm.resendLink')}
isLoading={props.account.isLoading}
onPress={() => (props.account.validated ? Session.resetPassword() : Session.resendValidationLink())}
isDisabled={props.network.isOffline}
/>
</View>
<OfflineIndicator containerStyles={[styles.mv1]} />
</>
);
};

ResendValidationForm.propTypes = propTypes;
ResendValidationForm.defaultProps = defaultProps;
Expand Down
4 changes: 2 additions & 2 deletions src/pages/workspace/WorkspaceMembersPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,10 @@ class WorkspaceMembersPage extends React.Component {
}) {
const canBeRemoved = this.props.policy.owner !== item.login && this.props.session.email !== item.login;
return (
<OfflineWithFeedback onClose={() => this.dismissError(item)} pendingAction={item.pendingAction} errors={item.errors}>
<OfflineWithFeedback errorRowStyles={[styles.peopleRowBorderBottom]} onClose={() => this.dismissError(item)} pendingAction={item.pendingAction} errors={item.errors}>
<Hoverable onHoverIn={() => this.willTooltipShowForLogin(item.login, true)} onHoverOut={() => this.setState({showTooltipForLogin: ''})}>
<TouchableOpacity
style={[styles.peopleRow, !canBeRemoved && styles.cursorDisabled]}
style={[styles.peopleRow, !item.errors && styles.peopleRowBorderBottom, !canBeRemoved && styles.cursorDisabled]}
onPress={() => this.toggleUser(item.login)}
activeOpacity={0.7}
>
Expand Down
Loading

0 comments on commit 8b510f8

Please sign in to comment.