diff --git a/README.md b/README.md index 8881a1ab3a71..55d9b8f1478e 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ For an M1 Mac, read this [SO](https://stackoverflow.com/c/expensify/questions/11 1. If you are having issues with **_Getting Started_**, please reference [React Native's Documentation](https://reactnative.dev/docs/environment-setup) 2. If you are running into CORS errors like (in the browser dev console) ```sh - Access to fetch at 'https://www.expensify.com/api?command=BeginSignIn' from origin 'http://localhost:8080' has been blocked by CORS policy + Access to fetch at 'https://www.expensify.com/api?command=GetAccountStatus' from origin 'http://localhost:8080' has been blocked by CORS policy ``` You probably have a misconfigured `.env` file - remove it (`rm .env`) and try again @@ -164,7 +164,7 @@ That action will then call `Onyx.merge()` to [set default data and a loading sta ```js function signIn(password, twoFactorAuthCode) { - Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: true}); + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: true}); Authentication.Authenticate({ ...defaultParams, password, @@ -177,7 +177,7 @@ function signIn(password, twoFactorAuthCode) { Onyx.merge(ONYXKEYS.ACCOUNT, {error: error.message}); }) .finally(() => { - Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); }); } ``` @@ -188,7 +188,7 @@ Keeping our `Onyx.merge()` out of the view layer and in actions helps organize t // Bad validateAndSubmitForm() { // validate... - this.setState({isLoading: true}); + this.setState({loading: true}); signIn() .then((response) => { if (result.jsonCode === 200) { @@ -198,7 +198,7 @@ validateAndSubmitForm() { this.setState({error: response.message}); }) .finally(() => { - this.setState({isLoading: false}); + this.setState({loading: false}); }); } diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index a6fe6476435f..17302ba46467 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -186,6 +186,9 @@ export default { // The policyID of the last workspace whose settings were accessed by the user LAST_ACCESSED_WORKSPACE_POLICY_ID: 'lastAccessedWorkspacePolicyID', + // Validating Email? + USER_SIGN_UP: 'userSignUp', + // List of Form ids FORMS: { ADD_DEBIT_CARD_FORM: 'addDebitCardForm', diff --git a/src/languages/en.js b/src/languages/en.js index cbafe0744149..b9360c8fdb3e 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -497,9 +497,11 @@ export default { }, resendValidationForm: { linkHasBeenResent: 'Link has been re-sent', - weSentYouMagicSignInLink: ({login, loginType}) => `I've sent a magic sign-in link to ${login}. Please check your ${loginType} to sign in.`, + weSentYouMagicSignInLink: ({login}) => `We've sent a magic sign in link to ${login}. Check your Inbox and your Spam folder and wait 5-10 minutes before trying again.`, resendLink: 'Resend link', validationCodeFailedMessage: 'It looks like there was an error with your validation link or it has expired.', + unvalidatedAccount: 'This account exists but isn\'t validated, please check your inbox for your magic link.', + newAccount: ({login, loginType}) => `Welcome ${login}, it's always great to see a new face around here! Please check your ${loginType} for a magic link to validate your account.`, }, detailsPage: { localTime: 'Local time', @@ -521,7 +523,7 @@ export default { passwordFormTitle: 'Welcome back to the New Expensify! Please set your password.', passwordNotSet: 'We were unable to set your new password. We have sent you a new password link to try again.', setPasswordLinkInvalid: 'This set password link is invalid or has expired. A new one is waiting for you in your email inbox!', - validatingAccount: 'Verifying account', + verifyingAccount: 'Verifying account', }, stepCounter: ({step, total}) => `Step ${step} of ${total}`, bankAccount: { diff --git a/src/languages/es.js b/src/languages/es.js index fdcb6778c643..86d9cd026e42 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -497,9 +497,11 @@ export default { }, resendValidationForm: { linkHasBeenResent: 'El enlace se ha reenviado', - weSentYouMagicSignInLink: ({login, loginType}) => `Te he enviado un hiperenlace mágico para iniciar sesión a ${login}. Por favor revisa tu ${loginType}`, + weSentYouMagicSignInLink: ({login}) => `Hemos enviado un enlace mágico de inicio de sesión a ${login}. Verifica tu bandeja de entrada y tu carpeta de correo no deseado y espera de 5 a 10 minutos antes de intentarlo de nuevo.`, resendLink: 'Reenviar enlace', validationCodeFailedMessage: 'Parece que hubo un error con el enlace de validación o ha caducado.', + unvalidatedAccount: 'Esta cuenta existe pero no está validada, por favor busca el enlace mágico en tu bandeja de entrada', + newAccount: ({login, loginType}) => `¡Bienvenido ${login}, es genial ver una cara nueva por aquí! En tu ${loginType} encontrarás un enlace para validar tu cuenta, por favor, revísalo`, }, detailsPage: { localTime: 'Hora local', @@ -521,7 +523,7 @@ export default { passwordFormTitle: '¡Bienvenido de vuelta al Nuevo Expensify! Por favor, elige una contraseña.', passwordNotSet: 'No pudimos cambiar tu clave. Te hemos enviado un nuevo enlace para que intentes cambiar la clave nuevamente.', setPasswordLinkInvalid: 'El enlace para configurar tu contraseña ha expirado. Te hemos enviado un nuevo enlace a tu correo.', - validatingAccount: 'Verificando cuenta', + verifyingAccount: 'Verificando cuenta', }, stepCounter: ({step, total}) => `Paso ${step} de ${total}`, bankAccount: { diff --git a/src/libs/Network/MainQueue.js b/src/libs/Network/MainQueue.js index bb0804565893..2143cee9d851 100644 --- a/src/libs/Network/MainQueue.js +++ b/src/libs/Network/MainQueue.js @@ -19,7 +19,7 @@ let networkRequestQueue = []; * @return {Boolean} */ function canMakeRequest(request) { - // Some requests are always made even when we are in the process of authenticating (typically because they require no authToken e.g. Log, BeginSignIn) + // Some requests are always made even when we are in the process of authenticating (typically because they require no authToken e.g. Log, GetAccountStatus) // However, if we are in the process of authenticating we always want to queue requests until we are no longer authenticating. return request.data.forceNetworkRequest === true || (!NetworkStore.isAuthenticating() && !SequentialQueue.isRunning()); } diff --git a/src/libs/Network/enhanceParameters.js b/src/libs/Network/enhanceParameters.js index b7f82f47dda5..093c0d96c087 100644 --- a/src/libs/Network/enhanceParameters.js +++ b/src/libs/Network/enhanceParameters.js @@ -15,10 +15,11 @@ function isAuthTokenRequired(command) { 'Log', 'Graphite_Timer', 'Authenticate', - 'BeginSignIn', + 'GetAccountStatus', 'SetPassword', 'User_SignUp', 'ResendValidateCode', + 'User_ReopenAccount', 'ValidateEmail', ], command); } diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index d88d35bfe304..5059a434ad43 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -18,12 +18,12 @@ import Timers from '../../Timers'; import * as Pusher from '../../Pusher/pusher'; import NetworkConnection from '../../NetworkConnection'; import * as User from '../User'; +import * as ValidationUtils from '../../ValidationUtils'; import * as Authentication from '../../Authentication'; import * as ErrorUtils from '../../ErrorUtils'; import * as Welcome from '../Welcome'; import * as API from '../../API'; import * as NetworkStore from '../../Network/NetworkStore'; -import DateUtils from '../../DateUtils'; let credentials = {}; Onyx.connect({ @@ -48,6 +48,28 @@ function setSuccessfulSignInData(data) { }); } +/** + * Create an account for the user logging in. + * This will send them a notification with a link to click on to validate the account and set a password + * + * @param {String} login + */ +function createAccount(login) { + Onyx.merge(ONYXKEYS.ACCOUNT, {error: ''}); + + DeprecatedAPI.User_SignUp({ + email: login, + }).then((response) => { + // A 405 means that the account needs to be validated. We should let the user proceed to the ResendValidationForm view. + if (response.jsonCode === 200 || response.jsonCode === 405) { + return; + } + + Onyx.merge(ONYXKEYS.CREDENTIALS, {login: null}); + Onyx.merge(ONYXKEYS.ACCOUNT, {error: response.message || `Unknown API Error: ${response.jsonCode}`}); + }); +} + /** * Clears the Onyx store and redirects user to the sign in page */ @@ -84,16 +106,29 @@ function signOutAndRedirectToSignIn() { Log.info('Redirecting to Sign In because signOut() was called'); } +/** + * Reopen the account and send the user a link to set password + * + * @param {String} [login] + */ +function reopenAccount(login = credentials.login) { + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: true}); + DeprecatedAPI.User_ReopenAccount({email: login}) + .finally(() => { + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); + }); +} + /** * Resend the validation link to the user that is validating their account * * @param {String} [login] */ function resendValidationLink(login = credentials.login) { - Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: true}); + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: true}); DeprecatedAPI.ResendValidateCode({email: login}) .finally(() => { - Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); }); } @@ -102,42 +137,45 @@ function resendValidationLink(login = credentials.login) { * * @param {String} login */ -function beginSignIn(login) { - const optimisticData = [ - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.ACCOUNT, - value: { - ...CONST.DEFAULT_ACCOUNT_DATA, - isLoading: true, - }, - }, - ]; - - const successData = [ - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.ACCOUNT, - value: { - isLoading: false, - }, - }, - ]; - - const failureData = [ - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.ACCOUNT, - value: { - isLoading: false, - errors: { - [DateUtils.getMicroseconds()]: 'Cannot get account details, please try again', - }, - }, - }, - ]; +function fetchAccountDetails(login) { + Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, loading: true}); - API.read('BeginSignIn', {email: login}, {optimisticData, successData, failureData}); + DeprecatedAPI.GetAccountStatus({email: login, forceNetworkRequest: true}) + .then((response) => { + if (response.jsonCode === 200) { + Onyx.merge(ONYXKEYS.CREDENTIALS, { + login: response.normalizedLogin, + }); + Onyx.merge(ONYXKEYS.ACCOUNT, { + accountExists: response.accountExists, + validated: response.validated, + closed: response.isClosed, + forgotPassword: false, + validateCodeExpired: false, + }); + + if (!response.accountExists) { + createAccount(login); + } else if (response.isClosed) { + reopenAccount(login); + } else if (!response.validated) { + resendValidationLink(login); + } + } else if (response.jsonCode === 402) { + Onyx.merge(ONYXKEYS.ACCOUNT, { + error: ValidationUtils.isNumericWithSpecialChars(login) + ? Localize.translateLocal('common.error.phoneNumber') + : Localize.translateLocal('loginForm.error.invalidFormatEmailLogin'), + }); + } else if (response.jsonCode === CONST.JSON_CODE.UNABLE_TO_RETRY) { + Onyx.merge(ONYXKEYS.ACCOUNT, {error: Localize.translateLocal('session.offlineMessageRetry')}); + } else { + Onyx.merge(ONYXKEYS.ACCOUNT, {error: response.message}); + } + }) + .finally(() => { + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); + }); } /** @@ -197,7 +235,7 @@ function createTemporaryLogin(authToken, email) { return createLoginResponse; }) .finally(() => { - Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); }); } @@ -210,7 +248,7 @@ function createTemporaryLogin(authToken, email) { * @param {String} [twoFactorAuthCode] */ function signIn(password, twoFactorAuthCode) { - Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, isLoading: true}); + Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, loading: true}); Authentication.Authenticate({ useExpensifyLogin: true, @@ -225,10 +263,10 @@ function signIn(password, twoFactorAuthCode) { if (response.jsonCode !== 200) { const errorMessage = ErrorUtils.getAuthenticateErrorMessage(response); if (errorMessage === 'passwordForm.error.twoFactorAuthenticationEnabled') { - Onyx.merge(ONYXKEYS.ACCOUNT, {requiresTwoFactorAuth: true, isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {requiresTwoFactorAuth: true, loading: false}); return; } - Onyx.merge(ONYXKEYS.ACCOUNT, {error: Localize.translateLocal(errorMessage), isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {error: Localize.translateLocal(errorMessage), loading: false}); return; } @@ -245,7 +283,7 @@ function signIn(password, twoFactorAuthCode) { * @param {String} exitTo */ function signInWithShortLivedToken(email, shortLivedToken) { - Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, isLoading: true}); + Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, loading: true}); createTemporaryLogin(shortLivedToken, email) .then((response) => { @@ -256,7 +294,7 @@ function signInWithShortLivedToken(email, shortLivedToken) { User.getUserDetails(); Onyx.merge(ONYXKEYS.ACCOUNT, {success: true}); }).finally(() => { - Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); }); } @@ -284,6 +322,7 @@ function resetPassword() { key: ONYXKEYS.ACCOUNT, value: { isLoading: false, + validateCodeExpired: false, }, }, ], @@ -293,6 +332,7 @@ function resetPassword() { key: ONYXKEYS.ACCOUNT, value: { isLoading: false, + validateCodeExpired: false, }, }, ], @@ -309,7 +349,7 @@ function resetPassword() { * @param {Number} accountID */ function setPassword(password, validateCode, accountID) { - Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, isLoading: true}); + Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, loading: true, validateCodeExpired: false}); DeprecatedAPI.SetPassword({ password, validateCode, @@ -325,7 +365,7 @@ function setPassword(password, validateCode, accountID) { Onyx.merge(ONYXKEYS.ACCOUNT, {error: response.message}); }) .finally(() => { - Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); }); } @@ -379,6 +419,7 @@ function changePasswordAndSignIn(authToken, password) { password, }) .then((responsePassword) => { + Onyx.merge(ONYXKEYS.USER_SIGN_UP, {authToken: null}); if (responsePassword.jsonCode === 200) { signIn(password); return; @@ -391,7 +432,7 @@ function changePasswordAndSignIn(authToken, password) { } if (responsePassword.jsonCode === CONST.JSON_CODE.NOT_AUTHENTICATED) { // authToken has expired, and we have the account email, so we request a new magic link. - Onyx.merge(ONYXKEYS.ACCOUNT, {error: null}); + Onyx.merge(ONYXKEYS.ACCOUNT, {accountExists: true, validateCodeExpired: true, error: null}); resetPassword(); Navigation.navigate(ROUTES.HOME); return; @@ -407,6 +448,7 @@ function changePasswordAndSignIn(authToken, password) { * @param {String} authToken */ function validateEmail(accountID, validateCode) { + Onyx.merge(ONYXKEYS.USER_SIGN_UP, {isValidating: true}); Onyx.merge(ONYXKEYS.SESSION, {error: ''}); DeprecatedAPI.ValidateEmail({ accountID, @@ -414,16 +456,19 @@ function validateEmail(accountID, validateCode) { }) .then((responseValidate) => { if (responseValidate.jsonCode === 200) { - Onyx.merge(ONYXKEYS.CREDENTIALS, {login: responseValidate.email, authToken: responseValidate.authToken}); + Onyx.merge(ONYXKEYS.USER_SIGN_UP, {authToken: responseValidate.authToken}); + Onyx.merge(ONYXKEYS.ACCOUNT, {accountExists: true, validated: true}); + Onyx.merge(ONYXKEYS.CREDENTIALS, {login: responseValidate.email}); return; } if (responseValidate.jsonCode === 666) { - Onyx.merge(ONYXKEYS.ACCOUNT, {validated: true}); + Onyx.merge(ONYXKEYS.ACCOUNT, {accountExists: true, validated: true}); } if (responseValidate.jsonCode === 401) { Onyx.merge(ONYXKEYS.SESSION, {error: 'setPasswordPage.setPasswordLinkInvalid'}); } - }); + }) + .finally(Onyx.merge(ONYXKEYS.USER_SIGN_UP, {isValidating: false})); } // It's necessary to throttle requests to reauthenticate since calling this multiple times will cause Pusher to @@ -492,12 +537,13 @@ function setShouldShowComposeInput(shouldShowComposeInput) { } export { - beginSignIn, + fetchAccountDetails, setPassword, signIn, signInWithShortLivedToken, signOut, signOutAndRedirectToSignIn, + reopenAccount, resendValidationLink, resetPassword, clearSignInData, diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index a7b71d9fd3b5..d41fcf3ef974 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -50,21 +50,21 @@ function updatePassword(oldPassword, password) { { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, - value: {...CONST.DEFAULT_ACCOUNT_DATA, isLoading: true}, + value: {...CONST.DEFAULT_ACCOUNT_DATA, loading: true}, }, ], successData: [ { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, - value: {isLoading: false}, + value: {loading: false}, }, ], failureData: [ { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, - value: {isLoading: false}, + value: {loading: false}, }, ], }); @@ -177,7 +177,7 @@ function updateNewsletterSubscription(isSubscribed) { * @returns {Promise} */ function setSecondaryLoginAndNavigate(login, password) { - Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, isLoading: true}); + Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, loading: true}); return DeprecatedAPI.User_SecondaryLogin_Send({ email: login, @@ -202,7 +202,7 @@ function setSecondaryLoginAndNavigate(login, password) { Onyx.merge(ONYXKEYS.USER, {error}); }).finally(() => { - Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); }); } @@ -215,7 +215,7 @@ function setSecondaryLoginAndNavigate(login, password) { function validateLogin(accountID, validateCode) { const isLoggedIn = !_.isEmpty(sessionAuthToken); const redirectRoute = isLoggedIn ? ROUTES.getReportRoute(currentlyViewedReportID) : ROUTES.HOME; - Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, isLoading: true}); + Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, loading: true}); DeprecatedAPI.ValidateEmail({ accountID, @@ -236,7 +236,7 @@ function validateLogin(accountID, validateCode) { Onyx.merge(ONYXKEYS.ACCOUNT, {error}); } }).finally(() => { - Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); Navigation.navigate(redirectRoute); }); } diff --git a/src/libs/deprecatedAPI.js b/src/libs/deprecatedAPI.js index 4ec69b3c60a6..ebbff2141e26 100644 --- a/src/libs/deprecatedAPI.js +++ b/src/libs/deprecatedAPI.js @@ -134,6 +134,18 @@ function Get(parameters, shouldUseSecure = false) { return Network.post(commandName, parameters, CONST.NETWORK.METHOD.POST, shouldUseSecure); } +/** + * @param {Object} parameters + * @param {String} parameters.email + * @param {Boolean} parameters.forceNetworkRequest + * @returns {Promise} + */ +function GetAccountStatus(parameters) { + const commandName = 'GetAccountStatus'; + requireParameters(['email'], parameters, commandName); + return Network.post(commandName, parameters); +} + /** * @param {Object} parameters * @param {String} parameters.debtorEmail @@ -329,6 +341,17 @@ function SetPassword(parameters) { return Network.post(commandName, parameters); } +/** + * @param {Object} parameters + * @param {String} parameters.email + * @returns {Promise} + */ +function User_ReopenAccount(parameters) { + const commandName = 'User_ReopenAccount'; + requireParameters(['email'], parameters, commandName); + return Network.post(commandName, parameters); +} + /** * @param {Object} parameters * @param {String} parameters.email @@ -694,6 +717,7 @@ export { DeleteLogin, DeleteBankAccount, Get, + GetAccountStatus, GetStatementPDF, GetIOUReport, GetFullPolicy, @@ -716,6 +740,7 @@ export { UpdatePolicy, User_SignUp, User_IsUsingExpensifyCard, + User_ReopenAccount, User_SecondaryLogin_Send, User_UploadAvatar, User_FixAccount, diff --git a/src/pages/SetPasswordPage.js b/src/pages/SetPasswordPage.js index d3b14a6d3a7c..ec5c3fb3599d 100755 --- a/src/pages/SetPasswordPage.js +++ b/src/pages/SetPasswordPage.js @@ -29,7 +29,7 @@ const propTypes = { error: PropTypes.string, /** Whether a sign on form is loading (being submitted) */ - isLoading: PropTypes.bool, + loading: PropTypes.bool, }), /** The credentials of the logged in person */ @@ -47,6 +47,15 @@ const propTypes = { error: PropTypes.string, }), + /** User signup object */ + userSignUp: PropTypes.shape({ + /** Is Validating Email */ + isValidating: PropTypes.bool, + + /** Auth token used to change password */ + authToken: PropTypes.string, + }), + /** The accountID and validateCode are passed via the URL */ route: validateLinkPropTypes, @@ -61,6 +70,10 @@ const defaultProps = { error: '', authToken: '', }, + userSignUp: { + isValidating: false, + authToken: '', + }, }; class SetPasswordPage extends Component { @@ -78,7 +91,7 @@ class SetPasswordPage extends Component { componentDidMount() { const accountID = lodashGet(this.props.route.params, 'accountID', ''); const validateCode = lodashGet(this.props.route.params, 'validateCode', ''); - if (this.props.credentials.authToken) { + if (this.props.userSignUp.authToken) { return; } Session.validateEmail(accountID, validateCode); @@ -91,15 +104,15 @@ class SetPasswordPage extends Component { const accountID = lodashGet(this.props.route.params, 'accountID', ''); const validateCode = lodashGet(this.props.route.params, 'validateCode', ''); - if (this.props.credentials.authToken) { - Session.changePasswordAndSignIn(this.props.credentials.authToken, this.state.password); + if (this.props.userSignUp.authToken) { + Session.changePasswordAndSignIn(this.props.userSignUp.authToken, this.state.password); } else { Session.setPassword(this.state.password, validateCode, accountID); } } render() { - const buttonText = !this.props.account.validated ? this.props.translate('setPasswordPage.validatingAccount') : this.props.translate('setPasswordPage.setPassword'); + const buttonText = this.props.userSignUp.isValidating ? this.props.translate('setPasswordPage.verifyingAccount') : this.props.translate('setPasswordPage.setPassword'); const sessionError = this.props.session.error && this.props.translate(this.props.session.error); const error = sessionError || this.props.account.error; return ( @@ -120,7 +133,7 @@ class SetPasswordPage extends Component { diff --git a/src/pages/signin/PasswordForm.js b/src/pages/signin/PasswordForm.js index 7936e59f1c74..0801487c8803 100755 --- a/src/pages/signin/PasswordForm.js +++ b/src/pages/signin/PasswordForm.js @@ -26,11 +26,14 @@ const propTypes = { /** The details about the account that the user is signing in with */ account: PropTypes.shape({ + /** Whether or not the account already exists */ + accountExists: PropTypes.bool, + /** Whether or not two factor authentication is required */ requiresTwoFactorAuth: PropTypes.bool, /** Whether or not a sign on form is loading (being submitted) */ - isLoading: PropTypes.bool, + loading: PropTypes.bool, }), ...withLocalizePropTypes, @@ -180,7 +183,7 @@ class PasswordForm extends React.Component { success style={[styles.mv3]} text={this.props.translate('common.signIn')} - isLoading={this.props.account.isLoading} + isLoading={this.props.account.loading} onPress={this.validateAndSubmitForm} /> diff --git a/src/pages/signin/ResendValidationForm.js b/src/pages/signin/ResendValidationForm.js index e643e94eed73..95956cb7b933 100755 --- a/src/pages/signin/ResendValidationForm.js +++ b/src/pages/signin/ResendValidationForm.js @@ -34,6 +34,12 @@ const propTypes = { /** Whether or not the account is validated */ validated: PropTypes.bool, + + /** Whether or not the account is closed */ + closed: PropTypes.bool, + + /** Whether or not the account already exists */ + accountExists: PropTypes.bool, }), /** Information about the network */ @@ -73,7 +79,9 @@ class ResendValidationForm extends React.Component { formSuccess: this.props.translate('resendValidationForm.linkHasBeenResent'), }); - if (!this.props.account.validated) { + if (this.props.account.closed) { + Session.reopenAccount(); + } else if (!this.props.account.validated) { Session.resendValidationLink(); } else { Session.resetPassword(); @@ -85,10 +93,27 @@ class ResendValidationForm extends React.Component { } render() { + const isNewAccount = !this.props.account.accountExists; + const isOldUnvalidatedAccount = this.props.account.accountExists && !this.props.account.validated; 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(); - + let message = ''; + + if (isNewAccount) { + message = this.props.translate('resendValidationForm.newAccount', { + login, + loginType, + }); + } else if (this.props.account.validateCodeExpired) { + message = this.props.translate('resendValidationForm.validationCodeFailedMessage'); + } else if (isOldUnvalidatedAccount) { + message = this.props.translate('resendValidationForm.unvalidatedAccount'); + } else { + message = this.props.translate('resendValidationForm.weSentYouMagicSignInLink', { + login, + }); + } return ( <> @@ -104,7 +129,7 @@ class ResendValidationForm extends React.Component { - {this.props.translate('resendValidationForm.weSentYouMagicSignInLink', {login, loginType})} + {message} {!_.isEmpty(this.state.formSuccess) && ( diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index c19d8ba09fe9..c0d7945e9945 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -4,6 +4,7 @@ import { } from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; +import lodashGet from 'lodash/get'; import ONYXKEYS from '../../ONYXKEYS'; import styles from '../../styles/styles'; import updateUnread from '../../libs/UnreadIndicatorUpdater/updateUnread/index'; @@ -19,11 +20,17 @@ const propTypes = { /** The details about the account that the user is signing in with */ account: PropTypes.shape({ + /** Whether or not the account already exists */ + accountExists: PropTypes.bool, + /** Error to display when there is an account error returned */ error: PropTypes.string, - /** Whether the account is validated */ + /** Whether or not the account is validated */ validated: PropTypes.bool, + + /** Whether or not the account is validated */ + forgotPassword: PropTypes.bool, }), /** The credentials of the person signing in */ @@ -53,21 +60,27 @@ class SignInPage extends Component { // - A login has not been entered yet const showLoginForm = !this.props.credentials.login; + const validateCodeExpired = lodashGet(this.props.account, 'validateCodeExpired', false); + + const validAccount = this.props.account.accountExists + && this.props.account.validated + && !this.props.account.forgotPassword + && !validateCodeExpired; + // Show the password form if // - A login has been entered + // - AND a GitHub username has been entered OR they already have access to New Expensify // - AND an account exists and is validated for this login // - AND a password hasn't been entered yet - // - AND haven't forgotten password const showPasswordForm = this.props.credentials.login - && this.props.account.validated - && !this.props.credentials.password - && !this.props.account.forgotPassword; + && validAccount + && !this.props.credentials.password; // Show the resend validation link form if // - A login has been entered - // - AND is not validated or password is forgotten - const shouldShowResendValidationLinkForm = this.props.credentials.login - && (!this.props.account.validated || this.props.account.forgotPassword); + // - AND a GitHub username has been entered OR they already have access to this app + // - AND an account did not exist or is not validated for that login + const shouldShowResendValidationLinkForm = this.props.credentials.login && !validAccount; const welcomeText = shouldShowResendValidationLinkForm ? '' diff --git a/tests/actions/ReimbursementAccountTest.js b/tests/actions/ReimbursementAccountTest.js index cd0ae97aeda6..0c0b53c25f90 100644 --- a/tests/actions/ReimbursementAccountTest.js +++ b/tests/actions/ReimbursementAccountTest.js @@ -259,30 +259,25 @@ describe('actions/BankAccounts', () => { expect(reimbursementAccount.error).toBe(''); expect(reimbursementAccount.achData.currentStep).toBe(CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT); - HttpUtils.xhr.mockImplementation((command) => { - // WHEN we mock a sucessful call to SetupWithdrawalAccount while on the ACHContractStep - switch (command) { - case 'BankAccount_SetupWithdrawal': - return Promise.resolve({ - jsonCode: 200, - achData: { - bankAccountID: TEST_BANK_ACCOUNT_ID, - }, - }); - - // And mock the response of Get&returnValueList=bankAccountList - case 'Get': - return Promise.resolve({ - jsonCode: 200, - bankAccountList: [{ - bankAccountID: TEST_BANK_ACCOUNT_ID, - state: BankAccount.STATE.PENDING, - }], - }); - default: - return Promise.resolve({jsonCode: 200}); - } - }); + // WHEN we mock a sucessful call to SetupWithdrawalAccount while on the ACHContractStep + HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve({ + jsonCode: 200, + achData: { + bankAccountID: TEST_BANK_ACCOUNT_ID, + }, + })); + + // And mock SetNameValuePair response + HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve({jsonCode: 200})); + + // And mock the response of Get&returnValueList=bankAccountList + HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve({ + jsonCode: 200, + bankAccountList: [{ + bankAccountID: TEST_BANK_ACCOUNT_ID, + state: BankAccount.STATE.PENDING, + }], + })); // WHEN we call setupWithdrawalAccount via the ACHContractStep BankAccounts.setupWithdrawalAccount({ diff --git a/tests/actions/ReportTest.js b/tests/actions/ReportTest.js index 3955959b2d1f..da8c4de756fb 100644 --- a/tests/actions/ReportTest.js +++ b/tests/actions/ReportTest.js @@ -204,6 +204,7 @@ describe('actions/Report', () => { it('should be updated correctly when new comments are added, deleted or marked as unread', () => { const REPORT_ID = 1; + let report; Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js index 0b5ce51ba248..b09b23d49645 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.js @@ -260,7 +260,7 @@ test('Request will not run until credentials are read from Onyx', () => { const spyHttpUtilsXhr = jest.spyOn(HttpUtils, 'xhr').mockImplementation(() => Promise.resolve({})); // When we make a request - Session.beginSignIn(TEST_USER_LOGIN); + Session.fetchAccountDetails(TEST_USER_LOGIN); // Then we should expect that no requests have been made yet expect(spyHttpUtilsXhr).not.toHaveBeenCalled(); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index e6a7dc5b3e97..d619e9d72ab2 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -20,32 +20,27 @@ function signInWithTestUser(accountID = 1, login = 'test@user.com', password = ' const originalXhr = HttpUtils.xhr; HttpUtils.xhr = jest.fn(); HttpUtils.xhr.mockImplementation(() => Promise.resolve({ - onyxData: [ - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.CREDENTIALS, - value: { - login, - }, - }, - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.ACCOUNT, - value: { - validated: true, - }, - }, - ], jsonCode: 200, + accountExists: true, + requiresTwoFactorAuth: false, + normalizedLogin: login, })); // Simulate user entering their login and populating the credentials.login - Session.beginSignIn(login); + Session.fetchAccountDetails(login); return waitForPromisesToResolve() .then(() => { - // Response is the same for calls to Authenticate and CreateLogin + // First call to Authenticate HttpUtils.xhr - .mockImplementation(() => Promise.resolve({ + .mockImplementationOnce(() => Promise.resolve({ + jsonCode: 200, + accountID, + authToken, + email: login, + })) + + // Next call to CreateLogin + .mockImplementationOnce(() => Promise.resolve({ jsonCode: 200, accountID, authToken,