diff --git a/README.md b/README.md
index 8dad49886e700..2bb7df4c7596a 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
1. Install `node` & `npm`: `brew install node`
2. Install `watchman`: `brew install watchman`
3. Install dependencies: `npm install`
-4. (_Optional, but recommended_) Start ngrok (`Expensidev/script/ngrok.sh`), replace value in `Network.js` with your ngrok value
+4. (_Optional, but recommended_) Start ngrok (`Expensidev/script/ngrok.sh`), replace `expensify.com.dev` value in `src/CONFIG.js` with your ngrok value
## Running the web app 💻
* To run a **Development Server**: `npm run web`
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 063304170df67..a510617ced23b 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -236,6 +236,8 @@ PODS:
- React-cxxreact (= 0.63.2)
- React-jsi (= 0.63.2)
- React-jsinspector (0.63.2)
+ - react-native-netinfo (5.9.5):
+ - React
- React-RCTActionSheet (0.63.2):
- React-Core/RCTActionSheetHeaders (= 0.63.2)
- React-RCTAnimation (0.63.2):
@@ -339,6 +341,7 @@ DEPENDENCIES:
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
+ - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
- React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
- React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`)
@@ -398,6 +401,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/jsiexecutor"
React-jsinspector:
:path: "../node_modules/react-native/ReactCommon/jsinspector"
+ react-native-netinfo:
+ :path: "../node_modules/@react-native-community/netinfo"
React-RCTActionSheet:
:path: "../node_modules/react-native/Libraries/ActionSheetIOS"
React-RCTAnimation:
@@ -450,6 +455,7 @@ SPEC CHECKSUMS:
React-jsi: 54245e1d5f4b690dec614a73a3795964eeef13a8
React-jsiexecutor: 8ca588cc921e70590820ce72b8789b02c67cce38
React-jsinspector: b14e62ebe7a66e9231e9581279909f2fc3db6606
+ react-native-netinfo: 7f3f3ed9e8f0e7ab3e7cac00cbfdc6997e25ecaf
React-RCTActionSheet: 910163b6b09685a35c4ebbc52b66d1bfbbe39fc5
React-RCTAnimation: 9a883bbe1e9d2e158d4fb53765ed64c8dc2200c6
React-RCTBlob: 39cf0ece1927996c4466510e25d2105f67010e13
@@ -464,6 +470,6 @@ SPEC CHECKSUMS:
Yoga: 7740b94929bbacbddda59bf115b5317e9a161598
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
-PODFILE CHECKSUM: c62ffd9a9a0605d86953b81048739f14f44c80a7
+PODFILE CHECKSUM: ebd627ee482cdc4819e853d5f3b81d787114e3ca
COCOAPODS: 1.9.3
diff --git a/package-lock.json b/package-lock.json
index 50a521f40b2d3..0c9ec2cf8be9b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1701,6 +1701,11 @@
"integrity": "sha512-W/J0fNYVO01tioHjvYWQ9m6RgndVtbElzYozBq1ZPrHO/iCzlqoySHl4gO/fpCl9QEFjvJfjPgtPMTMlsoq5DQ==",
"dev": true
},
+ "@react-native-community/netinfo": {
+ "version": "5.9.5",
+ "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-5.9.5.tgz",
+ "integrity": "sha512-PbSsRmhRwYIMdeVJTf9gJtvW0TVq/hmgz1xyjsrTIsQ7QS7wbMEiv1Eb/M/y6AEEsdUped5Axm5xykq9TGISHg=="
+ },
"@sinonjs/commons": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz",
@@ -11630,6 +11635,21 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
},
+ "pusher-js": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-7.0.0.tgz",
+ "integrity": "sha512-2ZSw8msMe6EKNTebQSthRInrWUK9bo3zXPmQx0bfeDFJdSnTWUROhdAhmpRQREHzqrL+l4imv/3uwgIQHUO0oQ==",
+ "requires": {
+ "tweetnacl": "^1.0.3"
+ },
+ "dependencies": {
+ "tweetnacl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
+ "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
+ }
+ }
+ },
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
diff --git a/package.json b/package.json
index 485a8b6122fa3..7a0fbd270444e 100644
--- a/package.json
+++ b/package.json
@@ -13,12 +13,14 @@
},
"dependencies": {
"@react-native-community/async-storage": "^1.11.0",
+ "@react-native-community/netinfo": "^5.9.5",
"html-entities": "^1.3.1",
"jquery": "^3.5.1",
"lodash.get": "^4.4.2",
"moment": "^2.27.0",
"moment-timezone": "^0.5.31",
"prop-types": "^15.7.2",
+ "pusher-js": "^7.0.0",
"react": "^16.13.1",
"react-beforeunload": "^2.2.2",
"react-dom": "^16.13.1",
diff --git a/src/lib/Network.js b/src/lib/Network.js
index f6f21e97f8974..47be13226a865 100644
--- a/src/lib/Network.js
+++ b/src/lib/Network.js
@@ -30,7 +30,7 @@ function request(command, data, type = 'post') {
if (responseData.jsonCode === 200) {
return responseData;
}
- console.error('[API] Error', responseData);
+ console.info('[API] Error', responseData);
})
// eslint-disable-next-line no-unused-vars
.catch(() => isAppOffline = true);
diff --git a/src/lib/Pusher/library/index.js b/src/lib/Pusher/library/index.js
new file mode 100644
index 0000000000000..5127108eb6f8d
--- /dev/null
+++ b/src/lib/Pusher/library/index.js
@@ -0,0 +1,3 @@
+import Pusher from 'pusher-js';
+
+export default Pusher;
diff --git a/src/lib/Pusher/library/index.native.js b/src/lib/Pusher/library/index.native.js
new file mode 100644
index 0000000000000..dffbcfb4b4e05
--- /dev/null
+++ b/src/lib/Pusher/library/index.native.js
@@ -0,0 +1,3 @@
+import Pusher from 'pusher-js/react-native';
+
+export default Pusher;
diff --git a/src/lib/Pusher/pusher.js b/src/lib/Pusher/pusher.js
new file mode 100644
index 0000000000000..ac9c994853977
--- /dev/null
+++ b/src/lib/Pusher/pusher.js
@@ -0,0 +1,295 @@
+import _ from 'underscore';
+import Pusher from './library';
+import CONFIG from '../../CONFIG';
+
+let socket;
+
+/**
+ * Initialize our pusher lib
+ * @param {String} appKey
+ * @param {Object} [params]
+ * @public
+ */
+function init(appKey, params) {
+ if (!socket) {
+ // Use this for debugging
+ // Pusher.log = (message) => {
+ // if (window.console && window.console.log) {
+ // window.console.log(message);
+ // }
+ // };
+ socket = new Pusher(CONFIG.PUSHER.APP_KEY, {
+ cluster: CONFIG.PUSHER.CLUSTER,
+ authEndpoint: `${CONFIG.PUSHER.AUTH_URL}/api.php?command=Push_Authenticate`,
+ });
+
+ // If we want to pass params in our requests to api.php we'll need to add it to socket.config.auth.params
+ // as per the documentation (https://pusher.com/docs/channels/using_channels/connection#channels-options-parameter).
+ // Any param mentioned here will show up in $_REQUEST when we call "Push_Authenticate". Params passed here need
+ // to pass our inputRules to show up in the request.
+ if (params) {
+ socket.config.auth = {};
+ socket.config.auth.params = params;
+ }
+
+ // Listen for connection errors and log them
+ socket.connection.bind('error', (error) => {
+ console.error('[Pusher] error', error);
+ });
+
+ socket.connection.bind('connected', () => {
+ console.debug('[Pusher] connected');
+ });
+
+ socket.connection.bind('disconnected', () => {
+ console.debug('[Pusher] disconnected');
+ });
+
+ socket.connection.bind('state_change', (states) => {
+ console.debug('[Pusher] state changed', states);
+ });
+ }
+}
+
+/**
+ * Returns a Pusher channel for a channel name
+ *
+ * @param {String} channelName
+ *
+ * @returns {Channel}
+ */
+function getChannel(channelName) {
+ return socket.channel(channelName);
+}
+
+/**
+ * Binds an event callback to a channel + eventName
+ * @param {Pusher.Channel} channel
+ * @param {String} eventName
+ * @param {Function} [eventCallback]
+ * @param {Boolean} [isChunked] Do we expect this channel to send chunked/separate blocks of data that need recombining?
+ *
+ * @private
+ */
+function bindEventToChannel(channel, eventName, eventCallback = () => {}, isChunked = false) {
+ if (!eventName) {
+ return;
+ }
+
+ const chunkedDataEvents = {};
+ const callback = (eventData) => {
+ if (!isChunked) {
+ let data;
+
+ try {
+ data = _.isObject(eventData) ? eventData : JSON.parse(eventData);
+ } catch (err) {
+ console.error('Unable to parse JSON response from Pusher', 0, {error: err, eventData});
+ return;
+ }
+
+ eventCallback(data);
+ return;
+ }
+
+ // If we are chunking the requests, we need to construct a rolling list of all packets that have come through Pusher.
+ // If we've completed one of these full packets, we'll combine the data and act on the event that it's assigned to.
+
+ // If we haven't seen this eventID yet, initialize it into our rolling list of packets.
+ if (!chunkedDataEvents[eventData.id]) {
+ chunkedDataEvents[eventData.id] = {chunks: [], receivedFinal: false};
+ }
+
+ // Add it to the rolling list.
+ const chunkedEvent = chunkedDataEvents[eventData.id];
+ chunkedEvent.chunks[eventData.index] = eventData.chunk;
+
+ // If this is the last packet, mark that we've hit the end.
+ if (eventData.final) {
+ chunkedEvent.receivedFinal = true;
+ }
+
+ // Only call the event callback if we've received the last packet and we don't have any holes in the complete packet.
+ if (chunkedEvent.receivedFinal && chunkedEvent.chunks.length === Object.keys(chunkedEvent.chunks).length) {
+ eventCallback(JSON.parse(chunkedEvent.chunks.join('')));
+ try {
+ eventCallback(JSON.parse(chunkedEvent.chunks.join('')));
+ } catch (err) {
+ console.error('[Pusher] Unable to parse chunked JSON response from Pusher', 0, {error: err, eventData: chunkedEvent.chunks.join('')});
+ }
+
+ delete chunkedDataEvents[eventData.id];
+ }
+ };
+
+ channel.bind(eventName, callback);
+}
+
+/**
+ * Subscribe to a channel and an event
+ *
+ * @param {String} channelName
+ * @param {String} eventName
+ * @param {Function} [eventCallback]
+ * @param {Boolean} [isChunked] This parameters tells us whether or not we expect the result to come in individual pieces/chunks (because it exceeds
+ * the 10kB limit that pusher has).
+ *
+ * @return {Promise}
+ *
+ * @public
+ */
+function subscribe(channelName, eventName, eventCallback = () => {}, isChunked = false) {
+ return new Promise((resolve, reject) => {
+ // We cannot call subscribe() before init(). Prevent any attempt to do this on dev.
+ if (!socket) {
+ throw new Error('[Pusher] instance not found. Pusher.subscribe() most likely has been called before Pusher.init()');
+ }
+
+ console.debug('[Pusher] Attempting to subscribe to channel', true, {channelName, eventName});
+ let channel = getChannel(channelName);
+
+ if (!channel || !channel.subscribed) {
+ channel = socket.subscribe(channelName);
+ channel.bind('pusher:subscription_succeeded', () => {
+ bindEventToChannel(channel, eventName, eventCallback, isChunked);
+ resolve();
+ });
+
+ channel.bind('pusher:subscription_error', (status) => {
+ if (status === 403) {
+ console.debug('[Pusher] Issue authenticating with Pusher during subscribe attempt.', 0, {channelName, status});
+ }
+
+ reject(status);
+ });
+ } else {
+ bindEventToChannel(channel, eventName, eventCallback, isChunked);
+ resolve();
+ }
+ });
+}
+
+/**
+ * Waits for the subscription_succeeded event to fire before returning members or
+ * returns the current members if the subscription has already succeeded.
+ *
+ * @param {String} channelName
+ *
+ * @return {Promise}
+ */
+function getChannelMembersAsync(channelName) {
+ return new Promise((resolve, reject) => subscribe(channelName)
+ .done(() => {
+ const channel = getChannel(channelName);
+ resolve(channel.members.members);
+ })
+ .fail(() => {
+ console.debug('[Pusher] Unable to subscribe to presence channel while getting channel members async');
+ reject();
+ }));
+}
+
+/**
+ * Unsubscribe from a channel and optionally a specific event
+ *
+ * @param {String} channelName
+ * @param {String} [eventName]
+ * @public
+ */
+function unsubscribe(channelName, eventName = '') {
+ const channel = getChannel(channelName);
+
+ if (!channel) {
+ console.debug('[Pusher] Attempted to unsubscribe or unbind from a channel, but Pusher-JS has no knowledge of it', 0, {channelName, eventName});
+ return;
+ }
+
+ if (eventName) {
+ console.debug('[Pusher] Unbinding event', true, {eventName, channelName});
+ channel.unbind(eventName);
+ } else {
+ if (!channel.subscribed) {
+ console.warn('[Pusher] Attempted to unsubscribe from channel, but we are not subscribed to begin with', 0, {channelName});
+ return;
+ }
+
+ channel.unbind();
+ socket.unsubscribe(channelName);
+ }
+}
+
+/**
+ * Are we already in the process of subscribing to this channel?
+ *
+ * @param {String} channelName
+ *
+ * @returns {Boolean}
+ */
+function isAlreadySubscribing(channelName) {
+ if (!socket) {
+ return false;
+ }
+
+ const channel = getChannel(channelName);
+ return channel ? channel.subscriptionPending : false;
+}
+
+/**
+ * Are we already subscribed to this channel?
+ *
+ * @param {String} channelName
+ *
+ * @returns {Boolean}
+ */
+function isSubscribed(channelName) {
+ if (!socket) {
+ return false;
+ }
+
+ const channel = getChannel(channelName);
+ return channel ? channel.subscribed : false;
+}
+
+/**
+ * Sends an event over a specific event/channel in pusher.
+ *
+ * @param {String} channelName
+ * @param {String} eventName
+ * @param {Object} payload
+ */
+function sendEvent(channelName, eventName, payload) {
+ socket.send_event(eventName, payload, channelName);
+}
+
+/**
+ * Sends an event across multiple pieces over a channel.
+ *
+ * @param {String} channelName
+ * @param {String} eventName
+ * @param {Object} payload
+ */
+function sendChunkedEvent(channelName, eventName, payload) {
+ const chunkSize = 9000;
+ const payloadString = JSON.stringify(payload);
+ const msgId = Math.random().toString();
+ for (let i = 0; i * chunkSize < payloadString.length; i++) {
+ socket.send_event(eventName, {
+ id: msgId,
+ index: i,
+ chunk: payloadString.substr(i * chunkSize, chunkSize),
+ final: chunkSize * (i + 1) >= payloadString.length
+ }, channelName);
+ }
+}
+
+export {
+ init,
+ subscribe,
+ unsubscribe,
+ getChannelMembersAsync,
+ getChannel,
+ isSubscribed,
+ isAlreadySubscribing,
+ sendEvent,
+ sendChunkedEvent,
+};
diff --git a/src/page/HomePage/HomePage.js b/src/page/HomePage/HomePage.js
index 1e6ba115569ee..d1032062898e2 100644
--- a/src/page/HomePage/HomePage.js
+++ b/src/page/HomePage/HomePage.js
@@ -9,22 +9,43 @@ import styles from '../../style/StyleSheet';
import Header from './HeaderView';
import Sidebar from './SidebarView';
import Main from './MainView';
+import * as Store from '../../store/Store';
+import STOREKEYS from '../../store/STOREKEYS';
+import {initPusher} from '../../store/actions/ReportActions';
+import * as pusher from '../../lib/Pusher/pusher';
-const App = () => (
- <>
-
-
-
-
-
-
-
-
+export default class App extends React.Component {
+ componentDidMount() {
+ Store.get(STOREKEYS.SESSION, 'authToken').then((authToken) => {
+ if (authToken) {
+ // Initialize the pusher connection
+ pusher.init(null, {
+ authToken,
+ });
+
+ // Setup the report action handler to subscribe to pusher
+ initPusher();
+ }
+ });
+ }
+
+ render() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
-
-
-
- >
-);
+
+ >
+ );
+ }
+}
App.displayName = 'App';
-export default App;
diff --git a/src/store/actions/PersonalDetailsActions.js b/src/store/actions/PersonalDetailsActions.js
index e0d72a94b77fc..482be1f42b0e2 100644
--- a/src/store/actions/PersonalDetailsActions.js
+++ b/src/store/actions/PersonalDetailsActions.js
@@ -32,6 +32,10 @@ function fetch() {
let currentLogin;
const requestPromise = Store.get(STOREKEYS.SESSION, 'email')
.then((login) => {
+ if (!login) {
+ throw Error('No login');
+ }
+
currentLogin = login;
return request('Get', {
returnValueList: 'personalDetailsList',
@@ -59,11 +63,19 @@ function fetch() {
}
};
}, {});
- const myPersonalDetails = allPersonalDetails[currentLogin];
+ const myPersonalDetails = allPersonalDetails[currentLogin] || {};
return Store.multiSet({
[STOREKEYS.PERSONAL_DETAILS]: allPersonalDetails,
[STOREKEYS.MY_PERSONAL_DETAILS]: myPersonalDetails,
});
+ })
+ .catch((error) => {
+ if (error.message === 'No login') {
+ console.info('No email in store, not fetching personal details.');
+ return;
+ }
+
+ console.error('Error fetching personal details', error);
});
// Refresh the personal details every 30 minutes
diff --git a/src/store/actions/ReportActions.js b/src/store/actions/ReportActions.js
index 8cde6d2632970..7de76b682e3a4 100644
--- a/src/store/actions/ReportActions.js
+++ b/src/store/actions/ReportActions.js
@@ -1,4 +1,4 @@
-/* globals moment */
+import moment from 'moment';
import _ from 'underscore';
import * as Store from '../Store';
import {request, delayedWrite} from '../../lib/Network';
@@ -6,9 +6,7 @@ import STOREKEYS from '../STOREKEYS';
import ExpensiMark from '../../lib/ExpensiMark';
import Guid from '../../lib/Guid';
import CONFIG from '../../CONFIG';
-
-// @TODO implement pusher
-// import * as pusher from '../../lib/pusher';
+import * as pusher from '../../lib/Pusher/pusher';
/**
* Sorts the report actions so that the newest actions are at the bottom
@@ -27,34 +25,32 @@ function sortReportActions(firstReport, secondReport) {
* @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 => Store.set(`${STOREKEYS.REPORT}_${reportID}_history`,
-// reportHistory.sort(sortReportActions)));
-// }
+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 => Store.set(`${STOREKEYS.REPORT}_${reportID}_history`, reportHistory.sort(sortReportActions)));
+}
/**
* Checks the report to see if there are any unread history items
@@ -87,12 +83,9 @@ function hasUnreadHistoryItems(accountID, report) {
*/
function initPusher() {
return Store.get(STOREKEYS.SESSION, 'accountID')
- .then(() => {
- // @TODO: need to implement pusher
- // return pusher.subscribe(`private-user-accountID-${accountID}`, 'reportComment', (pushJSON) => {
- // updateReportWithNewAction(pushJSON.reportID, pushJSON.reportAction);
- // });
- });
+ .then(accountID => pusher.subscribe(`private-user-accountID-${accountID}`, 'reportComment', (pushJSON) => {
+ updateReportWithNewAction(pushJSON.reportID, pushJSON.reportAction);
+ }));
}
/**
@@ -137,7 +130,8 @@ function fetchAll() {
_.each(data.reportListBeta, report => fetch(report.reportID));
return data;
})
- .then(data => Store.set(STOREKEYS.REPORTS, _.values(data.reports)));
+ .then(data => Store.set(STOREKEYS.REPORTS, _.values(data.reports)))
+ .catch((error) => { console.log('Error fetching report actions', error); });
}
return request('Get', {
@@ -151,7 +145,8 @@ function fetchAll() {
_.each(data.reportListBeta, report => fetch(report.reportID));
return data;
})
- .then(data => Store.set(STOREKEYS.REPORTS, _.values(data.reportListBeta)));
+ .then(data => Store.set(STOREKEYS.REPORTS, _.values(data.reportListBeta)))
+ .catch((error) => { console.log('Error fetching report actions', error); });
}
/**
diff --git a/src/store/actions/SessionActions.js b/src/store/actions/SessionActions.js
index ce7eb35dbf7b6..65702e78a76cd 100644
--- a/src/store/actions/SessionActions.js
+++ b/src/store/actions/SessionActions.js
@@ -148,7 +148,7 @@ function verifyAuthToken() {
}
return request('Get', {returnValueList: 'account'}).then((data) => {
- if (data.jsonCode === 200) {
+ if (data && data.jsonCode === 200) {
return Store.merge(STOREKEYS.SESSION, data);
}
diff --git a/src/style/StyleSheet.js b/src/style/StyleSheet.js
index e0ce99b4f6d78..9eb2708273565 100644
--- a/src/style/StyleSheet.js
+++ b/src/style/StyleSheet.js
@@ -89,10 +89,9 @@ const styles = {
flexGrow: 1,
},
historyItemAvatar: {
- borderRadius: '50%',
+ borderRadius: 20,
height: 40,
width: 40,
- lineHeight: 40,
},
historyItemHeaderTimestamp: {
color: '#7d8b8f',