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 +};