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:
parent
033a0c15bd
commit
d9f2bca9c0
8 changed files with 346 additions and 135 deletions
|
|
@ -259,4 +259,4 @@
|
|||
"yoda": [2, "never", {"exceptRange": false, "onlyEquality": false}],
|
||||
"mocha/no-exclusive-tests": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
58
share_extension/android/extension.js
Normal file
58
share_extension/android/extension.js
Normal 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/>;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
85
share_extension/ios/error_message.js
Normal file
85
share_extension/ios/error_message.js
Normal 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),
|
||||
},
|
||||
};
|
||||
});
|
||||
150
share_extension/ios/extension.js
Normal file
150
share_extension/ios/extension.js
Normal 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%',
|
||||
},
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue