Skip to content
Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
ef4067a
Remove Ion.get() from active client manager
tgolen Sep 4, 2020
f147cfd
Remove Ion.get() from singinredirect action
tgolen Sep 4, 2020
4b08616
Remove Ion.get() from chat switcher
tgolen Sep 4, 2020
0cb7111
Remove Ion.get() from personalDetails
tgolen Sep 4, 2020
1259771
Remove Ion.get() from report actions view
tgolen Sep 4, 2020
f2179d9
Remove the exported method \o/
tgolen Sep 4, 2020
08c231f
Remove Ion.multiGet() from session action
tgolen Sep 4, 2020
ce44cac
Remove Ion.multiGet()
tgolen Sep 4, 2020
035ec30
Fix timezone
tgolen Sep 4, 2020
d800ef0
Remove path and default value
tgolen Sep 5, 2020
f50712a
Move initialization of Ion values into connect()
tgolen Sep 5, 2020
b1e7dde
Remove addAsCollection
tgolen Sep 5, 2020
873c032
Move loaders into the home page
tgolen Sep 5, 2020
89220f1
Removed last loader
tgolen Sep 5, 2020
8dd64ed
Remove code for the loader
tgolen Sep 5, 2020
6ef5535
Add more stuff to the philosophy
tgolen Sep 7, 2020
48dab35
Improve explanation of callback connect
tgolen Sep 7, 2020
fe07f44
Move data flow to the beginning
tgolen Sep 7, 2020
b472695
Wrap code in code block
tgolen Sep 7, 2020
5d79d9f
Add more clarity
tgolen Sep 8, 2020
eec5bda
Add more info about how the app is structured
tgolen Sep 8, 2020
6b431c8
Fix typo
tgolen Sep 8, 2020
25b2a15
Merge branch 'master' into tgolen-remve-get
tgolen Sep 8, 2020
ae35b0d
Combine into a single merge
tgolen Sep 8, 2020
fea2c7b
Update code comments
tgolen Sep 8, 2020
12ad99c
Merge branch 'master' into tgolen-remve-get
tgolen Sep 9, 2020
e398193
Fix value initialization when the key doesn't exist
tgolen Sep 9, 2020
9f51a44
Updates from Carlos' review
tgolen Sep 9, 2020
eae6b7b
Updates to the readme
tgolen Sep 9, 2020
dcae0e8
Remove promises from the code
tgolen Sep 9, 2020
e24e1e5
Remove most of Ion.set() in favor of merge()
tgolen Sep 9, 2020
baf2658
Remove console logs
tgolen Sep 9, 2020
7a40e97
Merge branch 'master' into tgolen-remve-get
tgolen Sep 9, 2020
a4acbd8
Switch to merge
tgolen Sep 9, 2020
37bc9c5
Switch to redirect() in all places
tgolen Sep 9, 2020
a31a966
Fix lint errors
tgolen Sep 9, 2020
eab14a0
Fixes from PR review
tgolen Sep 10, 2020
d513330
Merge branch 'master' into tgolen-remve-get
tgolen Sep 10, 2020
2038817
Fix browser notifications and lint error
tgolen Sep 10, 2020
4f9dadf
Work with null values in the props better
tgolen Sep 10, 2020
5c729fc
Merge branch 'master' into tgolen-remve-get
tgolen Sep 10, 2020
9e2921a
Fix reports
tgolen Sep 10, 2020
3a0770b
Fix null network
tgolen Sep 10, 2020
cd77969
Fix an infinite redirect loop
tgolen Sep 10, 2020
b477dfd
Try to correct styles in the footer
tgolen Sep 10, 2020
affbdf2
Address PR comments
tgolen Sep 10, 2020
ed43ca1
Fix unread indicators
tgolen Sep 10, 2020
76291f2
Merge branch 'master' into tgolen-remve-get
tgolen Sep 10, 2020
887df40
Fix a few more Ion.set()
tgolen Sep 10, 2020
8c4f564
Disconnect from pusher when signing out
tgolen Sep 10, 2020
679351f
Fix notifications
tgolen Sep 10, 2020
b1e127b
Remove unused lib
tgolen Sep 10, 2020
aa194ec
Add exact path for root and redirect to HomePage or SignIn
marcaaron Sep 10, 2020
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
77 changes: 72 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,55 @@

