[#MM-12410] Custom Terms of Service (#2234)

* [#MM-12410] Custom Terms of Service Modal.

- Create a new screen for terms of service
- Register it in app navigation
- Open it as a modal when login succeeds or app is opened when already logged in.
- Get terms of service and render it as markdown

* Refactor service terms to terms of service

* Show terms modal when launching channel instead of componentDidMount

* Remove footer text

* Close modal before logging out

* Use dismissAllModals instead od this.close

* Update header styles

* Update the back icon

* Add left icon conditinally based on os

* Update en.json

* - Integrate Accept/Reject API
- Refactor show navigator to util
- Remove title
- Code refactoring

* Handle Accept/Reject API errors

* Update package-lock

* - Show Alert to handle API error
- Handle API error for GetTerms error, Accept/Reject error and Reject success
- Changes in en.json
- Remove error banner
- Open terms modal in sso and experimental certificate

* Enable navigator buttons after getting terms

* Rename Ok to OK in Alert

* Add retry for API error

* Fix check-style

* Fix FailedNetworkAction Error Message

* - Use Close button instead of back button
- Update en.json

* Remove unused import

* Disable hashtags and atmentions in terms modal

* Enable close button when terms text fails to load

* Minor changes

* Fix show terms on sso login

* Handle restartApp event

* Add test cases

* Revert change to app/components/formatted_markdown_text.js

* Show the terms modal on the channel page

* Remove remaining references to showTermsOfService

* Disable Hashtags, AtMentions and ChannelLinks

* Update the props to markdown component

* Pass the text as literal to renderText

* Return the generated renderTexts

* Fix lint

* Render ~, #, @ with Channel link, Hashtags and At Mentions

* Fix failing tests
Revert unwanted changes

* Hide the channel page content before the terms are accepted

* Fix lint and failing tests

* Revert changes to hide channel while terms are not accepted

* Open modal in componentDidMount instead of constructor
Code refactoring
Disable back button on android

* Remove unused import

* No need to await for showModal

* No need to await for accept and reject

* Update flag for updateTermsOfServiceStatus to updateMyTermsOfServiceStatus
- as phase 2 redux PR was mered.

* Fix check-style

* Change opacity in terms of service text

- Fix tests

* Revert package-lock.json changes
This commit is contained in:
Chetanya Kandhari 2018-11-14 20:06:45 +05:30 committed by Elias Nahum
parent e1b1635cf1
commit 68eff30eda
15 changed files with 1336 additions and 1 deletions

View file

@ -55,12 +55,18 @@ export default class Markdown extends PureComponent {
textStyles: PropTypes.object,
theme: PropTypes.object.isRequired,
value: PropTypes.string.isRequired,
disableHashtags: PropTypes.bool,
disableAtMentions: PropTypes.bool,
disableChannelLink: PropTypes.bool,
};
static defaultProps = {
textStyles: {},
blockStyles: {},
onLongPress: () => true,
disableHashtags: false,
disableAtMentions: false,
disableChannelLink: false,
};
constructor(props) {
@ -191,6 +197,10 @@ export default class Markdown extends PureComponent {
}
renderAtMention = ({context, mentionName}) => {
if (this.props.disableAtMentions) {
return this.renderText({context, literal: `@${mentionName}`});
}
return (
<AtMention
mentionStyle={this.props.textStyles.mention}
@ -205,6 +215,10 @@ export default class Markdown extends PureComponent {
}
renderChannelLink = ({context, channelName}) => {
if (this.props.disableChannelLink) {
return this.renderText({context, literal: `~${channelName}`});
}
return (
<ChannelLink
linkStyle={this.props.textStyles.link}
@ -225,7 +239,11 @@ export default class Markdown extends PureComponent {
);
}
renderHashtag = ({hashtag}) => {
renderHashtag = ({context, hashtag}) => {
if (this.props.disableHashtags) {
return this.renderText({context, literal: `#${hashtag}`});
}
return (
<Hashtag
hashtag={hashtag}

View file

@ -13,6 +13,7 @@ import {
View,
} from 'react-native';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import EmptyToolbar from 'app/components/start/empty_toolbar';
@ -65,12 +66,18 @@ export default class Channel extends PureComponent {
isLandscape: PropTypes.bool,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired,
showTermsOfService: PropTypes.bool,
disableTermsModal: PropTypes.bool,
};
static contextTypes = {
intl: intlShape.isRequired,
};
static defaultProps = {
disableTermsModal: false,
};
constructor(props) {
super(props);
@ -98,6 +105,10 @@ export default class Channel extends PureComponent {
this.props.actions.recordLoadTime('Start time', 'initialLoad');
}
if (this.props.showTermsOfService && !this.props.disableTermsModal) {
this.showTermsOfServiceModal();
}
EventEmitter.emit('renderDrawer');
}
@ -188,6 +199,29 @@ export default class Channel extends PureComponent {
}
};
showTermsOfServiceModal = async () => {
const {navigator, theme} = this.props;
const closeButton = await MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor);
navigator.showModal({
screen: 'TermsOfService',
animationType: 'slide-up',
title: '',
backButtonTitle: '',
animated: true,
navigatorStyle: {
navBarTextColor: theme.centerChannelColor,
navBarBackgroundColor: theme.centerChannelBg,
navBarButtonColor: theme.buttonBg,
screenBackgroundColor: theme.centerChannelBg,
modalPresentationStyle: 'overCurrentContext',
},
overrideBackPress: true,
passProps: {
closeButton,
},
});
};
goToChannelInfo = preventDoubleTap(() => {
const {intl} = this.context;
const {navigator, theme} = this.props;

View file

@ -10,6 +10,7 @@ import {RequestStatus} from 'mattermost-redux/constants';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {shouldShowTermsOfService} from 'mattermost-redux/selectors/entities/users';
import {
loadChannelsIfNecessary,
@ -32,6 +33,7 @@ function mapStateToProps(state) {
currentChannelId: getCurrentChannelId(state),
isLandscape: isLandscape(state),
theme: getTheme(state),
showTermsOfService: shouldShowTermsOfService(state),
};
}

View file

@ -63,6 +63,9 @@ export default class ErrorTeamsList extends PureComponent {
statusBarHideWithNavBar: false,
screenBackgroundColor: theme.centerChannelBg,
},
passProps: {
disableTermsModal: true,
},
});
};

View file

@ -57,6 +57,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('SSO', () => wrapWithContextProvider(require('app/screens/sso').default), store, Provider);
Navigation.registerComponent('Table', () => wrapWithContextProvider(require('app/screens/table').default), store, Provider);
Navigation.registerComponent('TableImage', () => wrapWithContextProvider(require('app/screens/table_image').default), store, Provider);
Navigation.registerComponent('TermsOfService', () => wrapWithContextProvider(require('app/screens/terms_of_service').default), store, Provider);
Navigation.registerComponent('TextPreview', () => wrapWithContextProvider(require('app/screens/text_preview').default), store, Provider);
Navigation.registerComponent('Thread', () => wrapWithContextProvider(require('app/screens/thread').default), store, Provider);
Navigation.registerComponent('TimezoneSettings', () => wrapWithContextProvider(require('app/screens/timezone').default), store, Provider);

View file

@ -253,6 +253,9 @@ export default class Permalink extends PureComponent {
statusBarHideWithNavBar: false,
screenBackgroundColor: theme.centerChannelBg,
},
passProps: {
disableTermsModal: true,
},
});
}

View file

@ -126,6 +126,9 @@ export default class SelectTeam extends PureComponent {
statusBarHideWithNavBar: false,
screenBackgroundColor: theme.centerChannelBg,
},
passProps: {
disableTermsModal: true,
},
});
};

