diff --git a/assets/images/expensify-wordmark.svg b/assets/images/expensify-wordmark.svg
index 73018497030b..69fbcbae6743 100644
--- a/assets/images/expensify-wordmark.svg
+++ b/assets/images/expensify-wordmark.svg
@@ -1,26 +1,23 @@
-
+
diff --git a/assets/images/product-illustrations/safe.svg b/assets/images/product-illustrations/safe.svg
new file mode 100644
index 000000000000..db2ac0707f7f
--- /dev/null
+++ b/assets/images/product-illustrations/safe.svg
@@ -0,0 +1,119 @@
+
+
+
diff --git a/assets/images/product-illustrations/todd-behind-cloud.svg b/assets/images/product-illustrations/todd-behind-cloud.svg
new file mode 100644
index 000000000000..6281ce0ef727
--- /dev/null
+++ b/assets/images/product-illustrations/todd-behind-cloud.svg
@@ -0,0 +1,58 @@
+
+
+
diff --git a/src/CONST.js b/src/CONST.js
index bc9850a3dec8..39bddd92d7dd 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -27,6 +27,13 @@ const CONST = {
MIN_SIZE: 240,
},
+ AUTO_AUTH_STATE: {
+ NOT_STARTED: 'not-started',
+ SIGNING_IN: 'signing-in',
+ JUST_SIGNED_IN: 'just-signed-in',
+ FAILED: 'failed',
+ },
+
AVATAR_MAX_ATTACHMENT_SIZE: 6291456,
AVATAR_ALLOWED_EXTENSIONS: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg'],
diff --git a/src/components/Icon/Illustrations.js b/src/components/Icon/Illustrations.js
index 3a038d66ca72..167d8e90f29f 100644
--- a/src/components/Icon/Illustrations.js
+++ b/src/components/Icon/Illustrations.js
@@ -17,8 +17,10 @@ import ReceiptsSearchYellow from '../../../assets/images/product-illustrations/r
import ReceiptYellow from '../../../assets/images/product-illustrations/receipt--yellow.svg';
import RocketBlue from '../../../assets/images/product-illustrations/rocket--blue.svg';
import RocketOrange from '../../../assets/images/product-illustrations/rocket--orange.svg';
+import SafeBlue from '../../../assets/images/product-illustrations/safe.svg';
import TadaYellow from '../../../assets/images/product-illustrations/tada--yellow.svg';
import TadaBlue from '../../../assets/images/product-illustrations/tada--blue.svg';
+import ToddBehindCloud from '../../../assets/images/product-illustrations/todd-behind-cloud.svg';
import GpsTrackOrange from '../../../assets/images/product-illustrations/gps-track--orange.svg';
import ShieldYellow from '../../../assets/images/simple-illustrations/simple-illustration__shield.svg';
import MoneyReceipts from '../../../assets/images/simple-illustrations/simple-illustration__money-receipts.svg';
@@ -58,8 +60,10 @@ export {
ReceiptYellow,
RocketBlue,
RocketOrange,
+ SafeBlue,
TadaYellow,
TadaBlue,
+ ToddBehindCloud,
GpsTrackOrange,
ShieldYellow,
MoneyReceipts,
diff --git a/src/components/ValidateCode/AbracadabraModal.js b/src/components/ValidateCode/AbracadabraModal.js
new file mode 100644
index 000000000000..fd54fdd7db7b
--- /dev/null
+++ b/src/components/ValidateCode/AbracadabraModal.js
@@ -0,0 +1,51 @@
+import React, {PureComponent} from 'react';
+import {View} from 'react-native';
+import colors from '../../styles/colors';
+import styles from '../../styles/styles';
+import Icon from '../Icon';
+import withLocalize, {withLocalizePropTypes} from '../withLocalize';
+import Text from '../Text';
+import * as Expensicons from '../Icon/Expensicons';
+import * as Illustrations from '../Icon/Illustrations';
+import variables from '../../styles/variables';
+
+const propTypes = {
+ ...withLocalizePropTypes,
+};
+
+class AbracadabraModal extends PureComponent {
+ render() {
+ return (
+
+
+
+
+
+
+ {this.props.translate('validateCodeModal.successfulSignInTitle')}
+
+
+
+ {this.props.translate('validateCodeModal.successfulSignInDescription')}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+AbracadabraModal.propTypes = propTypes;
+export default withLocalize(AbracadabraModal);
diff --git a/src/components/ValidateCode/ExpiredValidateCodeModal.js b/src/components/ValidateCode/ExpiredValidateCodeModal.js
new file mode 100644
index 000000000000..5a5fa65dc7e2
--- /dev/null
+++ b/src/components/ValidateCode/ExpiredValidateCodeModal.js
@@ -0,0 +1,112 @@
+import React, {PureComponent} from 'react';
+import {withOnyx} from 'react-native-onyx';
+import _, {compose} from 'underscore';
+import lodashGet from 'lodash/get';
+import {View} from 'react-native';
+import colors from '../../styles/colors';
+import styles from '../../styles/styles';
+import Icon from '../Icon';
+import withLocalize, {withLocalizePropTypes} from '../withLocalize';
+import Text from '../Text';
+import * as Expensicons from '../Icon/Expensicons';
+import * as Illustrations from '../Icon/Illustrations';
+import variables from '../../styles/variables';
+import TextLink from '../TextLink';
+import ONYXKEYS from '../../ONYXKEYS';
+import * as ErrorUtils from '../../libs/ErrorUtils';
+import * as Session from '../../libs/actions/Session';
+
+const propTypes = {
+ ...withLocalizePropTypes,
+};
+
+class ExpiredValidateCodeModal extends PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.requestNewCode = this.requestNewCode.bind(this);
+ }
+
+ shouldShowRequestCodeLink() {
+ return Boolean(lodashGet(this.props, 'credentials.login', null));
+ }
+
+ requestNewCode() {
+ Session.resendValidateCode();
+ }
+
+ render() {
+ const codeRequestedMessage = lodashGet(this.props, 'account.message', null);
+ const accountErrors = lodashGet(this.props, 'account.errors', {});
+ let codeRequestedErrors;
+ if (_.keys(accountErrors).length > 1) {
+ codeRequestedErrors = ErrorUtils.getLatestErrorMessage(this.props.account);
+ }
+ return (
+
+
+
+
+
+
+ {this.props.translate('validateCodeModal.expiredCodeTitle')}
+
+
+
+ {this.props.translate('validateCodeModal.expiredCodeDescription')}
+ {this.shouldShowRequestCodeLink() && !codeRequestedMessage
+ && (
+ <>
+
+ {this.props.translate('validateCodeModal.requestNewCode')}
+ {' '}
+
+ {this.props.translate('validateCodeModal.requestNewCodeLink')}
+
+ !
+ >
+ )}
+
+ {this.shouldShowRequestCodeLink() && codeRequestedErrors
+ && (
+
+
+
+ {codeRequestedErrors}
+
+ )}
+ {this.shouldShowRequestCodeLink() && codeRequestedMessage
+ && (
+
+
+
+ {codeRequestedMessage}
+
+ )}
+
+
+
+
+
+
+ );
+ }
+}
+
+ExpiredValidateCodeModal.propTypes = propTypes;
+export default compose(
+ withLocalize,
+ withOnyx({
+ account: {key: ONYXKEYS.ACCOUNT},
+ credentials: {key: ONYXKEYS.CREDENTIALS},
+ }),
+)(ExpiredValidateCodeModal);
diff --git a/src/components/ValidateCode/TfaRequiredModal.js b/src/components/ValidateCode/TfaRequiredModal.js
new file mode 100644
index 000000000000..5ac7802edc7a
--- /dev/null
+++ b/src/components/ValidateCode/TfaRequiredModal.js
@@ -0,0 +1,51 @@
+import React, {PureComponent} from 'react';
+import {View} from 'react-native';
+import colors from '../../styles/colors';
+import styles from '../../styles/styles';
+import Icon from '../Icon';
+import withLocalize, {withLocalizePropTypes} from '../withLocalize';
+import Text from '../Text';
+import * as Expensicons from '../Icon/Expensicons';
+import * as Illustrations from '../Icon/Illustrations';
+import variables from '../../styles/variables';
+
+const propTypes = {
+ ...withLocalizePropTypes,
+};
+
+class TfaRequiredModal extends PureComponent {
+ render() {
+ return (
+
+
+
+
+
+
+ {this.props.translate('validateCodeModal.tfaRequiredTitle')}
+
+
+
+ {this.props.translate('validateCodeModal.tfaRequiredDescription')}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+TfaRequiredModal.propTypes = propTypes;
+export default withLocalize(TfaRequiredModal);
diff --git a/src/components/ValidateCodeModal.js b/src/components/ValidateCode/ValidateCodeModal.js
similarity index 50%
rename from src/components/ValidateCodeModal.js
rename to src/components/ValidateCode/ValidateCodeModal.js
index 9bf52d2c4795..854cdf9cdbfe 100644
--- a/src/components/ValidateCodeModal.js
+++ b/src/components/ValidateCode/ValidateCodeModal.js
@@ -1,40 +1,43 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
+import {compose} from 'underscore';
+import {withOnyx} from 'react-native-onyx';
+import lodashGet from 'lodash/get';
import {View} from 'react-native';
-import colors from '../styles/colors';
-import styles from '../styles/styles';
-import Icon from './Icon';
-import withLocalize, {withLocalizePropTypes} from './withLocalize';
-import Text from './Text';
-import * as Expensicons from './Icon/Expensicons';
-import * as Illustrations from './Icon/Illustrations';
-import variables from '../styles/variables';
-import TextLink from './TextLink';
+import colors from '../../styles/colors';
+import styles from '../../styles/styles';
+import Icon from '../Icon';
+import withLocalize, {withLocalizePropTypes} from '../withLocalize';
+import Text from '../Text';
+import * as Expensicons from '../Icon/Expensicons';
+import * as Illustrations from '../Icon/Illustrations';
+import variables from '../../styles/variables';
+import TextLink from '../TextLink';
+import ONYXKEYS from '../../ONYXKEYS';
+import * as Session from '../../libs/actions/Session';
const propTypes = {
- /** Whether the user has been signed in with the link. */
- isSuccessfullySignedIn: PropTypes.bool,
-
/** Code to display. */
code: PropTypes.string.isRequired,
- /** Whether the user can get signed straight in the App from the current page */
- shouldShowSignInHere: PropTypes.bool,
-
- /** Callback to be called when user clicks the Sign in here link */
- onSignInHereClick: PropTypes.func,
+ /** The ID of the account to which the code belongs. */
+ accountID: PropTypes.string.isRequired,
...withLocalizePropTypes,
};
-const defaultProps = {
- isSuccessfullySignedIn: false,
- shouldShowSignInHere: false,
- onSignInHereClick: () => {},
-};
-
class ValidateCodeModal extends PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.signInHere = this.signInHere.bind(this);
+ }
+
+ signInHere() {
+ Session.signInWithValidateCode(this.props.accountID, this.props.code);
+ }
+
render() {
return (
@@ -42,22 +45,22 @@ class ValidateCodeModal extends PureComponent {
- {this.props.translate(this.props.isSuccessfullySignedIn ? 'validateCodeModal.successfulSignInTitle' : 'validateCodeModal.title')}
+ {this.props.translate('validateCodeModal.title')}
- {this.props.translate(this.props.isSuccessfullySignedIn ? 'validateCodeModal.successfulSignInDescription' : 'validateCodeModal.description')}
- {this.props.shouldShowSignInHere
+ {this.props.translate('validateCodeModal.description')}
+ {!lodashGet(this.props, 'session.authToken', null)
&& (
<>
{this.props.translate('validateCodeModal.or')}
{' '}
-
+
{this.props.translate('validateCodeModal.signInHere')}
>
@@ -65,13 +68,11 @@ class ValidateCodeModal extends PureComponent {
{this.props.shouldShowSignInHere ? '!' : '.'}
- {!this.props.isSuccessfullySignedIn && (
-
-
- {this.props.code}
-
-
- )}
+
+
+ {this.props.code}
+
+
Permissions.canUsePasswordlessLogins(this.props.betas);
+ componentDidUpdate() {
+ if (
+ lodashGet(this.props, 'credentials.login', null)
+ || !lodashGet(this.props, 'credentials.accountID', null)
+ || !lodashGet(this.props, 'account.requiresTwoFactorAuth', false)
+ ) {
+ return;
+ }
- /**
- * @returns {String}
- */
- accountID = () => lodashGet(this.props.route.params, 'accountID', '');
+ // The user clicked the option to sign in the current tab
+ Navigation.navigate(ROUTES.REPORT);
+ }
/**
* @returns {String}
*/
- validateCode = () => lodashGet(this.props.route.params, 'validateCode', '');
+ getAutoAuthState() {
+ return lodashGet(this.props, 'session.autoAuthState', CONST.AUTO_AUTH_STATE.NOT_STARTED);
+ }
/**
- * @returns {Boolean}
+ * @returns {String}
*/
- isAuthenticated = () => Boolean(lodashGet(this.props, 'session.authToken', null));
+ getAccountID() {
+ return lodashGet(this.props.route.params, 'accountID', '');
+ }
/**
- * Whether SignIn was initiated on the current browser.
- * @returns {Boolean}
+ * @returns {String}
*/
- isSignInInitiated = () => !this.isAuthenticated() && lodashGet(this.props, 'credentials.login', null);
+ getValidateCode() {
+ return lodashGet(this.props.route.params, 'validateCode', '');
+ }
render() {
+ const isTfaRequired = lodashGet(this.props, 'account.requiresTwoFactorAuth', false);
+ const isSignedIn = Boolean(lodashGet(this.props, 'session.authToken', null));
return (
- this.isOnPasswordlessBeta() && !this.isSignInInitiated() && !lodashGet(this.props, 'account.isLoading', true)
- ? (
- Session.signInWithValidateCodeAndNavigate(this.accountID(), this.validateCode())}
- />
- )
- :
+ <>
+ {this.getAutoAuthState() === CONST.AUTO_AUTH_STATE.FAILED && }
+ {this.getAutoAuthState() === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN && (!isTfaRequired || isSignedIn) && }
+ {this.getAutoAuthState() === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN && isTfaRequired && !isSignedIn && }
+ {this.getAutoAuthState() === CONST.AUTO_AUTH_STATE.NOT_STARTED && }
+ {this.getAutoAuthState() === CONST.AUTO_AUTH_STATE.SIGNING_IN && }
+ >
);
}
}
diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js
index 323c166b3ec4..f8c1ccaffa0b 100644
--- a/src/pages/signin/SignInPage.js
+++ b/src/pages/signin/SignInPage.js
@@ -1,6 +1,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
+import lodashGet from 'lodash/get';
import Str from 'expensify-common/lib/str';
import {SafeAreaView} from 'react-native-safe-area-context';
import ONYXKEYS from '../../ONYXKEYS';
@@ -90,7 +91,7 @@ class SignInPage extends Component {
// We will only know this after a user signs in successfully, without their 2FA code
welcomeText = this.props.translate('validateCodeForm.enterAuthenticatorCode');
} else {
- const userLogin = Str.removeSMSDomain(this.props.credentials.login);
+ const userLogin = Str.removeSMSDomain(lodashGet(this.props, 'credentials.login', ''));
welcomeText = this.props.account.validated
? this.props.translate('welcomeText.welcomeBackEnterMagicCode', {login: userLogin})
: this.props.translate('welcomeText.welcomeEnterMagicCode', {login: userLogin});
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 8c65d4f08d38..bcab5775a15b 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -3037,7 +3037,7 @@ const styles = {
justifyContent: 'space-between',
},
- magicCodeDigits: {
+ validateCodeDigits: {
color: themeColors.text,
fontFamily: fontFamily.EXP_NEUE,
fontSize: variables.fontSizeXXLarge,
@@ -3097,6 +3097,11 @@ const styles = {
top: 60,
zIndex: 100,
},
+
+ validateCodeMessage: {
+ width: variables.modalContentMaxWidth,
+ textAlign: 'center',
+ },
};
export default styles;
diff --git a/src/styles/variables.js b/src/styles/variables.js
index 375ee77a594a..c82d8286446a 100644
--- a/src/styles/variables.js
+++ b/src/styles/variables.js
@@ -99,8 +99,9 @@ export default {
modalTopIconHeight: 164,
modalTopBigIconHeight: 244,
modalWordmarkWidth: 154,
- modalWordmarkHeight: 34,
+ modalWordmarkHeight: 37,
verticalLogoHeight: 634,
verticalLogoWidth: 111,
badgeMaxWidth: 180,
+ modalContentMaxWidth: 360,
};