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 @@ - + + viewBox="0 0 78 19" style="enable-background:new 0 0 78 19;" xml:space="preserve"> - - - - - - - - - + + + + + + + + + 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, };