Share ext error (#1587)

* Wrap iOS share extension in error boundary component

* Wrap Android share extension in error boundary component

* Review feedback
This commit is contained in:
Chris Duarte 2018-04-17 12:23:37 -07:00 committed by Harrison Healey
parent 033a0c15bd
commit d9f2bca9c0
8 changed files with 346 additions and 135 deletions

View file

@ -259,4 +259,4 @@
"yoda": [2, "never", {"exceptRange": false, "onlyEquality": false}],
"mocha/no-exclusive-tests": 2
}
}
}

View file

@ -11,6 +11,7 @@ import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeam, getCurrentTeamMembership} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentChannel, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
export const LOGGER_EXTENSION = 'extension';
export const LOGGER_JAVASCRIPT = 'javascript';
export const LOGGER_JAVASCRIPT_WARNING = 'javascript_warning';
export const LOGGER_NATIVE = 'native';
@ -49,6 +50,18 @@ export function captureException(error, logger, store) {
}, store);
}
export function captureExceptionWithoutState(err, logger) {
if (!Config.SentryEnabled) {
return;
}
try {
Sentry.captureException(err, {logger});
} catch (error) {
// do nothing...
}
}
export function captureMessage(message, logger, store) {
capture(() => {
Sentry.captureMessage(message, {logger});

View file

@ -2269,6 +2269,9 @@
"mobile.settings.team_selection": "Team Selection",
"mobile.share_extension.cancel": "Cancel",
"mobile.share_extension.channel": "Channel",
"mobile.share_extension.error_message": "An error has occurred while using the share extension.",
"mobile.share_extension.error_close": "Close",
"mobile.share_extension.error_title": "Extension Error",
"mobile.share_extension.send": "Send",
"mobile.share_extension.team": "Team",
"mobile.suggestion.members": "Members",

View file

@ -0,0 +1,58 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Alert, NativeModules} from 'react-native';
import {intlShape} from 'react-intl';
import {captureException, initializeSentry, LOGGER_EXTENSION} from 'app/utils/sentry';
import Navigation from './navigation';
const ShareExtension = NativeModules.MattermostShare;
export default class ShareApp extends PureComponent {
static contextTypes = {
intl: intlShape,
store: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
initializeSentry();
}
componentDidCatch(error) {
const {intl, store} = this.context;
const {formatMessage} = intl;
captureException(error, LOGGER_EXTENSION, store);
Alert.alert(
formatMessage({
id: 'mobile.share_extension.error_title',
defaultMessage: 'Extension Error',
}),
formatMessage({
id: 'mobile.share_extension.error_message',
defaultMessage: 'An error has occurred while using the share extension.',
}),
[
{
text: formatMessage({
id: 'mobile.share_extension.error_close',
defaultMessage: 'Close',
}),
onPress: this.close,
},
]
);
}
close = () => ShareExtension.close(null)
render() {
return <Navigation/>;
}
}

View file

@ -13,7 +13,7 @@ import {getCurrentLocale} from 'app/selectors/i18n';
import configureStore from 'app/store';
import {extensionSelectTeamId} from './actions';
import Navigation from './navigation';
import Extension from './extension';
export default class ShareApp extends PureComponent {
constructor() {
@ -54,7 +54,7 @@ export default class ShareApp extends PureComponent {
locale={locale}
messages={getTranslations(locale)}
>
<Navigation/>
<Extension/>
</IntlProvider>
</Provider>
);

View file

@ -0,0 +1,85 @@
import React from 'react';
import PropTypes from 'prop-types';
import {TouchableOpacity, View} from 'react-native';
import {Preferences} from 'mattermost-redux/constants';
import FormattedText from 'app/components/formatted_text';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default function errorMessage(props) {
const {close} = props;
const theme = Preferences.THEMES.default;
const styles = getStyleSheet(theme);
return (
<View style={styles.errorWrapper}>
<View style={styles.errorContainer}>
<View style={styles.errorContent}>
<View style={styles.errorMessage}>
<FormattedText
style={styles.errorMessageText}
id={'mobile.share_extension.error_message'}
defaultMessage={'An error has occurred while using the share extension.'}
/>
</View>
<TouchableOpacity
style={styles.errorButton}
onPress={() => close()}
>
<FormattedText
style={styles.errorButtonText}
id={'mobile.share_extension.error_close'}
defaultMessage={'Close'}
/>
</TouchableOpacity>
</View>
</View>
</View>
);
}
errorMessage.propTypes = {
close: PropTypes.func.isRequired,
formatMessage: PropTypes.func.isRequired,
};
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
errorButton: {
alignItems: 'center',
justifyContent: 'center',
borderTopWidth: 2,
borderTopColor: changeOpacity(theme.linkColor, 0.3),
paddingVertical: 15,
},
errorButtonText: {
color: changeOpacity(theme.linkColor, 0.7),
fontSize: 18,
},
errorContainer: {
borderRadius: 5,
backgroundColor: 'white',
marginHorizontal: 35,
},
errorContent: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.05),
},
errorMessage: {
alignItems: 'center',
justifyContent: 'center',
padding: 25,
},
errorMessageText: {
color: theme.centerChannelColor,
fontSize: 16,
textAlign: 'center',
},
errorWrapper: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: changeOpacity(theme.centerChannelColor, 0.5),
},
};
});

