From a7089bce8f42ac410ebabb1d80780a5ae8776437 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 14 Aug 2020 09:53:01 -0600 Subject: [PATCH 1/5] WIP redirecting to sign in --- src/lib/Network.js | 4 ++-- src/lib/actions/ActionsSession.js | 11 ++++++----- src/lib/actions/ActionsSignInRedirect.js | 15 +++++++++++++++ src/page/HomePage/HomePage.js | 6 ++++++ 4 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 src/lib/actions/ActionsSignInRedirect.js diff --git a/src/lib/Network.js b/src/lib/Network.js index c13b542a4989f..227c20ea787a2 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; @@ -35,7 +35,7 @@ function request(command, data, type = 'post') { // AuthToken expired, go to the sign in page if (responseData.jsonCode === 407) { - return Ion.set(IONKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN); + return redirectToSignIn(); } // eslint-disable-next-line no-console diff --git a/src/lib/actions/ActionsSession.js b/src/lib/actions/ActionsSession.js index 59a874f554785..10f8183ceb1d0 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 @@ -69,8 +70,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 @@ -115,7 +116,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 +145,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 +153,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..3bf1c694da648 --- /dev/null +++ b/src/lib/actions/ActionsSignInRedirect.js @@ -0,0 +1,15 @@ +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.set(IONKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN); +} + +export default redirectToSignIn; diff --git a/src/page/HomePage/HomePage.js b/src/page/HomePage/HomePage.js index 698abcc99fd1b..0c4935e8dbed7 100644 --- a/src/page/HomePage/HomePage.js +++ b/src/page/HomePage/HomePage.js @@ -14,6 +14,7 @@ import Ion from '../../lib/Ion'; import IONKEYS from '../../IONKEYS'; import {initPusher} from '../../lib/actions/ActionsReport'; import * as pusher from '../../lib/Pusher/pusher'; +import redirectToSignIn from '../../lib/actions/ActionsSignInRedirect'; const windowSize = Dimensions.get('window'); const widthBreakPoint = 1000; @@ -46,6 +47,11 @@ export default class App extends React.Component { } }); Dimensions.addEventListener('change', this.onChange); + console.log(1) + setTimeout(() => { + console.log(2) + redirectToSignIn(); + }, 5000); } componentWillUnmount() { From 58f6502aa6bd22e0b8e2d4986ac093ca547c18c2 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 14 Aug 2020 12:51:07 -0600 Subject: [PATCH 2/5] Redirect to sign on with an exitTo param --- src/Expensify.js | 16 ++++++++++++++++ src/IONKEYS.js | 7 ++++--- src/lib/actions/ActionsSession.js | 2 +- src/lib/actions/ActionsSignInRedirect.js | 8 +++++++- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/Expensify.js b/src/Expensify.js index 1dfa69d35d86f..447c8c1d5a34c 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,6 +42,7 @@ 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/actions/ActionsSession.js b/src/lib/actions/ActionsSession.js index 10f8183ceb1d0..ff0ec60e67d52 100644 --- a/src/lib/actions/ActionsSession.js +++ b/src/lib/actions/ActionsSession.js @@ -60,7 +60,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 diff --git a/src/lib/actions/ActionsSignInRedirect.js b/src/lib/actions/ActionsSignInRedirect.js index 3bf1c694da648..5231f318489a9 100644 --- a/src/lib/actions/ActionsSignInRedirect.js +++ b/src/lib/actions/ActionsSignInRedirect.js @@ -9,7 +9,13 @@ import ROUTES from '../../ROUTES'; * @returns {Promise} */ function redirectToSignIn() { - return Ion.set(IONKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN); + 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; From f3180ae30374905e384cc2f190b4f06e3450a03f Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 14 Aug 2020 13:49:21 -0600 Subject: [PATCH 3/5] Redirect to the exitTo after signing in --- src/Expensify.js | 2 +- src/lib/actions/ActionsSession.js | 13 ++++++++----- src/page/SignInPage.js | 22 ++++++++++++++-------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/Expensify.js b/src/Expensify.js index 447c8c1d5a34c..650abb2a9abf7 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -45,7 +45,7 @@ class Expensify extends Component { - + diff --git a/src/lib/actions/ActionsSession.js b/src/lib/actions/ActionsSession.js index ff0ec60e67d52..d5ec7435dc712 100644 --- a/src/lib/actions/ActionsSession.js +++ b/src/lib/actions/ActionsSession.js @@ -28,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 + * @params {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(), }); } @@ -46,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', { @@ -80,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) => { diff --git a/src/page/SignInPage.js b/src/page/SignInPage.js index 6cb25af499f56..244183b6f0bba 100644 --- a/src/page/SignInPage.js +++ b/src/page/SignInPage.js @@ -8,6 +8,7 @@ import { Image, View, } from 'react-native'; +import {withRouter} from '../lib/Router'; import {signIn} from '../lib/actions/ActionsSession'; import IONKEYS from '../IONKEYS'; import WithIon from '../components/WithIon'; @@ -18,6 +19,8 @@ class App extends Component { constructor(props) { super(props); + this.submitForm = this.submitForm.bind(this); + this.state = { login: '', password: '', @@ -25,9 +28,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 +55,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 +65,7 @@ class App extends Component { secureTextEntry value={this.state.password} onChangeText={text => this.setState({password: text})} - onSubmitEditing={() => this.submitLogin()} + onSubmitEditing={this.submitForm} /> @@ -70,13 +76,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 +100,7 @@ class App extends Component { } } -export default WithIon({ +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)); From 05b90be6a332c9602bf6b2f31a8a0beb1f8ee189 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 14 Aug 2020 13:49:47 -0600 Subject: [PATCH 4/5] Refactor network requests so that jsonCodes are (un)handled better --- src/lib/Network.js | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/lib/Network.js b/src/lib/Network.js index 227c20ea787a2..3e45061dce2cb 100644 --- a/src/lib/Network.js +++ b/src/lib/Network.js @@ -26,28 +26,34 @@ 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 redirectToSignIn(); - } - - // 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; }); } From df1cbc6088677116b21839be7ca8af640db604b3 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 14 Aug 2020 13:58:21 -0600 Subject: [PATCH 5/5] lint --- src/lib/Network.js | 1 + src/lib/actions/ActionsSession.js | 2 +- src/page/HomePage/HomePage.js | 6 ------ src/page/SignInPage.js | 9 +++++++++ 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/lib/Network.js b/src/lib/Network.js index 3e45061dce2cb..139e282fc9bde 100644 --- a/src/lib/Network.js +++ b/src/lib/Network.js @@ -26,6 +26,7 @@ function request(command, data, type = 'post') { method: type, body: formData, })) + // This will catch any HTTP network errors (like 404s and such), not to be confused with jsonCode which this // does NOT catch .catch(() => { diff --git a/src/lib/actions/ActionsSession.js b/src/lib/actions/ActionsSession.js index d5ec7435dc712..bd457980cc2d2 100644 --- a/src/lib/actions/ActionsSession.js +++ b/src/lib/actions/ActionsSession.js @@ -30,7 +30,7 @@ function createLogin(authToken, login, password) { * Sets API data in the store when we make a successful "Authenticate"/"CreateLogin" request * * @param {object} data - * @params {string} exitTo + * @param {string} exitTo * @returns {Promise} */ function setSuccessfulSignInData(data, exitTo) { diff --git a/src/page/HomePage/HomePage.js b/src/page/HomePage/HomePage.js index 0c4935e8dbed7..698abcc99fd1b 100644 --- a/src/page/HomePage/HomePage.js +++ b/src/page/HomePage/HomePage.js @@ -14,7 +14,6 @@ import Ion from '../../lib/Ion'; import IONKEYS from '../../IONKEYS'; import {initPusher} from '../../lib/actions/ActionsReport'; import * as pusher from '../../lib/Pusher/pusher'; -import redirectToSignIn from '../../lib/actions/ActionsSignInRedirect'; const windowSize = Dimensions.get('window'); const widthBreakPoint = 1000; @@ -47,11 +46,6 @@ export default class App extends React.Component { } }); Dimensions.addEventListener('change', this.onChange); - console.log(1) - setTimeout(() => { - console.log(2) - redirectToSignIn(); - }, 5000); } componentWillUnmount() { diff --git a/src/page/SignInPage.js b/src/page/SignInPage.js index 244183b6f0bba..f1cff931b6c04 100644 --- a/src/page/SignInPage.js +++ b/src/page/SignInPage.js @@ -8,6 +8,7 @@ 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'; @@ -15,6 +16,12 @@ 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); @@ -100,6 +107,8 @@ class App extends Component { } } +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},