Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/Expensify.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (

Expand All @@ -27,9 +42,10 @@ class Expensify extends Component {
<Router>
{/* If there is ever a property for redirecting, we do the redirect here */}
{this.state && this.state.redirectTo && <Redirect to={this.state.redirectTo} />}
<Route path="*" render={this.recordCurrentRoute} />

<Switch>
<Route path="/signin" component={SignInPage} />
<Route path={['/signin/exitTo/:exitTo*', '/signin']} component={SignInPage} />
<Route path="/" component={HomePage} />
</Switch>
</Router>
Expand Down
7 changes: 4 additions & 3 deletions src/IONKEYS.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
39 changes: 23 additions & 16 deletions src/lib/Network.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does catch 404s or not? The way I read the comment is it does catch 404s and I thought it only caught errors when the fetch actually fails to make the request?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it catches 404s and any network layer failure. It does NOT catch jsonCode errors. Any suggestions to make the comment better?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok I didn't realize it caught 404s, so this is good!

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

Expand Down
26 changes: 15 additions & 11 deletions src/lib/actions/ActionsSession.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(),
});
}
Expand All @@ -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', {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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) => {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -144,13 +148,13 @@ 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();
});
});
}

export {
signIn,
signOut,
verifyAuthToken
verifyAuthToken,
};
21 changes: 21 additions & 0 deletions src/lib/actions/ActionsSignInRedirect.js
Original file line number Diff line number Diff line change
@@ -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;
31 changes: 23 additions & 8 deletions src/page/SignInPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,39 @@ 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: '',
twoFactorAuthCode: '',
};
}

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() {
Expand All @@ -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}
/>
</View>
<View style={[styles.mb4]}>
Expand All @@ -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}
/>
</View>
<View style={[styles.mb4]}>
Expand All @@ -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}
/>
</View>
<View>
<TouchableOpacity
style={[styles.button, styles.buttonSuccess, styles.mb4]}
onPress={() => this.submitLogin()}
onPress={this.submitForm}
underlayColor="#fff"
>
<Text style={[styles.buttonText, styles.buttonSuccessText]}>Log In</Text>
Expand All @@ -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));