View file

@ -0,0 +1,783 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TermsOfService should enable/disable navigator buttons on setNavigatorButtons true/false 1`] = `
<React.Fragment>
<Connect(StatusBar) />
<ScrollView
contentContainerStyle={
Object {
"paddingBottom": 50,
}
}
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"padding": 30,
}
}
>
<Connect(Markdown)
baseTextStyle={
Object {
"color": "#3d3c40",
"fontSize": 15,
"lineHeight": 20,
}
}
blockStyles={
Object {
"adjacentParagraph": Object {
"marginTop": 6,
},
"horizontalRule": Object {
"backgroundColor": "#3d3c40",
"flex": 1,
"height": 0.5,
"marginVertical": 10,
},
"quoteBlockIcon": Object {
"color": undefined,
"padding": 5,
},
}
}
disableAtMentions={true}
disableChannelLink={true}
disableHashtags={true}
navigator={
Object {
"dismissAllModals": [MockFunction],
"dismissModal": [MockFunction],
"setButtons": [MockFunction] {
"calls": Array [
Array [
Object {
"leftButtons": Array [
Object {
"disabled": true,
"icon": Object {},
"id": "reject-terms-of-service",
},
],
"rightButtons": Array [
Object {
"disabled": true,
"id": "accept-terms-of-service",
"showAsAction": "always",
"title": undefined,
},
],
},
],
Array [
Object {
"leftButtons": Array [
Object {
"disabled": true,
"icon": Object {},
"id": "reject-terms-of-service",
},
],
"rightButtons": Array [
Object {
"disabled": true,
"id": "accept-terms-of-service",
"showAsAction": "always",
"title": undefined,
},
],
},
],
Array [
Object {
"leftButtons": Array [
Object {
"disabled": false,
"icon": Object {},
"id": "reject-terms-of-service",
},
],
"rightButtons": Array [
Object {
"disabled": false,
"id": "accept-terms-of-service",
"showAsAction": "always",
"title": undefined,
},
],
},
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
Object {
"isThrow": false,
"value": undefined,
},
Object {
"isThrow": false,
"value": undefined,
},
],
},
"setOnNavigatorEvent": [MockFunction] {
"calls": Array [
Array [
[Function],
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
],
},
}
}
textStyles={
Object {
"code": Object {
"alignSelf": "center",
"backgroundColor": undefined,
"fontFamily": "Menlo",
},
"codeBlock": Object {
"fontFamily": "Menlo",
},
"del": Object {
"textDecorationLine": "line-through",
},
"emph": Object {
"fontStyle": "italic",
},
"error": Object {
"color": "#fd5960",
},
"heading1": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading1Text": Object {
"paddingBottom": 8,
},
"heading2": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading2Text": Object {
"paddingBottom": 8,
},
"heading3": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading3Text": Object {
"paddingBottom": 8,
},
"heading4": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading4Text": Object {
"paddingBottom": 8,
},
"heading5": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading5Text": Object {
"paddingBottom": 8,
},
"heading6": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading6Text": Object {
"paddingBottom": 8,
},
"link": Object {
"color": "#2389d7",
},
"mention": Object {
"color": "#2389d7",
},
"mention_highlight": Object {
"backgroundColor": "#ffe577",
},
"strong": Object {
"fontWeight": "bold",
},
"table_header_row": Object {
"fontWeight": "700",
},
}
}
value="Terms Text"
/>
</ScrollView>
</React.Fragment>
`;
exports[`TermsOfService should enable/disable navigator buttons on setNavigatorButtons true/false 2`] = `
<React.Fragment>
<Connect(StatusBar) />
<ScrollView
contentContainerStyle={
Object {
"paddingBottom": 50,
}
}
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"padding": 30,
}
}
>
<Connect(Markdown)
baseTextStyle={
Object {
"color": "#3d3c40",
"fontSize": 15,
"lineHeight": 20,
}
}
blockStyles={
Object {
"adjacentParagraph": Object {
"marginTop": 6,
},
"horizontalRule": Object {
"backgroundColor": "#3d3c40",
"flex": 1,
"height": 0.5,
"marginVertical": 10,
},
"quoteBlockIcon": Object {
"color": undefined,
"padding": 5,
},
}
}
disableAtMentions={true}
disableChannelLink={true}
disableHashtags={true}
navigator={
Object {
"dismissAllModals": [MockFunction],
"dismissModal": [MockFunction],
"setButtons": [MockFunction] {
"calls": Array [
Array [
Object {
"leftButtons": Array [
Object {
"disabled": true,
"icon": Object {},
"id": "reject-terms-of-service",
},
],
"rightButtons": Array [
Object {
"disabled": true,
"id": "accept-terms-of-service",
"showAsAction": "always",
"title": undefined,
},
],
},
],
Array [
Object {
"leftButtons": Array [
Object {
"disabled": true,
"icon": Object {},
"id": "reject-terms-of-service",
},
],
"rightButtons": Array [
Object {
"disabled": true,
"id": "accept-terms-of-service",
"showAsAction": "always",
"title": undefined,
},
],
},
],
Array [
Object {
"leftButtons": Array [
Object {
"disabled": false,
"icon": Object {},
"id": "reject-terms-of-service",
},
],
"rightButtons": Array [
Object {
"disabled": false,
"id": "accept-terms-of-service",
"showAsAction": "always",
"title": undefined,
},
],
},
],
Array [
Object {
"leftButtons": Array [
Object {
"disabled": true,
"icon": Object {},
"id": "reject-terms-of-service",
},
],
"rightButtons": Array [
Object {
"disabled": true,
"id": "accept-terms-of-service",
"showAsAction": "always",
"title": undefined,
},
],
},
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
Object {
"isThrow": false,
"value": undefined,
},
Object {
"isThrow": false,
"value": undefined,
},
Object {
"isThrow": false,
"value": undefined,
},
],
},
"setOnNavigatorEvent": [MockFunction] {
"calls": Array [
Array [
[Function],
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
],
},
}
}
textStyles={
Object {
"code": Object {
"alignSelf": "center",
"backgroundColor": undefined,
"fontFamily": "Menlo",
},
"codeBlock": Object {
"fontFamily": "Menlo",
},
"del": Object {
"textDecorationLine": "line-through",
},
"emph": Object {
"fontStyle": "italic",
},
"error": Object {
"color": "#fd5960",
},
"heading1": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading1Text": Object {
"paddingBottom": 8,
},
"heading2": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading2Text": Object {
"paddingBottom": 8,
},
"heading3": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading3Text": Object {
"paddingBottom": 8,
},
"heading4": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading4Text": Object {
"paddingBottom": 8,
},
"heading5": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading5Text": Object {
"paddingBottom": 8,
},
"heading6": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading6Text": Object {
"paddingBottom": 8,
},
"link": Object {
"color": "#2389d7",
},
"mention": Object {
"color": "#2389d7",
},
"mention_highlight": Object {
"backgroundColor": "#ffe577",
},
"strong": Object {
"fontWeight": "bold",
},
"table_header_row": Object {
"fontWeight": "700",
},
}
}
value="Terms Text"
/>
</ScrollView>
</React.Fragment>
`;
exports[`TermsOfService should match snapshot 1`] = `
<Loading
color="grey"
size="large"
style={Object {}}
/>
`;
exports[`TermsOfService should match snapshot for fail of get terms 1`] = `
<Component
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
}
}
>
<Connect(StatusBar) />
<FailedNetworkAction
errorDescription={
Object {
"defaultMessage": "Make sure you have an active internet connection and try again. If this issue persists, contact your System Administrator.",
"id": "mobile.terms_of_service.get_terms_error_description",
}
}
errorTitle={
Object {
"defaultMessage": "Unable to load terms of service.",
"id": "mobile.terms_of_service.get_terms_error_title",
}
}
onRetry={[Function]}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
</Component>
`;
exports[`TermsOfService should match snapshot for get terms 1`] = `
<Loading
color="grey"
size="large"
style={Object {}}
/>
`;
exports[`TermsOfService should match snapshot on enableNavigatorLogout 1`] = `
<React.Fragment>
<Connect(StatusBar) />
<ScrollView
contentContainerStyle={
Object {
"paddingBottom": 50,
}
}
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"padding": 30,
}
}
>
<Connect(Markdown)
baseTextStyle={
Object {
"color": "#3d3c40",
"fontSize": 15,
"lineHeight": 20,
}
}
blockStyles={
Object {
"adjacentParagraph": Object {
"marginTop": 6,
},
"horizontalRule": Object {
"backgroundColor": "#3d3c40",
"flex": 1,
"height": 0.5,
"marginVertical": 10,
},
"quoteBlockIcon": Object {
"color": undefined,
"padding": 5,
},
}
}
disableAtMentions={true}
disableChannelLink={true}
disableHashtags={true}
navigator={
Object {
"dismissAllModals": [MockFunction],
"dismissModal": [MockFunction],
"setButtons": [MockFunction] {
"calls": Array [
Array [
Object {
"leftButtons": Array [
Object {
"disabled": true,
"icon": Object {},
"id": "reject-terms-of-service",
},
],
"rightButtons": Array [
Object {
"disabled": true,
"id": "accept-terms-of-service",
"showAsAction": "always",
"title": undefined,
},
],
},
],
Array [
Object {
"leftButtons": Array [
Object {
"disabled": true,
"icon": Object {},
"id": "reject-terms-of-service",
},
],
"rightButtons": Array [
Object {
"disabled": true,
"id": "accept-terms-of-service",
"showAsAction": "always",
"title": undefined,
},
],
},
],
Array [
Object {
"leftButtons": Array [
Object {
"disabled": false,
"icon": Object {},
"id": "close-terms-of-service",
},
],
"rightButtons": Array [
Object {
"disabled": true,
"id": "accept-terms-of-service",
"showAsAction": "always",
"title": undefined,
},
],
},
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
Object {
"isThrow": false,
"value": undefined,
},
Object {
"isThrow": false,
"value": undefined,
},
],
},
"setOnNavigatorEvent": [MockFunction] {
"calls": Array [
Array [
[Function],
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
],
},
}
}
textStyles={
Object {
"code": Object {
"alignSelf": "center",
"backgroundColor": undefined,
"fontFamily": "Menlo",
},
"codeBlock": Object {
"fontFamily": "Menlo",
},
"del": Object {
"textDecorationLine": "line-through",
},
"emph": Object {
"fontStyle": "italic",
},
"error": Object {
"color": "#fd5960",
},
"heading1": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading1Text": Object {
"paddingBottom": 8,
},
"heading2": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading2Text": Object {
"paddingBottom": 8,
},
"heading3": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading3Text": Object {
"paddingBottom": 8,
},
"heading4": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading4Text": Object {
"paddingBottom": 8,
},
"heading5": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading5Text": Object {
"paddingBottom": 8,
},
"heading6": Object {
"fontSize": 17,
"fontWeight": "700",
"lineHeight": 25,
},
"heading6Text": Object {
"paddingBottom": 8,
},
"link": Object {
"color": "#2389d7",
},
"mention": Object {
"color": "#2389d7",
},
"mention_highlight": Object {
"backgroundColor": "#ffe577",
},
"strong": Object {
"fontWeight": "bold",
},
"table_header_row": Object {
"fontWeight": "700",
},
}
}
value="Terms Text"
/>
</ScrollView>
</React.Fragment>
`;

View file

@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getTermsOfService, logout, updateMyTermsOfServiceStatus} from 'mattermost-redux/actions/users';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import TermsOfService from './terms_of_service.js';
function mapStateToProps(state) {
const config = getConfig(state);
return {
siteName: config.SiteName,
theme: getTheme(state),
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getTermsOfService,
logout,
updateMyTermsOfServiceStatus,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(TermsOfService);

View file

@ -0,0 +1,305 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Alert,
ScrollView,
View,
} from 'react-native';
import {intlShape} from 'react-intl';
import FailedNetworkAction from 'app/components/failed_network_action';
import Loading from 'app/components/loading';
import Markdown from 'app/components/markdown';
import StatusBar from 'app/components/status_bar';
import {t} from 'app/utils/i18n';
import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown';
import {makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
const errorTitle = {
id: t('mobile.terms_of_service.get_terms_error_title'),
defaultMessage: 'Unable to load terms of service.',
};
const errorDescription = {
id: t('mobile.terms_of_service.get_terms_error_description'),
defaultMessage: 'Make sure you have an active internet connection and try again. If this issue persists, contact your System Administrator.',
};
export default class TermsOfService extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
logout: PropTypes.func.isRequired,
getTermsOfService: PropTypes.func.isRequired,
updateMyTermsOfServiceStatus: PropTypes.func.isRequired,
}).isRequired,
closeButton: PropTypes.object,
navigator: PropTypes.object,
siteName: PropTypes.string,
theme: PropTypes.object,
};
static contextTypes = {
intl: intlShape,
};
static defaultProps = {
siteName: 'Mattermost',
};
leftButton = {
id: 'reject-terms-of-service',
};
rightButton = {
id: 'accept-terms-of-service',
showAsAction: 'always',
};
constructor(props, context) {
super(props);
this.state = {
getTermsError: false,
loading: true,
termsId: '',
termsText: '',
};
this.rightButton.title = context.intl.formatMessage({id: 'terms_of_service.agreeButton', defaultMessage: 'I Agree'});
this.leftButton.icon = props.closeButton;
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
this.setNavigatorButtons(false);
}
componentDidMount() {
this.getTerms();
}
componentDidUpdate(prevProps) {
if (this.props.theme !== prevProps.theme) {
setNavigatorStyles(this.props.navigator, this.props.theme);
}
}
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
switch (event.id) {
case 'close-terms-of-service':
this.closeTermsAndLogout();
break;
case 'reject-terms-of-service':
this.handleRejectTerms();
break;
case 'accept-terms-of-service':
this.handleAcceptTerms();
break;
}
}
};
setNavigatorButtons = (enabled = true) => {
const buttons = {
leftButtons: [{...this.leftButton, disabled: !enabled}],
rightButtons: [{...this.rightButton, disabled: !enabled}],
};
this.props.navigator.setButtons(buttons);
};
enableNavigatorLogout = () => {
const buttons = {
leftButtons: [{...this.leftButton, id: 'close-terms-of-service', disabled: false}],
rightButtons: [{...this.rightButton, disabled: true}],
};
this.props.navigator.setButtons(buttons);
};
closeTermsAndLogout = () => {
const {actions} = this.props;
this.props.navigator.dismissAllModals();
actions.logout();
};
getTerms = async () => {
const {actions} = this.props;
this.setState({
termsId: '',
termsText: '',
loading: true,
getTermsError: false,
});
this.setNavigatorButtons(false);
const {data} = await actions.getTermsOfService();
if (data) {
this.setState({
termsId: data.id,
termsText: data.text,
loading: false,
}, () => {
this.setNavigatorButtons(true);
});
} else {
this.setState({
loading: false,
getTermsError: true,
});
this.enableNavigatorLogout();
}
};
handleAcceptTerms = () => {
this.registerUserAction(
true,
() => {
this.props.navigator.dismissModal({
animationType: 'slide-down',
});
},
this.handleAcceptTerms
);
};
handleRejectTerms = () => {
const {siteName} = this.props;
const {intl} = this.context;
this.registerUserAction(
false,
() => {
Alert.alert(
this.props.siteName,
intl.formatMessage({
id: 'mobile.terms_of_service.terms_rejected',
defaultMessage: 'You must agree to the terms of service before accessing {siteName}. Please contact your System Administrator for more details.',
}, {
siteName,
}),
[{
text: intl.formatMessage({id: 'mobile.terms_of_service.alert_ok', defaultMessage: 'Ok'}),
onPress: this.closeTermsAndLogout,
}],
);
},
this.handleRejectTerms
);
};
registerUserAction = async (accepted, success, retry) => {
const {actions} = this.props;
const {intl} = this.context;
this.setNavigatorButtons(false);
this.setState({
loading: true,
});
const {data} = await actions.updateMyTermsOfServiceStatus(this.state.termsId, accepted);
this.setState({
loading: false,
});
if (data) {
success(data);
this.setNavigatorButtons(true);
} else {
Alert.alert(
this.props.siteName,
intl.formatMessage({
id: 'terms_of_service.api_error',
defaultMessage: 'Unable to complete the request. If this issue persists, contact your System Administrator.',
}),
[{
text: intl.formatMessage({id: 'mobile.terms_of_service.alert_retry', defaultMessage: 'Try Again'}),
onPress: retry,
}, {
text: intl.formatMessage({id: 'mobile.terms_of_service.alert_cancel', defaultMessage: 'Cancel'}),
onPress: this.closeTermsAndLogout,
}],
);
}
};
render() {
const {navigator, theme} = this.props;
const styles = getStyleSheet(theme);
const blockStyles = getMarkdownBlockStyles(theme);
const textStyles = getMarkdownTextStyles(theme);
if (this.state.loading) {
return <Loading/>;
}
if (this.state.getTermsError) {
return (
<View style={styles.container}>
<StatusBar/>
<FailedNetworkAction
onRetry={this.getTerms}
theme={theme}
errorTitle={errorTitle}
errorDescription={errorDescription}
/>
</View>
);
}
return (
<React.Fragment>
<StatusBar/>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollViewContent}
>
<Markdown
baseTextStyle={styles.baseText}
navigator={navigator}
textStyles={textStyles}
blockStyles={blockStyles}
value={this.state.termsText}
disableHashtags={true}
disableAtMentions={true}
disableChannelLink={true}
/>
</ScrollView>
</React.Fragment>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
baseText: {
color: theme.centerChannelColor,
fontSize: 15,
lineHeight: 20,
},
container: {
backgroundColor: theme.centerChannelBg,
flex: 1,
},
linkText: {
color: theme.linkColor,
},
scrollView: {
flex: 1,
backgroundColor: theme.centerChannelBg,
padding: 30,
},
scrollViewContent: {
paddingBottom: 50,
},
};
});

View file

@ -0,0 +1,134 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import TermsOfService from './terms_of_service.js';
jest.mock('react-intl');
jest.mock('app/utils/theme', () => {
const original = require.requireActual('app/utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
describe('TermsOfService', () => {
const actions = {
getTermsOfService: jest.fn(),
updateMyTermsOfServiceStatus: jest.fn(),
logout: jest.fn(),
};
const baseProps = {
actions,
navigator: {
dismissAllModals: jest.fn(),
dismissModal: jest.fn(),
setButtons: jest.fn(),
setOnNavigatorEvent: jest.fn(),
},
theme: Preferences.THEMES.default,
closeButton: {},
siteName: 'Mattermost',
};
test('should match snapshot', () => {
const wrapper = shallow(
<TermsOfService {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for get terms', async () => {
const wrapper = shallow(
<TermsOfService {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
await actions.getTermsOfService();
wrapper.update();
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for fail of get terms', async () => {
const getTermsOfService = async () => {
return {
error: {},
};
};
const props = {
...baseProps,
actions: {
...actions,
getTermsOfService,
},
};
const wrapper = shallow(
<TermsOfService {...props}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.state('loading')).toEqual(true);
await getTermsOfService();
expect(wrapper.state('loading')).toEqual(false);
wrapper.update();
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should call props.navigator.setButtons on setNavigatorButtons', async () => {
const wrapper = shallow(
<TermsOfService {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
wrapper.setState({loading: false, termsId: 1, termsText: 'Terms Text'});
expect(baseProps.navigator.setButtons).toHaveBeenCalledTimes(2);
wrapper.instance().setNavigatorButtons(true);
expect(baseProps.navigator.setButtons).toHaveBeenCalledTimes(3);
wrapper.instance().setNavigatorButtons(false);
expect(baseProps.navigator.setButtons).toHaveBeenCalledTimes(4);
});
test('should enable/disable navigator buttons on setNavigatorButtons true/false', () => {
const wrapper = shallow(
<TermsOfService {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
wrapper.setState({loading: false, termsId: 1, termsText: 'Terms Text'});
wrapper.instance().setNavigatorButtons(true);
expect(wrapper.getElement()).toMatchSnapshot();
wrapper.instance().setNavigatorButtons(false);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot on enableNavigatorLogout', () => {
const wrapper = shallow(
<TermsOfService {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
wrapper.setState({loading: false, termsId: 1, termsText: 'Terms Text'});
wrapper.instance().enableNavigatorLogout();
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should call dismissAllModals on closeTermsAndLogout', () => {
const wrapper = shallow(
<TermsOfService {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
wrapper.setState({loading: false, termsId: 1, termsText: 'Terms Text'});
wrapper.instance().closeTermsAndLogout();
expect(baseProps.navigator.dismissAllModals).toHaveBeenCalledTimes(1);
});
});

View file

@ -120,6 +120,9 @@ export default class Thread extends PureComponent {
statusBarHideWithNavBar: false,
screenBackgroundColor: 'transparent',
},
passProps: {
disableTermsModal: true,
},
});
};

View file

@ -69,6 +69,9 @@ describe('thread', () => {
statusBarHideWithNavBar: false,
screenBackgroundColor: 'transparent',
},
passProps: {
disableTermsModal: true,
},
};
const newNavigator = {...navigator};
const wrapper = shallow(

View file

@ -65,6 +65,9 @@ export default class UserProfile extends PureComponent {
statusBarHideWithNavBar: false,
screenBackgroundColor: theme.centerChannelBg,
},
passProps: {
disableTermsModal: true,
},
});
};

View file

@ -415,6 +415,12 @@
"mobile.share_extension.send": "Send",
"mobile.share_extension.team": "Team",
"mobile.suggestion.members": "Members",
"mobile.terms_of_service.alert_cancel": "Cancel",
"mobile.terms_of_service.alert_ok": "OK",
"mobile.terms_of_service.alert_retry": "Try Again",
"mobile.terms_of_service.get_terms_error_description": "Make sure you have an active internet connection and try again. If this issue persists, contact your System Administrator.",
"mobile.terms_of_service.get_terms_error_title": "Unable to load terms of service.",
"mobile.terms_of_service.terms_rejected": "You must agree to the terms of service before accessing {siteName}. Please contact your System Administrator for more details.",
"mobile.timezone_settings.automatically": "Set automatically",
"mobile.timezone_settings.manual": "Change timezone",
"mobile.timezone_settings.select": "Select Timezone",
@ -499,6 +505,8 @@
"suggestion.search.direct": "Direct Messages",
"suggestion.search.private": "Private Channels",
"suggestion.search.public": "Public Channels",
"terms_of_service.agreeButton": "I Agree",
"terms_of_service.api_error": "Unable to complete the request. If this issue persists, contact your System Administrator.",
"user.settings.display.clockDisplay": "Clock Display",
"user.settings.display.militaryClock": "24-hour clock (example: 16:00)",
"user.settings.display.normalClock": "12-hour clock (example: 4:00 PM)",