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));