diff --git a/src/App.js b/src/App.js
index e4c8783f2e70..6fedeedafd79 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,4 +1,4 @@
-import Expensify from './Expensify.js';
+import Expensify from './Expensify';
import React from 'react';
export default () => ;
diff --git a/src/CONFIG.js b/src/CONFIG.js
new file mode 100644
index 000000000000..10512520c48f
--- /dev/null
+++ b/src/CONFIG.js
@@ -0,0 +1,15 @@
+// TODO: Figure out how to determine prod/dev on mobile, etc.
+const IS_IN_PRODUCTION = false;
+
+export default {
+ PUSHER: {
+ APP_KEY: IS_IN_PRODUCTION ? '268df511a204fbb60884' : 'ac6d22b891daae55283a',
+ AUTH_URL: IS_IN_PRODUCTION ? 'https://www.expensify.com' : 'https://www.expensify.com.dev',
+ CLUSTER: 'mt1',
+ },
+ EXPENSIFY: {
+ PARTNER_NAME: IS_IN_PRODUCTION ? 'chat-expensify-com' : 'android',
+ PARTNER_PASSWORD: IS_IN_PRODUCTION ? 'e21965746fd75f82bb66' : 'c3a9ac418ea3f152aae2',
+ API_ROOT: IS_IN_PRODUCTION ? 'https://www.expensify.com/api?' : 'https://www.expensify.com.dev/api?',
+ }
+};
diff --git a/src/Expensify.js b/src/Expensify.js
index 638e0a3e62f6..18a219c964d3 100644
--- a/src/Expensify.js
+++ b/src/Expensify.js
@@ -1,10 +1,10 @@
-import {init as StoreInit} from './store/Store.js';
-import SignInPage from './page/SignInPage.js';
-import HomePage from './page/HomePage/HomePage.js';
-import * as Store from './store/Store.js';
-import * as ActiveClientManager from './lib/ActiveClientManager.js';
-import {verifyAuthToken} from './store/actions/SessionActions.js';
-import STOREKEYS from './store/STOREKEYS.js';
+import {init as StoreInit} from './store/Store';
+import SignInPage from './page/SignInPage';
+import HomePage from './page/HomePage/HomePage';
+import * as Store from './store/Store';
+import * as ActiveClientManager from './lib/ActiveClientManager';
+import {verifyAuthToken} from './store/actions/SessionActions';
+import STOREKEYS from './store/STOREKEYS';
import React, {Component} from 'react';
import {Route, Router, Redirect, Switch} from './lib/Router';
diff --git a/src/config.js b/src/config.js
deleted file mode 100644
index 5eddf86f8b56..000000000000
--- a/src/config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const CONFIG = {
- PUSHER: {
- APP_KEY: '829fd8fd2a6036568469',
- CLUSTER: 'us3',
- },
-};
diff --git a/src/lib/ActiveClientManager.js b/src/lib/ActiveClientManager.js
index 83ea0d634072..99f679aae680 100644
--- a/src/lib/ActiveClientManager.js
+++ b/src/lib/ActiveClientManager.js
@@ -1,6 +1,6 @@
-import Guid from './Guid.js';
-import * as Store from '../store/Store.js';
-import STOREKEYS from '../store/STOREKEYS.js';
+import Guid from './Guid';
+import * as Store from '../store/Store';
+import STOREKEYS from '../store/STOREKEYS';
const clientID = Guid();
diff --git a/src/lib/DateUtils.js b/src/lib/DateUtils.js
index b291d96975dd..e3036ddc0dfd 100644
--- a/src/lib/DateUtils.js
+++ b/src/lib/DateUtils.js
@@ -1,5 +1,5 @@
import moment from 'moment';
-import Str from './Str.js';
+import Str from './Str';
// Non-Deprecated Methods
diff --git a/src/lib/ExpensiMark.js b/src/lib/ExpensiMark.js
index f2b9a6a11a7f..ea49c834a25f 100644
--- a/src/lib/ExpensiMark.js
+++ b/src/lib/ExpensiMark.js
@@ -1,4 +1,4 @@
-import Str from './Str.js';
+import Str from './Str';
export default class ExpensiMark {
constructor() {
diff --git a/src/lib/Network.js b/src/lib/Network.js
index 4104f6cdbb34..1a0446220844 100644
--- a/src/lib/Network.js
+++ b/src/lib/Network.js
@@ -1,5 +1,7 @@
import * as $ from 'jquery';
-import * as Store from '../store/Store.js';
+import * as Store from '../store/Store';
+import CONFIG from '../CONFIG';
+import STOREKEYS from '../store/STOREKEYS';
let isAppOffline = false;
@@ -9,27 +11,29 @@ let isAppOffline = false;
* @param {string} command
* @param {mixed} data
* @param {string} [type]
- * @returns {$.Deferred}
+ * @returns {Promise}
*/
-async function request(command, data, type = 'post') {
- console.debug(`Making "${command}" ${type} request`);
- const formData = new FormData();
- formData.append('authToken', await Store.get('session', 'authToken'));
- for (const property in data) {
- formData.append(property, data[property]);
- }
- try {
- let response = await fetch(
- `https://www.expensify.com.dev/api?command=${command}`,
- {
- method: type,
- body: formData,
- },
- );
- return await response.json();
- } catch (error) {
- isAppOffline = true;
- }
+function request(command, data, type = 'post') {
+ return Store.get(STOREKEYS.SESSION, 'authToken')
+ .then((authToken) => {
+ const formData = new FormData();
+ formData.append('authToken', authToken);
+ _.each(data, (val, key) => {
+ formData.append(key, val);
+ });
+ return formData;
+ })
+ .then((formData) => {
+ return fetch(
+ `${CONFIG.EXPENSIFY.API_ROOT}command=${command}`,
+ {
+ method: type,
+ body: formData,
+ },
+ )
+ })
+ .then(response => response.json())
+ .catch(() => isAppOffline = true);
}
// Holds a queue of all the write requests that need to happen
@@ -39,20 +43,18 @@ const delayedWriteQueue = [];
* A method to write data to the API in a delayed fashion that supports the app being offline
*
* @param {string} command
- * @param {midex} data
- * @returns {$.Deferred}
+ * @param {mixed} data
+ * @returns {Promise}
*/
-function delayedWrite(command, data, cb) {
- const promise = $.Deferred();
-
- // Add the write request to a queue of actions to perform
- delayedWriteQueue.push({
- command,
- data,
- promise,
+function delayedWrite(command, data) {
+ return new Promise((resolve) => {
+ // Add the write request to a queue of actions to perform
+ delayedWriteQueue.push({
+ command,
+ data,
+ callback: resolve,
+ });
});
-
- return promise;
}
/**
@@ -61,9 +63,8 @@ function delayedWrite(command, data, cb) {
function processWriteQueue() {
if (isAppOffline) {
// Make a simple request to see if we're online again
- request('Get', null, 'get').done(() => {
- isAppOffline = false;
- });
+ request('Get', null, 'get')
+ .then(() => isAppOffline = false);
return;
}
@@ -71,17 +72,14 @@ function processWriteQueue() {
return;
}
- for (let i = 0; i < delayedWriteQueue.length; i++) {
- // Take the request object out of the queue and make the request
- const delayedWriteRequest = delayedWriteQueue.shift();
-
+ _.each(delayedWriteQueue, (delayedWriteRequest) => {
request(delayedWriteRequest.command, delayedWriteRequest.data)
- .done(delayedWriteRequest.promise.resolve)
- .fail(() => {
+ .then(delayedWriteRequest.callback)
+ .catch(() => {
// If the request failed, we need to put the request object back into the queue
delayedWriteQueue.push(delayedWriteRequest);
});
- }
+ });
}
// TODO: Figure out setInterval
diff --git a/src/lib/PersistentStorage.js b/src/lib/PersistentStorage.js
index 925f61fb3d32..f5df6a87a694 100644
--- a/src/lib/PersistentStorage.js
+++ b/src/lib/PersistentStorage.js
@@ -21,6 +21,27 @@ function get(key) {
});
};
+/**
+ * Get the data for multiple keys
+ *
+ * @param {string[]} keys
+ * @returns {Promise}
+ */
+function multiGet(keys) {
+ // AsyncStorage returns the data in an array format like:
+ // [ ['@MyApp_user', 'myUserValue'], ['@MyApp_key', 'myKeyValue'] ]
+ // This method will transform the data into a better JSON format like:
+ // {'@MyApp_user': 'myUserValue', '@MyApp_key': 'myKeyValue'}
+ return AsyncStorage.multiGet(keys)
+ .then(arrayOfData => _.reduce(arrayOfData, (finalData, keyValuePair) => ({
+ ...finalData,
+ [keyValuePair[0]]: JSON.parse(keyValuePair[1]),
+ }), {}))
+ .catch((err) => {
+ console.error(`Unable to get item from persistent storage. Keys: ${JSON.stringify(keys)} Error: ${err}`);
+ });
+}
+
/**
* Write a key to storage
*
@@ -32,6 +53,24 @@ function set(key, val) {
return AsyncStorage.setItem(key, JSON.stringify(val));
};
+/**
+ * Set multiple keys at once
+ *
+ * @param {object} data where the keys and values will be stored
+ * @returns {Promise|Promise|*}
+ */
+function multiSet(data) {
+ // AsyncStorage expenses the data in an array like:
+ // [["@MyApp_user", "value_1"], ["@MyApp_key", "value_2"]]
+ // This method will transform the params from a better JSON format like:
+ // {'@MyApp_user': 'myUserValue', '@MyApp_key': 'myKeyValue'}
+ const keyValuePairs = _.reduce(data, (finalArray, val, key) => ([
+ ...finalArray,
+ [key, JSON.stringify(val)],
+ ]), []);
+ return AsyncStorage.multiSet(keyValuePairs);
+}
+
/**
* Empty out the storage (like when the user signs out)
*
@@ -41,8 +80,22 @@ function clear() {
return AsyncStorage.clear();
};
+/**
+ * Merges `val` into an existing key. Best used when updating an existing object
+ *
+ * @param {string} key
+ * @param {mixed} val
+ * @returns {Promise}
+ */
+function merge(key, val) {
+ return AsyncStorage.mergeItem(key, val);
+}
+
export {
get,
+ multiGet,
set,
+ multiSet,
+ merge,
clear,
};
diff --git a/src/page/SignInPage.js b/src/page/SignInPage.js
index 90b6427c93cc..96ebbdbd0234 100644
--- a/src/page/SignInPage.js
+++ b/src/page/SignInPage.js
@@ -12,9 +12,9 @@ import {
TextInput,
View,
} from 'react-native';
-import * as Store from '../store/Store.js';
-import {signIn} from '../store/actions/SessionActions.js';
-import STOREKEYS from '../store/STOREKEYS.js';
+import * as Store from '../store/Store';
+import {signIn} from '../store/actions/SessionActions';
+import STOREKEYS from '../store/STOREKEYS';
export default class App extends Component {
constructor(props) {
diff --git a/src/store/Store.js b/src/store/Store.js
index 92c4ee4403ab..3cfe83db3c7a 100644
--- a/src/store/Store.js
+++ b/src/store/Store.js
@@ -1,5 +1,5 @@
-import * as _ from 'lodash';
-import * as PersistentStorage from '../lib/PersistentStorage.js';
+import {get as lodashGet} from 'lodash';
+import * as PersistentStorage from '../lib/PersistentStorage';
// Holds all of the callbacks that have registered for a specific key pattern
const callbackMapping = {};
@@ -54,7 +54,7 @@ function unsubscribe(keyPattern, cb) {
* @param {mixed} data
*/
function keyChanged(key, data) {
- for (const [keyPattern, callbacks] of Object.entries(callbackMapping)) {
+ _.each(callbackMapping, (callbacks, keyPattern) => {
const regex = RegExp(keyPattern);
// If there is a callback whose regex matches the key that was changed, then the callback for that regex
@@ -65,7 +65,7 @@ function keyChanged(key, data) {
callback(data);
}
}
- }
+ });
}
/**
@@ -73,14 +73,15 @@ function keyChanged(key, data) {
*
* @param {string} key
* @param {mixed} val
+ * @returns {Promise}
*/
function set(key, val) {
- // Write the thing to local storage, which will trigger a storage event for any other tabs open on this domain
- PersistentStorage.set(key, val);
-
// The storage event doesn't trigger for the current window, so just call keyChanged() manually to mimic
// the storage event
keyChanged(key, val);
+
+ // Write the thing to persistent storage, which will trigger a storage event for any other tabs open on this domain
+ return PersistentStorage.set(key, val);
}
/**
@@ -92,12 +93,56 @@ function set(key, val) {
* we are looking for doesn't exist in the object yet
* @returns {*}
*/
-const get = async (key, extraPath, defaultValue) => {
- const val = await PersistentStorage.get(key);
- if (extraPath) {
- return _.get(val, extraPath, defaultValue);
- }
- return val;
+function get(key, extraPath, defaultValue) {
+ return PersistentStorage.get(key)
+ .then((val) => {
+ if (extraPath) {
+ return lodashGet(val, extraPath, defaultValue);
+ }
+ return val;
+ });
};
-export {subscribe, unsubscribe, set, get, init};
+/**
+ * Get multiple keys of data
+ *
+ * @param {string[]} keys
+ * @returns {Promise}
+ */
+function multiGet(keys) {
+ return PersistentStorage.multiGet(keys);
+}
+
+/**
+ * Sets multiple keys and values. Example
+ * Store.multiSet({'key1': 'a', 'key2': 'b'});
+ *
+ * @param {object} data
+ * @returns {Promise}
+ */
+function multiSet(data) {
+ return PersistentStorage.multiSet(data)
+ .then(() => {
+ _.each(data, (val, key) => keyChanged(key, val));
+ });
+}
+
+/**
+ * Clear out all the data in the store
+ *
+ * @returns {Promise}
+ */
+function clear() {
+ return PersistentStorage.clear();
+}
+
+export {
+ subscribe,
+ unsubscribe,
+ set,
+ multiSet,
+ get,
+ multiGet,
+ clear,
+ init
+};
diff --git a/src/store/actions/ReportActions.js b/src/store/actions/ReportActions.js
index 49892e782c5e..8e6fb67c106c 100644
--- a/src/store/actions/ReportActions.js
+++ b/src/store/actions/ReportActions.js
@@ -1,119 +1,261 @@
/* globals moment */
-import * as Store from '../Store.js';
-import {request, delayedWrite} from '../../lib/Network.js';
-import STOREKEYS from '../STOREKEYS.js';
-import ExpensiMark from '../../lib/ExpensiMark.js';
-import Guid from '../../lib/Guid.js';
+import _ from 'underscore';
+import * as Store from '../Store';
+import {request, delayedWrite} from '../../lib/Network';
+import STOREKEYS from '../STOREKEYS';
+import ExpensiMark from '../../lib/ExpensiMark';
+import Guid from '../../lib/Guid';
+// import * as pusher from '../../lib/pusher';
/**
- * Get all of our reports
+ * Sorts the report actions so that the newest actions are at the bottom
+ *
+ * @param {object} firstReport
+ * @param {object} secondReport
+ * @returns {number}
*/
-function fetchAll() {
- request('Get', {
- returnValueList: 'reportListBeta',
- sortBy: 'starred',
- offset: 0,
- limit: 10,
- }).done((data) => {
- Store.set(STOREKEYS.REPORTS, data.reportListBeta);
- });
+function sortReportActions(firstReport, secondReport) {
+ return firstReport.sequenceNumber - secondReport.sequenceNumber;
+}
+
+/**
+ * Updates a report in the store with a new report action
+ *
+ * @param {string} reportID
+ * @param {object} reportAction
+ */
+function updateReportWithNewAction(reportID, reportAction) {
+ // Get the comments for this report, and add the comment (being sure to sort and filter properly)
+ let foundExistingReportHistoryItem = false;
+
+ Store.get(`${STOREKEYS.REPORT}_${reportID}_history`)
+
+ // Use a reducer to replace an existing report history item if there is one
+ .then(reportHistory => _.map(reportHistory, (reportHistoryItem) => {
+ // If there is an existing reportHistoryItem, replace it
+ if (reportHistoryItem.sequenceNumber === reportAction.sequenceNumber) {
+ foundExistingReportHistoryItem = true;
+ return reportAction;
+ }
+ return reportHistoryItem;
+ }))
+ .then((reportHistory) => {
+ // If there was no existing history item, add it to the report history and mark the report for having unread
+ // items
+ if (!foundExistingReportHistoryItem) {
+ reportHistory.push(reportAction);
+ Store.merge(`${STOREKEYS.REPORT}_${reportID}`, {hasUnread: true});
+ }
+ return reportHistory;
+ })
+ .then((reportHistory) => {
+ return Store.set(`${STOREKEYS.REPORT}_${reportID}_history`, reportHistory.sort(sortReportActions));
+ });
+}
+
+/**
+ * Checks the report to see if there are any unread history items
+ *
+ * @param {string} accountID
+ * @param {object} report
+ * @returns {boolean}
+ */
+function hasUnreadHistoryItems(accountID, report) {
+ const usersLastReadActionID = report.reportNameValuePairs[`lastReadActionID_${accountID}`];
+ if (!usersLastReadActionID || report.reportActionList.length === 0) {
+ return false;
+ };
+
+ // Find the most recent sequence number from the report history
+ const highestSequenceNumber = _.chain(report.reportActionList)
+ .sort(sortReportActions)
+ .last()
+ .property('sequenceNumber')
+ .value();
+
+ // There are unread items if the last one the user has read is less than the highest sequence number we have
+ return usersLastReadActionID < highestSequenceNumber;
+}
+
+/**
+ * Initialize our pusher subscriptions to listen for new report comments
+ *
+ * @returns {Promise}
+ */
+function initPusher() {
+ return Store.get(STOREKEYS.SESSION, 'accountID')
+ .then((accountID) => {
+ // @TODO: need to implement pusher
+ // return pusher.subscribe(`private-user-accountID-${accountID}`, 'reportComment', (pushJSON) => {
+ // updateReportWithNewAction(pushJSON.reportID, pushJSON.reportAction);
+ // });
+ });
}
/**
* Get a single report
*/
function fetch(reportID) {
- request('Get', {
+ let fetchedReport;
+ return request('Get', {
returnValueList: 'reportStuff',
reportIDList: reportID,
shouldLoadOptionalKeys: true,
- }).done((data) => {
- Store.set(`${STOREKEYS.REPORT}_${reportID}`, data.reports[reportID]);
- });
+ })
+ .then(data => data.reports && data.reports[reportID])
+ .then((report) => {
+ fetchedReport = report;
+ return Store.get(STOREKEYS.SESSION, 'accountID');
+ })
+ .then((accountID) => {
+ // When we fetch a full report, we want to figure out if there are unread comments on it
+ fetchedReport.hasUnread = hasUnreadHistoryItems(accountID, fetchedReport);
+ return Store.set(`${STOREKEYS.REPORT}_${reportID}`, fetchedReport);
+ });
}
/**
- * Get the comments of a report
+ * Get all of our reports
+ *
+ * @returns {Promise}
*/
-function fetchComments(reportID) {
- request('Report_GetHistory', {
- reportID: reportID,
+function fetchAll() {
+ // @TODO Figure out how to tell if we are in production
+ if (IS_IN_PRODUCTION) {
+ return request('Get', {
+ returnValueList: 'reportStuff',
+ reportIDList: '63212778,63212795,63212764,63212607',
+ shouldLoadOptionalKeys: true,
+ })
+ .then((data) => {
+ // Load the full report of each one, it's OK to fire-and-forget these requests
+ _.each(data.reportListBeta, report => fetch(report.reportID));
+ return data;
+ })
+ .then(data => Store.set(STOREKEYS.REPORTS, _.values(data.reports)));
+ }
+
+ return request('Get', {
+ returnValueList: 'reportListBeta',
+ sortBy: 'starred',
offset: 0,
- }).done((data) => {
- const sortedData = data.history.sort(
- (a, b) => a.sequenceNumber - b.sequenceNumber,
- );
- Store.set(`${STOREKEYS.REPORT}_${reportID}_comments`, sortedData);
- });
+ limit: 10,
+ })
+ .then((data) => {
+ // Load the full report of each one, it's OK to fire-and-forget these requests
+ _.each(data.reportListBeta, report => fetch(report.reportID));
+ return data;
+ })
+ .then(data => Store.set(STOREKEYS.REPORTS, _.values(data.reports)));
}
/**
- * Add a comment to a report
+ * Get the history of a report
+ *
+ * @returns {Promise}
+ */
+function fetchHistory(reportID) {
+ return request('Report_GetHistory', {
+ reportID,
+ offset: 0,
+ })
+ .then(data => Store.set(`${STOREKEYS.REPORT}_${reportID}_history`, data.history.sort(sortReportActions)));
+}
+
+/**
+ * Add a history item to a report
+ *
* @param {string} reportID
* @param {string} commentText
+ * @returns {Promise}
*/
-function addComment(reportID, commentText) {
+function addHistoryItem(reportID, commentText) {
const messageParser = new ExpensiMark();
- const comments = Store.get(`${STOREKEYS.REPORT}_${reportID}_comments`);
- const newSequenceNumber =
- comments.length === 0
- ? 1
- : comments[comments.length - 1].sequenceNumber + 1;
const guid = Guid();
+ const historyKey = `${STOREKEYS.REPORT}_${reportID}_history`;
- // Optimistically add the new comment to the store before waiting to save it to the server
- Store.set(`${STOREKEYS.REPORT}_${reportID}_comments`, [
- ...comments,
- {
- tempGuid: guid,
- actionName: 'ADDCOMMENT',
- actorEmail: Store.get(STOREKEYS.SESSION, 'email'),
- person: [
- {
- style: 'strong',
- text: '',
- // text: this.props.userDisplayName,
- type: 'TEXT',
- },
- ],
- automatic: false,
- sequenceNumber: newSequenceNumber,
- avatar: '',
- // avatar: this.props.userAvatar,
- timestamp: moment.unix(),
- message: [
+ Store.multiGet([historyKey, STOREKEYS.SESSION, STOREKEYS.PERSONAL_DETAILS])
+ .then((values) => {
+ const reportHistory = values[historyKey];
+ const email = values[STOREKEYS.SESSION].email || '';
+ const personalDetails = values[STOREKEYS.PERSONAL_DETAILS][email];
+
+ // The new sequence number will be one higher than the highest
+ let highestSequenceNumber = _.chain(reportHistory)
+ .pluck('sequenceNumber')
+ .max()
+ .value() || 0;
+
+ // Optimistically add the new comment to the store before waiting to save it to the server
+ return Store.set(historyKey, [
+ ...reportHistory,
{
- type: 'COMMENT',
- html: messageParser.replace(commentText),
- text: commentText,
- },
- ],
- isFirstItem: false,
- isAttachmentPlaceHolder: false,
- },
- ]);
-
- delayedWrite('Report_AddComment', {
- reportID: reportID,
- reportComment: commentText,
- }).done(() => {
- // When the delayed write is finished, we find the optimistic comment that was added to the store
- // and remove it's tempGuid because we know it's been written to the server
- const comments =
- Store.get(`${STOREKEYS.REPORT}_${reportID}_comments`) || [];
- Store.set(
- `${STOREKEYS.REPORT}_${reportID}_comments`,
- comments.map((comment) => {
- if (comment.tempGuid && comment.tempGuid === guid) {
- return {
- ...comment,
- tempGuid: null,
- };
+ tempGuid: guid,
+ actionName: 'ADDCOMMENT',
+ actorEmail: Store.get(STOREKEYS.SESSION, 'email'),
+ person: [
+ {
+ style: 'strong',
+ text: personalDetails.displayName || email,
+ type: 'TEXT'
+ }
+ ],
+ automatic: false,
+ sequenceNumber: highestSequenceNumber++,
+ avatar: personalDetails.avatarURL,
+ timestamp: moment.unix(),
+ message: [
+ {
+ type: 'COMMENT',
+ html: messageParser.replace(commentText),
+ text: commentText,
+ }
+ ],
+ isFirstItem: false,
+ isAttachmentPlaceHolder: false,
}
- return comment;
- }),
- );
- });
+ ]);
+ })
+ .then(() => {
+ return delayedWrite('Report_AddComment', {
+ reportID,
+ reportComment: commentText,
+ });
+ });
+}
+
+/**
+ * Updates the last read action ID on the report. It optimistically makes the change to the store, and then let's the
+ * network layer handle the delayed write.
+ *
+ * @param {string} accountID
+ * @param {string} reportID
+ * @param {number} sequenceNumber
+ * @returns {Promise}
+ */
+function updateLastReadActionID(accountID, reportID, sequenceNumber) {
+ // Mark the report as not having any unread items
+ return Store.merge(`${STOREKEYS.REPORT}_${reportID}`, {
+ hasUnread: false,
+ reportNameValuePairs: {
+ [`lastReadActionID_${accountID}`]: sequenceNumber,
+ }
+ })
+ .then(() => {
+ // Update the lastReadActionID on the report optimistically
+ return delayedWrite('Report_SetLastReadActionID', {
+ accountID,
+ reportID,
+ sequenceNumber,
+ });
+ });
}
-export {fetchAll, fetch, fetchComments, addComment};
+export {
+ fetchAll,
+ fetch,
+ fetchHistory,
+ addHistoryItem,
+ updateLastReadActionID,
+ initPusher,
+};
diff --git a/src/store/actions/SessionActions.js b/src/store/actions/SessionActions.js
index 3adb4d2ab7bd..9c4dc718a148 100644
--- a/src/store/actions/SessionActions.js
+++ b/src/store/actions/SessionActions.js
@@ -1,16 +1,10 @@
-import * as Store from '../Store.js';
-import {request} from '../../lib/Network.js';
-import ROUTES from '../../ROUTES.js';
-import STOREKEYS from '../STOREKEYS.js';
-import * as PersistentStorage from '../../lib/PersistentStorage.js';
import * as _ from 'lodash';
-
-// TODO: Figure out how to determine prod/dev on mobile, etc.
-const IS_IN_PRODUCTION = false;
-const partnerName = IS_IN_PRODUCTION ? 'chat-expensify-com' : 'android';
-const partnerPassword = IS_IN_PRODUCTION
- ? 'e21965746fd75f82bb66'
- : 'c3a9ac418ea3f152aae2';
+import * as Store from '../Store';
+import {request} from '../../lib/Network';
+import ROUTES from '../../ROUTES';
+import STOREKEYS from '../STOREKEYS';
+import * as PersistentStorage from '../../lib/PersistentStorage';
+import CONFIG from '../../CONFIG';
/**
* Amount of time (in ms) after which an authToken is considered expired.
@@ -21,108 +15,115 @@ const partnerPassword = IS_IN_PRODUCTION
*/
const AUTH_TOKEN_EXPIRATION_TIME = 1000 * 60;
+/**
+ * Create login
+ * @param {string} authToken
+ * @param {string} login
+ * @param {string} password
+ */
+function createLogin(authToken, login, password) {
+ request('CreateLogin', {
+ authToken,
+ partnerName: CONFIG.EXPENSIFY.PARTNER_NAME,
+ partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD,
+ partnerUserID: login,
+ partnerUserSecret: password,
+ }).catch((err) => {
+ Store.set(STOREKEYS.SESSION, {error: err});
+ });
+}
+
/**
* Sign in with the API
+ *
* @param {string} login
* @param {string} password
* @param {boolean} useExpensifyLogin
+ * @returns {Promise}
*/
function signIn(login, password, useExpensifyLogin = false) {
- Store.set(STOREKEYS.CREDENTIALS, {login, password});
- Store.set(STOREKEYS.SESSION, {});
- return request('Authenticate', {
- useExpensifyLogin: useExpensifyLogin,
- partnerName: partnerName,
- partnerPassword: partnerPassword,
- partnerUserID: login,
- partnerUserSecret: password,
+ let authToken;
+ return Store.multiSet({
+ [STOREKEYS.CREDENTIALS]: {login, password},
+ [STOREKEYS.SESSION]: {},
})
+ .then(() => request('Authenticate', {
+ useExpensifyLogin,
+ partnerName: CONFIG.EXPENSIFY.PARTNER_NAME,
+ partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD,
+ partnerUserID: login,
+ partnerUserSecret: password,
+ }))
.then((data) => {
+ authToken = data && data.authToken;
+
// 404 We need to create a login
if (data.jsonCode === 404 && !useExpensifyLogin) {
- signIn(login, password, true).then((expensifyLoginData) => {
- createLogin(expensifyLoginData.authToken, login, password);
- });
- return;
+ return signIn(login, password, true)
+ .then((newAuthToken) => {
+ createLogin(newAuthToken, login, password);
+ });
}
// If we didn't get a 200 response from authenticate, the user needs to sign in again
if (data.jsonCode !== 200) {
- console.warn(
- 'Did not get a 200 from authenticate, going back to sign in page',
- );
- Store.set(STOREKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN);
- return;
+ console.warn('Did not get a 200 from authenticate, going back to sign in page');
+ return Store.set(STOREKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN);
}
- Store.set(STOREKEYS.SESSION, data);
- Store.set(STOREKEYS.APP_REDIRECT_TO, ROUTES.HOME);
- Store.set(STOREKEYS.LAST_AUTHENTICATED, new Date().getTime());
-
- return data;
- })
- .then((data) => {
- Store.set(STOREKEYS.SESSION, data);
- Store.set(STOREKEYS.APP_REDIRECT_TO, ROUTES.HOME);
+ return Store.multiSet({
+ [STOREKEYS.SESSION]: data,
+ [STOREKEYS.APP_REDIRECT_TO]: ROUTES.HOME,
+ [STOREKEYS.LAST_AUTHENTICATED]: new Date().getTime(),
+ });
})
+ .then(() => authToken)
.catch((err) => {
- console.warn(err);
+ console.error(err);
Store.set(STOREKEYS.SESSION, {error: err});
});
}
-/**
- * Create login
- * @param {string} authToken
- * @param {string} login
- * @param {string} password
- */
-function createLogin(authToken, login, password) {
- request('CreateLogin', {
- authToken: authToken,
- partnerName,
- partnerPassword,
- partnerUserID: login,
- partnerUserSecret: password,
- }).catch((err) => {
- Store.set(STOREKEYS.SESSION, {error: err});
- });
-}
-
/**
* Sign out of our application
+ *
+ * @returns {Promise}
*/
async function signOut() {
- Store.set(STOREKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN);
- await PersistentStorage.clear();
+ return Store.set(STOREKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN)
+ .then(Store.clear);
}
/**
* Make sure the authToken we have is OK to use
+ *
+ * @returns {Promise}
*/
async function verifyAuthToken() {
- const lastAuthenticated = await Store.get(STOREKEYS.LAST_AUTHENTICATED);
- const credentials = await Store.get(STOREKEYS.CREDENTIALS);
- const haveCredentials = !_.isNull(credentials);
- const haveExpiredAuthToken =
- lastAuthenticated < new Date().getTime() - AUTH_TOKEN_EXPIRATION_TIME;
+ return Store.multiGet([STOREKEYS.LAST_AUTHENTICATED, STOREKEYS.CREDENTIALS])
+ .then(({last_authenticated, credentials}) => {
+ const haveCredentials = !_.isNull(credentials);
+ const haveExpiredAuthToken = last_authenticated < new Date().getTime() - AUTH_TOKEN_EXPIRATION_TIME;
- if (haveExpiredAuthToken && haveCredentials) {
- console.debug('Invalid auth token: Token has expired.');
- signIn(credentials.login, credentials.password);
- return;
- }
+ if (haveExpiredAuthToken && haveCredentials) {
+ console.debug('Invalid auth token: Token has expired.');
+ return signIn(credentials.login, credentials.password);
+ }
- request('Get', {returnValueList: 'account'}).then((data) => {
- if (data.jsonCode === 200) {
- console.debug('We have valid auth token');
- Store.set(STOREKEYS.SESSION, data);
- return;
- }
+ return request('Get', {returnValueList: 'account'}).then((data) => {
+ if (data.jsonCode === 200) {
+ console.debug('We have valid auth token');
+ return Store.set(STOREKEYS.SESSION, data);
+ }
- // If the auth token is bad and we didn't have credentials saved, we want them to go to the sign in page
- Store.set(STOREKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN);
- });
+ // If the auth token is bad and we didn't have credentials saved, we want them to go to the sign in page
+ return Store.set(STOREKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN);
+ });
+ });
}
-export {signIn, signOut, verifyAuthToken};
+export {
+ signIn,
+ signOut,
+ verifyAuthToken
+};