aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
authorLibravatar Amine Mouafik <amine@mouafik.fr>2019-05-12 20:00:41 +0700
committerLibravatar Amine Mouafik <amine@mouafik.fr>2019-05-12 20:00:41 +0700
commitd8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61 (patch)
tree3974d449d8ef389fc61bf880ae758b5debc22a80 /src/components
parentUse dark background in SVG logo (diff)
parentUpdate CHANGELOG.md (diff)
downloadferdium-app-d8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61.tar.gz
ferdium-app-d8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61.tar.zst
ferdium-app-d8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61.zip
Merge tag 'v5.1.0'
# Conflicts: # README.md # src/components/layout/AppLayout.js
Diffstat (limited to 'src/components')
-rw-r--r--src/components/layout/AppLayout.js78
-rw-r--r--src/components/layout/Sidebar.js49
-rw-r--r--src/components/services/content/ServiceView.js139
-rw-r--r--src/components/services/content/ServiceWebview.js145
-rw-r--r--src/components/services/content/Services.js7
-rw-r--r--src/components/services/content/WebviewCrashHandler.js7
-rw-r--r--src/components/services/tabs/Tabbar.js4
-rw-r--r--src/components/settings/account/AccountDashboard.js199
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js46
-rw-r--r--src/components/settings/services/EditServiceForm.js16
-rw-r--r--src/components/settings/services/ServicesDashboard.js2
-rw-r--r--src/components/settings/settings/EditSettingsForm.js9
-rw-r--r--src/components/settings/team/TeamDashboard.js152
-rw-r--r--src/components/settings/user/EditUserForm.js25
-rw-r--r--src/components/subscription/SubscriptionForm.js67
-rw-r--r--src/components/subscription/SubscriptionPopup.js3
-rw-r--r--src/components/ui/AppLoader/index.js4
-rw-r--r--src/components/ui/FullscreenLoader/index.js4
-rw-r--r--src/components/ui/Infobox.js17
-rw-r--r--src/components/ui/Modal/index.js26
-rw-r--r--src/components/ui/Modal/styles.js1
-rw-r--r--src/components/ui/PremiumFeatureContainer/index.js21
-rw-r--r--src/components/ui/PremiumFeatureContainer/styles.js5
-rw-r--r--src/components/ui/ServiceIcon.js67
-rw-r--r--src/components/ui/WebviewLoader/index.js18
25 files changed, 747 insertions, 364 deletions
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index a1641bc4a..d0476ef04 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -3,14 +3,20 @@ import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import { TitleBar } from 'electron-react-titlebar'; 5import { TitleBar } from 'electron-react-titlebar';
6import injectSheet from 'react-jss';
6 7
7import InfoBar from '../ui/InfoBar'; 8import InfoBar from '../ui/InfoBar';
8import { Component as BasicAuth } from '../../features/basicAuth'; 9import { Component as BasicAuth } from '../../features/basicAuth';
10import { Component as ShareFranz } from '../../features/shareFranz';
9import ErrorBoundary from '../util/ErrorBoundary'; 11import ErrorBoundary from '../util/ErrorBoundary';
10 12
11// import globalMessages from '../../i18n/globalMessages'; 13// import globalMessages from '../../i18n/globalMessages';
12 14
13import { isWindows } from '../../environment'; 15import { isWindows } from '../../environment';
16import AnnouncementScreen from '../../features/announcements/components/AnnouncementScreen';
17import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator';
18import { workspaceStore } from '../../features/workspaces';
19import { announcementActions } from '../../features/announcements/actions';
14 20
15function createMarkup(HTMLString) { 21function createMarkup(HTMLString) {
16 return { __html: HTMLString }; 22 return { __html: HTMLString };
@@ -43,18 +49,30 @@ const messages = defineMessages({
43 }, 49 },
44}); 50});
45 51
46export default 52const styles = theme => ({
47@observer 53 appContent: {
54 width: `calc(100% + ${theme.workspaces.drawer.width}px)`,
55 transition: 'transform 0.5s ease',
56 transform() {
57 return workspaceStore.isWorkspaceDrawerOpen ? 'translateX(0)' : `translateX(-${theme.workspaces.drawer.width}px)`;
58 },
59 },
60});
61
62@injectSheet(styles) @observer
48class AppLayout extends Component { 63class AppLayout extends Component {
49 static propTypes = { 64 static propTypes = {
65 classes: PropTypes.object.isRequired,
50 isFullScreen: PropTypes.bool.isRequired, 66 isFullScreen: PropTypes.bool.isRequired,
51 sidebar: PropTypes.element.isRequired, 67 sidebar: PropTypes.element.isRequired,
68 workspacesDrawer: PropTypes.element.isRequired,
52 services: PropTypes.element.isRequired, 69 services: PropTypes.element.isRequired,
53 children: PropTypes.element, 70 children: PropTypes.element,
54 news: MobxPropTypes.arrayOrObservableArray.isRequired, 71 news: MobxPropTypes.arrayOrObservableArray.isRequired,
55 // isOnline: PropTypes.bool.isRequired, 72 // isOnline: PropTypes.bool.isRequired,
56 showServicesUpdatedInfoBar: PropTypes.bool.isRequired, 73 showServicesUpdatedInfoBar: PropTypes.bool.isRequired,
57 appUpdateIsDownloaded: PropTypes.bool.isRequired, 74 appUpdateIsDownloaded: PropTypes.bool.isRequired,
75 nextAppReleaseVersion: PropTypes.string,
58 removeNewsItem: PropTypes.func.isRequired, 76 removeNewsItem: PropTypes.func.isRequired,
59 reloadServicesAfterUpdate: PropTypes.func.isRequired, 77 reloadServicesAfterUpdate: PropTypes.func.isRequired,
60 installAppUpdate: PropTypes.func.isRequired, 78 installAppUpdate: PropTypes.func.isRequired,
@@ -63,10 +81,13 @@ class AppLayout extends Component {
63 retryRequiredRequests: PropTypes.func.isRequired, 81 retryRequiredRequests: PropTypes.func.isRequired,
64 areRequiredRequestsLoading: PropTypes.bool.isRequired, 82 areRequiredRequestsLoading: PropTypes.bool.isRequired,
65 darkMode: PropTypes.bool.isRequired, 83 darkMode: PropTypes.bool.isRequired,
84 isDelayAppScreenVisible: PropTypes.bool.isRequired,
85 isAnnouncementVisible: PropTypes.bool.isRequired,
66 }; 86 };
67 87
68 static defaultProps = { 88 static defaultProps = {
69 children: [], 89 children: [],
90 nextAppReleaseVersion: null,
70 }; 91 };
71 92
72 static contextTypes = { 93 static contextTypes = {
@@ -75,7 +96,9 @@ class AppLayout extends Component {
75 96
76 render() { 97 render() {
77 const { 98 const {
99 classes,
78 isFullScreen, 100 isFullScreen,
101 workspacesDrawer,
79 sidebar, 102 sidebar,
80 services, 103 services,
81 children, 104 children,
@@ -83,6 +106,7 @@ class AppLayout extends Component {
83 news, 106 news,
84 showServicesUpdatedInfoBar, 107 showServicesUpdatedInfoBar,
85 appUpdateIsDownloaded, 108 appUpdateIsDownloaded,
109 nextAppReleaseVersion,
86 removeNewsItem, 110 removeNewsItem,
87 reloadServicesAfterUpdate, 111 reloadServicesAfterUpdate,
88 installAppUpdate, 112 installAppUpdate,
@@ -91,6 +115,8 @@ class AppLayout extends Component {
91 retryRequiredRequests, 115 retryRequiredRequests,
92 areRequiredRequestsLoading, 116 areRequiredRequestsLoading,
93 darkMode, 117 darkMode,
118 isDelayAppScreenVisible,
119 isAnnouncementVisible,
94 } = this.props; 120 } = this.props;
95 121
96 const { intl } = this.context; 122 const { intl } = this.context;
@@ -99,29 +125,23 @@ class AppLayout extends Component {
99 <ErrorBoundary> 125 <ErrorBoundary>
100 <div className={darkMode ? 'theme__dark' : ''}> 126 <div className={darkMode ? 'theme__dark' : ''}>
101 <div className="app"> 127 <div className="app">
102 {isWindows && !isFullScreen && ( 128 {isWindows && !isFullScreen && <TitleBar menu={window.franz.menu.template} icon="assets/images/logo.svg" />}
103 <TitleBar 129 <div className={`app__content ${classes.appContent}`}>
104 menu={window.franz.menu.template} 130 {workspacesDrawer}
105 icon="assets/images/logo.svg"
106 />
107 )}
108 <div className="app__content">
109 {sidebar} 131 {sidebar}
110 <div className="app__service"> 132 <div className="app__service">
111 {news.length > 0 133 <WorkspaceSwitchingIndicator />
112 && news.map(item => ( 134 {news.length > 0 && news.map(item => (
113 <InfoBar 135 <InfoBar
114 key={item.id} 136 key={item.id}
115 position="top" 137 position="top"
116 type={item.type} 138 type={item.type}
117 sticky={item.sticky} 139 sticky={item.sticky}
118 onHide={() => removeNewsItem({ newsId: item.id })} 140 onHide={() => removeNewsItem({ newsId: item.id })}
119 > 141 >
120 <span 142 <span dangerouslySetInnerHTML={createMarkup(item.message)} />
121 dangerouslySetInnerHTML={createMarkup(item.message)} 143 </InfoBar>
122 /> 144 ))}
123 </InfoBar>
124 ))}
125 {/* {!isOnline && ( 145 {/* {!isOnline && (
126 <InfoBar 146 <InfoBar
127 type="danger" 147 type="danger"
@@ -164,12 +184,18 @@ class AppLayout extends Component {
164 <span className="mdi mdi-information" /> 184 <span className="mdi mdi-information" />
165 {intl.formatMessage(messages.updateAvailable)} 185 {intl.formatMessage(messages.updateAvailable)}
166 {' '} 186 {' '}
167 <a href="https://meetfranz.com/changelog" target="_blank"> 187 <button
188 className="info-bar__inline-button"
189 type="button"
190 onClick={() => announcementActions.show({ targetVersion: nextAppReleaseVersion })}
191 >
168 <u>{intl.formatMessage(messages.changelog)}</u> 192 <u>{intl.formatMessage(messages.changelog)}</u>
169 </a> 193 </button>
170 </InfoBar> 194 </InfoBar>
171 )} 195 )}
172 <BasicAuth /> 196 <BasicAuth />
197 <ShareFranz />
198 {isAnnouncementVisible && (<AnnouncementScreen />)}
173 {services} 199 {services}
174 </div> 200 </div>
175 </div> 201 </div>
@@ -180,3 +206,5 @@ class AppLayout extends Component {
180 ); 206 );
181 } 207 }
182} 208}
209
210export default AppLayout;
diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js
index 609a3b604..36c1f2e39 100644
--- a/src/components/layout/Sidebar.js
+++ b/src/components/layout/Sidebar.js
@@ -6,6 +6,8 @@ import { observer } from 'mobx-react';
6 6
7import Tabbar from '../services/tabs/Tabbar'; 7import Tabbar from '../services/tabs/Tabbar';
8import { ctrlKey } from '../../environment'; 8import { ctrlKey } from '../../environment';
9import { GA_CATEGORY_WORKSPACES, workspaceStore } from '../../features/workspaces';
10import { gaEvent } from '../../lib/analytics';
9 11
10const messages = defineMessages({ 12const messages = defineMessages({
11 settings: { 13 settings: {
@@ -24,6 +26,14 @@ const messages = defineMessages({
24 id: 'sidebar.unmuteApp', 26 id: 'sidebar.unmuteApp',
25 defaultMessage: '!!!Enable notifications & audio', 27 defaultMessage: '!!!Enable notifications & audio',
26 }, 28 },
29 openWorkspaceDrawer: {
30 id: 'sidebar.openWorkspaceDrawer',
31 defaultMessage: '!!!Open workspace drawer',
32 },
33 closeWorkspaceDrawer: {
34 id: 'sidebar.closeWorkspaceDrawer',
35 defaultMessage: '!!!Close workspace drawer',
36 },
27}); 37});
28 38
29export default @observer class Sidebar extends Component { 39export default @observer class Sidebar extends Component {
@@ -31,7 +41,9 @@ export default @observer class Sidebar extends Component {
31 openSettings: PropTypes.func.isRequired, 41 openSettings: PropTypes.func.isRequired,
32 toggleMuteApp: PropTypes.func.isRequired, 42 toggleMuteApp: PropTypes.func.isRequired,
33 isAppMuted: PropTypes.bool.isRequired, 43 isAppMuted: PropTypes.bool.isRequired,
34 } 44 isWorkspaceDrawerOpen: PropTypes.bool.isRequired,
45 toggleWorkspaceDrawer: PropTypes.func.isRequired,
46 };
35 47
36 static contextTypes = { 48 static contextTypes = {
37 intl: intlShape, 49 intl: intlShape,
@@ -53,9 +65,23 @@ export default @observer class Sidebar extends Component {
53 this.setState({ tooltipEnabled: false }); 65 this.setState({ tooltipEnabled: false });
54 } 66 }
55 67
68 updateToolTip() {
69 this.disableToolTip();
70 setTimeout(this.enableToolTip.bind(this));
71 }
72
56 render() { 73 render() {
57 const { openSettings, toggleMuteApp, isAppMuted } = this.props; 74 const {
75 openSettings,
76 toggleMuteApp,
77 isAppMuted,
78 isWorkspaceDrawerOpen,
79 toggleWorkspaceDrawer,
80 } = this.props;
58 const { intl } = this.context; 81 const { intl } = this.context;
82 const workspaceToggleMessage = (
83 isWorkspaceDrawerOpen ? messages.closeWorkspaceDrawer : messages.openWorkspaceDrawer
84 );
59 85
60 return ( 86 return (
61 <div className="sidebar"> 87 <div className="sidebar">
@@ -64,9 +90,26 @@ export default @observer class Sidebar extends Component {
64 enableToolTip={() => this.enableToolTip()} 90 enableToolTip={() => this.enableToolTip()}
65 disableToolTip={() => this.disableToolTip()} 91 disableToolTip={() => this.disableToolTip()}
66 /> 92 />
93 {workspaceStore.isFeatureEnabled ? (
94 <button
95 type="button"
96 onClick={() => {
97 toggleWorkspaceDrawer();
98 this.updateToolTip();
99 gaEvent(GA_CATEGORY_WORKSPACES, 'toggleDrawer', 'sidebar');
100 }}
101 className={`sidebar__button sidebar__button--workspaces ${isWorkspaceDrawerOpen ? 'is-active' : ''}`}
102 data-tip={`${intl.formatMessage(workspaceToggleMessage)} (${ctrlKey}+D)`}
103 >
104 <i className="mdi mdi-view-grid" />
105 </button>
106 ) : null}
67 <button 107 <button
68 type="button" 108 type="button"
69 onClick={toggleMuteApp} 109 onClick={() => {
110 toggleMuteApp();
111 this.updateToolTip();
112 }}
70 className={`sidebar__button sidebar__button--audio ${isAppMuted ? 'is-muted' : ''}`} 113 className={`sidebar__button sidebar__button--audio ${isAppMuted ? 'is-muted' : ''}`}
71 data-tip={`${intl.formatMessage(isAppMuted ? messages.unmute : messages.mute)} (${ctrlKey}+Shift+M)`} 114 data-tip={`${intl.formatMessage(isAppMuted ? messages.unmute : messages.mute)} (${ctrlKey}+Shift+M)`}
72 > 115 >
diff --git a/src/components/services/content/ServiceView.js b/src/components/services/content/ServiceView.js
new file mode 100644
index 000000000..13148b9b3
--- /dev/null
+++ b/src/components/services/content/ServiceView.js
@@ -0,0 +1,139 @@
1import React, { Component, Fragment } from 'react';
2import PropTypes from 'prop-types';
3import { autorun } from 'mobx';
4import { observer } from 'mobx-react';
5import classnames from 'classnames';
6
7import ServiceModel from '../../../models/Service';
8import StatusBarTargetUrl from '../../ui/StatusBarTargetUrl';
9import WebviewLoader from '../../ui/WebviewLoader';
10import WebviewCrashHandler from './WebviewCrashHandler';
11import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler';
12import ServiceDisabled from './ServiceDisabled';
13import ServiceWebview from './ServiceWebview';
14
15export default @observer class ServiceView extends Component {
16 static propTypes = {
17 service: PropTypes.instanceOf(ServiceModel).isRequired,
18 setWebviewReference: PropTypes.func.isRequired,
19 detachService: PropTypes.func.isRequired,
20 reload: PropTypes.func.isRequired,
21 edit: PropTypes.func.isRequired,
22 enable: PropTypes.func.isRequired,
23 isActive: PropTypes.bool,
24 };
25
26 static defaultProps = {
27 isActive: false,
28 };
29
30 state = {
31 forceRepaint: false,
32 targetUrl: '',
33 statusBarVisible: false,
34 };
35
36 autorunDisposer = null;
37
38 forceRepaintTimeout = null;
39
40 componentDidMount() {
41 this.autorunDisposer = autorun(() => {
42 if (this.props.service.isActive) {
43 this.setState({ forceRepaint: true });
44 this.forceRepaintTimeout = setTimeout(() => {
45 this.setState({ forceRepaint: false });
46 }, 100);
47 }
48 });
49 }
50
51 componentWillUnmount() {
52 this.autorunDisposer();
53 clearTimeout(this.forceRepaintTimeout);
54 }
55
56 updateTargetUrl = (event) => {
57 let visible = true;
58 if (event.url === '' || event.url === '#') {
59 visible = false;
60 }
61 this.setState({
62 targetUrl: event.url,
63 statusBarVisible: visible,
64 });
65 };
66
67 render() {
68 const {
69 detachService,
70 service,
71 setWebviewReference,
72 reload,
73 edit,
74 enable,
75 } = this.props;
76
77 const webviewClasses = classnames({
78 services__webview: true,
79 'services__webview-wrapper': true,
80 'is-active': service.isActive,
81 'services__webview--force-repaint': this.state.forceRepaint,
82 });
83
84 let statusBar = null;
85 if (this.state.statusBarVisible) {
86 statusBar = (
87 <StatusBarTargetUrl text={this.state.targetUrl} />
88 );
89 }
90
91 return (
92 <div className={webviewClasses}>
93 {service.isActive && service.isEnabled && (
94 <Fragment>
95 {service.hasCrashed && (
96 <WebviewCrashHandler
97 name={service.recipe.name}
98 webview={service.webview}
99 reload={reload}
100 />
101 )}
102 {service.isEnabled && service.isLoading && service.isFirstLoad && (
103 <WebviewLoader
104 loaded={false}
105 name={service.name}
106 />
107 )}
108 {service.isError && (
109 <WebviewErrorHandler
110 name={service.recipe.name}
111 errorMessage={service.errorMessage}
112 reload={reload}
113 edit={edit}
114 />
115 )}
116 </Fragment>
117 )}
118 {!service.isEnabled ? (
119 <Fragment>
120 {service.isActive && (
121 <ServiceDisabled
122 name={service.recipe.name}
123 webview={service.webview}
124 enable={enable}
125 />
126 )}
127 </Fragment>
128 ) : (
129 <ServiceWebview
130 service={service}
131 setWebviewReference={setWebviewReference}
132 detachService={detachService}
133 />
134 )}
135 {statusBar}
136 </div>
137 );
138 }
139}
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js
index bb577e4cc..7252c695f 100644
--- a/src/components/services/content/ServiceWebview.js
+++ b/src/components/services/content/ServiceWebview.js
@@ -1,145 +1,50 @@
1import React, { Component, Fragment } from 'react'; 1import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { autorun } from 'mobx';
4import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
5import Webview from 'react-electron-web-view'; 4import ElectronWebView from 'react-electron-web-view';
6import classnames from 'classnames';
7 5
8import ServiceModel from '../../../models/Service'; 6import ServiceModel from '../../../models/Service';
9import StatusBarTargetUrl from '../../ui/StatusBarTargetUrl';
10import WebviewLoader from '../../ui/WebviewLoader';
11import WebviewCrashHandler from './WebviewCrashHandler';
12import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler';
13import ServiceDisabled from './ServiceDisabled';
14 7
15export default @observer class ServiceWebview extends Component { 8@observer
9class ServiceWebview extends Component {
16 static propTypes = { 10 static propTypes = {
17 service: PropTypes.instanceOf(ServiceModel).isRequired, 11 service: PropTypes.instanceOf(ServiceModel).isRequired,
18 setWebviewReference: PropTypes.func.isRequired, 12 setWebviewReference: PropTypes.func.isRequired,
19 reload: PropTypes.func.isRequired, 13 detachService: PropTypes.func.isRequired,
20 edit: PropTypes.func.isRequired,
21 enable: PropTypes.func.isRequired,
22 isActive: PropTypes.bool,
23 }; 14 };
24 15
25 static defaultProps = {
26 isActive: false,
27 };
28
29 state = {
30 forceRepaint: false,
31 targetUrl: '',
32 statusBarVisible: false,
33 };
34
35 autorunDisposer = null;
36
37 webview = null; 16 webview = null;
38 17
39 componentDidMount() {
40 this.autorunDisposer = autorun(() => {
41 if (this.props.service.isActive) {
42 this.setState({ forceRepaint: true });
43 setTimeout(() => {
44 this.setState({ forceRepaint: false });
45 }, 100);
46 }
47 });
48 }
49
50 componentWillUnmount() { 18 componentWillUnmount() {
51 this.autorunDisposer(); 19 const { service, detachService } = this.props;
52 } 20 detachService({ service });
53
54 updateTargetUrl = (event) => {
55 let visible = true;
56 if (event.url === '' || event.url === '#') {
57 visible = false;
58 }
59 this.setState({
60 targetUrl: event.url,
61 statusBarVisible: visible,
62 });
63 } 21 }
64 22
65 render() { 23 render() {
66 const { 24 const {
67 service, 25 service,
68 setWebviewReference, 26 setWebviewReference,
69 reload,
70 edit,
71 enable,
72 } = this.props; 27 } = this.props;
73 28
74 const webviewClasses = classnames({
75 services__webview: true,
76 'services__webview-wrapper': true,
77 'is-active': service.isActive,
78 'services__webview--force-repaint': this.state.forceRepaint,
79 });
80
81 let statusBar = null;
82 if (this.state.statusBarVisible) {
83 statusBar = (
84 <StatusBarTargetUrl text={this.state.targetUrl} />
85 );
86 }
87
88 return ( 29 return (
89 <div className={webviewClasses}> 30 <ElectronWebView
90 {service.isActive && service.isEnabled && ( 31 ref={(webview) => { this.webview = webview; }}
91 <Fragment> 32 autosize
92 {service.hasCrashed && ( 33 src={service.url}
93 <WebviewCrashHandler 34 preload="./webview/recipe.js"
94 name={service.recipe.name} 35 partition={`persist:service-${service.id}`}
95 webview={service.webview} 36 onDidAttach={() => {
96 reload={reload} 37 setWebviewReference({
97 /> 38 serviceId: service.id,
98 )} 39 webview: this.webview.view,
99 {service.isEnabled && service.isLoading && service.isFirstLoad && ( 40 });
100 <WebviewLoader 41 }}
101 loaded={false} 42 onUpdateTargetUrl={this.updateTargetUrl}
102 name={service.name} 43 useragent={service.userAgent}
103 /> 44 allowpopups
104 )} 45 />
105 {service.isError && (
106 <WebviewErrorHandler
107 name={service.recipe.name}
108 errorMessage={service.errorMessage}
109 reload={reload}
110 edit={edit}
111 />
112 )}
113 </Fragment>
114 )}
115 {!service.isEnabled ? (
116 <Fragment>
117 {service.isActive && (
118 <ServiceDisabled
119 name={service.recipe.name}
120 webview={service.webview}
121 enable={enable}
122 />
123 )}
124 </Fragment>
125 ) : (
126 <Webview
127 ref={(element) => { this.webview = element; }}
128 autosize
129 src={service.url}
130 preload="./webview/recipe.js"
131 partition={`persist:service-${service.id}`}
132 onDidAttach={() => setWebviewReference({
133 serviceId: service.id,
134 webview: this.webview.view,
135 })}
136 onUpdateTargetUrl={this.updateTargetUrl}
137 useragent={service.userAgent}
138 allowpopups
139 />
140 )}
141 {statusBar}
142 </div>
143 ); 46 );
144 } 47 }
145} 48}
49
50export default ServiceWebview;
diff --git a/src/components/services/content/Services.js b/src/components/services/content/Services.js
index 54f16ba12..8f8c38a11 100644
--- a/src/components/services/content/Services.js
+++ b/src/components/services/content/Services.js
@@ -4,7 +4,7 @@ import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { Link } from 'react-router'; 4import { Link } from 'react-router';
5import { defineMessages, intlShape } from 'react-intl'; 5import { defineMessages, intlShape } from 'react-intl';
6 6
7import Webview from './ServiceWebview'; 7import ServiceView from './ServiceView';
8import Appear from '../../ui/effects/Appear'; 8import Appear from '../../ui/effects/Appear';
9 9
10const messages = defineMessages({ 10const messages = defineMessages({
@@ -22,6 +22,7 @@ export default @observer class Services extends Component {
22 static propTypes = { 22 static propTypes = {
23 services: MobxPropTypes.arrayOrObservableArray, 23 services: MobxPropTypes.arrayOrObservableArray,
24 setWebviewReference: PropTypes.func.isRequired, 24 setWebviewReference: PropTypes.func.isRequired,
25 detachService: PropTypes.func.isRequired,
25 handleIPCMessage: PropTypes.func.isRequired, 26 handleIPCMessage: PropTypes.func.isRequired,
26 openWindow: PropTypes.func.isRequired, 27 openWindow: PropTypes.func.isRequired,
27 reload: PropTypes.func.isRequired, 28 reload: PropTypes.func.isRequired,
@@ -42,6 +43,7 @@ export default @observer class Services extends Component {
42 services, 43 services,
43 handleIPCMessage, 44 handleIPCMessage,
44 setWebviewReference, 45 setWebviewReference,
46 detachService,
45 openWindow, 47 openWindow,
46 reload, 48 reload,
47 openSettings, 49 openSettings,
@@ -71,11 +73,12 @@ export default @observer class Services extends Component {
71 </Appear> 73 </Appear>
72 )} 74 )}
73 {services.map(service => ( 75 {services.map(service => (
74 <Webview 76 <ServiceView
75 key={service.id} 77 key={service.id}
76 service={service} 78 service={service}
77 handleIPCMessage={handleIPCMessage} 79 handleIPCMessage={handleIPCMessage}
78 setWebviewReference={setWebviewReference} 80 setWebviewReference={setWebviewReference}
81 detachService={detachService}
79 openWindow={openWindow} 82 openWindow={openWindow}
80 reload={() => reload({ serviceId: service.id })} 83 reload={() => reload({ serviceId: service.id })}
81 edit={() => openSettings({ path: `services/edit/${service.id}` })} 84 edit={() => openSettings({ path: `services/edit/${service.id}` })}
diff --git a/src/components/services/content/WebviewCrashHandler.js b/src/components/services/content/WebviewCrashHandler.js
index 42bc3c877..7a69dba87 100644
--- a/src/components/services/content/WebviewCrashHandler.js
+++ b/src/components/services/content/WebviewCrashHandler.js
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import ms from 'ms';
5 6
6import Button from '../../ui/Button'; 7import Button from '../../ui/Button';
7 8
@@ -35,12 +36,12 @@ export default @observer class WebviewCrashHandler extends Component {
35 }; 36 };
36 37
37 state = { 38 state = {
38 countdown: 10000, 39 countdown: ms('10s'),
39 } 40 }
40 41
41 countdownInterval = null; 42 countdownInterval = null;
42 43
43 countdownIntervalTimeout = 1000; 44 countdownIntervalTimeout = ms('1s');
44 45
45 46
46 componentDidMount() { 47 componentDidMount() {
@@ -75,7 +76,7 @@ export default @observer class WebviewCrashHandler extends Component {
75 <p className="footnote"> 76 <p className="footnote">
76 {intl.formatMessage(messages.autoReload, { 77 {intl.formatMessage(messages.autoReload, {
77 name, 78 name,
78 seconds: this.state.countdown / 1000, 79 seconds: this.state.countdown / ms('1s'),
79 })} 80 })}
80 </p> 81 </p>
81 </div> 82 </div>
diff --git a/src/components/services/tabs/Tabbar.js b/src/components/services/tabs/Tabbar.js
index dd5c2140f..5e8260ad0 100644
--- a/src/components/services/tabs/Tabbar.js
+++ b/src/components/services/tabs/Tabbar.js
@@ -19,7 +19,7 @@ export default @observer class TabBar extends Component {
19 updateService: PropTypes.func.isRequired, 19 updateService: PropTypes.func.isRequired,
20 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired, 20 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired,
21 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired, 21 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired,
22 } 22 };
23 23
24 onSortEnd = ({ oldIndex, newIndex }) => { 24 onSortEnd = ({ oldIndex, newIndex }) => {
25 const { 25 const {
@@ -45,7 +45,7 @@ export default @observer class TabBar extends Component {
45 redirect: false, 45 redirect: false,
46 }); 46 });
47 } 47 }
48 } 48 };
49 49
50 disableService({ serviceId }) { 50 disableService({ serviceId }) {
51 this.toggleService({ serviceId, isEnabled: false }); 51 this.toggleService({ serviceId, isEnabled: false });
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js
index 9c9543749..3f6964b6b 100644
--- a/src/components/settings/account/AccountDashboard.js
+++ b/src/components/settings/account/AccountDashboard.js
@@ -3,12 +3,11 @@ import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import ReactTooltip from 'react-tooltip'; 5import ReactTooltip from 'react-tooltip';
6import moment from 'moment'; 6import { ProBadge } from '@meetfranz/ui';
7 7
8import Loader from '../../ui/Loader'; 8import Loader from '../../ui/Loader';
9import Button from '../../ui/Button'; 9import Button from '../../ui/Button';
10import Infobox from '../../ui/Infobox'; 10import Infobox from '../../ui/Infobox';
11import Link from '../../ui/Link';
12import SubscriptionForm from '../../../containers/subscription/SubscriptionFormScreen'; 11import SubscriptionForm from '../../../containers/subscription/SubscriptionFormScreen';
13 12
14const messages = defineMessages({ 13const messages = defineMessages({
@@ -24,10 +23,6 @@ const messages = defineMessages({
24 id: 'settings.account.headlineUpgrade', 23 id: 'settings.account.headlineUpgrade',
25 defaultMessage: '!!!Upgrade your Account', 24 defaultMessage: '!!!Upgrade your Account',
26 }, 25 },
27 headlineInvoices: {
28 id: 'settings.account.headlineInvoices',
29 defaultMessage: '!!Invoices',
30 },
31 headlineDangerZone: { 26 headlineDangerZone: {
32 id: 'settings.account.headlineDangerZone', 27 id: 'settings.account.headlineDangerZone',
33 defaultMessage: '!!Danger Zone', 28 defaultMessage: '!!Danger Zone',
@@ -44,14 +39,14 @@ const messages = defineMessages({
44 id: 'settings.account.accountType.premium', 39 id: 'settings.account.accountType.premium',
45 defaultMessage: '!!!Premium Supporter Account', 40 defaultMessage: '!!!Premium Supporter Account',
46 }, 41 },
47 accountTypeEnterprise: {
48 id: 'settings.account.accountType.enterprise',
49 defaultMessage: '!!!Enterprise Account',
50 },
51 accountEditButton: { 42 accountEditButton: {
52 id: 'settings.account.account.editButton', 43 id: 'settings.account.account.editButton',
53 defaultMessage: '!!!Edit Account', 44 defaultMessage: '!!!Edit Account',
54 }, 45 },
46 invoicesButton: {
47 id: 'settings.account.headlineInvoices',
48 defaultMessage: '!!Invoices',
49 },
55 invoiceDownload: { 50 invoiceDownload: {
56 id: 'settings.account.invoiceDownload', 51 id: 'settings.account.invoiceDownload',
57 defaultMessage: '!!!Download', 52 defaultMessage: '!!!Download',
@@ -81,19 +76,17 @@ const messages = defineMessages({
81export default @observer class AccountDashboard extends Component { 76export default @observer class AccountDashboard extends Component {
82 static propTypes = { 77 static propTypes = {
83 user: MobxPropTypes.observableObject.isRequired, 78 user: MobxPropTypes.observableObject.isRequired,
84 orders: MobxPropTypes.arrayOrObservableArray.isRequired,
85 isLoading: PropTypes.bool.isRequired, 79 isLoading: PropTypes.bool.isRequired,
86 isLoadingOrdersInfo: PropTypes.bool.isRequired,
87 isLoadingPlans: PropTypes.bool.isRequired, 80 isLoadingPlans: PropTypes.bool.isRequired,
88 isCreatingPaymentDashboardUrl: PropTypes.bool.isRequired,
89 userInfoRequestFailed: PropTypes.bool.isRequired, 81 userInfoRequestFailed: PropTypes.bool.isRequired,
90 retryUserInfoRequest: PropTypes.func.isRequired, 82 retryUserInfoRequest: PropTypes.func.isRequired,
91 openDashboard: PropTypes.func.isRequired,
92 openExternalUrl: PropTypes.func.isRequired,
93 onCloseSubscriptionWindow: PropTypes.func.isRequired, 83 onCloseSubscriptionWindow: PropTypes.func.isRequired,
94 deleteAccount: PropTypes.func.isRequired, 84 deleteAccount: PropTypes.func.isRequired,
95 isLoadingDeleteAccount: PropTypes.bool.isRequired, 85 isLoadingDeleteAccount: PropTypes.bool.isRequired,
96 isDeleteAccountSuccessful: PropTypes.bool.isRequired, 86 isDeleteAccountSuccessful: PropTypes.bool.isRequired,
87 openEditAccount: PropTypes.func.isRequired,
88 openBilling: PropTypes.func.isRequired,
89 openInvoices: PropTypes.func.isRequired,
97 }; 90 };
98 91
99 static contextTypes = { 92 static contextTypes = {
@@ -103,12 +96,7 @@ export default @observer class AccountDashboard extends Component {
103 render() { 96 render() {
104 const { 97 const {
105 user, 98 user,
106 orders,
107 isLoading, 99 isLoading,
108 isCreatingPaymentDashboardUrl,
109 openDashboard,
110 openExternalUrl,
111 isLoadingOrdersInfo,
112 isLoadingPlans, 100 isLoadingPlans,
113 userInfoRequestFailed, 101 userInfoRequestFailed,
114 retryUserInfoRequest, 102 retryUserInfoRequest,
@@ -116,6 +104,9 @@ export default @observer class AccountDashboard extends Component {
116 deleteAccount, 104 deleteAccount,
117 isLoadingDeleteAccount, 105 isLoadingDeleteAccount,
118 isDeleteAccountSuccessful, 106 isDeleteAccountSuccessful,
107 openEditAccount,
108 openBilling,
109 openInvoices,
119 } = this.props; 110 } = this.props;
120 const { intl } = this.context; 111 const { intl } = this.context;
121 112
@@ -153,116 +144,56 @@ export default @observer class AccountDashboard extends Component {
153 src="./assets/images/logo.svg" 144 src="./assets/images/logo.svg"
154 alt="" 145 alt=""
155 /> 146 />
156 {user.isPremium && (
157 <span
158 className="account__avatar-premium emoji"
159 data-tip="Premium Supporter Account"
160 >
161 <img src="./assets/images/emoji/star.png" alt="" />
162 </span>
163 )}
164 </div> 147 </div>
165 <div className="account__info"> 148 <div className="account__info">
166 <h2> 149 <h2>
167 {`${user.firstname} ${user.lastname}`} 150 <span className="username">{`${user.firstname} ${user.lastname}`}</span>
151 {user.isPremium && (
152 <>
153 {' '}
154 <ProBadge />
155 <span className="badge badge--premium">{intl.formatMessage(messages.accountTypePremium)}</span>
156 </>
157 )}
168 </h2> 158 </h2>
169 {user.organization && `${user.organization}, `} 159 {user.organization && `${user.organization}, `}
170 {user.email} 160 {user.email}
171 <br />
172 {!user.isEnterprise && !user.isPremium && (
173 <span className="badge badge">{intl.formatMessage(messages.accountTypeBasic)}</span>
174 )}
175 {user.isPremium && ( 161 {user.isPremium && (
176 <span className="badge badge--premium">{intl.formatMessage(messages.accountTypePremium)}</span> 162 <div className="manage-user-links">
177 )} 163 <Button
178 {user.isEnterprise && ( 164 label={intl.formatMessage(messages.accountEditButton)}
179 <span className="badge badge--success">{intl.formatMessage(messages.accountTypeEnterprise)}</span> 165 className="franz-form__button--inverted"
166 onClick={openEditAccount}
167 />
168 {user.isSubscriptionOwner && (
169 <>
170 <Button
171 label={intl.formatMessage(messages.manageSubscriptionButtonLabel)}
172 className="franz-form__button--inverted"
173 onClick={openBilling}
174 />
175 <Button
176 label={intl.formatMessage(messages.invoicesButton)}
177 className="franz-form__button--inverted"
178 onClick={openInvoices}
179 />
180 </>
181 )}
182 </div>
180 )} 183 )}
181 </div> 184 </div>
182 <Link to="/settings/user/edit" className="button"> 185 {!user.isPremium && (
183 {intl.formatMessage(messages.accountEditButton)} 186 <Button
184 </Link> 187 label={intl.formatMessage(messages.accountEditButton)}
185 {user.emailValidated} 188 className="franz-form__button--inverted"
186 </div> 189 onClick={openEditAccount}
187 </div> 190 />
188 )}
189
190 {user.isSubscriptionOwner && (
191 isLoadingOrdersInfo ? (
192 <Loader />
193 ) : (
194 <div className="account franz-form">
195 {orders.length > 0 && (
196 <Fragment>
197 <div className="account__box">
198 <h2>{intl.formatMessage(messages.headlineSubscription)}</h2>
199 <div className="account__subscription">
200 {orders[0].name}
201 <span className="badge">{orders[0].price}</span>
202 <Button
203 label={intl.formatMessage(messages.manageSubscriptionButtonLabel)}
204 className="account__subscription-button franz-form__button--inverted"
205 loaded={!isCreatingPaymentDashboardUrl}
206 onClick={() => openDashboard()}
207 />
208 </div>
209 </div>
210 <div className="account__box">
211 <h2>{intl.formatMessage(messages.headlineInvoices)}</h2>
212 <table className="invoices">
213 <tbody>
214 {orders.map(order => (
215 <tr key={order.id}>
216 <td className="invoices__date">
217 {moment(order.date).format('DD.MM.YYYY')}
218 </td>
219 <td className="invoices__action">
220 <button
221 type="button"
222 onClick={() => openExternalUrl(order.invoiceUrl)}
223 >
224 {intl.formatMessage(messages.invoiceDownload)}
225 </button>
226 </td>
227 </tr>
228 ))}
229 </tbody>
230 </table>
231 </div>
232 </Fragment>
233 )} 191 )}
234 </div> 192 </div>
235 )
236 )}
237
238 {user.isEnterprise && (
239 <div className="account">
240 <div className="account__box">
241 <h2>{user.company.name}</h2>
242 <p>
243 Technical contact:&nbsp;
244 <Link
245 className="link"
246 target="_blank"
247 to={`mailto:${user.company.contact.technical}?subject=Franz`}
248 >
249 {user.company.contact.technical}
250 </Link>
251 <br />
252 General contact:&nbsp;
253 <Link
254 className="link"
255 target="_blank"
256 to={`mailto:${user.company.contact.default}?subject=Franz`}
257 >
258 {user.company.contact.default}
259 </Link>
260 </p>
261 </div>
262 </div> 193 </div>
263 )} 194 )}
264 195
265 {!user.isEnterprise && !user.isPremium && ( 196 {!user.isPremium && (
266 isLoadingPlans ? ( 197 isLoadingPlans ? (
267 <Loader /> 198 <Loader />
268 ) : ( 199 ) : (
@@ -277,27 +208,25 @@ export default @observer class AccountDashboard extends Component {
277 ) 208 )
278 )} 209 )}
279 210
280 {!user.isEnterprise && ( 211 <div className="account franz-form">
281 <div className="account franz-form"> 212 <div className="account__box">
282 <div className="account__box"> 213 <h2>{intl.formatMessage(messages.headlineDangerZone)}</h2>
283 <h2>{intl.formatMessage(messages.headlineDangerZone)}</h2> 214 {!isDeleteAccountSuccessful && (
284 {!isDeleteAccountSuccessful && ( 215 <div className="account__subscription">
285 <div className="account__subscription"> 216 <p>{intl.formatMessage(messages.deleteInfo)}</p>
286 <p>{intl.formatMessage(messages.deleteInfo)}</p> 217 <Button
287 <Button 218 label={intl.formatMessage(messages.deleteAccount)}
288 label={intl.formatMessage(messages.deleteAccount)} 219 buttonType="danger"
289 buttonType="danger" 220 onClick={() => deleteAccount()}
290 onClick={() => deleteAccount()} 221 loaded={!isLoadingDeleteAccount}
291 loaded={!isLoadingDeleteAccount} 222 />
292 />
293 </div>
294 )}
295 {isDeleteAccountSuccessful && (
296 <p>{intl.formatMessage(messages.deleteEmailSent)}</p>
297 )}
298 </div> 223 </div>
224 )}
225 {isDeleteAccountSuccessful && (
226 <p>{intl.formatMessage(messages.deleteEmailSent)}</p>
227 )}
299 </div> 228 </div>
300 )} 229 </div>
301 </Fragment> 230 </Fragment>
302 )} 231 )}
303 </div> 232 </div>
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js
index 953f702f8..df4b3b3b2 100644
--- a/src/components/settings/navigation/SettingsNavigation.js
+++ b/src/components/settings/navigation/SettingsNavigation.js
@@ -2,8 +2,12 @@ import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { defineMessages, intlShape } from 'react-intl'; 3import { defineMessages, intlShape } from 'react-intl';
4import { inject, observer } from 'mobx-react'; 4import { inject, observer } from 'mobx-react';
5import { ProBadge } from '@meetfranz/ui';
5 6
6import Link from '../../ui/Link'; 7import Link from '../../ui/Link';
8import { workspaceStore } from '../../../features/workspaces';
9import UIStore from '../../../stores/UIStore';
10import UserStore from '../../../stores/UserStore';
7 11
8const messages = defineMessages({ 12const messages = defineMessages({
9 availableServices: { 13 availableServices: {
@@ -14,10 +18,18 @@ const messages = defineMessages({
14 id: 'settings.navigation.yourServices', 18 id: 'settings.navigation.yourServices',
15 defaultMessage: '!!!Your services', 19 defaultMessage: '!!!Your services',
16 }, 20 },
21 yourWorkspaces: {
22 id: 'settings.navigation.yourWorkspaces',
23 defaultMessage: '!!!Your workspaces',
24 },
17 account: { 25 account: {
18 id: 'settings.navigation.account', 26 id: 'settings.navigation.account',
19 defaultMessage: '!!!Account', 27 defaultMessage: '!!!Account',
20 }, 28 },
29 team: {
30 id: 'settings.navigation.team',
31 defaultMessage: '!!!Manage Team',
32 },
21 settings: { 33 settings: {
22 id: 'settings.navigation.settings', 34 id: 'settings.navigation.settings',
23 defaultMessage: '!!!Settings', 35 defaultMessage: '!!!Settings',
@@ -34,7 +46,12 @@ const messages = defineMessages({
34 46
35export default @inject('stores') @observer class SettingsNavigation extends Component { 47export default @inject('stores') @observer class SettingsNavigation extends Component {
36 static propTypes = { 48 static propTypes = {
49 stores: PropTypes.shape({
50 ui: PropTypes.instanceOf(UIStore).isRequired,
51 user: PropTypes.instanceOf(UserStore).isRequired,
52 }).isRequired,
37 serviceCount: PropTypes.number.isRequired, 53 serviceCount: PropTypes.number.isRequired,
54 workspaceCount: PropTypes.number.isRequired,
38 }; 55 };
39 56
40 static contextTypes = { 57 static contextTypes = {
@@ -42,7 +59,9 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp
42 }; 59 };
43 60
44 render() { 61 render() {
45 const { serviceCount } = this.props; 62 const { serviceCount, workspaceCount, stores } = this.props;
63 const { isDarkThemeActive } = stores.ui;
64 const { router, user } = stores;
46 const { intl } = this.context; 65 const { intl } = this.context;
47 66
48 return ( 67 return (
@@ -63,6 +82,21 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp
63 {' '} 82 {' '}
64 <span className="badge">{serviceCount}</span> 83 <span className="badge">{serviceCount}</span>
65 </Link> 84 </Link>
85 {workspaceStore.isFeatureEnabled ? (
86 <Link
87 to="/settings/workspaces"
88 className="settings-navigation__link"
89 activeClassName="is-active"
90 >
91 {intl.formatMessage(messages.yourWorkspaces)}
92 {' '}
93 {workspaceStore.isPremiumUpgradeRequired ? (
94 <ProBadge inverted={!isDarkThemeActive && workspaceStore.isSettingsRouteActive} />
95 ) : (
96 <span className="badge">{workspaceCount}</span>
97 )}
98 </Link>
99 ) : null}
66 <Link 100 <Link
67 to="/settings/user" 101 to="/settings/user"
68 className="settings-navigation__link" 102 className="settings-navigation__link"
@@ -71,6 +105,16 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp
71 {intl.formatMessage(messages.account)} 105 {intl.formatMessage(messages.account)}
72 </Link> 106 </Link>
73 <Link 107 <Link
108 to="/settings/team"
109 className="settings-navigation__link"
110 activeClassName="is-active"
111 >
112 {intl.formatMessage(messages.team)}
113 {!user.data.isPremium && (
114 <ProBadge inverted={!isDarkThemeActive && router.location.pathname === '/settings/team'} />
115 )}
116 </Link>
117 <Link
74 to="/settings/app" 118 to="/settings/app"
75 className="settings-navigation__link" 119 className="settings-navigation__link"
76 activeClassName="is-active" 120 activeClassName="is-active"
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js
index 468d85c45..4ba2eb844 100644
--- a/src/components/settings/services/EditServiceForm.js
+++ b/src/components/settings/services/EditServiceForm.js
@@ -128,7 +128,8 @@ export default @observer class EditServiceForm extends Component {
128 isSaving: PropTypes.bool.isRequired, 128 isSaving: PropTypes.bool.isRequired,
129 isDeleting: PropTypes.bool.isRequired, 129 isDeleting: PropTypes.bool.isRequired,
130 isProxyFeatureEnabled: PropTypes.bool.isRequired, 130 isProxyFeatureEnabled: PropTypes.bool.isRequired,
131 isProxyFeaturePremiumFeature: PropTypes.bool.isRequired, 131 isProxyPremiumFeature: PropTypes.bool.isRequired,
132 isSpellcheckerPremiumFeature: PropTypes.bool.isRequired,
132 }; 133 };
133 134
134 static defaultProps = { 135 static defaultProps = {
@@ -191,7 +192,8 @@ export default @observer class EditServiceForm extends Component {
191 isDeleting, 192 isDeleting,
192 onDelete, 193 onDelete,
193 isProxyFeatureEnabled, 194 isProxyFeatureEnabled,
194 isProxyFeaturePremiumFeature, 195 isProxyPremiumFeature,
196 isSpellcheckerPremiumFeature,
195 } = this.props; 197 } = this.props;
196 const { intl } = this.context; 198 const { intl } = this.context;
197 199
@@ -339,14 +341,20 @@ export default @observer class EditServiceForm extends Component {
339 </div> 341 </div>
340 </div> 342 </div>
341 343
342 <PremiumFeatureContainer> 344 <PremiumFeatureContainer
345 condition={isSpellcheckerPremiumFeature}
346 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }}
347 >
343 <div className="settings__settings-group"> 348 <div className="settings__settings-group">
344 <Select field={form.$('spellcheckerLanguage')} /> 349 <Select field={form.$('spellcheckerLanguage')} />
345 </div> 350 </div>
346 </PremiumFeatureContainer> 351 </PremiumFeatureContainer>
347 352
348 {isProxyFeatureEnabled && ( 353 {isProxyFeatureEnabled && (
349 <PremiumFeatureContainer condition={isProxyFeaturePremiumFeature}> 354 <PremiumFeatureContainer
355 condition={isProxyPremiumFeature}
356 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'proxy' }}
357 >
350 <div className="settings__settings-group"> 358 <div className="settings__settings-group">
351 <h3> 359 <h3>
352 {intl.formatMessage(messages.headlineProxy)} 360 {intl.formatMessage(messages.headlineProxy)}
diff --git a/src/components/settings/services/ServicesDashboard.js b/src/components/settings/services/ServicesDashboard.js
index a12df7372..53bae12df 100644
--- a/src/components/settings/services/ServicesDashboard.js
+++ b/src/components/settings/services/ServicesDashboard.js
@@ -65,7 +65,7 @@ export default @observer class ServicesDashboard extends Component {
65 65
66 static defaultProps = { 66 static defaultProps = {
67 searchNeedle: '', 67 searchNeedle: '',
68 } 68 };
69 69
70 static contextTypes = { 70 static contextTypes = {
71 intl: intlShape, 71 intl: intlShape,
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index a92e559f3..efd453356 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -81,6 +81,10 @@ const messages = defineMessages({
81 id: 'settings.app.restartRequired', 81 id: 'settings.app.restartRequired',
82 defaultMessage: '!!!Changes require restart', 82 defaultMessage: '!!!Changes require restart',
83 }, 83 },
84 languageDisclaimer: {
85 id: 'settings.app.languageDisclaimer',
86 defaultMessage: '!!!Official translations are English & German. All other languages are community based translations.',
87 },
84}); 88});
85 89
86export default @observer class EditSettingsForm extends Component { 90export default @observer class EditSettingsForm extends Component {
@@ -170,6 +174,7 @@ export default @observer class EditSettingsForm extends Component {
170 <Select field={form.$('locale')} showLabel={false} /> 174 <Select field={form.$('locale')} showLabel={false} />
171 <PremiumFeatureContainer 175 <PremiumFeatureContainer
172 condition={isSpellcheckerPremiumFeature} 176 condition={isSpellcheckerPremiumFeature}
177 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }}
173 > 178 >
174 <Fragment> 179 <Fragment>
175 <Toggle 180 <Toggle
@@ -238,6 +243,10 @@ export default @observer class EditSettingsForm extends Component {
238 {intl.formatMessage(messages.currentVersion)} 243 {intl.formatMessage(messages.currentVersion)}
239 {' '} 244 {' '}
240 {remote.app.getVersion()} 245 {remote.app.getVersion()}
246 <p className="settings__message">
247 <span className="mdi mdi-information" />
248 {intl.formatMessage(messages.languageDisclaimer)}
249 </p>
241 </form> 250 </form>
242 </div> 251 </div>
243 </div> 252 </div>
diff --git a/src/components/settings/team/TeamDashboard.js b/src/components/settings/team/TeamDashboard.js
new file mode 100644
index 000000000..82c517fcb
--- /dev/null
+++ b/src/components/settings/team/TeamDashboard.js
@@ -0,0 +1,152 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import ReactTooltip from 'react-tooltip';
6import injectSheet from 'react-jss';
7
8import Loader from '../../ui/Loader';
9import Button from '../../ui/Button';
10import Infobox from '../../ui/Infobox';
11import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
12
13const messages = defineMessages({
14 headline: {
15 id: 'settings.team.headline',
16 defaultMessage: '!!!Team',
17 },
18 contentHeadline: {
19 id: 'settings.team.contentHeadline',
20 defaultMessage: '!!!Franz for Teams',
21 },
22 intro: {
23 id: 'settings.team.intro',
24 defaultMessage: '!!!You and your team use Franz? You can now manage Premium subscriptions for as many colleagues, friends or family members as you want, all from within one account.',
25 },
26 copy: {
27 id: 'settings.team.copy',
28 defaultMessage: '!!!Franz for Teams gives you the option to invite co-workers to your team by sending them email invitations and manage their subscriptions in your account’s preferences. Don’t waste time setting up subscriptions for every team member individually, forget about multiple invoices and different billing cycles - one team to rule them all!',
29 },
30 manageButton: {
31 id: 'settings.team.manageAction',
32 defaultMessage: '!!!Manage your Team on meetfranz.com',
33 },
34 upgradeButton: {
35 id: 'settings.team.upgradeAction',
36 defaultMessage: '!!!Upgrade your Account',
37 },
38});
39
40const styles = {
41 cta: {
42 margin: [40, 'auto'],
43 },
44 container: {
45 display: 'flex',
46 flexDirection: 'column',
47 height: 'auto',
48
49 '@media(min-width: 800px)': {
50 flexDirection: 'row',
51 },
52 },
53 content: {
54 height: 'auto',
55 order: 1,
56
57 '@media(min-width: 800px)': {
58 order: 0,
59 },
60 },
61 image: {
62 display: 'block',
63 height: 150,
64 order: 0,
65 margin: [0, 'auto', 40, 'auto'],
66
67 '@media(min-width: 800px)': {
68 marginLeft: 40,
69 order: 1,
70 },
71 },
72};
73
74
75export default @injectSheet(styles) @observer class TeamDashboard extends Component {
76 static propTypes = {
77 isLoading: PropTypes.bool.isRequired,
78 userInfoRequestFailed: PropTypes.bool.isRequired,
79 retryUserInfoRequest: PropTypes.func.isRequired,
80 openTeamManagement: PropTypes.func.isRequired,
81 classes: PropTypes.object.isRequired,
82 };
83
84 static contextTypes = {
85 intl: intlShape,
86 };
87
88 render() {
89 const {
90 isLoading,
91 userInfoRequestFailed,
92 retryUserInfoRequest,
93 openTeamManagement,
94 classes,
95 } = this.props;
96 const { intl } = this.context;
97
98 return (
99 <div className="settings__main">
100 <div className="settings__header">
101 <span className="settings__header-item">
102 {intl.formatMessage(messages.headline)}
103 </span>
104 </div>
105 <div className="settings__body">
106 {isLoading && (
107 <Loader />
108 )}
109
110 {!isLoading && userInfoRequestFailed && (
111 <Infobox
112 icon="alert"
113 type="danger"
114 ctaLabel={intl.formatMessage(messages.tryReloadUserInfoRequest)}
115 ctaLoading={isLoading}
116 ctaOnClick={retryUserInfoRequest}
117 >
118 {intl.formatMessage(messages.userInfoRequestFailed)}
119 </Infobox>
120 )}
121
122 {!userInfoRequestFailed && (
123 <>
124 {!isLoading && (
125 <>
126 <PremiumFeatureContainer>
127 <>
128 <h1>{intl.formatMessage(messages.contentHeadline)}</h1>
129 <div className={classes.container}>
130 <div className={classes.content}>
131 <p>{intl.formatMessage(messages.intro)}</p>
132 <p>{intl.formatMessage(messages.copy)}</p>
133 </div>
134 <img className={classes.image} src="https://cdn.franzinfra.com/announcements/assets/teams.png" alt="Franz for Teams" />
135 </div>
136 <Button
137 label={intl.formatMessage(messages.manageButton)}
138 onClick={openTeamManagement}
139 className={classes.cta}
140 />
141 </>
142 </PremiumFeatureContainer>
143 </>
144 )}
145 </>
146 )}
147 </div>
148 <ReactTooltip place="right" type="dark" effect="solid" />
149 </div>
150 );
151 }
152}
diff --git a/src/components/settings/user/EditUserForm.js b/src/components/settings/user/EditUserForm.js
index 0e3ac6b10..a1a353e57 100644
--- a/src/components/settings/user/EditUserForm.js
+++ b/src/components/settings/user/EditUserForm.js
@@ -3,11 +3,10 @@ import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import { Link } from 'react-router'; 5import { Link } from 'react-router';
6 6import { Input } from '@meetfranz/forms';
7// import { Link } from 'react-router';
8 7
9import Form from '../../../lib/Form'; 8import Form from '../../../lib/Form';
10import Input from '../../ui/Input'; 9// import Input from '../../ui/Input';
11import Button from '../../ui/Button'; 10import Button from '../../ui/Button';
12import Radio from '../../ui/Radio'; 11import Radio from '../../ui/Radio';
13import Infobox from '../../ui/Infobox'; 12import Infobox from '../../ui/Infobox';
@@ -39,13 +38,12 @@ const messages = defineMessages({
39 }, 38 },
40}); 39});
41 40
42export default @observer class EditServiceForm extends Component { 41export default @observer class EditUserForm extends Component {
43 static propTypes = { 42 static propTypes = {
44 status: MobxPropTypes.observableArray.isRequired, 43 status: MobxPropTypes.observableArray.isRequired,
45 form: PropTypes.instanceOf(Form).isRequired, 44 form: PropTypes.instanceOf(Form).isRequired,
46 onSubmit: PropTypes.func.isRequired, 45 onSubmit: PropTypes.func.isRequired,
47 isSaving: PropTypes.bool.isRequired, 46 isSaving: PropTypes.bool.isRequired,
48 isEnterprise: PropTypes.bool.isRequired,
49 }; 47 };
50 48
51 static contextTypes = { 49 static contextTypes = {
@@ -68,7 +66,6 @@ export default @observer class EditServiceForm extends Component {
68 // user, 66 // user,
69 status, 67 status,
70 form, 68 form,
71 isEnterprise,
72 isSaving, 69 isSaving,
73 } = this.props; 70 } = this.props;
74 const { intl } = this.context; 71 const { intl } = this.context;
@@ -98,23 +95,21 @@ export default @observer class EditServiceForm extends Component {
98 )} 95 )}
99 <h2>{intl.formatMessage(messages.headlineAccount)}</h2> 96 <h2>{intl.formatMessage(messages.headlineAccount)}</h2>
100 <div className="grid__row"> 97 <div className="grid__row">
101 <Input field={form.$('firstname')} focus /> 98 <Input {...form.$('firstname').bind()} focus />
102 <Input field={form.$('lastname')} /> 99 <Input {...form.$('lastname').bind()} />
103 </div> 100 </div>
104 <Input field={form.$('email')} /> 101 <Input {...form.$('email').bind()} />
105 {!isEnterprise && ( 102 <Radio field={form.$('accountType')} />
106 <Radio field={form.$('accountType')} /> 103 {form.$('accountType').value === 'company' && (
107 )}
108 {!isEnterprise && form.$('accountType').value === 'company' && (
109 <Input field={form.$('organization')} /> 104 <Input field={form.$('organization')} />
110 )} 105 )}
111 <h2>{intl.formatMessage(messages.headlinePassword)}</h2> 106 <h2>{intl.formatMessage(messages.headlinePassword)}</h2>
112 <Input 107 <Input
113 field={form.$('oldPassword')} 108 {...form.$('oldPassword').bind()}
114 showPasswordToggle 109 showPasswordToggle
115 /> 110 />
116 <Input 111 <Input
117 field={form.$('newPassword')} 112 {...form.$('newPassword').bind()}
118 showPasswordToggle 113 showPasswordToggle
119 scorePassword 114 scorePassword
120 /> 115 />
diff --git a/src/components/subscription/SubscriptionForm.js b/src/components/subscription/SubscriptionForm.js
index 90da8ddc3..50f1e0522 100644
--- a/src/components/subscription/SubscriptionForm.js
+++ b/src/components/subscription/SubscriptionForm.js
@@ -35,31 +35,33 @@ const messages = defineMessages({
35 id: 'subscription.includedFeatures', 35 id: 'subscription.includedFeatures',
36 defaultMessage: '!!!The Franz Premium Supporter Account includes', 36 defaultMessage: '!!!The Franz Premium Supporter Account includes',
37 }, 37 },
38 features: { 38 onpremise: {
39 onpremise: { 39 id: 'subscription.features.onpremise.mattermost',
40 id: 'subscription.features.onpremise.mattermost', 40 defaultMessage: '!!!Add on-premise/hosted services like Mattermost',
41 defaultMessage: '!!!Add on-premise/hosted services like Mattermost', 41 },
42 }, 42 noInterruptions: {
43 noInterruptions: { 43 id: 'subscription.features.noInterruptions',
44 id: 'subscription.features.noInterruptions', 44 defaultMessage: '!!!No app delays & nagging to upgrade license',
45 defaultMessage: '!!!No app delays & nagging to upgrade license', 45 },
46 }, 46 proxy: {
47 proxy: { 47 id: 'subscription.features.proxy',
48 id: 'subscription.features.proxy', 48 defaultMessage: '!!!Proxy support for services',
49 defaultMessage: '!!!Proxy support for services', 49 },
50 }, 50 spellchecker: {
51 spellchecker: { 51 id: 'subscription.features.spellchecker',
52 id: 'subscription.features.spellchecker', 52 defaultMessage: '!!!Support for Spellchecker',
53 defaultMessage: '!!!Support for Spellchecker', 53 },
54 }, 54 workspaces: {
55 ads: { 55 id: 'subscription.features.workspaces',
56 id: 'subscription.features.ads', 56 defaultMessage: '!!!Organize your services in workspaces',
57 defaultMessage: '!!!No ads, ever!', 57 },
58 }, 58 ads: {
59 comingSoon: { 59 id: 'subscription.features.ads',
60 id: 'subscription.features.comingSoon', 60 defaultMessage: '!!!No ads, ever!',
61 defaultMessage: '!!!coming soon', 61 },
62 }, 62 comingSoon: {
63 id: 'subscription.features.comingSoon',
64 defaultMessage: '!!!coming soon',
63 }, 65 },
64 euTaxInfo: { 66 euTaxInfo: {
65 id: 'subscription.euTaxInfo', 67 id: 'subscription.euTaxInfo',
@@ -85,7 +87,7 @@ export default @observer class SubscriptionForm extends Component {
85 showSkipOption: false, 87 showSkipOption: false,
86 skipAction: () => null, 88 skipAction: () => null,
87 skipButtonLabel: '', 89 skipButtonLabel: '',
88 } 90 };
89 91
90 static contextTypes = { 92 static contextTypes = {
91 intl: intlShape, 93 intl: intlShape,
@@ -162,18 +164,21 @@ export default @observer class SubscriptionForm extends Component {
162 </p> 164 </p>
163 <div className="subscription"> 165 <div className="subscription">
164 <ul className="subscription__premium-features"> 166 <ul className="subscription__premium-features">
165 <li>{intl.formatMessage(messages.features.onpremise)}</li> 167 <li>{intl.formatMessage(messages.onpremise)}</li>
168 <li>
169 {intl.formatMessage(messages.noInterruptions)}
170 </li>
166 <li> 171 <li>
167 {intl.formatMessage(messages.features.noInterruptions)} 172 {intl.formatMessage(messages.spellchecker)}
168 </li> 173 </li>
169 <li> 174 <li>
170 {intl.formatMessage(messages.features.spellchecker)} 175 {intl.formatMessage(messages.proxy)}
171 </li> 176 </li>
172 <li> 177 <li>
173 {intl.formatMessage(messages.features.proxy)} 178 {intl.formatMessage(messages.workspaces)}
174 </li> 179 </li>
175 <li> 180 <li>
176 {intl.formatMessage(messages.features.ads)} 181 {intl.formatMessage(messages.ads)}
177 </li> 182 </li>
178 </ul> 183 </ul>
179 </div> 184 </div>
diff --git a/src/components/subscription/SubscriptionPopup.js b/src/components/subscription/SubscriptionPopup.js
index b5d7c4b2d..0f6f0260f 100644
--- a/src/components/subscription/SubscriptionPopup.js
+++ b/src/components/subscription/SubscriptionPopup.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import Webview from 'react-electron-web-view'; 5import Webview from 'react-electron-web-view';
6import ms from 'ms';
6 7
7import Button from '../ui/Button'; 8import Button from '../ui/Button';
8 9
@@ -42,7 +43,7 @@ export default @observer class SubscriptionPopup extends Component {
42 43
43 setTimeout(() => { 44 setTimeout(() => {
44 this.props.closeWindow(); 45 this.props.closeWindow();
45 }, 4000); 46 }, ms('4s'));
46 } 47 }
47 48
48 render() { 49 render() {
diff --git a/src/components/ui/AppLoader/index.js b/src/components/ui/AppLoader/index.js
index 61053f6d1..b0c7fed7b 100644
--- a/src/components/ui/AppLoader/index.js
+++ b/src/components/ui/AppLoader/index.js
@@ -23,11 +23,11 @@ export default @injectSheet(styles) @withTheme class AppLoader extends Component
23 static propTypes = { 23 static propTypes = {
24 classes: PropTypes.object.isRequired, 24 classes: PropTypes.object.isRequired,
25 theme: PropTypes.object.isRequired, 25 theme: PropTypes.object.isRequired,
26 } 26 };
27 27
28 state = { 28 state = {
29 step: 0, 29 step: 0,
30 } 30 };
31 31
32 interval = null; 32 interval = null;
33 33
diff --git a/src/components/ui/FullscreenLoader/index.js b/src/components/ui/FullscreenLoader/index.js
index 6ecf4d395..06dab1eb6 100644
--- a/src/components/ui/FullscreenLoader/index.js
+++ b/src/components/ui/FullscreenLoader/index.js
@@ -16,13 +16,13 @@ export default @observer @withTheme @injectSheet(styles) class FullscreenLoader
16 theme: PropTypes.object.isRequired, 16 theme: PropTypes.object.isRequired,
17 spinnerColor: PropTypes.string, 17 spinnerColor: PropTypes.string,
18 children: PropTypes.node, 18 children: PropTypes.node,
19 } 19 };
20 20
21 static defaultProps = { 21 static defaultProps = {
22 className: null, 22 className: null,
23 spinnerColor: null, 23 spinnerColor: null,
24 children: null, 24 children: null,
25 } 25 };
26 26
27 render() { 27 render() {
28 const { 28 const {
diff --git a/src/components/ui/Infobox.js b/src/components/ui/Infobox.js
index a33c6474a..0917ee9f0 100644
--- a/src/components/ui/Infobox.js
+++ b/src/components/ui/Infobox.js
@@ -13,6 +13,8 @@ export default @observer class Infobox extends Component {
13 ctaLabel: PropTypes.string, 13 ctaLabel: PropTypes.string,
14 ctaLoading: PropTypes.bool, 14 ctaLoading: PropTypes.bool,
15 dismissable: PropTypes.bool, 15 dismissable: PropTypes.bool,
16 onDismiss: PropTypes.func,
17 onSeen: PropTypes.func,
16 }; 18 };
17 19
18 static defaultProps = { 20 static defaultProps = {
@@ -22,12 +24,19 @@ export default @observer class Infobox extends Component {
22 ctaOnClick: () => null, 24 ctaOnClick: () => null,
23 ctaLabel: '', 25 ctaLabel: '',
24 ctaLoading: false, 26 ctaLoading: false,
27 onDismiss: () => null,
28 onSeen: () => null,
25 }; 29 };
26 30
27 state = { 31 state = {
28 dismissed: false, 32 dismissed: false,
29 }; 33 };
30 34
35 componentDidMount() {
36 const { onSeen } = this.props;
37 if (onSeen) onSeen();
38 }
39
31 render() { 40 render() {
32 const { 41 const {
33 children, 42 children,
@@ -37,6 +46,7 @@ export default @observer class Infobox extends Component {
37 ctaLoading, 46 ctaLoading,
38 ctaOnClick, 47 ctaOnClick,
39 dismissable, 48 dismissable,
49 onDismiss,
40 } = this.props; 50 } = this.props;
41 51
42 if (this.state.dismissed) { 52 if (this.state.dismissed) {
@@ -76,9 +86,10 @@ export default @observer class Infobox extends Component {
76 {dismissable && ( 86 {dismissable && (
77 <button 87 <button
78 type="button" 88 type="button"
79 onClick={() => this.setState({ 89 onClick={() => {
80 dismissed: true, 90 this.setState({ dismissed: true });
81 })} 91 if (onDismiss) onDismiss();
92 }}
82 className="infobox__delete mdi mdi-close" 93 className="infobox__delete mdi mdi-close"
83 /> 94 />
84 )} 95 )}
diff --git a/src/components/ui/Modal/index.js b/src/components/ui/Modal/index.js
index d84e4c713..0b7154760 100644
--- a/src/components/ui/Modal/index.js
+++ b/src/components/ui/Modal/index.js
@@ -3,9 +3,12 @@ import ReactModal from 'react-modal';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import classnames from 'classnames'; 4import classnames from 'classnames';
5import injectCSS from 'react-jss'; 5import injectCSS from 'react-jss';
6import { Icon } from '@meetfranz/ui';
6 7
7import styles from './styles'; 8import styles from './styles';
8 9
10// ReactModal.setAppElement('#root');
11
9export default @injectCSS(styles) class Modal extends Component { 12export default @injectCSS(styles) class Modal extends Component {
10 static propTypes = { 13 static propTypes = {
11 children: PropTypes.node.isRequired, 14 children: PropTypes.node.isRequired,
@@ -14,11 +17,15 @@ export default @injectCSS(styles) class Modal extends Component {
14 isOpen: PropTypes.bool.isRequired, 17 isOpen: PropTypes.bool.isRequired,
15 portal: PropTypes.string, 18 portal: PropTypes.string,
16 close: PropTypes.func.isRequired, 19 close: PropTypes.func.isRequired,
20 shouldCloseOnOverlayClick: PropTypes.bool,
21 showClose: PropTypes.bool,
17 } 22 }
18 23
19 static defaultProps = { 24 static defaultProps = {
20 className: null, 25 className: null,
21 portal: 'modal-portal', 26 portal: 'modal-portal',
27 shouldCloseOnOverlayClick: false,
28 showClose: true,
22 } 29 }
23 30
24 render() { 31 render() {
@@ -29,6 +36,8 @@ export default @injectCSS(styles) class Modal extends Component {
29 isOpen, 36 isOpen,
30 portal, 37 portal,
31 close, 38 close,
39 shouldCloseOnOverlayClick,
40 showClose,
32 } = this.props; 41 } = this.props;
33 42
34 return ( 43 return (
@@ -42,14 +51,17 @@ export default @injectCSS(styles) class Modal extends Component {
42 overlayClassName={classes.overlay} 51 overlayClassName={classes.overlay}
43 portal={portal} 52 portal={portal}
44 onRequestClose={close} 53 onRequestClose={close}
54 shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
45 > 55 >
46 {/* <button 56 {showClose && close && (
47 type="button" 57 <button
48 className={classnames({ 58 type="button"
49 [`${classes.close}`]: true, 59 className={classes.close}
50 'mdi mdi-close': true, 60 onClick={close}
51 })} 61 >
52 /> */} 62 <Icon icon="mdiClose" size={1.5} />
63 </button>
64 )}
53 <div className={classes.content}> 65 <div className={classes.content}>
54 {children} 66 {children}
55 </div> 67 </div>
diff --git a/src/components/ui/Modal/styles.js b/src/components/ui/Modal/styles.js
index 56fecbf55..49b970c97 100644
--- a/src/components/ui/Modal/styles.js
+++ b/src/components/ui/Modal/styles.js
@@ -28,5 +28,6 @@ export default theme => ({
28 position: 'absolute', 28 position: 'absolute',
29 top: 0, 29 top: 0,
30 right: 0, 30 right: 0,
31 padding: 20,
31 }, 32 },
32}); 33});
diff --git a/src/components/ui/PremiumFeatureContainer/index.js b/src/components/ui/PremiumFeatureContainer/index.js
index 67cd6af0b..3c1e0fac3 100644
--- a/src/components/ui/PremiumFeatureContainer/index.js
+++ b/src/components/ui/PremiumFeatureContainer/index.js
@@ -9,6 +9,7 @@ import { oneOrManyChildElements } from '../../../prop-types';
9import UserStore from '../../../stores/UserStore'; 9import UserStore from '../../../stores/UserStore';
10 10
11import styles from './styles'; 11import styles from './styles';
12import { gaEvent } from '../../../lib/analytics';
12 13
13const messages = defineMessages({ 14const messages = defineMessages({
14 action: { 15 action: {
@@ -17,14 +18,21 @@ const messages = defineMessages({
17 }, 18 },
18}); 19});
19 20
20export default @inject('stores', 'actions') @injectSheet(styles) @observer class PremiumFeatureContainer extends Component { 21@inject('stores', 'actions') @injectSheet(styles) @observer
22class PremiumFeatureContainer extends Component {
21 static propTypes = { 23 static propTypes = {
22 classes: PropTypes.object.isRequired, 24 classes: PropTypes.object.isRequired,
23 condition: PropTypes.bool, 25 condition: PropTypes.bool,
26 gaEventInfo: PropTypes.shape({
27 category: PropTypes.string.isRequired,
28 event: PropTypes.string.isRequired,
29 label: PropTypes.string,
30 }),
24 }; 31 };
25 32
26 static defaultProps = { 33 static defaultProps = {
27 condition: true, 34 condition: true,
35 gaEventInfo: null,
28 }; 36 };
29 37
30 static contextTypes = { 38 static contextTypes = {
@@ -38,6 +46,7 @@ export default @inject('stores', 'actions') @injectSheet(styles) @observer class
38 actions, 46 actions,
39 condition, 47 condition,
40 stores, 48 stores,
49 gaEventInfo,
41 } = this.props; 50 } = this.props;
42 51
43 const { intl } = this.context; 52 const { intl } = this.context;
@@ -49,7 +58,13 @@ export default @inject('stores', 'actions') @injectSheet(styles) @observer class
49 <button 58 <button
50 className={classes.actionButton} 59 className={classes.actionButton}
51 type="button" 60 type="button"
52 onClick={() => actions.ui.openSettings({ path: 'user' })} 61 onClick={() => {
62 actions.ui.openSettings({ path: 'user' });
63 if (gaEventInfo) {
64 const { category, event, label } = gaEventInfo;
65 gaEvent(category, event, label);
66 }
67 }}
53 > 68 >
54 {intl.formatMessage(messages.action)} 69 {intl.formatMessage(messages.action)}
55 </button> 70 </button>
@@ -73,3 +88,5 @@ PremiumFeatureContainer.wrappedComponent.propTypes = {
73 }).isRequired, 88 }).isRequired,
74 }).isRequired, 89 }).isRequired,
75}; 90};
91
92export default PremiumFeatureContainer;
diff --git a/src/components/ui/PremiumFeatureContainer/styles.js b/src/components/ui/PremiumFeatureContainer/styles.js
index 81d6666c6..41881e044 100644
--- a/src/components/ui/PremiumFeatureContainer/styles.js
+++ b/src/components/ui/PremiumFeatureContainer/styles.js
@@ -6,6 +6,7 @@ export default theme => ({
6 padding: 20, 6 padding: 20,
7 'border-radius': theme.borderRadius, 7 'border-radius': theme.borderRadius,
8 pointerEvents: 'none', 8 pointerEvents: 'none',
9 height: 'auto',
9 }, 10 },
10 titleContainer: { 11 titleContainer: {
11 display: 'flex', 12 display: 'flex',
@@ -19,14 +20,14 @@ export default theme => ({
19 color: theme.colorSubscriptionContainerActionButtonColor, 20 color: theme.colorSubscriptionContainerActionButtonColor,
20 'margin-left': 'auto', 21 'margin-left': 'auto',
21 'border-radius': theme.borderRadiusSmall, 22 'border-radius': theme.borderRadiusSmall,
22 padding: [2, 4], 23 padding: [4, 8],
23 'font-size': 12, 24 'font-size': 12,
24 pointerEvents: 'initial', 25 pointerEvents: 'initial',
25 }, 26 },
26 content: { 27 content: {
27 opacity: 0.5, 28 opacity: 0.5,
28 'margin-top': 20, 29 'margin-top': 20,
29 '& :last-child': { 30 '& > :last-child': {
30 'margin-bottom': 0, 31 'margin-bottom': 0,
31 }, 32 },
32 }, 33 },
diff --git a/src/components/ui/ServiceIcon.js b/src/components/ui/ServiceIcon.js
new file mode 100644
index 000000000..0b9155a4e
--- /dev/null
+++ b/src/components/ui/ServiceIcon.js
@@ -0,0 +1,67 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import classnames from 'classnames';
6
7import ServiceModel from '../../models/Service';
8
9const styles = theme => ({
10 root: {
11 height: 'auto',
12 },
13 icon: {
14 width: theme.serviceIcon.width,
15 },
16 isCustomIcon: {
17 width: theme.serviceIcon.isCustom.width,
18 border: theme.serviceIcon.isCustom.border,
19 borderRadius: theme.serviceIcon.isCustom.borderRadius,
20 },
21 isDisabled: {
22 filter: 'grayscale(100%)',
23 opacity: '.5',
24 },
25});
26
27@injectSheet(styles) @observer
28class ServiceIcon extends Component {
29 static propTypes = {
30 classes: PropTypes.object.isRequired,
31 service: PropTypes.instanceOf(ServiceModel).isRequired,
32 className: PropTypes.string,
33 };
34
35 static defaultProps = {
36 className: '',
37 };
38
39 render() {
40 const {
41 classes,
42 className,
43 service,
44 } = this.props;
45
46 return (
47 <div
48 className={classnames([
49 classes.root,
50 className,
51 ])}
52 >
53 <img
54 src={service.icon}
55 className={classnames([
56 classes.icon,
57 service.isEnabled ? null : classes.isDisabled,
58 service.hasCustomIcon ? classes.isCustomIcon : null,
59 ])}
60 alt=""
61 />
62 </div>
63 );
64 }
65}
66
67export default ServiceIcon;
diff --git a/src/components/ui/WebviewLoader/index.js b/src/components/ui/WebviewLoader/index.js
index 3a3dbbe49..58b6b6f1b 100644
--- a/src/components/ui/WebviewLoader/index.js
+++ b/src/components/ui/WebviewLoader/index.js
@@ -2,23 +2,35 @@ import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 4import injectSheet from 'react-jss';
5import { defineMessages, intlShape } from 'react-intl';
5 6
6import FullscreenLoader from '../FullscreenLoader'; 7import FullscreenLoader from '../FullscreenLoader';
7
8import styles from './styles'; 8import styles from './styles';
9 9
10const messages = defineMessages({
11 loading: {
12 id: 'service.webviewLoader.loading',
13 defaultMessage: '!!!Loading',
14 },
15});
16
10export default @observer @injectSheet(styles) class WebviewLoader extends Component { 17export default @observer @injectSheet(styles) class WebviewLoader extends Component {
11 static propTypes = { 18 static propTypes = {
12 name: PropTypes.string.isRequired, 19 name: PropTypes.string.isRequired,
13 classes: PropTypes.object.isRequired, 20 classes: PropTypes.object.isRequired,
14 } 21 };
22
23 static contextTypes = {
24 intl: intlShape,
25 };
15 26
16 render() { 27 render() {
17 const { classes, name } = this.props; 28 const { classes, name } = this.props;
29 const { intl } = this.context;
18 return ( 30 return (
19 <FullscreenLoader 31 <FullscreenLoader
20 className={classes.component} 32 className={classes.component}
21 title={`Loading ${name}`} 33 title={`${intl.formatMessage(messages.loading)} ${name}`}
22 /> 34 />
23 ); 35 );
24 } 36 }