RN-235 Report errors to email and restart app if needed (#683)
* RN-235 Report errors to email and restart app if needed * Feedback review
This commit is contained in:
parent
055e4173e8
commit
3a233455a6
10 changed files with 159 additions and 19 deletions
33
NOTICE.txt
33
NOTICE.txt
|
|
@ -1057,3 +1057,36 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-exception-handler
|
||||
|
||||
This product contains 'react-native-exception-handler', A react native module that lets you to register a global error handler that can capture fatal/non fatal uncaught exceptions.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/master-atul/react-native-exception-handler
|
||||
|
||||
* LICENSE:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Max
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import keyMirror from 'mattermost-redux/utils/key_mirror';
|
|||
const NavigationTypes = keyMirror({
|
||||
NAVIGATION_RESET: null,
|
||||
NAVIGATION_CLOSE_MODAL: null,
|
||||
NAVIGATION_NO_TEAMS: null
|
||||
NAVIGATION_NO_TEAMS: null,
|
||||
RESTART_APP: null
|
||||
});
|
||||
|
||||
export default NavigationTypes;
|
||||
|
|
|
|||
|
|
@ -74,10 +74,6 @@ const state = {
|
|||
status: 'not_started',
|
||||
error: null
|
||||
},
|
||||
leaveChannel: {
|
||||
status: 'not_started',
|
||||
error: null
|
||||
},
|
||||
joinChannel: {
|
||||
status: 'not_started',
|
||||
error: null
|
||||
|
|
@ -270,7 +266,8 @@ const state = {
|
|||
notification: null,
|
||||
root: {
|
||||
appInitializing: false,
|
||||
hydrationComplete: false
|
||||
hydrationComplete: false,
|
||||
purge: false
|
||||
},
|
||||
selectServer: {
|
||||
serverUrl: Config.DefaultServerUrl
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'babel-polyfill';
|
|||
import Orientation from 'react-native-orientation';
|
||||
import {Provider} from 'react-redux';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import {IntlProvider} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
AppState,
|
||||
|
|
@ -12,10 +13,12 @@ import {
|
|||
Platform
|
||||
} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import {setJSExceptionHandler} from 'react-native-exception-handler';
|
||||
import semver from 'semver';
|
||||
|
||||
import {setAppState, setDeviceToken, setServerVersion} from 'mattermost-redux/actions/general';
|
||||
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
|
||||
import {logError} from 'mattermost-redux/actions/errors';
|
||||
import {logout} from 'mattermost-redux/actions/users';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
|
@ -24,6 +27,7 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
|||
import {goToNotification, loadConfigAndLicense, queueNotification} from 'app/actions/views/root';
|
||||
import {setChannelDisplayName} from 'app/actions/views/channel';
|
||||
import {NavigationTypes, ViewTypes} from 'app/constants';
|
||||
import {getTranslations} from 'app/i18n';
|
||||
import initialState from 'app/initial_state';
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
import {registerScreens} from 'app/screens';
|
||||
|
|
@ -37,17 +41,50 @@ registerScreens(store, Provider);
|
|||
export default class Mattermost {
|
||||
constructor() {
|
||||
this.isConfigured = false;
|
||||
setJSExceptionHandler(this.errorHandler, true);
|
||||
Orientation.lockToPortrait();
|
||||
this.unsubscribeFromStore = store.subscribe(this.listenForHydration);
|
||||
AppState.addEventListener('change', this.handleAppStateChange);
|
||||
EventEmitter.on(General.CONFIG_CHANGED, this.handleConfigChanged);
|
||||
EventEmitter.on(NavigationTypes.NAVIGATION_RESET, this.handleReset);
|
||||
EventEmitter.on(General.DEFAULT_CHANNEL, this.handleResetDisplayName);
|
||||
EventEmitter.on(NavigationTypes.RESTART_APP, this.restartApp);
|
||||
|
||||
this.handleAppStateChange(AppState.currentState);
|
||||
Client4.setUserAgent(DeviceInfo.getUserAgent());
|
||||
}
|
||||
|
||||
errorHandler = (e, isFatal) => {
|
||||
const intl = this.getIntl();
|
||||
logError(e)(store.dispatch);
|
||||
|
||||
if (isFatal) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({id: 'mobile.error_handler.title', defaultMessage: 'Unexpected error occurred'}),
|
||||
intl.formatMessage({id: 'mobile.error_handler.description', defaultMessage: '\nClick relaunch to open the app again. After restart, you can report the problem from the settings menu.\n'}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'mobile.error_handler.button', defaultMessage: 'Relaunch'}),
|
||||
onPress: () => {
|
||||
// purge the store
|
||||
store.dispatch({type: General.OFFLINE_STORE_PURGE});
|
||||
}
|
||||
}]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getIntl = () => {
|
||||
const state = store.getState();
|
||||
let locale = DeviceInfo.getDeviceLocale().split('-')[0];
|
||||
if (state.views.i18n.locale) {
|
||||
locale = state.views.i18n.locale;
|
||||
}
|
||||
|
||||
const intlProvider = new IntlProvider({locale, messages: getTranslations(locale)}, {});
|
||||
const {intl} = intlProvider.getChildContext();
|
||||
return intl;
|
||||
};
|
||||
|
||||
handleAppStateChange = (appState) => {
|
||||
const {dispatch, getState} = store;
|
||||
setAppState(appState === 'active')(dispatch, getState);
|
||||
|
|
@ -56,13 +93,15 @@ export default class Mattermost {
|
|||
handleConfigChanged = (serverVersion) => {
|
||||
const {dispatch, getState} = store;
|
||||
const version = serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g)[0];
|
||||
const intl = this.getIntl();
|
||||
|
||||
if (serverVersion) {
|
||||
if (semver.valid(version) && semver.lt(version, Config.MinServerVersion)) {
|
||||
Alert.alert(
|
||||
'Server upgrade required',
|
||||
'A server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.',
|
||||
intl.formatMessage({id: 'mobile.server_upgrade.title', defaultMessage: 'Server upgrade required'}),
|
||||
intl.formatMessage({id: 'mobile.server_upgrade.description', defaultMessage: '\nA server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.\n'}),
|
||||
[{
|
||||
text: 'OK',
|
||||
text: intl.formatMessage({id: 'mobile.server_upgrade.button', defaultMessage: 'OK'}),
|
||||
onPress: this.handleVersionUpgrade
|
||||
}]
|
||||
);
|
||||
|
|
@ -154,6 +193,11 @@ export default class Mattermost {
|
|||
}
|
||||
};
|
||||
|
||||
restartApp = () => {
|
||||
Navigation.dismissModal({animationType: 'none'});
|
||||
this.startApp('fade');
|
||||
};
|
||||
|
||||
startApp = (animationType = 'none') => {
|
||||
if (!this.isConfigured) {
|
||||
this.configurePushNotifications();
|
||||
|
|
|
|||
|
|
@ -28,7 +28,17 @@ function hydrationComplete(state = false, action) {
|
|||
}
|
||||
}
|
||||
|
||||
function purge(state = false, action) {
|
||||
switch (action.type) {
|
||||
case General.OFFLINE_STORE_PURGE:
|
||||
return true;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
appInitializing,
|
||||
hydrationComplete
|
||||
hydrationComplete,
|
||||
purge
|
||||
});
|
||||
|
|
|
|||
|
|
@ -52,10 +52,16 @@ class Settings extends PureComponent {
|
|||
`App Platform: ${Platform.OS}`
|
||||
];
|
||||
if (errors.length) {
|
||||
const errorArray = errors.map((e) => {
|
||||
const {error} = e;
|
||||
const stack = error.stack || '';
|
||||
return `Date: ${e.date}\nMessage: ${error.message}\nStack trace:\n${stack}\n\n`;
|
||||
}).join('');
|
||||
|
||||
contents = contents.concat([
|
||||
'',
|
||||
'Errors:',
|
||||
JSON.stringify(errors.map((e) => e.error))
|
||||
errorArray
|
||||
]);
|
||||
}
|
||||
return contents.join('\n');
|
||||
|
|
@ -135,8 +141,9 @@ class Settings extends PureComponent {
|
|||
openErrorEmail = () => {
|
||||
const recipient = 'feedback@mattermost.com';
|
||||
const subject = 'Problem with Mattermost React Native app';
|
||||
const body = this.errorEmailBody();
|
||||
Linking.openURL(
|
||||
`mailto:${recipient}?subject=${subject}&body=${this.errorEmailBody()}`
|
||||
`mailto:${recipient}?subject=${subject}&body=${body}`
|
||||
);
|
||||
this.props.actions.clearErrors();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@
|
|||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
import {AsyncStorage} from 'react-native';
|
||||
import configureStore from 'mattermost-redux/store';
|
||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {General, RequestStatus} from 'mattermost-redux/constants';
|
||||
import {createBlacklistFilter} from 'redux-persist-transform-filter';
|
||||
import {createTransform, persistStore} from 'redux-persist';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {ErrorTypes, GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {General, RequestStatus} from 'mattermost-redux/constants';
|
||||
import configureStore from 'mattermost-redux/store';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {NavigationTypes, ViewTypes} from 'app/constants';
|
||||
import appReducer from 'app/reducers';
|
||||
|
||||
import {transformSet} from './utils';
|
||||
|
|
@ -108,6 +110,41 @@ export default function configureAppStore(initialState) {
|
|||
setTimeout(() => {
|
||||
purging = false;
|
||||
}, 500);
|
||||
} else if (state.views.root.purge && !purging) {
|
||||
purging = true;
|
||||
|
||||
await persistor.purge();
|
||||
|
||||
store.dispatch(batchActions([
|
||||
{
|
||||
type: General.OFFLINE_STORE_RESET,
|
||||
data: initialState
|
||||
},
|
||||
{
|
||||
type: ErrorTypes.RESTORE_ERRORS,
|
||||
data: [...state.errors]
|
||||
},
|
||||
{
|
||||
type: GeneralTypes.RECEIVED_APP_DEVICE_TOKEN,
|
||||
data: state.entities.general.deviceToken
|
||||
},
|
||||
{
|
||||
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,
|
||||
data: {
|
||||
url: state.entities.general.credentials.url,
|
||||
token: state.entities.general.credentials.token
|
||||
}
|
||||
},
|
||||
{
|
||||
type: ViewTypes.SERVER_URL_CHANGED,
|
||||
serverUrl: state.entities.general.credentials.url || state.views.selectServer.serverUrl
|
||||
}
|
||||
], 'BATCH_FOR_RESTART'));
|
||||
|
||||
setTimeout(() => {
|
||||
purging = false;
|
||||
EventEmitter.emit(NavigationTypes.RESTART_APP);
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -117,7 +154,7 @@ export default function configureAppStore(initialState) {
|
|||
autoRehydrate: {
|
||||
log: false
|
||||
},
|
||||
blacklist: ['errors', 'navigation', 'offline', 'requests'],
|
||||
blacklist: ['navigation', 'offline', 'requests'],
|
||||
debounce: 500,
|
||||
transforms: [
|
||||
setTransformer,
|
||||
|
|
@ -126,5 +163,5 @@ export default function configureAppStore(initialState) {
|
|||
}
|
||||
};
|
||||
|
||||
return configureStore({}, appReducer, offlineOptions, getAppReducer);
|
||||
return configureStore(initialState, appReducer, offlineOptions, getAppReducer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1718,6 +1718,9 @@
|
|||
"mobile.custom_list.no_results": "No Results",
|
||||
"mobile.drawer.teamsTitle": "Teams",
|
||||
"mobile.edit_post.title": "Editing Message",
|
||||
"mobile.error_handler.button": "Relaunch",
|
||||
"mobile.error_handler.description": "\nClick relaunch to open the app again. After restart, you can report the problem from the settings menu.\n",
|
||||
"mobile.error_handler.title": "Unexpected error occurred",
|
||||
"mobile.file_upload.camera": "Take Photo or Video",
|
||||
"mobile.file_upload.library": "Photo Library",
|
||||
"mobile.file_upload.more": "More",
|
||||
|
|
@ -1769,6 +1772,9 @@
|
|||
"mobile.select_team.join_open": "Open teams you can join",
|
||||
"mobile.select_team.no_teams": "There are no available teams for you to join.",
|
||||
"mobile.server_ping_failed": "Cannot connect to the server. Please check your server URL and internet connection.",
|
||||
"mobile.server_upgrade.button": "OK",
|
||||
"mobile.server_upgrade.description": "\nA server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.\n",
|
||||
"mobile.server_upgrade.title": "Server upgrade required",
|
||||
"mobile.server_url.invalid_format": "URL must start with http:// or https://",
|
||||
"mobile.session_expired": "Session Expired: Please log in to continue receiving notifications.",
|
||||
"mobile.settings.team_selection": "Team Selection",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"react-native-cookies": "3.1.0",
|
||||
"react-native-device-info": "0.10.2",
|
||||
"react-native-drawer": "2.3.0",
|
||||
"react-native-exception-handler": "1.1.0",
|
||||
"react-native-image-picker": "jp928/react-native-image-picker#6ee35b69f3dbd6c7c66f580fd4d9eabf398703d4",
|
||||
"react-native-keyboard-aware-scroll-view": "0.2.8",
|
||||
"react-native-linear-gradient": "2.0.0",
|
||||
|
|
|
|||
|
|
@ -3645,7 +3645,7 @@ makeerror@1.0.x:
|
|||
|
||||
mattermost-redux@mattermost/mattermost-redux#master:
|
||||
version "0.0.1"
|
||||
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/caf81fd083fb4f883c787e4ca11bf17e5898e031"
|
||||
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/6f9a8c71aa857ef043cc0794957917ac6f304f6a"
|
||||
dependencies:
|
||||
deep-equal "1.0.1"
|
||||
harmony-reflect "1.5.1"
|
||||
|
|
@ -4476,6 +4476,10 @@ react-native-drawer@2.3.0:
|
|||
dependencies:
|
||||
tween-functions "^1.0.1"
|
||||
|
||||
react-native-exception-handler@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-exception-handler/-/react-native-exception-handler-1.1.0.tgz#320b7fc9c104e11d66173a2b0daeac97831a1a37"
|
||||
|
||||
react-native-image-picker@jp928/react-native-image-picker#6ee35b69f3dbd6c7c66f580fd4d9eabf398703d4:
|
||||
version "0.26.2"
|
||||
resolved "https://codeload.github.com/jp928/react-native-image-picker/tar.gz/6ee35b69f3dbd6c7c66f580fd4d9eabf398703d4"
|
||||
|
|
|
|||
Loading…
Reference in a new issue