diff --git a/src/Expensify.js b/src/Expensify.js index 1dfa69d35d86f..650abb2a9abf7 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -19,6 +19,21 @@ import { Ion.init(); class Expensify extends Component { + constructor(props) { + super(props); + + this.recordCurrentRoute = this.recordCurrentRoute.bind(this); + } + + /** + * Keep the current route match stored in Ion so other libs can access it + * + * @param {object} params.match + */ + recordCurrentRoute({match}) { + Ion.set(IONKEYS.CURRENT_URL, match.url); + } + render() { return ( @@ -27,9 +42,10 @@ class Expensify extends Component { {/* If there is ever a property for redirecting, we do the redirect here */} {this.state && this.state.redirectTo && } + - + diff --git a/src/IONKEYS.js b/src/IONKEYS.js index 34264d3e0d6e8..ee6a7459dd2c0 100644 --- a/src/IONKEYS.js +++ b/src/IONKEYS.js @@ -4,13 +4,14 @@ export default { ACTIVE_CLIENT_IDS: 'activeClientIDs', APP_REDIRECT_TO: 'app_redirectTo', + CURRENT_URL: 'current_url', CREDENTIALS: 'credentials', + LAST_AUTHENTICATED: 'last_authenticated', + MY_PERSONAL_DETAILS: 'my_personal_details', + PERSONAL_DETAILS: 'personal_details', REPORT: 'report', REPORT_HISTORY: 'report_history', REPORT_ACTION: 'reportAction', REPORTS: 'reports', SESSION: 'session', - LAST_AUTHENTICATED: 'last_authenticated', - PERSONAL_DETAILS: 'personal_details', - MY_PERSONAL_DETAILS: 'my_personal_details', }; diff --git a/src/lib/Network.js b/src/lib/Network.js index c13b542a4989f..139e282fc9bde 100644 --- a/src/lib/Network.js +++ b/src/lib/Network.js @@ -2,7 +2,7 @@ import _ from 'underscore'; import Ion from './Ion'; import CONFIG from '../CONFIG'; import IONKEYS from '../IONKEYS'; -import ROUTES from '../ROUTES'; +import redirectToSignIn from './actions/ActionsSignInRedirect'; let isAppOffline = false; @@ -26,28 +26,35 @@ function request(command, data, type = 'post') { method: type, body: formData, })) - .then(response => response.json()) - .then((responseData) => { - // Successful request - if (responseData.jsonCode === 200) { - return responseData; - } - // AuthToken expired, go to the sign in page - if (responseData.jsonCode === 407) { - return Ion.set(IONKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN); - } - - // eslint-disable-next-line no-console - console.info('[API] UnhandledError', responseData); - }) - // eslint-disable-next-line no-unused-vars + // This will catch any HTTP network errors (like 404s and such), not to be confused with jsonCode which this + // does NOT catch .catch(() => { isAppOffline = true; // Throw a new error to prevent any other `then()` in the promise chain from being triggered (until another // catch() happens throw new Error('API is offline'); + }) + + // Convert the data into JSON + .then(response => response.json()) + + // Handle any of our jsonCodes + .then((responseData) => { + if (responseData) { + // AuthToken expired, go to the sign in page + if (responseData.jsonCode === 407) { + redirectToSignIn(); + throw new Error('[API] Auth token expired'); + } + + if (responseData.jsonCode !== 200) { + throw new Error(responseData.message); + } + } + + return responseData; }); } diff --git a/src/lib/actions/ActionsSession.js b/src/lib/actions/ActionsSession.js index 59a874f554785..bd457980cc2d2 100644 --- a/src/lib/actions/ActionsSession.js +++ b/src/lib/actions/ActionsSession.js @@ -6,6 +6,7 @@ import IONKEYS from '../../IONKEYS'; import CONFIG from '../../CONFIG'; import Str from '../Str'; import Guid from '../Guid'; +import redirectToSignIn from './ActionsSignInRedirect'; /** * Create login @@ -27,13 +28,15 @@ function createLogin(authToken, login, password) { /** * Sets API data in the store when we make a successful "Authenticate"/"CreateLogin" request + * * @param {object} data + * @param {string} exitTo * @returns {Promise} */ -function setSuccessfulSignInData(data) { +function setSuccessfulSignInData(data, exitTo) { return Ion.multiSet({ [IONKEYS.SESSION]: data, - [IONKEYS.APP_REDIRECT_TO]: ROUTES.HOME, + [IONKEYS.APP_REDIRECT_TO]: `/${exitTo}` || ROUTES.HOME, [IONKEYS.LAST_AUTHENTICATED]: new Date().getTime(), }); } @@ -45,9 +48,10 @@ function setSuccessfulSignInData(data) { * @param {string} password * @param {string} twoFactorAuthCode * @param {boolean} useExpensifyLogin + * @param {string} exitTo * @returns {Promise} */ -function signIn(login, password, twoFactorAuthCode = '', useExpensifyLogin = false) { +function signIn(login, password, twoFactorAuthCode = '', useExpensifyLogin = false, exitTo) { console.debug('[SIGNIN] Authenticating with expensify login?', useExpensifyLogin ? 'yes' : 'no'); let authToken; return request('Authenticate', { @@ -59,7 +63,7 @@ function signIn(login, password, twoFactorAuthCode = '', useExpensifyLogin = fal twoFactorAuthCode }) .then((data) => { - console.debug('[SIGNIN] Authentication result. Code:', data.jsonCode); + console.debug('[SIGNIN] Authentication result. Code:', data && data.jsonCode); authToken = data && data.authToken; // If we didn't get a 200 response from authenticate, the user needs to sign in again @@ -69,8 +73,8 @@ function signIn(login, password, twoFactorAuthCode = '', useExpensifyLogin = fal return Ion.multiSet({ [IONKEYS.CREDENTIALS]: {}, [IONKEYS.SESSION]: {error: data.message}, - [IONKEYS.APP_REDIRECT_TO]: ROUTES.SIGNIN, - }); + }) + .then(redirectToSignIn); } // If Expensify login, it's the users first time logging in and we need to create a login for the user @@ -79,12 +83,12 @@ function signIn(login, password, twoFactorAuthCode = '', useExpensifyLogin = fal return createLogin(data.authToken, Str.generateDeviceLoginID(), Guid()) .then(() => { console.debug('[SIGNIN] Successful sign in', 2); - return setSuccessfulSignInData(data); + return setSuccessfulSignInData(data, exitTo); }); } console.debug('[SIGNIN] Successful sign in', 1); - return setSuccessfulSignInData(data); + return setSuccessfulSignInData(data, exitTo); }) .then(() => authToken) .catch((err) => { @@ -115,7 +119,7 @@ function deleteLogin(authToken, login) { * @returns {Promise} */ function signOut() { - return Ion.set(IONKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN) + return redirectToSignIn() .then(() => Ion.multiGet([IONKEYS.SESSION, IONKEYS.CREDENTIALS])) .then(data => deleteLogin(data.session.authToken, data.credentials.login)) .then(Ion.clear) @@ -144,7 +148,7 @@ function verifyAuthToken() { } // If the auth token is bad and we didn't have credentials saved, we want them to go to the sign in page - return Ion.set(IONKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN); + return redirectToSignIn(); }); }); } @@ -152,5 +156,5 @@ function verifyAuthToken() { export { signIn, signOut, - verifyAuthToken + verifyAuthToken, }; diff --git a/src/lib/actions/ActionsSignInRedirect.js b/src/lib/actions/ActionsSignInRedirect.js new file mode 100644 index 0000000000000..5231f318489a9 --- /dev/null +++ b/src/lib/actions/ActionsSignInRedirect.js @@ -0,0 +1,21 @@ +import Ion from '../Ion'; +import IONKEYS from '../../IONKEYS'; +import ROUTES from '../../ROUTES'; + +/** + * Redirects to the sign in page and handles adding any exitTo params to the URL. + * Normally this method would live in ActionsSession.js, but that would cause a circular dependency with Network.js. + * + * @returns {Promise} + */ +function redirectToSignIn() { + return Ion.get(IONKEYS.CURRENT_URL) + .then((url) => { + const urlWithExitTo = url !== '/' + ? `${ROUTES.SIGNIN}/exitTo${url}` + : ROUTES.SIGNIN; + return Ion.set(IONKEYS.APP_REDIRECT_TO, urlWithExitTo); + }); +} + +export default redirectToSignIn; diff --git a/src/page/SignInPage.js b/src/page/SignInPage.js index 6cb25af499f56..f1cff931b6c04 100644 --- a/src/page/SignInPage.js +++ b/src/page/SignInPage.js @@ -8,16 +8,26 @@ import { Image, View, } from 'react-native'; +import PropTypes from 'prop-types'; +import {withRouter} from '../lib/Router'; import {signIn} from '../lib/actions/ActionsSession'; import IONKEYS from '../IONKEYS'; import WithIon from '../components/WithIon'; import styles from '../style/StyleSheet'; import logo from '../../assets/images/expensify-logo_reversed.png'; +const propTypes = { + // These are from withRouter + // eslint-disable-next-line react/forbid-prop-types + match: PropTypes.object.isRequired, +}; + class App extends Component { constructor(props) { super(props); + this.submitForm = this.submitForm.bind(this); + this.state = { login: '', password: '', @@ -25,9 +35,12 @@ class App extends Component { }; } - submitLogin() { + /** + * Sign into the application when the form is submitted + */ + submitForm() { signIn(this.state.login, this.state.password, - this.state.twoFactorAuthCode, true); + this.state.twoFactorAuthCode, true, this.props.match.params.exitTo); } render() { @@ -49,7 +62,7 @@ class App extends Component { style={[styles.textInput, styles.textInputReversed]} value={this.state.login} onChangeText={text => this.setState({login: text})} - onSubmitEditing={() => this.submitLogin()} + onSubmitEditing={this.submitForm} /> @@ -59,7 +72,7 @@ class App extends Component { secureTextEntry value={this.state.password} onChangeText={text => this.setState({password: text})} - onSubmitEditing={() => this.submitLogin()} + onSubmitEditing={this.submitForm} /> @@ -70,13 +83,13 @@ class App extends Component { placeholder="Required when 2FA is enabled" placeholderTextColor="#C6C9CA" onChangeText={text => this.setState({twoFactorAuthCode: text})} - onSubmitEditing={() => this.submitLogin()} + onSubmitEditing={this.submitForm} /> this.submitLogin()} + onPress={this.submitForm} underlayColor="#fff" > Log In @@ -94,7 +107,9 @@ class App extends Component { } } -export default WithIon({ +App.propTypes = propTypes; + +export default withRouter(WithIon({ // Bind this.state.error to the error in the session object error: {key: IONKEYS.SESSION, path: 'error', defaultValue: null}, -})(App); +})(App));