# Philosophy
This application is built with the following principles.
1. **Offline first** - All data that is brought into the app should be stored immediately in Ion which puts the data into persistent storage (eg. localStorage on browser platforms).
1. **UI Binds to Ion** - UI components bind to Ion so that any change to the Ion data is automatically reflected in the component by calling setState() with the changed data.
1. **Actions manage Ion Data** - When the UI needs to request or write data from the server, this is done through Actions exclusively.
1. Actions should never return data, see the first point. Example: if the action is `fetchReports()`, it does not return the reports, `fetchReports()` returns nothing. The action makes an XHR, then puts the data into Ion (using `Ion.set()` or `Ion.merge()`). Any UI that is subscribed to that piece of data in Ion is automatically updated.
1. **Data Flow** - Ideally, this is how data flows through the app:
1. Server pushes data to the disk of any client (Server -> Pusher event -> Action listening to pusher event -> Ion). Currently the code only does this with report comments. Until we make more server changes, this steps is actually done by the client requesting data from the server via XHR and then storing the response in Ion.
1. Disk pushes data to the UI (Ion -> withIon()/connect() -> React component).
1. UI pushes data to people's brains (React component -> device screen).
1. Brain pushes data into UI inputs (Device input -> React component).
1. UI inputs push data to the server (React component -> Action -> XHR to server).
1. Go to 1
1. **Offline first**
- All data that is brought into the app and is necessary to display the app when offline should be stored on disk in persistent storage (eg. localStorage on browser platforms). [AsyncStorage](https://react-native-community.github.io/async-storage/) is a cross-platform abstraction layer that is used to access persistent storage.
- All data that is displayed, comes from persistent storage.
1. **UI Binds to data on disk**
- Ion is a Pub/Sub library to connect the application to the data stored on disk.
- UI components subscribe to Ion (using `withIon()`) and any change to the Ion data is published to the component by calling `setState()` with the changed data.
- Libraries subscribe to Ion (with `Ion.connect()`) and any change to the Ion data is published to the callback callback with the changed data.
Copy link
Contributor

Choose a reason for hiding this comment

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

callback callback

- The UI should never call any Ion methods except for `Ion.connect()`. That is the job of Actions (see next section).
- The UI always triggers an Action when something needs to happen (eg. a person inputs data, the UI triggers an Action with this data).
- The UI should be as flexible as possible when it comes to:
- Incomplete or missing data. Always assume data is incomplete or not there. For example, when a comment is pushed to the client from a pusher event, it's possible that Ion does not have data for that report yet. That's OK. A partial report object is added to Ion for the report key `report_1234 = {reportID: 1234, isUnread: true}`. Then there is code that monitors Ion for reports with incomplete data, and calls `fetchChatReportsByIDs(1234)` to get the full data for that report. The UI should be able to gracefully handle the report object not being complete. In this example, the sidebar wouldn't display any report that doesn't have a report name.
- The order that actions are done in. All actions should be done in parallel instead of sequence.
- Parallel actions are asynchronous methods that don't return promises. Any number of these actions can be called at one time and it doesn't matter what order they happen in or when they complete.
- In-Sequence actions are asynchronous methods that return promises. This is necessary when one asynchronous method depends on the results from a previous asynchronous method. Example: Making an XHR to `command=CreateChatReport` which returns a reportID which is used to call `command=Get&rvl=reportStuff`.
1. **Actions manage Ion Data**
- When data needs to be written to or read from the server, this is done through Actions only.
- Public action methods should never return anything (not data or a promise). This is done to ensure that action methods can be called in parallel with no dependency on other methods (see discussion above).
- Actions should favor using `Ion.merge()` over `Ion.set()` so that other values in an object aren't completely overwritten.
Copy link
Contributor

Choose a reason for hiding this comment

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

Ooh just thought of this. But is there anything wrong with using Ion.merge() on things that are not objects? If not, should we just make Ion.merge() do a Ion.set() first when a key does not exist? Then we could make Ion.set() private and avoid using it entirely and wouldn't need to say anything here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a really good idea. I looked at all the places we use .set() and that should be OK. I'll try it out and make sure there are no issues.

- In general, the operations that happen inside an action should be done in parallel and not in sequence ((eg. don't use the promise of one Ion method to trigger a second Ion method). Ion is built so that every operation is done in parallel and it doesn't matter what order they finish in. XHRs on the other hand need to be handled in sequence with promise chains in order to access and act upon the response.
Copy link
Contributor

Choose a reason for hiding this comment

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

((eg

- If an Action needs to access data stored on disk, use a local variable and `Ion.connect()`
- Data should be optimistically stored on disk whenever possible without waiting for a server response. Example of creating a new optimistic comment:
1. user adds a comment
2. comment is shown in the UI (by mocking the expected response from the server)
3. comment is created in the server
4. server responds
5. UI updates with data from the server

1. **Cross Platform 99.9999%**
1. A feature isn't done until it works on all platforms. Accordingly, don't even bother writing a platform-specific code block because you're just going to need to undo it.
1. If the reason you can't write cross platform code is because there is a bug in ReactNative that is preventing it from working, the correct action is to fix RN and submit a PR upstream -- not to hack around RN bugs with platform-specific code paths.
1. If there is a feature that simply doesn't exist on all platforms and thus doesn't exist in RN, rather than doing if (platform=iOS) { }, instead write a "shim" library that is implemented with NOOPs on the other platforms. For example, rather than injecting platform-specific multi-tab code (which can only work on browsers, because it's the only platform with multiple tabs), write a TabManager class that just is NOOP for non-browser platforms. This encapsulates the platform-specific code into a platform library, rather than sprinkling through the business logic.
1. Put all platform specific code in a dedicated branch, like /platform, and reject any PR that attempts to put platform-specific code anywhere else. This maintains a strict separation between business logic and platform code.
1. Put all platform specific code in dedicated files and folders, like /platform, and reject any PR that attempts to put platform-specific code anywhere else. This maintains a strict separation between business logic and platform code.

# Local development
## Getting started
1. Install `node` & `npm`: `brew install node`
2. Install `watchman`: `brew install watchman`
3. Install dependencies: `npm install`
4. Run `cp .env.example .env` and edit `.env` to have your local config options(for example, we are curretly hardcoding the pinned chat reports IDs with the `REPORT_IDS` config option).

You can use any IDE or code editing tool for developing on any platform. Use your favorite!

## Running the web app 🕸
* To run a **Development Server**: `npm run web`
* To build a **production build**: `npm run build`
Expand Down Expand Up @@ -54,6 +87,40 @@ This application is built with the following principles.
2. This will allow you to attach a debugger in your IDE, React Developer Tools, or your browser.
3. For more information on how to attach a debugger, see [React Native Debugging Documentation](https://reactnative.dev/docs/debugging#chrome-developer-tools)

## Things to know or brush up on before jumping into the code
1. The major difference between React-Native and React are the [components](https://reactnative.dev/docs/components-and-apis) that are used in the `render()` method. Everything else is exactly the same. If you learn React, you've already learned 98% of React-Native.
1. The application uses [React-Router](https://reactrouter.com/native/guides/quick-start) for navigating between parts of the app.
1. [Higher Order Components](https://reactjs.org/docs/higher-order-components.html) are used to connect React components to persistent storage via Ion.

## Structure of the app
These are the main pieces of the application.

### Ion
This is a persistent storage solution wrapped in a Pub/Sub library. In general that means:

- Ion stores and retrieves data from persistent storage
- Data is stored as key/value pairs, where the value can be anything from a single piece of data to a complex object
- Collections of data are usually not stored as a single key (eg. an array with multiple objects), but as individual keys+ID (eg. `report_1234`, `report_4567`, etc.). Store collections as individual keys when a component will bind directly to one of those keys. For example: reports are stored as individual keys because `SidebarLink.js` binds to the individual report keys for each link. However, report actions are stored as an array of objects because nothing binds directly to a single report action.
- Ion allows other code to subscribe to changes in data, and then publishes change events whenever data is changed
- Anything needing to read Ion data needs to:
1. Know what key the data is stored in (for web, you can find this by looking in the JS console > Application > local storage)
2. Subscribe to changes of the data for a particular key or set of keys. React components use `withIon()` and non-React libs use `Ion.connect()`.
3. Get initialized with the current value of that key from persistent storage (Ion does this by calling `setState()` or triggering the `callback` with the values currently on disk as part of the connection process)
- Subscribing to Ion keys is done using a regex pattern. For example, since all reports are stored as individual keys like `report_1234`, then if code needs to know about all the reports (eg. display a list of them in the nav menu), then it would subscribe to the key pattern `report_[0-9]+$`.

### Actions
Actions are responsible for managing what is on disk. This is usually:

- Subscribing to Pusher events to receive data from the server that will get put immediately into Ion
- Making XHRs to request necessary data from the server and then immediately putting that data into Ion
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I am reading this wrong (which is a good thing since we can clarify it right now) - but this seems to contradict the data flow philosophy as explained above.

Should we add some context to this README that explains why this is OK?

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we please address this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you make any suggestions on how to improve this? I am hesitant to make any changes because the conversation is still ongoing and nothing has been settled. If anything, maybe it's best to remove this entirely from the readme for now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure. I would update this part of the readme to reflect how things work right now

https://github.com/Expensify/ReactNativeChat/blob/d5133301ab9baa98aa101283c078bcd5dc4ccd6d/README.md#L5-L6

Server payloads are pushed to the disk of any client (Server -> Pusher event or XHR response -> Actions subscribe to Pusher events or make API requests then handle the payload -> Ion).

- Handling any business logic with input coming from the UI layer

### The UI layer
This layer is solely responsible for:

- Reflecting exactly the data that is in persistent storage by using `withIon()` to bind to Ion data.
- Taking user input and passing it to an action

# Deploying
## Continuous deployment / GitHub workflows
Every PR merged into `master` will kick off the **Create a new version** GitHub workflow defined in `.github/workflows/version.yml`.
Expand Down
12 changes: 8 additions & 4 deletions src/Expensify.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,21 @@ class Expensify extends Component {
}

componentDidMount() {
Ion.connect({key: IONKEYS.SESSION, path: 'authToken', callback: this.removeLoadingState});
Ion.connect({
key: IONKEYS.SESSION,
callback: this.removeLoadingState,
});
}

/**
* When the authToken is updated, the app should remove the loading state and handle the authToken
*
* @param {string} authToken
* @param {object} session
* @param {string} session.authToken
*/
removeLoadingState(authToken) {
removeLoadingState(session) {
this.setState({
authToken,
authToken: session ? session.authToken : null,
isLoading: false,
});
}
Expand Down
33 changes: 2 additions & 31 deletions src/components/withIon.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,25 +67,17 @@ export default function (mapIonToState) {
* Takes a single mapping and binds the state of the component to the store
*
* @param {object} mapping
* @param {string} [mapping.path] a specific path of the store object to map to the state
* @param {mixed} [mapping.defaultValue] Used in conjunction with mapping.path to return if the there is
* nothing at mapping.path
* @param {boolean} [mapping.addAsCollection] rather than setting a single state value, this will add things
* to an array
* @param {string} [mapping.collectionID] the name of the ID property to use for the collection
* @param {string} statePropertyName the name of the state property that Ion will add the data to
* @param {string} [mapping.indexBy] the name of the ID property to use for the collection
* @param {string} [mapping.pathForProps] the statePropertyName can contain the string %DATAFROMPROPS% wich
* will be replaced with data from the props matching this path. That way, the component can connect to an
* Ion key that uses data from this.props.
*
* For example, if a component wants to connect to the Ion key "report_22" and
* "22" comes from this.props.match.params.reportID. The statePropertyName would be set to
* "report_%DATAFROMPROPS%" and pathForProps would be set to "match.params.reportID"
* @param {function} [mapping.loader] a method that will be called after connection to Ion in order to load
* it with data. Typically this will be a method that makes an XHR to load data from the API.
* @param {mixed[]} [mapping.loaderParams] An array of params to be passed to the loader method
* @param {boolean} [mapping.initWithStoredValues] If set to false, then no data will be prefilled into the
* component
* @param {string} statePropertyName the name of the state property that Ion will add the data to
*/
connectMappingToIon(mapping, statePropertyName) {
const ionConnectionConfig = {
Expand All @@ -112,27 +104,6 @@ export default function (mapIonToState) {
connectionID = Ion.connect(ionConnectionConfig);
this.actionConnectionIDs[connectionID] = connectionID;
}

// Pre-fill the state with any data already in the store
if (mapping.initWithStoredValues !== false) {
Ion.getInitialStateFromConnectionID(connectionID)
.then(data => this.setState({[statePropertyName]: data}));
}

// Load the data from an API request if necessary
if (mapping.loader) {
const paramsForLoaderFunction = _.map(mapping.loaderParams, (loaderParam) => {
// Some params might com from the props data
if (loaderParam === '%DATAFROMPROPS%') {
return lodashGet(this.props, mapping.pathForProps);
}
return loaderParam;
});

// Call the loader function and pass it any params. The loader function will take care of putting
// data into Ion
mapping.loader(...paramsForLoaderFunction || []);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

👏👏👏

}

render() {
Expand Down
44 changes: 25 additions & 19 deletions src/lib/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ROUTES from '../ROUTES';
import Str from './Str';
import Guid from './Guid';
import redirectToSignIn from './actions/SignInRedirect';
import {redirect} from './actions/App';

// Holds all of the callbacks that need to be triggered when the network reconnects
const reconnectionCallbacks = [];
Expand All @@ -21,35 +22,44 @@ let networkRequestQueue = [];
let reauthenticating = false;

let authToken;
Ion.connect({key: IONKEYS.SESSION, path: 'authToken', callback: val => authToken = val});
Ion.connect({
key: IONKEYS.SESSION,
callback: val => authToken = val ? val.authToken : null,
});

// We susbcribe to changes to the online/offline status of the network to determine when we should fire off API calls
// vs queueing them for later. When going reconnecting, ie, going from offline to online, we fire off all the API calls
// that we have in the queue
// We subscribe to changes to the online/offline status of the network to determine when we should fire off API calls
// vs queueing them for later. When reconnecting, ie, going from offline to online, all the reconnection callbacks
// are triggered (this is usually Actions that need to re-download data from the server)
let isOffline;
Ion.connect({
key: IONKEYS.NETWORK,
path: 'isOffline',
callback: (isCurrentlyOffline) => {
if (isOffline && !isCurrentlyOffline) {
callback: (val) => {
if (isOffline && !val.isOffline) {
_.each(reconnectionCallbacks, callback => callback());
}
isOffline = isCurrentlyOffline;
isOffline = val && val.isOffline;
}
});

// When the user authenticates for the first time we create a login and store credentials in Ion.
// When the user's authToken expires we use this login to re-authenticate and get a new authToken
// and use that new authToken in subsequent API calls
let credentials;
Ion.connect({key: IONKEYS.CREDENTIALS, callback: ionCredentials => credentials = ionCredentials});
Ion.connect({
key: IONKEYS.CREDENTIALS,
callback: ionCredentials => credentials = ionCredentials,
});

/**
* @param {string} login
* @param {string} password
* @returns {Promise}
*/
function createLogin(login, password) {
if (!authToken) {
throw new Error('createLogin() can\'t be called when there is no authToken');
}

// Using xhr instead of request becasue request has logic to re-try API commands when we get a 407 authToken expired
// in the response, and we call CreateLogin after getting a successful resposne to Authenticate so it's unlikely
// that we'll get a 407.
Expand All @@ -60,7 +70,7 @@ function createLogin(login, password) {
partnerUserID: login,
partnerUserSecret: password,
})
.then(() => Ion.set(IONKEYS.CREDENTIALS, {login, password}))
.then(() => Ion.merge(IONKEYS.CREDENTIALS, {login, password}))
.catch(err => Ion.merge(IONKEYS.SESSION, {error: err}));
}

Expand Down Expand Up @@ -91,7 +101,6 @@ function queueRequest(command, data) {
*
* @param {object} data
* @param {string} exitTo
* @returns {Promise}
*/
function setSuccessfulSignInData(data, exitTo) {
let redirectTo;
Expand All @@ -103,12 +112,8 @@ function setSuccessfulSignInData(data, exitTo) {
} else {
redirectTo = ROUTES.HOME;
}
return Ion.multiSet({
// The response from Authenticate includes requestID, jsonCode, etc
// but we only care about setting these three values in Ion
[IONKEYS.SESSION]: _.pick(data, 'authToken', 'accountID', 'email'),
[IONKEYS.APP_REDIRECT_TO]: redirectTo,
});
redirect(redirectTo);
Ion.merge(IONKEYS.SESSION, _.pick(data, 'authToken', 'accountID', 'email'));
}

/**
Expand Down Expand Up @@ -148,7 +153,8 @@ function request(command, parameters, type = 'post') {

// We need to return the promise from setSuccessfulSignInData to ensure the authToken is updated before
// we try to create a login below
return setSuccessfulSignInData(response, parameters.exitTo);
setSuccessfulSignInData(response, parameters.exitTo);
authToken = response.authToken;
Comment on lines 154 to +157
Copy link
Contributor

Choose a reason for hiding this comment

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

We should revert this change (or remove the comment, and make sure we know exactly why this works but the previous version didn't)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, I can do that, however, considering that the previous version wasn't what we wanted, I don't think there is a whole lot of value in that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, I just quickly looked at it and I can tell right away why the return wasn't working. It's because setSuccessfulSignInData() was not returning the promise from Ion.merge() at the end of it, so the race condition still existed where the authToken was being accessed before it was set in Ion.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good catch, the previous version wasn't what we wanted because it was relying on the return value of a promise right? If that's the case I agree. However, I'd also argue this isn't what we want, because we're now updating the local auth token in two places, here and the Ion connection, do you agree?

If so, what's the best way to solve this? maybe put something in ion that dictates we're ready to continue that flow, and use an Ion connection to that something to make the calls that we're making here when the value is updated

Copy link
Contributor Author

Choose a reason for hiding this comment

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

because we're now updating the local auth token in two places

Yeah, I understand what you're saying. I don't think it's really the most ideal, no. If all code started updating these local variables whenever they want, then you could end up with bugs where the local variable is not the same as what is (or will be) in Ion.

I think I keep going back to the best way to solve it is what I had posted in Slack:

start Ion off with:

SESSION: {
    authToken: null,
    login: null,
    password: null,
}

Then connect to Ion:

Ion.connect({key: SESSION, callback: (val) => {
    if (!val.authToken) {
        // Need to authenticate()
        Ion.set('authenticating', true);
    } else if (!val.login) {
        // Need to create()
        Ion.set('authenticating', false);
    }
}});
let authenticating;
Ion.connect({key: 'authenticating', callback: (isCurrentlyAuthenticating) => {
    // When the app is no longer authenticating
    if (authenticating && !isCurrentlyAuthenticating) {
        // Restart the network queue
    }
}});

Then:

  • 407
  • Add request to network queue
  • Ion.set(SESSION.AUTHTOKEN, null) (and at this point all the steps to fill out a complete session are automatically handled)

This is a more ideal solution because essentially EVERYTHING that needs to happen is triggered off of Ion events once the data in Ion has been updated.

Copy link
Contributor

Choose a reason for hiding this comment

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

Perfect, I can do that here #454

})
.then(() => {
// If Expensify login, it's the users first time signing in and we need to
Expand Down Expand Up @@ -355,7 +361,7 @@ function authenticate(parameters) {
.catch((err) => {
console.error(err);
console.debug('[SIGNIN] Request error');
return Ion.merge(IONKEYS.SESSION, {error: err.message});
Ion.merge(IONKEYS.SESSION, {error: err.message});
});
}

Expand Down
Loading