View file

@ -0,0 +1,150 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Animated, Dimensions, NavigatorIOS, StyleSheet, View} from 'react-native';
import {Preferences} from 'mattermost-redux/constants';
import mattermostBucket from 'app/mattermost_bucket';
import ExtensionPost from './extension_post';
const {View: AnimatedView} = Animated;
export default class SharedApp extends PureComponent {
static propTypes = {
appGroupId: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
const {height, width} = Dimensions.get('window');
const isLandscape = width > height;
this.state = {
backdropOpacity: new Animated.Value(0),
containerOpacity: new Animated.Value(0),
isLandscape,
};
mattermostBucket.readFromFile('entities', props.appGroupId).then((value) => {
this.entities = value;
this.setState({init: true});
});
}
componentDidMount() {
Animated.parallel([
Animated.timing(this.state.backdropOpacity, {
toValue: 0.5,
duration: 100,
}),
Animated.timing(this.state.containerOpacity, {
toValue: 1,
duration: 250,
}),
]).start();
}
onLayout = (e) => {
const {height, width} = e.nativeEvent.layout;
const isLandscape = width > height;
if (this.state.isLandscape !== isLandscape) {
this.setState({isLandscape});
}
};
userIsLoggedIn = () => {
if (
this.entities &&
this.entities.general &&
this.entities.general.credentials &&
this.entities.general.credentials.token &&
this.entities.general.credentials.url
) {
return true;
}
return false;
};
render() {
const {init, isLandscape} = this.state;
if (!init) {
return null;
}
const theme = Preferences.THEMES.default;
const initialRoute = {
component: ExtensionPost,
title: 'Mattermost',
passProps: {
authenticated: this.userIsLoggedIn(),
entities: this.entities,
onClose: this.props.onClose,
isLandscape,
theme,
},
wrapperStyle: {
borderRadius: 10,
backgroundColor: theme.centerChannelBg,
},
};
return (
<View
style={styles.flex}
onLayout={this.onLayout}
>
<AnimatedView style={[styles.backdrop, {opacity: this.state.backdropOpacity}]}/>
<View style={styles.wrapper}>
<AnimatedView
style={[
styles.container,
{
opacity: this.state.containerOpacity,
height: this.userIsLoggedIn() ? 250 : 130,
top: isLandscape ? 20 : 65,
},
]}
>
<NavigatorIOS
initialRoute={initialRoute}
style={styles.flex}
navigationBarHidden={true}
/>
</AnimatedView>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
flex: {
flex: 1,
},
backdrop: {
position: 'absolute',
flex: 1,
backgroundColor: '#000',
height: '100%',
width: '100%',
},
wrapper: {
flex: 1,
marginHorizontal: 20,
},
container: {
backgroundColor: 'white',
borderRadius: 10,
position: 'relative',
width: '100%',
},
});

View file

@ -2,164 +2,66 @@
// See License.txt for license information.
import React, {PureComponent} from 'react';
import {NativeModules} from 'react-native';
import {IntlProvider} from 'react-intl';
import DeviceInfo from 'react-native-device-info';
import {
Animated,
Dimensions,
NativeModules,
NavigatorIOS,
StyleSheet,
View,
} from 'react-native';
import {Preferences} from 'mattermost-redux/constants';
import {getTranslations} from 'app/i18n';
import mattermostBucket from 'app/mattermost_bucket';
import Config from 'assets/config';
import {getTranslations} from 'app/i18n';
import ExtensionPost from './extension_post';
import {captureExceptionWithoutState, initializeSentry, LOGGER_EXTENSION} from 'app/utils/sentry';
import ErrorMessage from './error_message';
import Extension from './extension';
const {View: AnimatedView} = Animated;
const ShareExtension = NativeModules.MattermostShare;
export default class SharedApp extends PureComponent {
export class SharedApp extends PureComponent {
constructor(props) {
super(props);
const {height, width} = Dimensions.get('window');
const isLandscape = width > height;
initializeSentry();
this.state = {
backdropOpacity: new Animated.Value(0),
containerOpacity: new Animated.Value(0),
isLandscape,
hasError: false,
};
mattermostBucket.readFromFile('entities', Config.AppGroupId).then((value) => {
this.entities = value;
this.setState({init: true});
});
}
componentDidMount() {
Animated.parallel([
Animated.timing(
this.state.backdropOpacity,
{
toValue: 0.5,
duration: 100,
}),
Animated.timing(
this.state.containerOpacity,
{
toValue: 1,
duration: 250,
}),
]).start();
componentDidCatch(error) {
this.setState({hasError: true});
captureExceptionWithoutState(error, LOGGER_EXTENSION);
}
onClose = (data) => {
close = (data) => {
ShareExtension.close(data, Config.AppGroupId);
};
onLayout = (e) => {
const {height, width} = e.nativeEvent.layout;
const isLandscape = width > height;
if (this.state.isLandscape !== isLandscape) {
this.setState({isLandscape});
}
};
userIsLoggedIn = () => {
if (this.entities && this.entities.general && this.entities.general.credentials &&
this.entities.general.credentials.token && this.entities.general.credentials.url) {
return true;
}
return false;
};
render() {
const {init, isLandscape} = this.state;
if (!init) {
return null;
if (this.state.hasError) {
return (
<ErrorMessage close={this.close}/>
);
}
const theme = Preferences.THEMES.default;
const locale = DeviceInfo.getDeviceLocale().split('-')[0];
const initialRoute = {
component: ExtensionPost,
title: 'Mattermost',
passProps: {
authenticated: this.userIsLoggedIn(),
entities: this.entities,
onClose: this.onClose,
isLandscape,
theme,
},
wrapperStyle: {
borderRadius: 10,
backgroundColor: theme.centerChannelBg,
},
};
return (
<IntlProvider
locale={locale}
messages={getTranslations(locale)}
>
<View
style={styles.flex}
onLayout={this.onLayout}
>
<AnimatedView style={[styles.backdrop, {opacity: this.state.backdropOpacity}]}/>
<View style={styles.wrapper}>
<AnimatedView
style={[
styles.container,
{
opacity: this.state.containerOpacity,
height: this.userIsLoggedIn() ? 250 : 130,
top: isLandscape ? 20 : 65,
},
]}
>
<NavigatorIOS
initialRoute={initialRoute}
style={styles.flex}
navigationBarHidden={true}
/>
</AnimatedView>
</View>
</View>
</IntlProvider>
<Extension
appGroupId={Config.AppGroupId}
onClose={this.close}
/>
);
}
}
const styles = StyleSheet.create({
flex: {
flex: 1,
},
backdrop: {
position: 'absolute',
flex: 1,
backgroundColor: '#000',
height: '100%',
width: '100%',
},
wrapper: {
flex: 1,
marginHorizontal: 20,
},
container: {
backgroundColor: 'white',
borderRadius: 10,
position: 'relative',
width: '100%',
},
});
export default function ShareExtensionProvider() {
const locale = DeviceInfo.getDeviceLocale().split('-')[0];
return (
<IntlProvider
locale={locale}
messages={getTranslations(locale)}
>
<SharedApp/>
</IntlProvider>
);
}