From ad9330e0fa366b0c4926703df2f4972f15ecbd02 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 8 Aug 2020 10:23:57 -0600 Subject: [PATCH 01/11] Switch store to return promises --- src/store/Store.js | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/store/Store.js b/src/store/Store.js index 92c4ee4403ab..d690982a7ed4 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 * 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,23 @@ 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); +const get = (key, extraPath, defaultValue) => { + return PersistentStorage.get(key) + .then(val => { + if (extraPath) { + return _.get(val, extraPath, defaultValue); + } + return val; + }); if (extraPath) { return _.get(val, extraPath, defaultValue); } - return val; }; -export {subscribe, unsubscribe, set, get, init}; +export { + subscribe, + unsubscribe, + set, + get, + init +}; From fb3278904b5c7a237c47316cc81704e4f2dcb790 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 8 Aug 2020 10:25:57 -0600 Subject: [PATCH 02/11] Swith signin to be more async --- src/store/actions/SessionActions.js | 104 ++++++++++++++-------------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/src/store/actions/SessionActions.js b/src/store/actions/SessionActions.js index 3adb4d2ab7bd..1199d385a994 100644 --- a/src/store/actions/SessionActions.js +++ b/src/store/actions/SessionActions.js @@ -21,56 +21,6 @@ const partnerPassword = IS_IN_PRODUCTION */ const AUTH_TOKEN_EXPIRATION_TIME = 1000 * 60; -/** - * Sign in with the API - * @param {string} login - * @param {string} password - * @param {boolean} useExpensifyLogin - */ -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, - }) - .then((data) => { - // 404 We need to create a login - if (data.jsonCode === 404 && !useExpensifyLogin) { - signIn(login, password, true).then((expensifyLoginData) => { - createLogin(expensifyLoginData.authToken, login, password); - }); - return; - } - - // 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; - } - - 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); - }) - .catch((err) => { - console.warn(err); - Store.set(STOREKEYS.SESSION, {error: err}); - }); -} - /** * Create login * @param {string} authToken @@ -79,7 +29,7 @@ function signIn(login, password, useExpensifyLogin = false) { */ function createLogin(authToken, login, password) { request('CreateLogin', { - authToken: authToken, + authToken, partnerName, partnerPassword, partnerUserID: login, @@ -89,6 +39,58 @@ function createLogin(authToken, login, password) { }); } +/** + * Sign in with the API + * @param {string} login + * @param {string} password + * @param {boolean} useExpensifyLogin + */ +function signIn(login, password, useExpensifyLogin = false) { + Store.set(STOREKEYS.CREDENTIALS, {login, password}); + Store.set(STOREKEYS.SESSION, {}) + .then(() => { + return request('Authenticate', { + useExpensifyLogin: useExpensifyLogin, + partnerName: partnerName, + partnerPassword: partnerPassword, + partnerUserID: login, + partnerUserSecret: password, + }) + .then((data) => { + // 404 We need to create a login + if (data.jsonCode === 404 && !useExpensifyLogin) { + signIn(login, password, true).then((expensifyLoginData) => { + createLogin(expensifyLoginData.authToken, login, password); + }); + return; + } + + // 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; + } + + 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); + }) + .catch((err) => { + console.warn(err); + Store.set(STOREKEYS.SESSION, {error: err}); + }); + }); +} + /** * Sign out of our application */ From 16510157dc9b4fd03b030f1e19bd9490b8dee5d0 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 8 Aug 2020 10:26:25 -0600 Subject: [PATCH 03/11] Remove JS extensions in import statements --- src/Expensify.js | 14 +++++++------- src/lib/ActiveClientManager.js | 6 +++--- src/page/SignInPage.js | 6 +++--- src/store/actions/ReportActions.js | 10 +++++----- 4 files changed, 18 insertions(+), 18 deletions(-) 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/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/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/actions/ReportActions.js b/src/store/actions/ReportActions.js index 49892e782c5e..4007eb5e3df8 100644 --- a/src/store/actions/ReportActions.js +++ b/src/store/actions/ReportActions.js @@ -1,9 +1,9 @@ /* 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 * as Store from '../Store'; +import {request, delayedWrite} from '../../lib/Network'; +import STOREKEYS from '../STOREKEYS'; +import ExpensiMark from '../../lib/ExpensiMark'; +import Guid from '../../lib/Guid'; /** * Get all of our reports From e14b28f42e473386a45951b121ce6debcaafedee Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 8 Aug 2020 10:26:46 -0600 Subject: [PATCH 04/11] remove js extensions in import statements --- src/App.js | 2 +- src/lib/DateUtils.js | 2 +- src/lib/ExpensiMark.js | 2 +- src/lib/Network.js | 2 +- src/store/actions/SessionActions.js | 10 +++++----- 5 files changed, 9 insertions(+), 9 deletions(-) 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/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..5b8c63d297d2 100644 --- a/src/lib/Network.js +++ b/src/lib/Network.js @@ -1,5 +1,5 @@ import * as $ from 'jquery'; -import * as Store from '../store/Store.js'; +import * as Store from '../store/Store'; let isAppOffline = false; diff --git a/src/store/actions/SessionActions.js b/src/store/actions/SessionActions.js index 1199d385a994..49825cce575b 100644 --- a/src/store/actions/SessionActions.js +++ b/src/store/actions/SessionActions.js @@ -1,8 +1,8 @@ -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 Store from '../Store'; +import {request} from '../../lib/Network'; +import ROUTES from '../../ROUTES'; +import STOREKEYS from '../STOREKEYS'; +import * as PersistentStorage from '../../lib/PersistentStorage'; import * as _ from 'lodash'; // TODO: Figure out how to determine prod/dev on mobile, etc. From 0d2a3f9746c8c4ad4cc8b1576b612c505c235260 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 8 Aug 2020 10:33:40 -0600 Subject: [PATCH 05/11] Move config stuff to config file --- src/CONFIG.js | 14 ++++++++++++++ src/config.js | 6 ------ src/lib/Network.js | 3 ++- src/store/actions/SessionActions.js | 20 +++++++------------- 4 files changed, 23 insertions(+), 20 deletions(-) create mode 100644 src/CONFIG.js delete mode 100644 src/config.js diff --git a/src/CONFIG.js b/src/CONFIG.js new file mode 100644 index 000000000000..2159e49de0c9 --- /dev/null +++ b/src/CONFIG.js @@ -0,0 +1,14 @@ +// TODO: Figure out how to determine prod/dev on mobile, etc. +const IS_IN_PRODUCTION = false; + +export default { + PUSHER: { + APP_KEY: '829fd8fd2a6036568469', + CLUSTER: 'us3', + }, + 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/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/Network.js b/src/lib/Network.js index 5b8c63d297d2..a3f8036d7fda 100644 --- a/src/lib/Network.js +++ b/src/lib/Network.js @@ -1,5 +1,6 @@ import * as $ from 'jquery'; import * as Store from '../store/Store'; +import CONFIG from '../CONFIG'; let isAppOffline = false; @@ -20,7 +21,7 @@ async function request(command, data, type = 'post') { } try { let response = await fetch( - `https://www.expensify.com.dev/api?command=${command}`, + `${CONFIG.EXPENSIFY.API_ROOT}command=${command}`, { method: type, body: formData, diff --git a/src/store/actions/SessionActions.js b/src/store/actions/SessionActions.js index 49825cce575b..67dcf65b3f4c 100644 --- a/src/store/actions/SessionActions.js +++ b/src/store/actions/SessionActions.js @@ -1,16 +1,10 @@ +import * as _ from 'lodash'; 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 * 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 CONFIG from '../../CONFIG'; /** * Amount of time (in ms) after which an authToken is considered expired. @@ -30,8 +24,8 @@ const AUTH_TOKEN_EXPIRATION_TIME = 1000 * 60; function createLogin(authToken, login, password) { request('CreateLogin', { authToken, - partnerName, - partnerPassword, + partnerName: CONFIG.EXPENSIFY.PARTNER_NAME, + partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD, partnerUserID: login, partnerUserSecret: password, }).catch((err) => { @@ -50,9 +44,9 @@ function signIn(login, password, useExpensifyLogin = false) { Store.set(STOREKEYS.SESSION, {}) .then(() => { return request('Authenticate', { - useExpensifyLogin: useExpensifyLogin, - partnerName: partnerName, - partnerPassword: partnerPassword, + useExpensifyLogin, + partnerName: CONFIG.EXPENSIFY.PARTNER_NAME, + partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD, partnerUserID: login, partnerUserSecret: password, }) From 93b81922ecd5c7047663e8165e3ad9a29aef0633 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 8 Aug 2020 10:35:59 -0600 Subject: [PATCH 06/11] Add the proper pusher info to the config file --- src/CONFIG.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CONFIG.js b/src/CONFIG.js index 2159e49de0c9..10512520c48f 100644 --- a/src/CONFIG.js +++ b/src/CONFIG.js @@ -3,8 +3,9 @@ const IS_IN_PRODUCTION = false; export default { PUSHER: { - APP_KEY: '829fd8fd2a6036568469', - CLUSTER: 'us3', + 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', From 64f6d14840c2cdfdc9152461668e604e607e41b6 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 8 Aug 2020 11:27:57 -0600 Subject: [PATCH 07/11] WIP switching to promises --- src/lib/PersistentStorage.js | 50 +++++++++++ src/store/Store.js | 40 +++++++-- src/store/actions/SessionActions.js | 125 ++++++++++++++-------------- 3 files changed, 148 insertions(+), 67 deletions(-) diff --git a/src/lib/PersistentStorage.js b/src/lib/PersistentStorage.js index 925f61fb3d32..a1d9e8deeb72 100644 --- a/src/lib/PersistentStorage.js +++ b/src/lib/PersistentStorage.js @@ -21,6 +21,24 @@ 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, val, key) => ({ + ...finalData, + [key]: val, + }), {})); +} + /** * Write a key to storage * @@ -32,6 +50,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, val], + ]), []); + return AsyncStorage.multiSet(keyValuePairs); +} + /** * Empty out the storage (like when the user signs out) * @@ -41,8 +77,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/store/Store.js b/src/store/Store.js index d690982a7ed4..1eef63e581c3 100644 --- a/src/store/Store.js +++ b/src/store/Store.js @@ -93,23 +93,53 @@ function set(key, val) { * we are looking for doesn't exist in the object yet * @returns {*} */ -const get = (key, extraPath, defaultValue) => { +function get(key, extraPath, defaultValue) { return PersistentStorage.get(key) - .then(val => { + .then((val) => { if (extraPath) { return _.get(val, extraPath, defaultValue); } return val; }); - if (extraPath) { - return _.get(val, extraPath, defaultValue); - } }; +/** + * 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); +} + +/** + * 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/SessionActions.js b/src/store/actions/SessionActions.js index 67dcf65b3f4c..8ea190ed3310 100644 --- a/src/store/actions/SessionActions.js +++ b/src/store/actions/SessionActions.js @@ -35,90 +35,91 @@ function createLogin(authToken, login, password) { /** * 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, {}) - .then(() => { - return request('Authenticate', { - useExpensifyLogin, - partnerName: CONFIG.EXPENSIFY.PARTNER_NAME, - partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD, - partnerUserID: login, - partnerUserSecret: password, - }) - .then((data) => { - // 404 We need to create a login - if (data.jsonCode === 404 && !useExpensifyLogin) { - signIn(login, password, true).then((expensifyLoginData) => { - createLogin(expensifyLoginData.authToken, login, password); - }); - return; - } - - // 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; - } + 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) => { + // 404 We need to create a login + if (data.jsonCode === 404 && !useExpensifyLogin) { + return signIn(login, password, true) + .then((expensifyLoginData) => { + createLogin(expensifyLoginData.authToken, login, password); + }); + } - Store.set(STOREKEYS.SESSION, data); - Store.set(STOREKEYS.APP_REDIRECT_TO, ROUTES.HOME); - Store.set(STOREKEYS.LAST_AUTHENTICATED, new Date().getTime()); + // 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'); + return Store.set(STOREKEYS.APP_REDIRECT_TO, ROUTES.SIGNIN); + } - return data; - }) - .then((data) => { - Store.set(STOREKEYS.SESSION, data); - Store.set(STOREKEYS.APP_REDIRECT_TO, ROUTES.HOME); - }) - .catch((err) => { - console.warn(err); - Store.set(STOREKEYS.SESSION, {error: err}); - }); + return Store.multiSet({ + [STOREKEYS.SESSION]: data, + [STOREKEYS.APP_REDIRECT_TO]: ROUTES.HOME, + [STOREKEYS.LAST_AUTHENTICATED]: new Date().getTime(), + }); + }) + .catch((err) => { + console.error(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 +}; From da1c8afdbd6fec8134edf7d79c60cca1e15f2105 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 8 Aug 2020 12:19:09 -0600 Subject: [PATCH 08/11] rewrite the async of report actions --- src/store/Store.js | 4 +- src/store/actions/ReportActions.js | 308 +++++++++++++++++++++-------- 2 files changed, 227 insertions(+), 85 deletions(-) diff --git a/src/store/Store.js b/src/store/Store.js index 1eef63e581c3..ac00d3d7c144 100644 --- a/src/store/Store.js +++ b/src/store/Store.js @@ -1,4 +1,4 @@ -import * as _ from 'lodash'; +import {lodashGet} from 'lodash'; import * as PersistentStorage from '../lib/PersistentStorage'; // Holds all of the callbacks that have registered for a specific key pattern @@ -97,7 +97,7 @@ function get(key, extraPath, defaultValue) { return PersistentStorage.get(key) .then((val) => { if (extraPath) { - return _.get(val, extraPath, defaultValue); + return lodashGet(val, extraPath, defaultValue); } return val; }); diff --git a/src/store/actions/ReportActions.js b/src/store/actions/ReportActions.js index 4007eb5e3df8..d4816d1a0158 100644 --- a/src/store/actions/ReportActions.js +++ b/src/store/actions/ReportActions.js @@ -1,119 +1,261 @@ /* globals moment */ +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, +} From bca46e552a136de40f1917ef38f2134cda457541 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 8 Aug 2020 12:33:19 -0600 Subject: [PATCH 09/11] Fix some bugs with sign in --- src/lib/PersistentStorage.js | 11 +++++++---- src/store/Store.js | 2 +- src/store/actions/ReportActions.js | 2 +- src/store/actions/SessionActions.js | 8 ++++++-- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/lib/PersistentStorage.js b/src/lib/PersistentStorage.js index a1d9e8deeb72..f5df6a87a694 100644 --- a/src/lib/PersistentStorage.js +++ b/src/lib/PersistentStorage.js @@ -33,10 +33,13 @@ function multiGet(keys) { // 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, val, key) => ({ + .then(arrayOfData => _.reduce(arrayOfData, (finalData, keyValuePair) => ({ ...finalData, - [key]: val, - }), {})); + [keyValuePair[0]]: JSON.parse(keyValuePair[1]), + }), {})) + .catch((err) => { + console.error(`Unable to get item from persistent storage. Keys: ${JSON.stringify(keys)} Error: ${err}`); + }); } /** @@ -63,7 +66,7 @@ function multiSet(data) { // {'@MyApp_user': 'myUserValue', '@MyApp_key': 'myKeyValue'} const keyValuePairs = _.reduce(data, (finalArray, val, key) => ([ ...finalArray, - [key, val], + [key, JSON.stringify(val)], ]), []); return AsyncStorage.multiSet(keyValuePairs); } diff --git a/src/store/Store.js b/src/store/Store.js index ac00d3d7c144..333b5dc4d817 100644 --- a/src/store/Store.js +++ b/src/store/Store.js @@ -1,4 +1,4 @@ -import {lodashGet} from 'lodash'; +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 diff --git a/src/store/actions/ReportActions.js b/src/store/actions/ReportActions.js index d4816d1a0158..8e6fb67c106c 100644 --- a/src/store/actions/ReportActions.js +++ b/src/store/actions/ReportActions.js @@ -258,4 +258,4 @@ export { addHistoryItem, updateLastReadActionID, initPusher, -} +}; diff --git a/src/store/actions/SessionActions.js b/src/store/actions/SessionActions.js index 8ea190ed3310..248513733206 100644 --- a/src/store/actions/SessionActions.js +++ b/src/store/actions/SessionActions.js @@ -42,6 +42,7 @@ function createLogin(authToken, login, password) { * @returns {Promise} */ function signIn(login, password, useExpensifyLogin = false) { + let authToken; return Store.multiSet({ [STOREKEYS.CREDENTIALS]: {login, password}, [STOREKEYS.SESSION]: {}, @@ -54,11 +55,13 @@ function signIn(login, password, useExpensifyLogin = false) { partnerUserSecret: password, })) .then((data) => { + authToken = data.authToken; + // 404 We need to create a login if (data.jsonCode === 404 && !useExpensifyLogin) { return signIn(login, password, true) - .then((expensifyLoginData) => { - createLogin(expensifyLoginData.authToken, login, password); + .then((newAuthToken) => { + createLogin(newAuthToken, login, password); }); } @@ -74,6 +77,7 @@ function signIn(login, password, useExpensifyLogin = false) { [STOREKEYS.LAST_AUTHENTICATED]: new Date().getTime(), }); }) + .then(() => authToken) .catch((err) => { console.error(err); Store.set(STOREKEYS.SESSION, {error: err}); From f201bb2b56a4808cbe4eb3d73142d2a9f035fc74 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 8 Aug 2020 12:37:37 -0600 Subject: [PATCH 10/11] Get sign in working --- src/store/Store.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/store/Store.js b/src/store/Store.js index 333b5dc4d817..3cfe83db3c7a 100644 --- a/src/store/Store.js +++ b/src/store/Store.js @@ -121,7 +121,10 @@ function multiGet(keys) { * @returns {Promise} */ function multiSet(data) { - return PersistentStorage.multiSet(data); + return PersistentStorage.multiSet(data) + .then(() => { + _.each(data, (val, key) => keyChanged(key, val)); + }); } /** From 8944ef27f52972d1de8b2b402b369fc19584321b Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 8 Aug 2020 12:50:19 -0600 Subject: [PATCH 11/11] Convert network to using promises --- src/lib/Network.js | 81 ++++++++++++++--------------- src/store/actions/SessionActions.js | 2 +- 2 files changed, 40 insertions(+), 43 deletions(-) diff --git a/src/lib/Network.js b/src/lib/Network.js index a3f8036d7fda..1a0446220844 100644 --- a/src/lib/Network.js +++ b/src/lib/Network.js @@ -1,6 +1,7 @@ import * as $ from 'jquery'; import * as Store from '../store/Store'; import CONFIG from '../CONFIG'; +import STOREKEYS from '../store/STOREKEYS'; let isAppOffline = false; @@ -10,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( - `${CONFIG.EXPENSIFY.API_ROOT}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 @@ -40,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; } /** @@ -62,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; } @@ -72,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/store/actions/SessionActions.js b/src/store/actions/SessionActions.js index 248513733206..9c4dc718a148 100644 --- a/src/store/actions/SessionActions.js +++ b/src/store/actions/SessionActions.js @@ -55,7 +55,7 @@ function signIn(login, password, useExpensifyLogin = false) { partnerUserSecret: password, })) .then((data) => { - authToken = data.authToken; + authToken = data && data.authToken; // 404 We need to create a login if (data.jsonCode === 404 && !useExpensifyLogin) {