aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/features')
-rw-r--r--src/features/announcements/api.js8
-rw-r--r--src/features/announcements/components/AnnouncementScreen.js2
-rw-r--r--src/features/communityRecipes/index.js19
-rw-r--r--src/features/communityRecipes/store.js7
-rw-r--r--src/features/delayApp/Component.js120
-rw-r--r--src/features/delayApp/constants.js6
-rw-r--r--src/features/delayApp/index.js80
-rw-r--r--src/features/delayApp/styles.js22
-rw-r--r--src/features/planSelection/actions.js9
-rw-r--r--src/features/planSelection/api.js26
-rw-r--r--src/features/planSelection/components/PlanItem.js215
-rw-r--r--src/features/planSelection/components/PlanSelection.js269
-rw-r--r--src/features/planSelection/containers/PlanSelectionScreen.js120
-rw-r--r--src/features/planSelection/index.js28
-rw-r--r--src/features/planSelection/store.js68
-rw-r--r--src/features/publishDebugInfo/Component.js6
-rw-r--r--src/features/quickSwitch/Component.js4
-rw-r--r--src/features/serviceLimit/components/LimitReachedInfobox.js75
-rw-r--r--src/features/serviceLimit/index.js31
-rw-r--r--src/features/serviceLimit/store.js42
-rw-r--r--src/features/serviceProxy/index.js2
-rw-r--r--src/features/shareFranz/Component.js6
-rw-r--r--src/features/shareFranz/index.js13
-rw-r--r--src/features/spellchecker/index.js27
-rw-r--r--src/features/todos/components/TodosWebview.js20
-rw-r--r--src/features/todos/containers/TodosScreen.js4
-rw-r--r--src/features/todos/store.js15
-rw-r--r--src/features/trialStatusBar/actions.js13
-rw-r--r--src/features/trialStatusBar/components/ProgressBar.js45
-rw-r--r--src/features/trialStatusBar/components/TrialStatusBar.js135
-rw-r--r--src/features/trialStatusBar/containers/TrialStatusBarScreen.js112
-rw-r--r--src/features/trialStatusBar/index.js30
-rw-r--r--src/features/trialStatusBar/store.js72
-rw-r--r--src/features/utils/ActionBinding.js4
-rw-r--r--src/features/utils/FeatureStore.js8
-rw-r--r--src/features/webControls/components/WebControls.js63
-rw-r--r--src/features/webControls/containers/WebControlsScreen.js2
-rw-r--r--src/features/workspaces/api.js2
-rw-r--r--src/features/workspaces/components/WorkspaceDrawer.js143
-rw-r--r--src/features/workspaces/components/WorkspaceDrawerItem.js47
-rw-r--r--src/features/workspaces/components/WorkspaceItem.js2
-rw-r--r--src/features/workspaces/components/WorkspaceServiceListItem.js2
-rw-r--r--src/features/workspaces/components/WorkspaceSwitchingIndicator.js21
-rw-r--r--src/features/workspaces/components/WorkspacesDashboard.js129
-rw-r--r--src/features/workspaces/containers/WorkspacesScreen.js4
-rw-r--r--src/features/workspaces/models/Workspace.js4
-rw-r--r--src/features/workspaces/store.js47
47 files changed, 201 insertions, 1928 deletions
diff --git a/src/features/announcements/api.js b/src/features/announcements/api.js
index 53df69eed..a7fe24bb1 100644
--- a/src/features/announcements/api.js
+++ b/src/features/announcements/api.js
@@ -1,7 +1,7 @@
1import { app } from '@electron/remote'; 1import { app } from '@electron/remote';
2import Request from '../../stores/lib/Request'; 2import Request from '../../stores/lib/Request';
3import apiBase from '../../api/apiBase'; 3import apiBase from '../../api/apiBase';
4import { GITHUB_FERDI_REPO_NAME, GITHUB_ORG_NAME } from '../../config'; 4import { GITHUB_FERDI_REPO_NAME, GITHUB_NIGHTLIES_REPO_NAME, GITHUB_ORG_NAME } from '../../config';
5 5
6const debug = require('debug')('Ferdi:feature:announcements:api'); 6const debug = require('debug')('Ferdi:feature:announcements:api');
7 7
@@ -12,8 +12,8 @@ export const announcementsApi = {
12 }, 12 },
13 13
14 async getChangelog(version) { 14 async getChangelog(version) {
15 // TODO: This doesn't seem to handle the different 'nightlies' repo that we currently use. Needs to be fixed. 15 const ferdiRepoName = version.includes('nightly') ? GITHUB_NIGHTLIES_REPO_NAME : GITHUB_FERDI_REPO_NAME;
16 const url = `https://api.github.com/repos/${GITHUB_ORG_NAME}/${GITHUB_FERDI_REPO_NAME}/releases/tags/v${version}`; 16 const url = `https://api.github.com/repos/${GITHUB_ORG_NAME}/${ferdiRepoName}/releases/tags/v${version}`;
17 debug(`fetching release changelog from Github url: ${url}`); 17 debug(`fetching release changelog from Github url: ${url}`);
18 const request = await window.fetch(url, { method: 'GET' }); 18 const request = await window.fetch(url, { method: 'GET' });
19 if (!request.ok) return null; 19 if (!request.ok) return null;
@@ -22,7 +22,7 @@ export const announcementsApi = {
22 }, 22 },
23 23
24 async getAnnouncement(version) { 24 async getAnnouncement(version) {
25 const url = `${apiBase(true)}/announcements/${version}`; 25 const url = `${apiBase()}/announcements/${version}`;
26 debug(`fetching release announcement from api url: ${url}`); 26 debug(`fetching release announcement from api url: ${url}`);
27 const response = await window.fetch(url, { method: 'GET' }); 27 const response = await window.fetch(url, { method: 'GET' });
28 if (!response.ok) return null; 28 if (!response.ok) return null;
diff --git a/src/features/announcements/components/AnnouncementScreen.js b/src/features/announcements/components/AnnouncementScreen.js
index 21964b108..315843db3 100644
--- a/src/features/announcements/components/AnnouncementScreen.js
+++ b/src/features/announcements/components/AnnouncementScreen.js
@@ -25,7 +25,7 @@ const messages = defineMessages({
25 25
26const smallScreen = '1000px'; 26const smallScreen = '1000px';
27 27
28const styles = theme => ({ 28const styles = (theme) => ({
29 container: { 29 container: {
30 background: theme.colorBackground, 30 background: theme.colorBackground,
31 position: 'relative', 31 position: 'relative',
diff --git a/src/features/communityRecipes/index.js b/src/features/communityRecipes/index.js
index 39f7e9cd6..828c6d867 100644
--- a/src/features/communityRecipes/index.js
+++ b/src/features/communityRecipes/index.js
@@ -1,26 +1,7 @@
1import { reaction } from 'mobx';
2import { CommunityRecipesStore } from './store'; 1import { CommunityRecipesStore } from './store';
3 2
4const debug = require('debug')('Ferdi:feature:communityRecipes');
5
6export const communityRecipesStore = new CommunityRecipesStore(); 3export const communityRecipesStore = new CommunityRecipesStore();
7 4
8export default function initCommunityRecipes(stores, actions) { 5export default function initCommunityRecipes(stores, actions) {
9 const { features } = stores;
10
11 communityRecipesStore.start(stores, actions); 6 communityRecipesStore.start(stores, actions);
12
13 // Toggle communityRecipe premium status
14 reaction(
15 () => (
16 features.features.isCommunityRecipesIncludedInCurrentPlan
17 ),
18 (isPremiumFeature) => {
19 debug('Community recipes is premium feature: ', isPremiumFeature);
20 communityRecipesStore.isCommunityRecipesIncludedInCurrentPlan = true;
21 },
22 {
23 fireImmediately: true,
24 },
25 );
26} 7}
diff --git a/src/features/communityRecipes/store.js b/src/features/communityRecipes/store.js
index 3a60e5449..a3614dd11 100644
--- a/src/features/communityRecipes/store.js
+++ b/src/features/communityRecipes/store.js
@@ -1,11 +1,9 @@
1import { computed, observable } from 'mobx'; 1import { computed } from 'mobx';
2import { FeatureStore } from '../utils/FeatureStore'; 2import { FeatureStore } from '../utils/FeatureStore';
3 3
4const debug = require('debug')('Ferdi:feature:communityRecipes:store'); 4const debug = require('debug')('Ferdi:feature:communityRecipes:store');
5 5
6export class CommunityRecipesStore extends FeatureStore { 6export class CommunityRecipesStore extends FeatureStore {
7 @observable isCommunityRecipesIncludedInCurrentPlan = true;
8
9 start(stores, actions) { 7 start(stores, actions) {
10 debug('start'); 8 debug('start');
11 this.stores = stores; 9 this.stores = stores;
@@ -21,7 +19,8 @@ export class CommunityRecipesStore extends FeatureStore {
21 if (!this.stores) return []; 19 if (!this.stores) return [];
22 20
23 return this.stores.recipePreviews.dev.map((r) => { 21 return this.stores.recipePreviews.dev.map((r) => {
24 r.isDevRecipe = !!r.author.find(a => a.email === this.stores.user.data.email); 22 // TODO: Need to figure out if this is even necessary/used
23 r.isDevRecipe = !!r.author.find((a) => a.email === this.stores.user.data.email);
25 24
26 return r; 25 return r;
27 }); 26 });
diff --git a/src/features/delayApp/Component.js b/src/features/delayApp/Component.js
deleted file mode 100644
index 6471240ab..000000000
--- a/src/features/delayApp/Component.js
+++ /dev/null
@@ -1,120 +0,0 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss';
6
7import { Button } from '@meetfranz/forms';
8
9import { config } from './constants';
10import styles from './styles';
11import UserStore from '../../stores/UserStore';
12import UIStore from '../../stores/UIStore';
13import { FeatureStore } from '../utils/FeatureStore';
14
15const messages = defineMessages({
16 headline: {
17 id: 'feature.delayApp.headline',
18 defaultMessage: '!!!Please purchase license to skip waiting',
19 },
20 headlineTrial: {
21 id: 'feature.delayApp.trial.headline',
22 defaultMessage: '!!!Get the free Franz Professional 14 day trial and skip the line',
23 },
24 action: {
25 id: 'feature.delayApp.upgrade.action',
26 defaultMessage: '!!!Upgrade Franz',
27 },
28 actionTrial: {
29 id: 'feature.delayApp.trial.action',
30 defaultMessage: '!!!Yes, I want the free 14 day trial of Franz Professional',
31 },
32 text: {
33 id: 'feature.delayApp.text',
34 defaultMessage: '!!!Ferdi will continue in {seconds} seconds.',
35 },
36});
37
38export default @inject('stores', 'actions') @injectSheet(styles) @observer class DelayApp extends Component {
39 static propTypes = {
40 // eslint-disable-next-line
41 classes: PropTypes.object.isRequired,
42 };
43
44 static contextTypes = {
45 intl: intlShape,
46 };
47
48 state = {
49 countdown: config.delayDuration,
50 };
51
52 countdownInterval = null;
53
54 countdownIntervalTimeout = 1000;
55
56 componentDidMount() {
57 this.countdownInterval = setInterval(() => {
58 this.setState(prevState => ({
59 countdown: prevState.countdown - this.countdownIntervalTimeout,
60 }));
61
62 if (this.state.countdown <= 0) {
63 // reload();
64 clearInterval(this.countdownInterval);
65 }
66 }, this.countdownIntervalTimeout);
67 }
68
69 componentWillUnmount() {
70 clearInterval(this.countdownInterval);
71 }
72
73 handleCTAClick() {
74 const { actions, stores } = this.props;
75 const { hadSubscription } = stores.user.data;
76 const { defaultTrialPlan } = stores.features.features;
77
78 if (!hadSubscription) {
79 actions.user.activateTrial({ planId: defaultTrialPlan });
80 } else {
81 actions.ui.openSettings({ path: 'user' });
82 }
83 }
84
85 render() {
86 const { classes, stores } = this.props;
87 const { intl } = this.context;
88
89 const { hadSubscription } = stores.user.data;
90
91 return (
92 <div className={`${classes.container}`}>
93 <h1 className={classes.headline}>{intl.formatMessage(hadSubscription ? messages.headline : messages.headlineTrial)}</h1>
94 <Button
95 label={intl.formatMessage(hadSubscription ? messages.action : messages.actionTrial)}
96 className={classes.button}
97 buttonType="inverted"
98 onClick={this.handleCTAClick.bind(this)}
99 busy={stores.user.activateTrialRequest.isExecuting}
100 />
101 <p className="footnote">
102 {intl.formatMessage(messages.text, {
103 seconds: this.state.countdown / 1000,
104 })}
105 </p>
106 </div>
107 );
108 }
109}
110
111DelayApp.wrappedComponent.propTypes = {
112 stores: PropTypes.shape({
113 user: PropTypes.instanceOf(UserStore).isRequired,
114 features: PropTypes.instanceOf(FeatureStore).isRequired,
115 }).isRequired,
116 actions: PropTypes.shape({
117 ui: PropTypes.instanceOf(UIStore).isRequired,
118 user: PropTypes.instanceOf(UserStore).isRequired,
119 }).isRequired,
120};
diff --git a/src/features/delayApp/constants.js b/src/features/delayApp/constants.js
deleted file mode 100644
index 72cc4246e..000000000
--- a/src/features/delayApp/constants.js
+++ /dev/null
@@ -1,6 +0,0 @@
1import { DEFAULT_FEATURES_CONFIG } from '../../config';
2
3export const config = {
4 delayOffset: DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.delayOffset,
5 delayDuration: DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.wait,
6};
diff --git a/src/features/delayApp/index.js b/src/features/delayApp/index.js
deleted file mode 100644
index f0c2bdc82..000000000
--- a/src/features/delayApp/index.js
+++ /dev/null
@@ -1,80 +0,0 @@
1import { autorun, observable, reaction } from 'mobx';
2import moment from 'moment';
3import DelayAppComponent from './Component';
4import { config } from './constants';
5import { DEFAULT_FEATURES_CONFIG } from '../../config';
6import { getUserWorkspacesRequest } from '../workspaces/api';
7
8const debug = require('debug')('Ferdi:feature:delayApp');
9
10export const state = observable({
11 isDelayAppScreenVisible: DEFAULT_FEATURES_CONFIG.needToWaitToProceed,
12});
13
14function setVisibility(value) {
15 Object.assign(state, {
16 isDelayAppScreenVisible: value,
17 });
18}
19
20export default function init(stores) {
21 debug('Initializing `delayApp` feature');
22
23 let shownAfterLaunch = false;
24 let timeLastDelay = moment();
25
26 window.ferdi.features.delayApp = {
27 state,
28 };
29
30 reaction(
31 () => (
32 stores.user.isLoggedIn
33 && stores.services.allServicesRequest.wasExecuted
34 && getUserWorkspacesRequest.wasExecuted
35 && stores.features.features.needToWaitToProceed
36 && !stores.user.data.isPremium
37 ),
38 (isEnabled) => {
39 if (isEnabled) {
40 debug('Enabling `delayApp` feature');
41
42 const { needToWaitToProceedConfig: globalConfig } = stores.features.features;
43
44 config.delayOffset = globalConfig.delayOffset !== undefined ? globalConfig.delayOffset : DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.delayOffset;
45 config.delayDuration = globalConfig.wait !== undefined ? globalConfig.wait : DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.wait;
46
47 autorun(() => {
48 const { isAnnouncementShown } = stores.announcements;
49 if (stores.services.allDisplayed.length === 0 || isAnnouncementShown) {
50 shownAfterLaunch = true;
51 setVisibility(false);
52 return;
53 }
54
55 const diff = moment().diff(timeLastDelay);
56 const itsTimeToWait = diff >= config.delayOffset;
57 if (!isAnnouncementShown && ((stores.app.isFocused && itsTimeToWait) || !shownAfterLaunch)) {
58 debug(`App will be delayed for ${config.delayDuration / 1000}s`);
59
60 setVisibility(true);
61
62 setTimeout(() => {
63 debug('Resetting app delay');
64
65 shownAfterLaunch = true;
66 timeLastDelay = moment();
67 setVisibility(false);
68 }, config.delayDuration + 1000); // timer needs to be able to hit 0
69 } else {
70 setVisibility(false);
71 }
72 });
73 } else {
74 setVisibility(false);
75 }
76 },
77 );
78}
79
80export const Component = DelayAppComponent;
diff --git a/src/features/delayApp/styles.js b/src/features/delayApp/styles.js
deleted file mode 100644
index 69c3c7a27..000000000
--- a/src/features/delayApp/styles.js
+++ /dev/null
@@ -1,22 +0,0 @@
1export default theme => ({
2 container: {
3 background: theme.colorBackground,
4 top: 0,
5 width: '100%',
6 display: 'flex',
7 'flex-direction': 'column',
8 'align-items': 'center',
9 'justify-content': 'center',
10 'z-index': 150,
11 },
12 headline: {
13 color: theme.colorHeadline,
14 margin: [25, 0, 40],
15 'max-width': 500,
16 'text-align': 'center',
17 'line-height': '1.3em',
18 },
19 button: {
20 margin: [40, 0, 20],
21 },
22});
diff --git a/src/features/planSelection/actions.js b/src/features/planSelection/actions.js
deleted file mode 100644
index 83f58bfd7..000000000
--- a/src/features/planSelection/actions.js
+++ /dev/null
@@ -1,9 +0,0 @@
1import PropTypes from 'prop-types';
2import { createActionsFromDefinitions } from '../../actions/lib/actions';
3
4export const planSelectionActions = createActionsFromDefinitions({
5 downgradeAccount: {},
6 hideOverlay: {},
7}, PropTypes.checkPropTypes);
8
9export default planSelectionActions;
diff --git a/src/features/planSelection/api.js b/src/features/planSelection/api.js
deleted file mode 100644
index 16bf9ff2d..000000000
--- a/src/features/planSelection/api.js
+++ /dev/null
@@ -1,26 +0,0 @@
1import { sendAuthRequest } from '../../api/utils/auth';
2import Request from '../../stores/lib/Request';
3import apiBase from '../../api/apiBase';
4
5const debug = require('debug')('Ferdi:feature:planSelection:api');
6
7export const planSelectionApi = {
8 downgrade: async () => {
9 const url = `${apiBase()}/payment/downgrade`;
10 const options = {
11 method: 'PUT',
12 };
13 debug('downgrade UPDATE', url, options);
14 const result = await sendAuthRequest(url, options);
15 debug('downgrade RESULT', result);
16 if (!result.ok) throw result;
17
18 return result.ok;
19 },
20};
21
22export const downgradeUserRequest = new Request(planSelectionApi, 'downgrade');
23
24export const resetApiRequests = () => {
25 downgradeUserRequest.reset();
26};
diff --git a/src/features/planSelection/components/PlanItem.js b/src/features/planSelection/components/PlanItem.js
deleted file mode 100644
index e90532dec..000000000
--- a/src/features/planSelection/components/PlanItem.js
+++ /dev/null
@@ -1,215 +0,0 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss';
6import classnames from 'classnames';
7import color from 'color';
8
9import { H2 } from '@meetfranz/ui';
10
11import { Button } from '@meetfranz/forms';
12import { mdiArrowRight } from '@mdi/js';
13
14const messages = defineMessages({
15 perMonth: {
16 id: 'subscription.interval.perMonth',
17 defaultMessage: '!!!per month',
18 },
19 perMonthPerUser: {
20 id: 'subscription.interval.perMonthPerUser',
21 defaultMessage: '!!!per month & user',
22 },
23 bestValue: {
24 id: 'subscription.bestValue',
25 defaultMessage: '!!!Best value',
26 },
27});
28
29const styles = theme => ({
30 root: {
31 display: 'flex',
32 flexDirection: 'column',
33 borderRadius: theme.borderRadius,
34 flex: 1,
35 color: theme.styleTypes.primary.accent,
36 overflow: 'hidden',
37 textAlign: 'center',
38
39 '& h2': {
40 textAlign: 'center',
41 marginBottom: 10,
42 fontSize: 30,
43 color: theme.styleTypes.primary.contrast,
44 },
45 },
46 currency: {
47 fontSize: 35,
48 },
49 priceWrapper: {
50 height: 50,
51 marginBottom: 0,
52 marginTop: ({ text }) => (!text ? 15 : 0),
53 },
54 price: {
55 fontSize: 50,
56
57 '& sup': {
58 fontSize: 20,
59 verticalAlign: 20,
60 },
61 },
62 text: {
63 marginBottom: 'auto',
64 },
65 cta: {
66 background: theme.styleTypes.primary.accent,
67 color: theme.styleTypes.primary.contrast,
68 margin: [30, 'auto', 0, 'auto'],
69 },
70 divider: {
71 width: 40,
72 border: 0,
73 borderTop: [1, 'solid', theme.styleTypes.primary.contrast],
74 margin: [15, 'auto', 20],
75 },
76 header: {
77 padding: 20,
78 background: color(theme.styleTypes.primary.accent).darken(0.25).hex(),
79 color: theme.styleTypes.primary.contrast,
80 position: 'relative',
81 height: 'auto',
82 },
83 content: {
84 padding: [10, 20, 20],
85 background: '#EFEFEF',
86 display: 'flex',
87 flexDirection: 'column',
88 justifyContent: 'space-between',
89 },
90 simpleCTA: {
91 background: 'none',
92 color: theme.styleTypes.primary.accent,
93
94 '& svg': {
95 fill: theme.styleTypes.primary.accent,
96 },
97 },
98 bestValue: {
99 background: theme.styleTypes.success.accent,
100 color: theme.styleTypes.success.contrast,
101 right: -66,
102 top: -40,
103 height: 'auto',
104 position: 'absolute',
105 transform: 'rotateZ(45deg)',
106 textAlign: 'center',
107 padding: [5, 50],
108 transformOrigin: 'left bottom',
109 fontSize: 12,
110 boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
111 },
112});
113
114export default @observer @injectSheet(styles) class PlanItem extends Component {
115 static propTypes = {
116 name: PropTypes.string.isRequired,
117 text: PropTypes.string.isRequired,
118 price: PropTypes.number.isRequired,
119 currency: PropTypes.string.isRequired,
120 upgrade: PropTypes.func.isRequired,
121 ctaLabel: PropTypes.string.isRequired,
122 simpleCTA: PropTypes.bool,
123 perUser: PropTypes.bool,
124 classes: PropTypes.object.isRequired,
125 bestValue: PropTypes.bool,
126 className: PropTypes.string,
127 children: PropTypes.element,
128 };
129
130 static defaultProps = {
131 simpleCTA: false,
132 perUser: false,
133 children: null,
134 bestValue: false,
135 className: '',
136 }
137
138 static contextTypes = {
139 intl: intlShape,
140 };
141
142 render() {
143 const {
144 name,
145 text,
146 price,
147 currency,
148 classes,
149 upgrade,
150 ctaLabel,
151 simpleCTA,
152 perUser,
153 bestValue,
154 className,
155 children,
156 } = this.props;
157 const { intl } = this.context;
158
159 const priceParts = `${price}`.split('.');
160
161 return (
162 <div className={classnames({
163 [classes.root]: true,
164 [className]: className,
165 })}
166 >
167 <div className={classes.header}>
168 {bestValue && (
169 <div className={classes.bestValue}>
170 {intl.formatMessage(messages.bestValue)}
171 </div>
172 )}
173 <H2 className={classes.planName}>{name}</H2>
174 {text && (
175 <>
176 <p className={classes.text}>
177 {text}
178 </p>
179 <hr className={classes.divider} />
180 </>
181 )}
182 <p className={classes.priceWrapper}>
183 <span className={classes.currency}>{currency}</span>
184 <span className={classes.price}>
185 {priceParts[0]}
186 <sup>{priceParts[1]}</sup>
187 </span>
188 </p>
189 <p className={classes.interval}>
190 {intl.formatMessage(perUser ? messages.perMonthPerUser : messages.perMonth)}
191 </p>
192 </div>
193
194 <div className={classes.content}>
195 {children}
196
197 <Button
198 className={classnames({
199 [classes.cta]: true,
200 [classes.simpleCTA]: simpleCTA,
201 })}
202 icon={simpleCTA ? mdiArrowRight : null}
203 label={(
204 <>
205 {ctaLabel}
206 </>
207 )}
208 onClick={upgrade}
209 />
210 </div>
211
212 </div>
213 );
214 }
215}
diff --git a/src/features/planSelection/components/PlanSelection.js b/src/features/planSelection/components/PlanSelection.js
deleted file mode 100644
index 819a9df5b..000000000
--- a/src/features/planSelection/components/PlanSelection.js
+++ /dev/null
@@ -1,269 +0,0 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import { defineMessages, intlShape } from 'react-intl';
6import { H1, H2, Icon } from '@meetfranz/ui';
7import color from 'color';
8
9import { mdiArrowRight } from '@mdi/js';
10import PlanItem from './PlanItem';
11import { i18nPlanName } from '../../../helpers/plan-helpers';
12import { DEV_API_FRANZ_WEBSITE, PLANS } from '../../../config';
13import { FeatureList } from '../../../components/ui/FeatureList';
14import Appear from '../../../components/ui/effects/Appear';
15
16const messages = defineMessages({
17 welcome: {
18 id: 'feature.planSelection.fullscreen.welcome',
19 defaultMessage: '!!!Are you ready to choose, {name}',
20 },
21 subheadline: {
22 id: 'feature.planSelection.fullscreen.subheadline',
23 defaultMessage: '!!!It\'s time to make a choice. Franz works best on our Personal and Professional plans. Please have a look and choose the best one for you.',
24 },
25 textFree: {
26 id: 'feature.planSelection.free.text',
27 defaultMessage: '!!!Basic functionality',
28 },
29 textPersonal: {
30 id: 'feature.planSelection.personal.text',
31 defaultMessage: '!!!More services, no waiting - ideal for personal use.',
32 },
33 textProfessional: {
34 id: 'feature.planSelection.pro.text',
35 defaultMessage: '!!!Unlimited services and professional features for you - and your team.',
36 },
37 ctaStayOnFree: {
38 id: 'feature.planSelection.cta.stayOnFree',
39 defaultMessage: '!!!Stay on Free',
40 },
41 ctaDowngradeFree: {
42 id: 'feature.planSelection.cta.ctaDowngradeFree',
43 defaultMessage: '!!!Downgrade to Free',
44 },
45 actionTrial: {
46 id: 'feature.planSelection.cta.trial',
47 defaultMessage: '!!!Start my free 14-days Trial',
48 },
49 shortActionPersonal: {
50 id: 'feature.planSelection.cta.upgradePersonal',
51 defaultMessage: '!!!Choose Personal',
52 },
53 shortActionPro: {
54 id: 'feature.planSelection.cta.upgradePro',
55 defaultMessage: '!!!Choose Professional',
56 },
57 fullFeatureList: {
58 id: 'feature.planSelection.fullFeatureList',
59 defaultMessage: '!!!Complete comparison of all plans',
60 },
61 pricesBasedOnAnnualPayment: {
62 id: 'feature.planSelection.pricesBasedOnAnnualPayment',
63 defaultMessage: '!!!All prices based on yearly payment',
64 },
65});
66
67const styles = theme => ({
68 root: {
69 background: theme.colorModalOverlayBackground,
70 width: '100%',
71 height: '100%',
72 position: 'absolute',
73 top: 0,
74 left: 0,
75 display: 'flex',
76 justifyContent: 'center',
77 alignItems: 'center',
78 zIndex: 999999,
79 overflowY: 'scroll',
80 },
81 container: {
82 // width: '80%',
83 height: 'auto',
84 // background: theme.styleTypes.primary.accent,
85 // padding: 40,
86 borderRadius: theme.borderRadius,
87 maxWidth: 1000,
88
89 '& h1, & h2': {
90 textAlign: 'center',
91 color: theme.styleTypes.primary.contrast,
92 },
93 },
94 plans: {
95 display: 'flex',
96 margin: [40, 0, 0],
97 height: 'auto',
98
99 '& > div': {
100 margin: [0, 15],
101 flex: 1,
102 height: 'auto',
103 background: theme.styleTypes.primary.contrast,
104 boxShadow: [0, 2, 30, color('#000').alpha(0.1).rgb().string()],
105 },
106 },
107 headline: {
108 fontSize: 40,
109 },
110 subheadline: {
111 maxWidth: 660,
112 fontSize: 22,
113 lineHeight: 1.1,
114 margin: [0, 'auto'],
115 },
116 featureList: {
117 '& li': {
118 borderBottom: [1, 'solid', '#CECECE'],
119 },
120 },
121 footer: {
122 display: 'flex',
123 color: theme.styleTypes.primary.contrast,
124 marginTop: 20,
125 padding: [0, 15],
126 },
127 fullFeatureList: {
128 marginRight: 'auto',
129 textAlign: 'center',
130 display: 'flex',
131 justifyContent: 'center',
132 alignItems: 'center',
133 color: `${theme.styleTypes.primary.contrast} !important`,
134
135 '& svg': {
136 marginRight: 5,
137 },
138 },
139 scrollContainer: {
140 border: '1px solid red',
141 overflow: 'scroll-x',
142 },
143 featuredPlan: {
144 transform: ({ isPersonalPlanAvailable }) => (isPersonalPlanAvailable ? 'scale(1.05)' : null),
145 },
146 disclaimer: {
147 textAlign: 'right',
148 margin: [10, 15, 0, 0],
149 },
150});
151
152@injectSheet(styles) @observer
153class PlanSelection extends Component {
154 static propTypes = {
155 classes: PropTypes.object.isRequired,
156 firstname: PropTypes.string.isRequired,
157 plans: PropTypes.object.isRequired,
158 currency: PropTypes.string.isRequired,
159 subscriptionExpired: PropTypes.bool.isRequired,
160 upgradeAccount: PropTypes.func.isRequired,
161 stayOnFree: PropTypes.func.isRequired,
162 hadSubscription: PropTypes.bool.isRequired,
163 isPersonalPlanAvailable: PropTypes.bool,
164 };
165
166 static defaultProps = {
167 isPersonalPlanAvailable: true,
168 }
169
170 static contextTypes = {
171 intl: intlShape,
172 };
173
174 componentDidMount() {
175 }
176
177 render() {
178 const {
179 classes,
180 firstname,
181 plans,
182 currency,
183 subscriptionExpired,
184 upgradeAccount,
185 stayOnFree,
186 hadSubscription,
187 isPersonalPlanAvailable,
188 } = this.props;
189
190 const { intl } = this.context;
191
192 return (
193 <Appear>
194 <div
195 className={classes.root}
196 >
197 <div className={classes.container}>
198 <H1 className={classes.headline}>{intl.formatMessage(messages.welcome, { name: firstname })}</H1>
199 {isPersonalPlanAvailable && (
200 <H2 className={classes.subheadline}>{intl.formatMessage(messages.subheadline)}</H2>
201 )}
202 <div className={classes.plans}>
203 <PlanItem
204 name={i18nPlanName(PLANS.FREE, intl)}
205 text={isPersonalPlanAvailable ? intl.formatMessage(messages.textFree) : null}
206 price={0}
207 currency={currency}
208 ctaLabel={intl.formatMessage(subscriptionExpired ? messages.ctaDowngradeFree : messages.ctaStayOnFree)}
209 upgrade={() => stayOnFree()}
210 simpleCTA
211 >
212 <FeatureList
213 plan={PLANS.FREE}
214 className={classes.featureList}
215 />
216 </PlanItem>
217 <PlanItem
218 name={i18nPlanName(plans.pro.yearly.id, intl)}
219 text={isPersonalPlanAvailable ? intl.formatMessage(messages.textProfessional) : null}
220 price={plans.pro.yearly.price}
221 currency={currency}
222 ctaLabel={intl.formatMessage(hadSubscription ? messages.shortActionPro : messages.actionTrial)}
223 upgrade={() => upgradeAccount(plans.pro.yearly.id)}
224 className={classes.featuredPlan}
225 perUser
226 bestValue={isPersonalPlanAvailable}
227 >
228 <FeatureList
229 plan={isPersonalPlanAvailable ? PLANS.PRO : null}
230 className={classes.featureList}
231 />
232 </PlanItem>
233 {isPersonalPlanAvailable && (
234 <PlanItem
235 name={i18nPlanName(plans.personal.yearly.id, intl)}
236 text={intl.formatMessage(messages.textPersonal)}
237 price={plans.personal.yearly.price}
238 currency={currency}
239 ctaLabel={intl.formatMessage(hadSubscription ? messages.shortActionPersonal : messages.actionTrial)}
240 upgrade={() => upgradeAccount(plans.personal.yearly.id)}
241 >
242 <FeatureList
243 plan={PLANS.PERSONAL}
244 className={classes.featureList}
245 />
246 </PlanItem>
247 )}
248 </div>
249 <div className={classes.footer}>
250 <a
251 href={`${DEV_API_FRANZ_WEBSITE}/pricing`}
252 target="_blank"
253 className={classes.fullFeatureList}
254 >
255 <Icon icon={mdiArrowRight} />
256 {intl.formatMessage(messages.fullFeatureList)}
257 </a>
258 {/* <p className={classes.disclaimer}> */}
259 {intl.formatMessage(messages.pricesBasedOnAnnualPayment)}
260 {/* </p> */}
261 </div>
262 </div>
263 </div>
264 </Appear>
265 );
266 }
267}
268
269export default PlanSelection;
diff --git a/src/features/planSelection/containers/PlanSelectionScreen.js b/src/features/planSelection/containers/PlanSelectionScreen.js
deleted file mode 100644
index 594829c01..000000000
--- a/src/features/planSelection/containers/PlanSelectionScreen.js
+++ /dev/null
@@ -1,120 +0,0 @@
1import React, { Component } from 'react';
2import { observer, inject } from 'mobx-react';
3import PropTypes from 'prop-types';
4import { dialog, app } from '@electron/remote';
5import { defineMessages, intlShape } from 'react-intl';
6
7import FeaturesStore from '../../../stores/FeaturesStore';
8import UserStore from '../../../stores/UserStore';
9import PlanSelection from '../components/PlanSelection';
10import ErrorBoundary from '../../../components/util/ErrorBoundary';
11import { planSelectionStore } from '..';
12import PaymentStore from '../../../stores/PaymentStore';
13
14const messages = defineMessages({
15 dialogTitle: {
16 id: 'feature.planSelection.fullscreen.dialog.title',
17 defaultMessage: '!!!Downgrade your Franz Plan',
18 },
19 dialogMessage: {
20 id: 'feature.planSelection.fullscreen.dialog.message',
21 defaultMessage: '!!!You\'re about to downgrade to our Free account. Are you sure? Click here instead to get more services and functionality for just {currency}{price} a month.',
22 },
23 dialogCTADowngrade: {
24 id: 'feature.planSelection.fullscreen.dialog.cta.downgrade',
25 defaultMessage: '!!!Downgrade to Free',
26 },
27 dialogCTAUpgrade: {
28 id: 'feature.planSelection.fullscreen.dialog.cta.upgrade',
29 defaultMessage: '!!!Choose Personal',
30 },
31});
32
33@inject('stores', 'actions') @observer
34class PlanSelectionScreen extends Component {
35 static contextTypes = {
36 intl: intlShape,
37 };
38
39 upgradeAccount(planId) {
40 const { upgradeAccount } = this.props.actions.payment;
41
42 upgradeAccount({
43 planId,
44 });
45 }
46
47 render() {
48 if (!planSelectionStore || !planSelectionStore.isFeatureActive || !planSelectionStore.showPlanSelectionOverlay) {
49 return null;
50 }
51
52 const { intl } = this.context;
53
54 const { user, features } = this.props.stores;
55 const { isPersonalPlanAvailable, pricingConfig } = features.features;
56 const { plans, currency } = pricingConfig;
57 const { activateTrial } = this.props.actions.user;
58 const { downgradeAccount, hideOverlay } = this.props.actions.planSelection;
59
60 return (
61 <ErrorBoundary>
62 <PlanSelection
63 firstname={user.data.firstname}
64 plans={plans}
65 currency={currency}
66 upgradeAccount={(planId) => {
67 if (user.data.hadSubscription) {
68 this.upgradeAccount(planId);
69 } else {
70 activateTrial({
71 planId,
72 });
73 }
74 }}
75 stayOnFree={() => {
76 const selection = dialog.showMessageBoxSync(app.mainWindow, {
77 type: 'question',
78 message: intl.formatMessage(messages.dialogTitle),
79 detail: intl.formatMessage(messages.dialogMessage, {
80 currency,
81 price: plans.personal.yearly.price,
82 }),
83 buttons: [
84 intl.formatMessage(messages.dialogCTADowngrade),
85 intl.formatMessage(messages.dialogCTAUpgrade),
86 ],
87 });
88
89 if (selection === 0) {
90 downgradeAccount();
91 hideOverlay();
92 } else {
93 this.upgradeAccount(plans.personal.yearly.id);
94 }
95 }}
96 subscriptionExpired={user.team && user.team.state === 'expired' && !user.team.userHasDowngraded}
97 hadSubscription={user.data.hadSubscription}
98 isPersonalPlanAvailable={isPersonalPlanAvailable}
99 />
100 </ErrorBoundary>
101 );
102 }
103}
104
105export default PlanSelectionScreen;
106
107PlanSelectionScreen.wrappedComponent.propTypes = {
108 stores: PropTypes.shape({
109 features: PropTypes.instanceOf(FeaturesStore).isRequired,
110 user: PropTypes.instanceOf(UserStore).isRequired,
111 }).isRequired,
112 actions: PropTypes.shape({
113 payment: PropTypes.instanceOf(PaymentStore),
114 planSelection: PropTypes.shape({
115 downgradeAccount: PropTypes.func.isRequired,
116 hideOverlay: PropTypes.func.isRequired,
117 }),
118 user: PropTypes.instanceOf(UserStore).isRequired,
119 }).isRequired,
120};
diff --git a/src/features/planSelection/index.js b/src/features/planSelection/index.js
deleted file mode 100644
index b96ad6d8f..000000000
--- a/src/features/planSelection/index.js
+++ /dev/null
@@ -1,28 +0,0 @@
1import { reaction } from 'mobx';
2import PlanSelectionStore from './store';
3
4const debug = require('debug')('Ferdi:feature:planSelection');
5
6export const planSelectionStore = new PlanSelectionStore();
7
8export default function initPlanSelection(stores, actions) {
9 stores.planSelection = planSelectionStore;
10 const { features } = stores;
11
12 // Toggle planSelection feature
13 reaction(
14 () => features.features.isPlanSelectionEnabled,
15 (isEnabled) => {
16 if (isEnabled) {
17 debug('Initializing `planSelection` feature');
18 planSelectionStore.start(stores, actions);
19 } else if (planSelectionStore.isFeatureActive) {
20 debug('Disabling `planSelection` feature');
21 planSelectionStore.stop();
22 }
23 },
24 {
25 fireImmediately: true,
26 },
27 );
28}
diff --git a/src/features/planSelection/store.js b/src/features/planSelection/store.js
deleted file mode 100644
index de8fc7584..000000000
--- a/src/features/planSelection/store.js
+++ /dev/null
@@ -1,68 +0,0 @@
1import {
2 action,
3 observable,
4 computed,
5} from 'mobx';
6
7import { planSelectionActions } from './actions';
8import { FeatureStore } from '../utils/FeatureStore';
9import { createActionBindings } from '../utils/ActionBinding';
10import { downgradeUserRequest } from './api';
11
12const debug = require('debug')('Ferdi:feature:planSelection:store');
13
14export default class PlanSelectionStore extends FeatureStore {
15 @observable isFeatureEnabled = false;
16
17 @observable isFeatureActive = false;
18
19 @observable hideOverlay = false;
20
21 @computed get showPlanSelectionOverlay() {
22 const { team, isPremium } = this.stores.user;
23 if (team && !this.hideOverlay && !isPremium) {
24 return team.state === 'expired' && !team.userHasDowngraded;
25 }
26
27 return false;
28 }
29
30 // ========== PUBLIC API ========= //
31
32 @action start(stores, actions, api) {
33 debug('PlanSelectionStore::start');
34 this.stores = stores;
35 this.actions = actions;
36 this.api = api;
37
38 // ACTIONS
39
40 this._registerActions(createActionBindings([
41 [planSelectionActions.downgradeAccount, this._downgradeAccount],
42 [planSelectionActions.hideOverlay, this._hideOverlay],
43 ]));
44
45 this.isFeatureActive = true;
46 }
47
48 @action stop() {
49 super.stop();
50 debug('PlanSelectionStore::stop');
51 this.isFeatureActive = false;
52 }
53
54 // ========== PRIVATE METHODS ========= //
55
56 // Actions
57 @action _downgradeAccount = () => {
58 downgradeUserRequest.execute();
59 }
60
61 @action _hideOverlay = () => {
62 this.hideOverlay = true;
63 }
64
65 @action _showOverlay = () => {
66 this.hideOverlay = false;
67 }
68}
diff --git a/src/features/publishDebugInfo/Component.js b/src/features/publishDebugInfo/Component.js
index f97a7c750..5387bd358 100644
--- a/src/features/publishDebugInfo/Component.js
+++ b/src/features/publishDebugInfo/Component.js
@@ -46,7 +46,7 @@ const messages = defineMessages({
46 }, 46 },
47}); 47});
48 48
49const styles = theme => ({ 49const styles = (theme) => ({
50 container: { 50 container: {
51 minWidth: '70vw', 51 minWidth: '70vw',
52 }, 52 },
@@ -186,10 +186,10 @@ export default @injectSheet(styles) @inject('stores', 'actions') @observer class
186 <> 186 <>
187 <p className={classes.info}>{intl.formatMessage(messages.info)}</p> 187 <p className={classes.info}>{intl.formatMessage(messages.info)}</p>
188 188
189 <a href={`${DEBUG_API}/privacy.html`} target="_blank" className={classes.link}> 189 <a href={`${DEBUG_API}/privacy.html`} target="_blank" className={classes.link} rel="noreferrer">
190 {intl.formatMessage(messages.privacy)} 190 {intl.formatMessage(messages.privacy)}
191 </a> 191 </a>
192 <a href={`${DEBUG_API}/terms.html`} target="_blank" className={classes.link}> 192 <a href={`${DEBUG_API}/terms.html`} target="_blank" className={classes.link} rel="noreferrer">
193 {intl.formatMessage(messages.terms)} 193 {intl.formatMessage(messages.terms)}
194 </a> 194 </a>
195 195
diff --git a/src/features/quickSwitch/Component.js b/src/features/quickSwitch/Component.js
index 04822db71..812f2c04b 100644
--- a/src/features/quickSwitch/Component.js
+++ b/src/features/quickSwitch/Component.js
@@ -28,7 +28,7 @@ const messages = defineMessages({
28 }, 28 },
29}); 29});
30 30
31const styles = theme => ({ 31const styles = (theme) => ({
32 modal: { 32 modal: {
33 width: '80%', 33 width: '80%',
34 maxWidth: 600, 34 maxWidth: 600,
@@ -139,7 +139,7 @@ export default @injectSheet(styles) @inject('stores', 'actions') @observer class
139 if (this.state.search && compact(invoke(this.state.search, 'match', /^[a-z0-9]/i)).length > 0) { 139 if (this.state.search && compact(invoke(this.state.search, 'match', /^[a-z0-9]/i)).length > 0) {
140 // Apply simple search algorythm to list of all services 140 // Apply simple search algorythm to list of all services
141 services = this.props.stores.services.allDisplayed; 141 services = this.props.stores.services.allDisplayed;
142 services = services.filter(service => service.name.toLowerCase().search(this.state.search.toLowerCase()) !== -1); 142 services = services.filter((service) => service.name.toLowerCase().search(this.state.search.toLowerCase()) !== -1);
143 } else { 143 } else {
144 // Add the currently active service first 144 // Add the currently active service first
145 const currentService = this.props.stores.services.active; 145 const currentService = this.props.stores.services.active;
diff --git a/src/features/serviceLimit/components/LimitReachedInfobox.js b/src/features/serviceLimit/components/LimitReachedInfobox.js
deleted file mode 100644
index 424c92990..000000000
--- a/src/features/serviceLimit/components/LimitReachedInfobox.js
+++ /dev/null
@@ -1,75 +0,0 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss';
6import { Infobox } from '@meetfranz/ui';
7
8const messages = defineMessages({
9 limitReached: {
10 id: 'feature.serviceLimit.limitReached',
11 defaultMessage: '!!!You have added {amount} of {limit} services. Please upgrade your account to add more services.',
12 },
13 action: {
14 id: 'premiumFeature.button.upgradeAccount',
15 defaultMessage: '!!!Upgrade account',
16 },
17});
18
19const styles = theme => ({
20 container: {
21 height: 'auto',
22 background: theme.styleTypes.warning.accent,
23 color: theme.styleTypes.warning.contrast,
24 borderRadius: 0,
25 marginBottom: 0,
26
27 '& > div': {
28 marginBottom: 0,
29 },
30
31 '& button': {
32 color: theme.styleTypes.primary.contrast,
33 },
34 },
35});
36
37@inject('stores', 'actions') @injectSheet(styles) @observer
38class LimitReachedInfobox extends Component {
39 static propTypes = {
40 classes: PropTypes.object.isRequired,
41 stores: PropTypes.object.isRequired,
42 actions: PropTypes.object.isRequired,
43 };
44
45 static contextTypes = {
46 intl: intlShape,
47 };
48
49 render() {
50 const { classes, stores, actions } = this.props;
51 const { intl } = this.context;
52
53 const {
54 serviceLimit,
55 } = stores;
56
57 if (!serviceLimit.userHasReachedServiceLimit) return null;
58
59 return (
60 <Infobox
61 icon="mdiInformation"
62 type="warning"
63 className={classes.container}
64 ctaLabel={intl.formatMessage(messages.action)}
65 ctaOnClick={() => {
66 actions.ui.openSettings({ path: 'user' });
67 }}
68 >
69 {intl.formatMessage(messages.limitReached, { amount: serviceLimit.serviceCount, limit: serviceLimit.serviceLimit })}
70 </Infobox>
71 );
72 }
73}
74
75export default LimitReachedInfobox;
diff --git a/src/features/serviceLimit/index.js b/src/features/serviceLimit/index.js
deleted file mode 100644
index f867e3d87..000000000
--- a/src/features/serviceLimit/index.js
+++ /dev/null
@@ -1,31 +0,0 @@
1import { reaction } from 'mobx';
2import { ServiceLimitStore } from './store';
3
4const debug = require('debug')('Ferdi:feature:serviceLimit');
5
6let store = null;
7
8export const serviceLimitStore = new ServiceLimitStore();
9
10export default function initServiceLimit(stores, actions) {
11 const { features } = stores;
12
13 // Toggle serviceLimit feature
14 reaction(
15 () => (
16 features.features.isServiceLimitEnabled
17 ),
18 (isEnabled) => {
19 if (isEnabled) {
20 debug('Initializing `serviceLimit` feature');
21 store = serviceLimitStore.start(stores, actions);
22 } else if (store) {
23 debug('Disabling `serviceLimit` feature');
24 serviceLimitStore.stop();
25 }
26 },
27 {
28 fireImmediately: true,
29 },
30 );
31}
diff --git a/src/features/serviceLimit/store.js b/src/features/serviceLimit/store.js
deleted file mode 100644
index b1e55a1fc..000000000
--- a/src/features/serviceLimit/store.js
+++ /dev/null
@@ -1,42 +0,0 @@
1import { computed, observable } from 'mobx';
2import { FeatureStore } from '../utils/FeatureStore';
3import { DEFAULT_SERVICE_LIMIT } from '../../config';
4
5const debug = require('debug')('Ferdi:feature:serviceLimit:store');
6
7export class ServiceLimitStore extends FeatureStore {
8 @observable isServiceLimitEnabled = false;
9
10 start(stores, actions) {
11 debug('start');
12 this.stores = stores;
13 this.actions = actions;
14
15 this.isServiceLimitEnabled = false;
16 }
17
18 stop() {
19 super.stop();
20
21 this.isServiceLimitEnabled = false;
22 }
23
24 @computed get userHasReachedServiceLimit() {
25 return false;
26 // if (!this.isServiceLimitEnabled) return false;
27
28 // return this.serviceLimit !== 0 && this.serviceCount >= this.serviceLimit;
29 }
30
31 @computed get serviceLimit() {
32 if (!this.isServiceLimitEnabled || this.stores.features.features.serviceLimitCount === 0) return 0;
33
34 return this.stores.features.features.serviceLimitCount || DEFAULT_SERVICE_LIMIT;
35 }
36
37 @computed get serviceCount() {
38 return this.stores.services.all.length;
39 }
40}
41
42export default ServiceLimitStore;
diff --git a/src/features/serviceProxy/index.js b/src/features/serviceProxy/index.js
index f74f5f0b2..eb7116651 100644
--- a/src/features/serviceProxy/index.js
+++ b/src/features/serviceProxy/index.js
@@ -5,7 +5,6 @@ const debug = require('debug')('Ferdi:feature:serviceProxy');
5 5
6export const config = observable({ 6export const config = observable({
7 isEnabled: true, 7 isEnabled: true,
8 isPremium: true,
9}); 8});
10 9
11export default function init(stores) { 10export default function init(stores) {
@@ -13,7 +12,6 @@ export default function init(stores) {
13 12
14 autorun(() => { 13 autorun(() => {
15 config.isEnabled = true; 14 config.isEnabled = true;
16 config.isIncludedInCurrentPlan = true;
17 15
18 const services = stores.services.enabled; 16 const services = stores.services.enabled;
19 const proxySettings = stores.settings.proxy; 17 const proxySettings = stores.settings.proxy;
diff --git a/src/features/shareFranz/Component.js b/src/features/shareFranz/Component.js
index f7f8dc41c..cc2e81b70 100644
--- a/src/features/shareFranz/Component.js
+++ b/src/features/shareFranz/Component.js
@@ -36,15 +36,15 @@ const messages = defineMessages({
36 }, 36 },
37 shareTextEmail: { 37 shareTextEmail: {
38 id: 'feature.shareFranz.shareText.email', 38 id: 'feature.shareFranz.shareText.email',
39 defaultMessage: '!!! I\'ve added {count} services to Franz! Get the free app for WhatsApp, Messenger, Slack, Skype and co at www.meetfranz.com', 39 defaultMessage: '!!! I\'ve added {count} services to Ferdi! Get the free app for WhatsApp, Messenger, Slack, Skype and co at www.meetfranz.com',
40 }, 40 },
41 shareTextTwitter: { 41 shareTextTwitter: {
42 id: 'feature.shareFranz.shareText.twitter', 42 id: 'feature.shareFranz.shareText.twitter',
43 defaultMessage: '!!! I\'ve added {count} services to Franz! Get the free app for WhatsApp, Messenger, Slack, Skype and co at www.meetfranz.com /cc @FranzMessenger', 43 defaultMessage: '!!! I\'ve added {count} services to Ferdi! Get the free app for WhatsApp, Messenger, Slack, Skype and co at www.meetfranz.com /cc @FranzMessenger',
44 }, 44 },
45}); 45});
46 46
47const styles = theme => ({ 47const styles = (theme) => ({
48 modal: { 48 modal: {
49 width: '80%', 49 width: '80%',
50 maxWidth: 600, 50 maxWidth: 600,
diff --git a/src/features/shareFranz/index.js b/src/features/shareFranz/index.js
index 34475f674..9add0f65e 100644
--- a/src/features/shareFranz/index.js
+++ b/src/features/shareFranz/index.js
@@ -1,8 +1,6 @@
1import { reaction } from 'mobx'; 1import { reaction } from 'mobx';
2import ms from 'ms'; 2import ms from 'ms';
3import { state as ModalState } from './store'; 3import { state as ModalState } from './store';
4import { state as delayAppState } from '../delayApp';
5import { planSelectionStore } from '../planSelection';
6 4
7export { default as Component } from './Component'; 5export { default as Component } from './Component';
8 6
@@ -19,21 +17,14 @@ export default function initialize(stores) {
19 17
20 function showModal() { 18 function showModal() {
21 debug('Would have showed share window'); 19 debug('Would have showed share window');
22
23 // state.isModalVisible = true;
24 } 20 }
25 21
26 reaction( 22 reaction(
27 () => stores.user.isLoggedIn, 23 () => stores.user.isLoggedIn,
28 () => { 24 () => {
29 setTimeout(() => { 25 setTimeout(() => {
30 if (stores.settings.stats.appStarts % 50 === 0 && !planSelectionStore.showPlanSelectionOverlay) { 26 if (stores.settings.stats.appStarts % 50 === 0) {
31 if (delayAppState.isDelayAppScreenVisible) { 27 showModal();
32 debug('Delaying share modal by 5 minutes');
33 setTimeout(() => showModal(), ms('5m'));
34 } else {
35 showModal();
36 }
37 } 28 }
38 }, ms('2s')); 29 }, ms('2s'));
39 }, 30 },
diff --git a/src/features/spellchecker/index.js b/src/features/spellchecker/index.js
deleted file mode 100644
index 6a393e250..000000000
--- a/src/features/spellchecker/index.js
+++ /dev/null
@@ -1,27 +0,0 @@
1import { autorun, observable } from 'mobx';
2
3import { DEFAULT_FEATURES_CONFIG } from '../../config';
4
5const debug = require('debug')('Ferdi:feature:spellchecker');
6
7export const config = observable({
8 isIncludedInCurrentPlan: DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan,
9});
10
11export default function init() {
12 debug('Initializing `spellchecker` feature');
13
14 autorun(() => {
15 // const { isSpellcheckerIncludedInCurrentPlan } = stores.features.features;
16
17 // config.isIncludedInCurrentPlan = isSpellcheckerIncludedInCurrentPlan !== undefined ? isSpellcheckerIncludedInCurrentPlan : DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan;
18
19 // if (!stores.user.data.isPremium && config.isIncludedInCurrentPlan && stores.settings.app.enableSpellchecking) {
20 // debug('Override settings.spellcheckerEnabled flag to false');
21
22 // Object.assign(stores.settings.app, {
23 // enableSpellchecking: false,
24 // });
25 // }
26 });
27}
diff --git a/src/features/todos/components/TodosWebview.js b/src/features/todos/components/TodosWebview.js
index ca8460f94..2dc30cdf2 100644
--- a/src/features/todos/components/TodosWebview.js
+++ b/src/features/todos/components/TodosWebview.js
@@ -35,26 +35,6 @@ const styles = (theme) => ({
35 zIndex: 400, 35 zIndex: 400,
36 background: theme.todos.dragIndicator.background, 36 background: theme.todos.dragIndicator.background,
37 }, 37 },
38 premiumContainer: {
39 display: 'flex',
40 flexDirection: 'column',
41 justifyContent: 'center',
42 alignItems: 'center',
43 width: '80%',
44 maxWidth: 300,
45 margin: [0, 'auto'],
46 textAlign: 'center',
47 },
48 premiumIcon: {
49 marginBottom: 40,
50 background: theme.styleTypes.primary.accent,
51 fill: theme.styleTypes.primary.contrast,
52 padding: 10,
53 borderRadius: 10,
54 },
55 premiumCTA: {
56 marginTop: 40,
57 },
58 isTodosServiceActive: { 38 isTodosServiceActive: {
59 width: 'calc(100% - 368px)', 39 width: 'calc(100% - 368px)',
60 position: 'absolute', 40 position: 'absolute',
diff --git a/src/features/todos/containers/TodosScreen.js b/src/features/todos/containers/TodosScreen.js
index 6425746e6..d05e24e56 100644
--- a/src/features/todos/containers/TodosScreen.js
+++ b/src/features/todos/containers/TodosScreen.js
@@ -24,10 +24,10 @@ class TodosScreen extends Component {
24 isVisible={todosStore.isTodosPanelVisible} 24 isVisible={todosStore.isTodosPanelVisible}
25 togglePanel={todoActions.toggleTodosPanel} 25 togglePanel={todoActions.toggleTodosPanel}
26 handleClientMessage={todoActions.handleClientMessage} 26 handleClientMessage={todoActions.handleClientMessage}
27 setTodosWebview={webview => todoActions.setTodosWebview({ webview })} 27 setTodosWebview={(webview) => todoActions.setTodosWebview({ webview })}
28 width={todosStore.width} 28 width={todosStore.width}
29 minWidth={TODOS_MIN_WIDTH} 29 minWidth={TODOS_MIN_WIDTH}
30 resize={width => todoActions.resize({ width })} 30 resize={(width) => todoActions.resize({ width })}
31 userAgent={todosStore.userAgent} 31 userAgent={todosStore.userAgent}
32 todoUrl={todosStore.todoUrl} 32 todoUrl={todosStore.todoUrl}
33 isTodoUrlValid={todosStore.isTodoUrlValid} 33 isTodoUrlValid={todosStore.isTodoUrlValid}
diff --git a/src/features/todos/store.js b/src/features/todos/store.js
index 429507927..f283c1e59 100644
--- a/src/features/todos/store.js
+++ b/src/features/todos/store.js
@@ -20,7 +20,6 @@ import { FeatureStore } from '../utils/FeatureStore';
20import { createReactions } from '../../stores/lib/Reaction'; 20import { createReactions } from '../../stores/lib/Reaction';
21import { createActionBindings } from '../utils/ActionBinding'; 21import { createActionBindings } from '../utils/ActionBinding';
22import { IPC, TODOS_ROUTES } from './constants'; 22import { IPC, TODOS_ROUTES } from './constants';
23import { state as delayAppState } from '../delayApp';
24import UserAgent from '../../models/UserAgent'; 23import UserAgent from '../../models/UserAgent';
25 24
26const debug = require('debug')('Ferdi:feature:todos:store'); 25const debug = require('debug')('Ferdi:feature:todos:store');
@@ -46,7 +45,7 @@ export default class TodoStore extends FeatureStore {
46 45
47 @computed get isTodosPanelForceHidden() { 46 @computed get isTodosPanelForceHidden() {
48 const { isAnnouncementShown } = this.stores.announcements; 47 const { isAnnouncementShown } = this.stores.announcements;
49 return delayAppState.isDelayAppScreenVisible || !this.isFeatureEnabledByUser || isAnnouncementShown; 48 return !this.isFeatureEnabledByUser || isAnnouncementShown;
50 } 49 }
51 50
52 @computed get isTodosPanelVisible() { 51 @computed get isTodosPanelVisible() {
@@ -123,12 +122,6 @@ export default class TodoStore extends FeatureStore {
123 this._registerReactions(this._allReactions); 122 this._registerReactions(this._allReactions);
124 123
125 this.isFeatureActive = true; 124 this.isFeatureActive = true;
126
127 if (this.settings.isFeatureEnabledByUser === undefined) {
128 this._updateSettings({
129 isFeatureEnabledByUser: DEFAULT_IS_FEATURE_ENABLED_BY_USER,
130 });
131 }
132 } 125 }
133 126
134 @action stop() { 127 @action stop() {
@@ -266,6 +259,12 @@ export default class TodoStore extends FeatureStore {
266 _firstLaunchReaction = () => { 259 _firstLaunchReaction = () => {
267 const { stats } = this.stores.settings.all; 260 const { stats } = this.stores.settings.all;
268 261
262 if (this.settings.isFeatureEnabledByUser === undefined) {
263 this._updateSettings({
264 isFeatureEnabledByUser: DEFAULT_IS_FEATURE_ENABLED_BY_USER,
265 });
266 }
267
269 // Hide todos layer on first app start but show on second 268 // Hide todos layer on first app start but show on second
270 if (stats.appStarts <= 1) { 269 if (stats.appStarts <= 1) {
271 this._updateSettings({ 270 this._updateSettings({
diff --git a/src/features/trialStatusBar/actions.js b/src/features/trialStatusBar/actions.js
deleted file mode 100644
index 38df76458..000000000
--- a/src/features/trialStatusBar/actions.js
+++ /dev/null
@@ -1,13 +0,0 @@
1import PropTypes from 'prop-types';
2import { createActionsFromDefinitions } from '../../actions/lib/actions';
3
4export const trialStatusBarActions = createActionsFromDefinitions({
5 upgradeAccount: {
6 planId: PropTypes.string.isRequired,
7 onCloseWindow: PropTypes.func.isRequired,
8 },
9 downgradeAccount: {},
10 hideOverlay: {},
11}, PropTypes.checkPropTypes);
12
13export default trialStatusBarActions;
diff --git a/src/features/trialStatusBar/components/ProgressBar.js b/src/features/trialStatusBar/components/ProgressBar.js
deleted file mode 100644
index 41b74d396..000000000
--- a/src/features/trialStatusBar/components/ProgressBar.js
+++ /dev/null
@@ -1,45 +0,0 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5
6const styles = theme => ({
7 root: {
8 background: theme.trialStatusBar.progressBar.background,
9 width: '25%',
10 maxWidth: 200,
11 height: 8,
12 display: 'flex',
13 alignItems: 'center',
14 borderRadius: theme.borderRadius,
15 overflow: 'hidden',
16 },
17 progress: {
18 background: theme.trialStatusBar.progressBar.progressIndicator,
19 width: ({ percent }) => `${percent}%`,
20 height: '100%',
21 },
22});
23
24@injectSheet(styles) @observer
25class ProgressBar extends Component {
26 static propTypes = {
27 classes: PropTypes.object.isRequired,
28 };
29
30 render() {
31 const {
32 classes,
33 } = this.props;
34
35 return (
36 <div
37 className={classes.root}
38 >
39 <div className={classes.progress} />
40 </div>
41 );
42 }
43}
44
45export default ProgressBar;
diff --git a/src/features/trialStatusBar/components/TrialStatusBar.js b/src/features/trialStatusBar/components/TrialStatusBar.js
deleted file mode 100644
index b8fe4acc9..000000000
--- a/src/features/trialStatusBar/components/TrialStatusBar.js
+++ /dev/null
@@ -1,135 +0,0 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import { defineMessages, intlShape } from 'react-intl';
6import { Icon } from '@meetfranz/ui';
7import { mdiArrowRight, mdiWindowClose } from '@mdi/js';
8import classnames from 'classnames';
9
10import ProgressBar from './ProgressBar';
11
12const messages = defineMessages({
13 restTime: {
14 id: 'feature.trialStatusBar.restTime',
15 defaultMessage: '!!!Your Free Franz {plan} Trial ends in {time}.',
16 },
17 expired: {
18 id: 'feature.trialStatusBar.expired',
19 defaultMessage: '!!!Your free Franz {plan} Trial has expired, please upgrade your account.',
20 },
21 cta: {
22 id: 'feature.trialStatusBar.cta',
23 defaultMessage: '!!!Upgrade now',
24 },
25});
26
27const styles = theme => ({
28 root: {
29 background: theme.trialStatusBar.bar.background,
30 width: '100%',
31 height: 25,
32 order: 10,
33 display: 'flex',
34 alignItems: 'center',
35 fontSize: 12,
36 padding: [0, 10],
37 justifyContent: 'flex-end',
38 },
39 ended: {
40 background: theme.styleTypes.warning.accent,
41 color: theme.styleTypes.warning.contrast,
42 },
43 message: {
44 marginLeft: 20,
45 },
46 action: {
47 marginLeft: 20,
48 fontSize: 12,
49 color: theme.colorText,
50 textDecoration: 'underline',
51 display: 'flex',
52
53 '& svg': {
54 margin: [1, 2, 0, 0],
55 },
56 },
57});
58
59@injectSheet(styles) @observer
60class TrialStatusBar extends Component {
61 static propTypes = {
62 planName: PropTypes.string.isRequired,
63 percent: PropTypes.number.isRequired,
64 upgradeAccount: PropTypes.func.isRequired,
65 hideOverlay: PropTypes.func.isRequired,
66 trialEnd: PropTypes.string.isRequired,
67 hasEnded: PropTypes.bool.isRequired,
68 classes: PropTypes.object.isRequired,
69 };
70
71 static contextTypes = {
72 intl: intlShape,
73 };
74
75 render() {
76 const {
77 planName,
78 percent,
79 upgradeAccount,
80 hideOverlay,
81 trialEnd,
82 hasEnded,
83 classes,
84 } = this.props;
85
86 const { intl } = this.context;
87
88 return (
89 <div
90 className={classnames({
91 [classes.root]: true,
92 [classes.ended]: hasEnded,
93 })}
94 >
95 <ProgressBar
96 percent={percent}
97 />
98 {' '}
99 <span className={classes.message}>
100 {!hasEnded ? (
101 intl.formatMessage(messages.restTime, {
102 plan: planName,
103 time: trialEnd,
104 })
105 ) : (
106 intl.formatMessage(messages.expired, {
107 plan: planName,
108 })
109 )}
110 </span>
111 <button
112 className={classes.action}
113 type="button"
114 onClick={() => {
115 upgradeAccount();
116 }}
117 >
118 <Icon icon={mdiArrowRight} />
119 {intl.formatMessage(messages.cta)}
120 </button>
121 <button
122 className={classes.action}
123 type="button"
124 onClick={() => {
125 hideOverlay();
126 }}
127 >
128 <Icon icon={mdiWindowClose} />
129 </button>
130 </div>
131 );
132 }
133}
134
135export default TrialStatusBar;
diff --git a/src/features/trialStatusBar/containers/TrialStatusBarScreen.js b/src/features/trialStatusBar/containers/TrialStatusBarScreen.js
deleted file mode 100644
index e0f5ab5f2..000000000
--- a/src/features/trialStatusBar/containers/TrialStatusBarScreen.js
+++ /dev/null
@@ -1,112 +0,0 @@
1import React, { Component } from 'react';
2import { observer, inject } from 'mobx-react';
3import PropTypes from 'prop-types';
4import ms from 'ms';
5import { intlShape } from 'react-intl';
6
7import FeaturesStore from '../../../stores/FeaturesStore';
8import UserStore from '../../../stores/UserStore';
9import TrialStatusBar from '../components/TrialStatusBar';
10import ErrorBoundary from '../../../components/util/ErrorBoundary';
11import { trialStatusBarStore } from '..';
12import { i18nPlanName } from '../../../helpers/plan-helpers';
13import PaymentStore from '../../../stores/PaymentStore';
14
15@inject('stores', 'actions')
16@observer
17class TrialStatusBarScreen extends Component {
18 static contextTypes = {
19 intl: intlShape,
20 };
21
22 state = {
23 showOverlay: true,
24 percent: 0,
25 restTime: '',
26 hasEnded: false,
27 };
28
29 percentInterval = null;
30
31 componentDidMount() {
32 this.percentInterval = setInterval(() => {
33 this.calculateRestTime();
34 }, ms('1m'));
35
36 this.calculateRestTime();
37 }
38
39 componentWillUnmount() {
40 clearInterval(this.percentInterval);
41 }
42
43 calculateRestTime() {
44 const { trialEndTime } = trialStatusBarStore;
45 const percent = (
46 Math.abs(100 - Math.abs(trialEndTime.asMilliseconds()) * 100) / ms('14d')
47 ).toFixed(2);
48 const restTime = trialEndTime.humanize();
49 const hasEnded = trialEndTime.asMilliseconds() > 0;
50
51 this.setState({
52 percent,
53 restTime,
54 hasEnded,
55 });
56 }
57
58 hideOverlay() {
59 this.setState({
60 showOverlay: false,
61 });
62 }
63
64 render() {
65 const { intl } = this.context;
66
67 const {
68 showOverlay, percent, restTime, hasEnded,
69 } = this.state;
70
71 if (
72 !trialStatusBarStore
73 || !trialStatusBarStore.isFeatureActive
74 || !showOverlay
75 || !trialStatusBarStore.showTrialStatusBarOverlay
76 ) {
77 return null;
78 }
79
80 const { user } = this.props.stores;
81 const { upgradeAccount } = this.props.actions.payment;
82
83 const planName = i18nPlanName(user.team.plan, intl);
84
85 return (
86 <ErrorBoundary>
87 <TrialStatusBar
88 planName={planName}
89 percent={parseFloat(percent < 5 ? 5 : percent)}
90 trialEnd={restTime}
91 upgradeAccount={() => upgradeAccount({
92 planId: user.team.plan,
93 })}
94 hideOverlay={() => this.hideOverlay()}
95 hasEnded={hasEnded}
96 />
97 </ErrorBoundary>
98 );
99 }
100}
101
102export default TrialStatusBarScreen;
103
104TrialStatusBarScreen.wrappedComponent.propTypes = {
105 stores: PropTypes.shape({
106 features: PropTypes.instanceOf(FeaturesStore).isRequired,
107 user: PropTypes.instanceOf(UserStore).isRequired,
108 }).isRequired,
109 actions: PropTypes.shape({
110 payment: PropTypes.instanceOf(PaymentStore),
111 }).isRequired,
112};
diff --git a/src/features/trialStatusBar/index.js b/src/features/trialStatusBar/index.js
deleted file mode 100644
index 987b5c04e..000000000
--- a/src/features/trialStatusBar/index.js
+++ /dev/null
@@ -1,30 +0,0 @@
1import { reaction } from 'mobx';
2import TrialStatusBarStore from './store';
3
4const debug = require('debug')('Ferdi:feature:trialStatusBar');
5
6export const GA_CATEGORY_TRIAL_STATUS_BAR = 'trialStatusBar';
7
8export const trialStatusBarStore = new TrialStatusBarStore();
9
10export default function initTrialStatusBar(stores, actions) {
11 stores.trialStatusBar = trialStatusBarStore;
12 const { features } = stores;
13
14 // Toggle trialStatusBar feature
15 reaction(
16 () => features.features.isTrialStatusBarEnabled,
17 (isEnabled) => {
18 if (isEnabled) {
19 debug('Initializing `trialStatusBar` feature');
20 trialStatusBarStore.start(stores, actions);
21 } else if (trialStatusBarStore.isFeatureActive) {
22 debug('Disabling `trialStatusBar` feature');
23 trialStatusBarStore.stop();
24 }
25 },
26 {
27 fireImmediately: true,
28 },
29 );
30}
diff --git a/src/features/trialStatusBar/store.js b/src/features/trialStatusBar/store.js
deleted file mode 100644
index 858a08238..000000000
--- a/src/features/trialStatusBar/store.js
+++ /dev/null
@@ -1,72 +0,0 @@
1import {
2 action,
3 observable,
4 computed,
5} from 'mobx';
6import moment from 'moment';
7
8import { trialStatusBarActions } from './actions';
9import { FeatureStore } from '../utils/FeatureStore';
10import { createActionBindings } from '../utils/ActionBinding';
11
12const debug = require('debug')('Ferdi:feature:trialStatusBar:store');
13
14export default class TrialStatusBarStore extends FeatureStore {
15 @observable isFeatureActive = false;
16
17 @observable isFeatureEnabled = false;
18
19 @computed get showTrialStatusBarOverlay() {
20 if (this.isFeatureActive) {
21 const { team } = this.stores.user;
22 if (team && !this.hideOverlay) {
23 return team.state !== 'expired' && team.isTrial;
24 }
25 }
26
27 return false;
28 }
29
30 @computed get trialEndTime() {
31 if (this.isFeatureActive) {
32 const { team } = this.stores.user;
33
34 if (team && !this.hideOverlay) {
35 return moment.duration(moment().diff(team.trialEnd));
36 }
37 }
38
39 return moment.duration();
40 }
41
42 // ========== PUBLIC API ========= //
43
44 @action start(stores, actions, api) {
45 debug('TrialStatusBarStore::start');
46 this.stores = stores;
47 this.actions = actions;
48 this.api = api;
49
50 // ACTIONS
51
52 this._registerActions(createActionBindings([
53 [trialStatusBarActions.hideOverlay, this._hideOverlay],
54 ]));
55
56 this.isFeatureActive = true;
57 }
58
59 @action stop() {
60 super.stop();
61 debug('TrialStatusBarStore::stop');
62 this.isFeatureActive = false;
63 }
64
65 // ========== PRIVATE METHODS ========= //
66
67 // Actions
68
69 @action _hideOverlay = () => {
70 this.hideOverlay = true;
71 }
72}
diff --git a/src/features/utils/ActionBinding.js b/src/features/utils/ActionBinding.js
index 497aa071b..787166d44 100644
--- a/src/features/utils/ActionBinding.js
+++ b/src/features/utils/ActionBinding.js
@@ -24,6 +24,6 @@ export default class ActionBinding {
24 } 24 }
25} 25}
26 26
27export const createActionBindings = actions => ( 27export const createActionBindings = (actions) => (
28 actions.map(a => new ActionBinding(a)) 28 actions.map((a) => new ActionBinding(a))
29); 29);
diff --git a/src/features/utils/FeatureStore.js b/src/features/utils/FeatureStore.js
index 0bc10e176..4d4e217a9 100644
--- a/src/features/utils/FeatureStore.js
+++ b/src/features/utils/FeatureStore.js
@@ -16,11 +16,11 @@ export class FeatureStore {
16 } 16 }
17 17
18 _startActions(actions = this._actions) { 18 _startActions(actions = this._actions) {
19 actions.forEach(a => a.start()); 19 actions.forEach((a) => a.start());
20 } 20 }
21 21
22 _stopActions(actions = this._actions) { 22 _stopActions(actions = this._actions) {
23 actions.forEach(a => a.stop()); 23 actions.forEach((a) => a.stop());
24 } 24 }
25 25
26 // REACTIONS 26 // REACTIONS
@@ -31,10 +31,10 @@ export class FeatureStore {
31 } 31 }
32 32
33 _startReactions(reactions = this._reactions) { 33 _startReactions(reactions = this._reactions) {
34 reactions.forEach(r => r.start()); 34 reactions.forEach((r) => r.start());
35 } 35 }
36 36
37 _stopReactions(reactions = this._reactions) { 37 _stopReactions(reactions = this._reactions) {
38 reactions.forEach(r => r.stop()); 38 reactions.forEach((r) => r.stop());
39 } 39 }
40} 40}
diff --git a/src/features/webControls/components/WebControls.js b/src/features/webControls/components/WebControls.js
index b9403bd0d..9a95eb2d2 100644
--- a/src/features/webControls/components/WebControls.js
+++ b/src/features/webControls/components/WebControls.js
@@ -6,7 +6,11 @@ import { Icon } from '@meetfranz/ui';
6import { defineMessages, intlShape } from 'react-intl'; 6import { defineMessages, intlShape } from 'react-intl';
7 7
8import { 8import {
9 mdiReload, mdiArrowRight, mdiArrowLeft, mdiHomeOutline, mdiEarth, 9 mdiReload,
10 mdiArrowRight,
11 mdiArrowLeft,
12 mdiHomeOutline,
13 mdiEarth,
10} from '@mdi/js'; 14} from '@mdi/js';
11 15
12const messages = defineMessages({ 16const messages = defineMessages({
@@ -32,6 +36,12 @@ const messages = defineMessages({
32 }, 36 },
33}); 37});
34 38
39let buttonTransition = 'none';
40
41if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
42 buttonTransition = 'opacity 0.25s';
43}
44
35const styles = theme => ({ 45const styles = theme => ({
36 root: { 46 root: {
37 background: theme.colorBackground, 47 background: theme.colorBackground,
@@ -51,7 +61,7 @@ const styles = theme => ({
51 button: { 61 button: {
52 width: 30, 62 width: 30,
53 height: 50, 63 height: 50,
54 transition: 'opacity 0.25s', 64 transition: buttonTransition,
55 65
56 '&:hover': { 66 '&:hover': {
57 opacity: 0.8, 67 opacity: 0.8,
@@ -83,7 +93,8 @@ const styles = theme => ({
83 }, 93 },
84}); 94});
85 95
86@injectSheet(styles) @observer 96@injectSheet(styles)
97@observer
87class WebControls extends Component { 98class WebControls extends Component {
88 static propTypes = { 99 static propTypes = {
89 classes: PropTypes.object.isRequired, 100 classes: PropTypes.object.isRequired,
@@ -96,7 +107,7 @@ class WebControls extends Component {
96 openInBrowser: PropTypes.func.isRequired, 107 openInBrowser: PropTypes.func.isRequired,
97 url: PropTypes.string.isRequired, 108 url: PropTypes.string.isRequired,
98 navigate: PropTypes.func.isRequired, 109 navigate: PropTypes.func.isRequired,
99 } 110 };
100 111
101 static contextTypes = { 112 static contextTypes = {
102 intl: intlShape, 113 intl: intlShape,
@@ -119,7 +130,7 @@ class WebControls extends Component {
119 state = { 130 state = {
120 inputUrl: '', 131 inputUrl: '',
121 editUrl: false, 132 editUrl: false,
122 } 133 };
123 134
124 render() { 135 render() {
125 const { 136 const {
@@ -135,10 +146,7 @@ class WebControls extends Component {
135 navigate, 146 navigate,
136 } = this.props; 147 } = this.props;
137 148
138 const { 149 const { inputUrl, editUrl } = this.state;
139 inputUrl,
140 editUrl,
141 } = this.state;
142 150
143 const { intl } = this.context; 151 const { intl } = this.context;
144 152
@@ -151,10 +159,7 @@ class WebControls extends Component {
151 data-tip={intl.formatMessage(messages.goHome)} 159 data-tip={intl.formatMessage(messages.goHome)}
152 data-place="bottom" 160 data-place="bottom"
153 > 161 >
154 <Icon 162 <Icon icon={mdiHomeOutline} className={classes.icon} />
155 icon={mdiHomeOutline}
156 className={classes.icon}
157 />
158 </button> 163 </button>
159 <button 164 <button
160 onClick={goBack} 165 onClick={goBack}
@@ -164,10 +169,7 @@ class WebControls extends Component {
164 data-tip={intl.formatMessage(messages.back)} 169 data-tip={intl.formatMessage(messages.back)}
165 data-place="bottom" 170 data-place="bottom"
166 > 171 >
167 <Icon 172 <Icon icon={mdiArrowLeft} className={classes.icon} />
168 icon={mdiArrowLeft}
169 className={classes.icon}
170 />
171 </button> 173 </button>
172 <button 174 <button
173 onClick={goForward} 175 onClick={goForward}
@@ -177,10 +179,7 @@ class WebControls extends Component {
177 data-tip={intl.formatMessage(messages.forward)} 179 data-tip={intl.formatMessage(messages.forward)}
178 data-place="bottom" 180 data-place="bottom"
179 > 181 >
180 <Icon 182 <Icon icon={mdiArrowRight} className={classes.icon} />
181 icon={mdiArrowRight}
182 className={classes.icon}
183 />
184 </button> 183 </button>
185 <button 184 <button
186 onClick={reload} 185 onClick={reload}
@@ -189,25 +188,24 @@ class WebControls extends Component {
189 data-tip={intl.formatMessage(messages.reload)} 188 data-tip={intl.formatMessage(messages.reload)}
190 data-place="bottom" 189 data-place="bottom"
191 > 190 >
192 <Icon 191 <Icon icon={mdiReload} className={classes.icon} />
193 icon={mdiReload}
194 className={classes.icon}
195 />
196 </button> 192 </button>
197 <input 193 <input
198 value={editUrl ? inputUrl : url} 194 value={editUrl ? inputUrl : url}
199 className={classes.input} 195 className={classes.input}
200 onChange={event => this.setState({ 196 onChange={event =>
201 inputUrl: event.target.value, 197 this.setState({
202 })} 198 inputUrl: event.target.value,
203 onFocus={(event) => { 199 })
200 }
201 onFocus={event => {
204 console.log('on focus event'); 202 console.log('on focus event');
205 event.target.select(); 203 event.target.select();
206 this.setState({ 204 this.setState({
207 editUrl: true, 205 editUrl: true,
208 }); 206 });
209 }} 207 }}
210 onKeyDown={(event) => { 208 onKeyDown={event => {
211 if (event.key === 'Enter') { 209 if (event.key === 'Enter') {
212 this.setState({ 210 this.setState({
213 editUrl: false, 211 editUrl: false,
@@ -231,10 +229,7 @@ class WebControls extends Component {
231 data-tip={intl.formatMessage(messages.openInBrowser)} 229 data-tip={intl.formatMessage(messages.openInBrowser)}
232 data-place="bottom" 230 data-place="bottom"
233 > 231 >
234 <Icon 232 <Icon icon={mdiEarth} className={classes.icon} />
235 icon={mdiEarth}
236 className={classes.icon}
237 />
238 </button> 233 </button>
239 </div> 234 </div>
240 ); 235 );
diff --git a/src/features/webControls/containers/WebControlsScreen.js b/src/features/webControls/containers/WebControlsScreen.js
index d638b831c..e1e1b9991 100644
--- a/src/features/webControls/containers/WebControlsScreen.js
+++ b/src/features/webControls/containers/WebControlsScreen.js
@@ -114,7 +114,7 @@ class WebControlsScreen extends Component {
114 goBack={() => this.goBack()} 114 goBack={() => this.goBack()}
115 canGoForward={this.canGoForward} 115 canGoForward={this.canGoForward}
116 goForward={() => this.goForward()} 116 goForward={() => this.goForward()}
117 navigate={url => this.navigate(url)} 117 navigate={(url) => this.navigate(url)}
118 url={this.url} 118 url={this.url}
119 /> 119 />
120 ); 120 );
diff --git a/src/features/workspaces/api.js b/src/features/workspaces/api.js
index 30fbd84be..322695ed2 100644
--- a/src/features/workspaces/api.js
+++ b/src/features/workspaces/api.js
@@ -14,7 +14,7 @@ export const workspaceApi = {
14 debug('getUserWorkspaces RESULT', result); 14 debug('getUserWorkspaces RESULT', result);
15 if (!result.ok) throw result; 15 if (!result.ok) throw result;
16 const workspaces = await result.json(); 16 const workspaces = await result.json();
17 return workspaces.map(data => new Workspace(data)); 17 return workspaces.map((data) => new Workspace(data));
18 }, 18 },
19 19
20 createWorkspace: async (name) => { 20 createWorkspace: async (name) => {
diff --git a/src/features/workspaces/components/WorkspaceDrawer.js b/src/features/workspaces/components/WorkspaceDrawer.js
index bf7016e2f..1138f23d7 100644
--- a/src/features/workspaces/components/WorkspaceDrawer.js
+++ b/src/features/workspaces/components/WorkspaceDrawer.js
@@ -2,12 +2,11 @@ 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, FormattedHTMLMessage, intlShape } from 'react-intl'; 5import { defineMessages, intlShape } from 'react-intl';
6import { H1, Icon, ProBadge } from '@meetfranz/ui'; 6import { H1, Icon } from '@meetfranz/ui';
7import { Button } from '@meetfranz/forms/lib';
8import ReactTooltip from 'react-tooltip'; 7import ReactTooltip from 'react-tooltip';
9 8
10import { mdiPlusBox, mdiSettings, mdiStar } from '@mdi/js'; 9import { mdiPlusBox, mdiSettings } from '@mdi/js';
11import WorkspaceDrawerItem from './WorkspaceDrawerItem'; 10import WorkspaceDrawerItem from './WorkspaceDrawerItem';
12import { workspaceActions } from '../actions'; 11import { workspaceActions } from '../actions';
13import { workspaceStore } from '../index'; 12import { workspaceStore } from '../index';
@@ -29,25 +28,13 @@ const messages = defineMessages({
29 id: 'workspaceDrawer.workspaceFeatureInfo', 28 id: 'workspaceDrawer.workspaceFeatureInfo',
30 defaultMessage: '!!!Info about workspace feature', 29 defaultMessage: '!!!Info about workspace feature',
31 }, 30 },
32 premiumCtaButtonLabel: {
33 id: 'workspaceDrawer.premiumCtaButtonLabel',
34 defaultMessage: '!!!Create your first workspace',
35 },
36 reactivatePremiumAccount: {
37 id: 'workspaceDrawer.reactivatePremiumAccountLabel',
38 defaultMessage: '!!!Reactivate premium account',
39 },
40 addNewWorkspaceLabel: { 31 addNewWorkspaceLabel: {
41 id: 'workspaceDrawer.addNewWorkspaceLabel', 32 id: 'workspaceDrawer.addNewWorkspaceLabel',
42 defaultMessage: '!!!add new workspace', 33 defaultMessage: '!!!add new workspace',
43 }, 34 },
44 premiumFeatureBadge: {
45 id: 'workspaceDrawer.proFeatureBadge',
46 defaultMessage: '!!!Premium feature',
47 },
48}); 35});
49 36
50const styles = theme => ({ 37const styles = (theme) => ({
51 drawer: { 38 drawer: {
52 background: theme.workspaces.drawer.background, 39 background: theme.workspaces.drawer.background,
53 width: `${theme.workspaces.drawer.width}px`, 40 width: `${theme.workspaces.drawer.width}px`,
@@ -60,9 +47,6 @@ const styles = theme => ({
60 marginBottom: '25px', 47 marginBottom: '25px',
61 marginLeft: theme.workspaces.drawer.padding, 48 marginLeft: theme.workspaces.drawer.padding,
62 }, 49 },
63 headlineProBadge: {
64 marginRight: 15,
65 },
66 workspacesSettingsButton: { 50 workspacesSettingsButton: {
67 float: 'right', 51 float: 'right',
68 marginRight: theme.workspaces.drawer.padding, 52 marginRight: theme.workspaces.drawer.padding,
@@ -78,16 +62,6 @@ const styles = theme => ({
78 height: 'auto', 62 height: 'auto',
79 overflowY: 'auto', 63 overflowY: 'auto',
80 }, 64 },
81 premiumAnnouncement: {
82 padding: '20px',
83 paddingTop: '0',
84 height: 'auto',
85 },
86 premiumCtaButton: {
87 marginTop: '20px',
88 width: '100%',
89 color: 'white !important',
90 },
91 addNewWorkspaceLabel: { 65 addNewWorkspaceLabel: {
92 height: 'auto', 66 height: 'auto',
93 color: theme.workspaces.drawer.buttons.color, 67 color: theme.workspaces.drawer.buttons.color,
@@ -116,7 +90,6 @@ class WorkspaceDrawer extends Component {
116 static propTypes = { 90 static propTypes = {
117 classes: PropTypes.object.isRequired, 91 classes: PropTypes.object.isRequired,
118 getServicesForWorkspace: PropTypes.func.isRequired, 92 getServicesForWorkspace: PropTypes.func.isRequired,
119 onUpgradeAccountClick: PropTypes.func.isRequired,
120 }; 93 };
121 94
122 static contextTypes = { 95 static contextTypes = {
@@ -131,7 +104,6 @@ class WorkspaceDrawer extends Component {
131 const { 104 const {
132 classes, 105 classes,
133 getServicesForWorkspace, 106 getServicesForWorkspace,
134 onUpgradeAccountClick,
135 } = this.props; 107 } = this.props;
136 const { intl } = this.context; 108 const { intl } = this.context;
137 const { 109 const {
@@ -144,14 +116,6 @@ class WorkspaceDrawer extends Component {
144 return ( 116 return (
145 <div className={`${classes.drawer} workspaces-drawer`}> 117 <div className={`${classes.drawer} workspaces-drawer`}>
146 <H1 className={classes.headline}> 118 <H1 className={classes.headline}>
147 {workspaceStore.isPremiumUpgradeRequired && (
148 <span
149 className={classes.headlineProBadge}
150 data-tip={`${intl.formatMessage(messages.premiumFeatureBadge)}`}
151 >
152 <ProBadge />
153 </span>
154 )}
155 {intl.formatMessage(messages.headline)} 119 {intl.formatMessage(messages.headline)}
156 <span 120 <span
157 className={classes.workspacesSettingsButton} 121 className={classes.workspacesSettingsButton}
@@ -167,75 +131,48 @@ class WorkspaceDrawer extends Component {
167 /> 131 />
168 </span> 132 </span>
169 </H1> 133 </H1>
170 {workspaceStore.isPremiumUpgradeRequired ? ( 134 <div className={classes.workspaces}>
171 <div className={classes.premiumAnnouncement}> 135 <WorkspaceDrawerItem
172 <FormattedHTMLMessage {...messages.workspaceFeatureInfo} /> 136 name={intl.formatMessage(messages.allServices)}
173 {workspaceStore.userHasWorkspaces ? ( 137 onClick={() => {
174 <Button 138 workspaceActions.deactivate();
175 className={classes.premiumCtaButton} 139 workspaceActions.toggleWorkspaceDrawer();
176 buttonType="primary" 140 }}
177 label={intl.formatMessage(messages.reactivatePremiumAccount)} 141 services={getServicesForWorkspace(null)}
178 icon={mdiStar} 142 isActive={actualWorkspace == null}
179 onClick={() => { 143 shortcutIndex={0}
180 onUpgradeAccountClick(); 144 />
181 }} 145 {workspaces.map((workspace, index) => (
182 />
183 ) : (
184 <Button
185 className={classes.premiumCtaButton}
186 buttonType="primary"
187 label={intl.formatMessage(messages.premiumCtaButtonLabel)}
188 icon={mdiPlusBox}
189 onClick={() => {
190 workspaceActions.openWorkspaceSettings();
191 }}
192 />
193 )}
194 </div>
195 ) : (
196 <div className={classes.workspaces}>
197 <WorkspaceDrawerItem 146 <WorkspaceDrawerItem
198 name={intl.formatMessage(messages.allServices)} 147 key={workspace.id}
148 name={workspace.name}
149 isActive={actualWorkspace === workspace}
199 onClick={() => { 150 onClick={() => {
200 workspaceActions.deactivate(); 151 if (actualWorkspace === workspace) return;
152 workspaceActions.activate({ workspace });
201 workspaceActions.toggleWorkspaceDrawer(); 153 workspaceActions.toggleWorkspaceDrawer();
202 }} 154 }}
203 services={getServicesForWorkspace(null)} 155 onContextMenuEditClick={() => workspaceActions.edit({ workspace })}
204 isActive={actualWorkspace == null} 156 services={getServicesForWorkspace(workspace)}
205 shortcutIndex={0} 157 shortcutIndex={index + 1}
206 /> 158 />
207 {workspaces.map((workspace, index) => ( 159 ))}
208 <WorkspaceDrawerItem 160 <div
209 key={workspace.id} 161 className={classes.addNewWorkspaceLabel}
210 name={workspace.name} 162 onClick={() => {
211 isActive={actualWorkspace === workspace} 163 workspaceActions.openWorkspaceSettings();
212 onClick={() => { 164 }}
213 if (actualWorkspace === workspace) return; 165 >
214 workspaceActions.activate({ workspace }); 166 <Icon
215 workspaceActions.toggleWorkspaceDrawer(); 167 icon={mdiPlusBox}
216 }} 168 size={1}
217 onContextMenuEditClick={() => workspaceActions.edit({ workspace })} 169 className={classes.workspacesSettingsButtonIcon}
218 services={getServicesForWorkspace(workspace)} 170 />
219 shortcutIndex={index + 1} 171 <span>
220 /> 172 {intl.formatMessage(messages.addNewWorkspaceLabel)}
221 ))} 173 </span>
222 <div
223 className={classes.addNewWorkspaceLabel}
224 onClick={() => {
225 workspaceActions.openWorkspaceSettings();
226 }}
227 >
228 <Icon
229 icon={mdiPlusBox}
230 size={1}
231 className={classes.workspacesSettingsButtonIcon}
232 />
233 <span>
234 {intl.formatMessage(messages.addNewWorkspaceLabel)}
235 </span>
236 </div>
237 </div> 174 </div>
238 )} 175 </div>
239 <ReactTooltip place="right" type="dark" effect="solid" /> 176 <ReactTooltip place="right" type="dark" effect="solid" />
240 </div> 177 </div>
241 ); 178 );
diff --git a/src/features/workspaces/components/WorkspaceDrawerItem.js b/src/features/workspaces/components/WorkspaceDrawerItem.js
index 2e58b70d6..fff607330 100644
--- a/src/features/workspaces/components/WorkspaceDrawerItem.js
+++ b/src/features/workspaces/components/WorkspaceDrawerItem.js
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react';
5import injectSheet from 'react-jss'; 5import injectSheet from 'react-jss';
6import classnames from 'classnames'; 6import classnames from 'classnames';
7import { defineMessages, intlShape } from 'react-intl'; 7import { defineMessages, intlShape } from 'react-intl';
8import { ctrlKey } from '../../../environment'; 8import { altKey, shortcutKey } from '../../../environment';
9 9
10const messages = defineMessages({ 10const messages = defineMessages({
11 noServicesAddedYet: { 11 noServicesAddedYet: {
@@ -18,12 +18,18 @@ const messages = defineMessages({
18 }, 18 },
19}); 19});
20 20
21let itemTransition = 'none';
22
23if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
24 itemTransition = 'background-color 300ms ease-out';
25}
26
21const styles = theme => ({ 27const styles = theme => ({
22 item: { 28 item: {
23 height: '67px', 29 height: '67px',
24 padding: `15px ${theme.workspaces.drawer.padding}px`, 30 padding: `15px ${theme.workspaces.drawer.padding}px`,
25 borderBottom: `1px solid ${theme.workspaces.drawer.listItem.border}`, 31 borderBottom: `1px solid ${theme.workspaces.drawer.listItem.border}`,
26 transition: 'background-color 300ms ease-out', 32 transition: itemTransition,
27 '&:first-child': { 33 '&:first-child': {
28 borderTop: `1px solid ${theme.workspaces.drawer.listItem.border}`, 34 borderTop: `1px solid ${theme.workspaces.drawer.listItem.border}`,
29 }, 35 },
@@ -59,7 +65,8 @@ const styles = theme => ({
59 }, 65 },
60}); 66});
61 67
62@injectSheet(styles) @observer 68@injectSheet(styles)
69@observer
63class WorkspaceDrawerItem extends Component { 70class WorkspaceDrawerItem extends Component {
64 static propTypes = { 71 static propTypes = {
65 classes: PropTypes.object.isRequired, 72 classes: PropTypes.object.isRequired,
@@ -91,15 +98,19 @@ class WorkspaceDrawerItem extends Component {
91 } = this.props; 98 } = this.props;
92 const { intl } = this.context; 99 const { intl } = this.context;
93 100
94 const contextMenuTemplate = [{ 101 const contextMenuTemplate = [
95 label: name, 102 {
96 enabled: false, 103 label: name,
97 }, { 104 enabled: false,
98 type: 'separator', 105 },
99 }, { 106 {
100 label: intl.formatMessage(messages.contextMenuEdit), 107 type: 'separator',
101 click: onContextMenuEditClick, 108 },
102 }]; 109 {
110 label: intl.formatMessage(messages.contextMenuEdit),
111 click: onContextMenuEditClick,
112 },
113 ];
103 114
104 const contextMenu = Menu.buildFromTemplate(contextMenuTemplate); 115 const contextMenu = Menu.buildFromTemplate(contextMenuTemplate);
105 116
@@ -110,10 +121,12 @@ class WorkspaceDrawerItem extends Component {
110 isActive ? classes.isActiveItem : null, 121 isActive ? classes.isActiveItem : null,
111 ])} 122 ])}
112 onClick={onClick} 123 onClick={onClick}
113 onContextMenu={() => ( 124 onContextMenu={() =>
114 onContextMenuEditClick && contextMenu.popup(getCurrentWindow()) 125 onContextMenuEditClick && contextMenu.popup(getCurrentWindow())
115 )} 126 }
116 data-tip={`${shortcutIndex <= 9 ? `(${ctrlKey}+Alt+${shortcutIndex})` : ''}`} 127 data-tip={`${
128 shortcutIndex <= 9 ? `(${shortcutKey(false)}+${altKey}+${shortcutIndex})` : ''
129 }`}
117 > 130 >
118 <span 131 <span
119 className={classnames([ 132 className={classnames([
@@ -129,7 +142,9 @@ class WorkspaceDrawerItem extends Component {
129 isActive ? classes.activeServices : null, 142 isActive ? classes.activeServices : null,
130 ])} 143 ])}
131 > 144 >
132 {services.length ? services.join(', ') : intl.formatMessage(messages.noServicesAddedYet)} 145 {services.length
146 ? services.join(', ')
147 : intl.formatMessage(messages.noServicesAddedYet)}
133 </span> 148 </span>
134 </div> 149 </div>
135 ); 150 );
diff --git a/src/features/workspaces/components/WorkspaceItem.js b/src/features/workspaces/components/WorkspaceItem.js
index cc4b1a3ba..85fc02d51 100644
--- a/src/features/workspaces/components/WorkspaceItem.js
+++ b/src/features/workspaces/components/WorkspaceItem.js
@@ -6,7 +6,7 @@ import injectSheet from 'react-jss';
6 6
7import Workspace from '../models/Workspace'; 7import Workspace from '../models/Workspace';
8 8
9const styles = theme => ({ 9const styles = (theme) => ({
10 row: { 10 row: {
11 height: theme.workspaces.settings.listItems.height, 11 height: theme.workspaces.settings.listItems.height,
12 borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`, 12 borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`,
diff --git a/src/features/workspaces/components/WorkspaceServiceListItem.js b/src/features/workspaces/components/WorkspaceServiceListItem.js
index e05b21440..f6e2a2786 100644
--- a/src/features/workspaces/components/WorkspaceServiceListItem.js
+++ b/src/features/workspaces/components/WorkspaceServiceListItem.js
@@ -8,7 +8,7 @@ import { Toggle } from '@meetfranz/forms';
8import Service from '../../../models/Service'; 8import Service from '../../../models/Service';
9import ServiceIcon from '../../../components/ui/ServiceIcon'; 9import ServiceIcon from '../../../components/ui/ServiceIcon';
10 10
11const styles = theme => ({ 11const styles = (theme) => ({
12 listItem: { 12 listItem: {
13 height: theme.workspaces.settings.listItems.height, 13 height: theme.workspaces.settings.listItems.height,
14 borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`, 14 borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`,
diff --git a/src/features/workspaces/components/WorkspaceSwitchingIndicator.js b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
index a70d1d66f..c8ec0bc4c 100644
--- a/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
+++ b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
@@ -15,12 +15,18 @@ const messages = defineMessages({
15 }, 15 },
16}); 16});
17 17
18let wrapperTransition = 'none';
19
20if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
21 wrapperTransition = 'width 0.5s ease';
22}
23
18const styles = theme => ({ 24const styles = theme => ({
19 wrapper: { 25 wrapper: {
20 display: 'flex', 26 display: 'flex',
21 alignItems: 'flex-start', 27 alignItems: 'flex-start',
22 position: 'absolute', 28 position: 'absolute',
23 transition: 'width 0.5s ease', 29 transition: wrapperTransition,
24 width: `calc(100% - ${theme.workspaces.drawer.width}px)`, 30 width: `calc(100% - ${theme.workspaces.drawer.width}px)`,
25 marginTop: '20px', 31 marginTop: '20px',
26 }, 32 },
@@ -47,7 +53,8 @@ const styles = theme => ({
47 }, 53 },
48}); 54});
49 55
50@injectSheet(styles) @observer 56@injectSheet(styles)
57@observer
51class WorkspaceSwitchingIndicator extends Component { 58class WorkspaceSwitchingIndicator extends Component {
52 static propTypes = { 59 static propTypes = {
53 classes: PropTypes.object.isRequired, 60 classes: PropTypes.object.isRequired,
@@ -63,13 +70,11 @@ class WorkspaceSwitchingIndicator extends Component {
63 const { intl } = this.context; 70 const { intl } = this.context;
64 const { isSwitchingWorkspace, nextWorkspace } = workspaceStore; 71 const { isSwitchingWorkspace, nextWorkspace } = workspaceStore;
65 if (!isSwitchingWorkspace) return null; 72 if (!isSwitchingWorkspace) return null;
66 const nextWorkspaceName = nextWorkspace ? nextWorkspace.name : 'All services'; 73 const nextWorkspaceName = nextWorkspace
74 ? nextWorkspace.name
75 : 'All services';
67 return ( 76 return (
68 <div 77 <div className={classnames([classes.wrapper])}>
69 className={classnames([
70 classes.wrapper,
71 ])}
72 >
73 <div className={classes.component}> 78 <div className={classes.component}>
74 <Loader 79 <Loader
75 className={classes.spinner} 80 className={classes.spinner}
diff --git a/src/features/workspaces/components/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.js
index cfaacd56e..8319d3bc6 100644
--- a/src/features/workspaces/components/WorkspacesDashboard.js
+++ b/src/features/workspaces/components/WorkspacesDashboard.js
@@ -1,9 +1,9 @@
1import React, { Component, Fragment } from 'react'; 1import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes, inject } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes, inject } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss'; 5import injectSheet from 'react-jss';
6import { Infobox, Badge } from '@meetfranz/ui'; 6import { Infobox } from '@meetfranz/ui';
7 7
8import { mdiCheckboxMarkedCircleOutline } from '@mdi/js'; 8import { mdiCheckboxMarkedCircleOutline } from '@mdi/js';
9import Loader from '../../../components/ui/Loader'; 9import Loader from '../../../components/ui/Loader';
@@ -11,10 +11,7 @@ import WorkspaceItem from './WorkspaceItem';
11import CreateWorkspaceForm from './CreateWorkspaceForm'; 11import CreateWorkspaceForm from './CreateWorkspaceForm';
12import Request from '../../../stores/lib/Request'; 12import Request from '../../../stores/lib/Request';
13import Appear from '../../../components/ui/effects/Appear'; 13import Appear from '../../../components/ui/effects/Appear';
14import { workspaceStore } from '../index';
15import UIStore from '../../../stores/UIStore'; 14import UIStore from '../../../stores/UIStore';
16import globalMessages from '../../../i18n/globalMessages';
17import UpgradeButton from '../../../components/ui/UpgradeButton';
18 15
19const messages = defineMessages({ 16const messages = defineMessages({
20 headline: { 17 headline: {
@@ -64,12 +61,6 @@ const styles = () => ({
64 appear: { 61 appear: {
65 height: 'auto', 62 height: 'auto',
66 }, 63 },
67 premiumAnnouncement: {
68 height: 'auto',
69 },
70 premiumAnnouncementContainer: {
71 display: 'flex',
72 },
73 announcementHeadline: { 64 announcementHeadline: {
74 marginBottom: 0, 65 marginBottom: 0,
75 }, 66 },
@@ -78,12 +69,6 @@ const styles = () => ({
78 margin: [-8, 0, 0, 20], 69 margin: [-8, 0, 0, 20],
79 alignSelf: 'center', 70 alignSelf: 'center',
80 }, 71 },
81 upgradeCTA: {
82 margin: [40, 'auto'],
83 },
84 proRequired: {
85 margin: [10, 0, 40],
86 },
87}); 72});
88 73
89@inject('stores') @injectSheet(styles) @observer 74@inject('stores') @injectSheet(styles) @observer
@@ -152,77 +137,53 @@ class WorkspacesDashboard extends Component {
152 </Appear> 137 </Appear>
153 )} 138 )}
154 139
155 {workspaceStore.isPremiumUpgradeRequired && ( 140 {/* ===== Create workspace form ===== */}
156 <div className={classes.premiumAnnouncement}> 141 <div className={classes.createForm}>
157 142 <CreateWorkspaceForm
158 <h1 className={classes.announcementHeadline}>{intl.formatMessage(messages.workspaceFeatureHeadline)}</h1> 143 isSubmitting={createWorkspaceRequest.isExecuting}
159 <Badge className={classes.proRequired}>{intl.formatMessage(globalMessages.proRequired)}</Badge> 144 onSubmit={onCreateWorkspaceSubmit}
160 <div className={classes.premiumAnnouncementContainer}> 145 />
161 <div className={classes.premiumAnnouncementContent}> 146 </div>
162 <p>{intl.formatMessage(messages.workspaceFeatureInfo)}</p> 147 {getUserWorkspacesRequest.isExecuting ? (
163 <UpgradeButton 148 <Loader />
164 className={classes.upgradeCTA} 149 ) : (
165 gaEventInfo={{ category: 'Workspaces', event: 'upgrade' }}
166 short
167 requiresPro
168 />
169 </div>
170 <img src={`https://cdn.franzinfra.com/announcements/assets/workspaces_${this.props.stores.ui.isDarkThemeActive ? 'dark' : 'light'}.png`} className={classes.teaserImage} alt="" />
171 </div>
172 </div>
173 )}
174
175 {!workspaceStore.isPremiumUpgradeRequired && (
176 <> 150 <>
177 {/* ===== Create workspace form ===== */} 151 {/* ===== Workspace could not be loaded error ===== */}
178 <div className={classes.createForm}> 152 {getUserWorkspacesRequest.error ? (
179 <CreateWorkspaceForm 153 <Infobox
180 isSubmitting={createWorkspaceRequest.isExecuting} 154 icon="alert"
181 onSubmit={onCreateWorkspaceSubmit} 155 type="danger"
182 /> 156 ctaLabel={intl.formatMessage(messages.tryReloadWorkspaces)}
183 </div> 157 ctaLoading={getUserWorkspacesRequest.isExecuting}
184 {getUserWorkspacesRequest.isExecuting ? ( 158 ctaOnClick={getUserWorkspacesRequest.retry}
185 <Loader /> 159 >
160 {intl.formatMessage(messages.workspacesRequestFailed)}
161 </Infobox>
186 ) : ( 162 ) : (
187 <> 163 <>
188 {/* ===== Workspace could not be loaded error ===== */} 164 {workspaces.length === 0 ? (
189 {getUserWorkspacesRequest.error ? ( 165 <div className="align-middle settings__empty-state">
190 <Infobox 166 {/* ===== Workspaces empty state ===== */}
191 icon="alert" 167 <p className="settings__empty-text">
192 type="danger" 168 <span className="emoji">
193 ctaLabel={intl.formatMessage(messages.tryReloadWorkspaces)} 169 <img src="./assets/images/emoji/sad.png" alt="" />
194 ctaLoading={getUserWorkspacesRequest.isExecuting} 170 </span>
195 ctaOnClick={getUserWorkspacesRequest.retry} 171 {intl.formatMessage(messages.noServicesAdded)}
196 > 172 </p>
197 {intl.formatMessage(messages.workspacesRequestFailed)} 173 </div>
198 </Infobox>
199 ) : ( 174 ) : (
200 <> 175 <table className={classes.table}>
201 {workspaces.length === 0 ? ( 176 {/* ===== Workspaces list ===== */}
202 <div className="align-middle settings__empty-state"> 177 <tbody>
203 {/* ===== Workspaces empty state ===== */} 178 {workspaces.map((workspace) => (
204 <p className="settings__empty-text"> 179 <WorkspaceItem
205 <span className="emoji"> 180 key={workspace.id}
206 <img src="./assets/images/emoji/sad.png" alt="" /> 181 workspace={workspace}
207 </span> 182 onItemClick={(w) => onWorkspaceClick(w)}
208 {intl.formatMessage(messages.noServicesAdded)} 183 />
209 </p> 184 ))}
210 </div> 185 </tbody>
211 ) : ( 186 </table>
212 <table className={classes.table}>
213 {/* ===== Workspaces list ===== */}
214 <tbody>
215 {workspaces.map(workspace => (
216 <WorkspaceItem
217 key={workspace.id}
218 workspace={workspace}
219 onItemClick={w => onWorkspaceClick(w)}
220 />
221 ))}
222 </tbody>
223 </table>
224 )}
225 </>
226 )} 187 )}
227 </> 188 </>
228 )} 189 )}
diff --git a/src/features/workspaces/containers/WorkspacesScreen.js b/src/features/workspaces/containers/WorkspacesScreen.js
index c241cd622..4828658f9 100644
--- a/src/features/workspaces/containers/WorkspacesScreen.js
+++ b/src/features/workspaces/containers/WorkspacesScreen.js
@@ -30,8 +30,8 @@ class WorkspacesScreen extends Component {
30 createWorkspaceRequest={createWorkspaceRequest} 30 createWorkspaceRequest={createWorkspaceRequest}
31 deleteWorkspaceRequest={deleteWorkspaceRequest} 31 deleteWorkspaceRequest={deleteWorkspaceRequest}
32 updateWorkspaceRequest={updateWorkspaceRequest} 32 updateWorkspaceRequest={updateWorkspaceRequest}
33 onCreateWorkspaceSubmit={data => actions.workspaces.create(data)} 33 onCreateWorkspaceSubmit={(data) => actions.workspaces.create(data)}
34 onWorkspaceClick={w => actions.workspaces.edit({ workspace: w })} 34 onWorkspaceClick={(w) => actions.workspaces.edit({ workspace: w })}
35 /> 35 />
36 </ErrorBoundary> 36 </ErrorBoundary>
37 ); 37 );
diff --git a/src/features/workspaces/models/Workspace.js b/src/features/workspaces/models/Workspace.js
index 77c4e05f4..11ee377cd 100644
--- a/src/features/workspaces/models/Workspace.js
+++ b/src/features/workspaces/models/Workspace.js
@@ -22,13 +22,13 @@ export default class Workspace {
22 this.name = data.name; 22 this.name = data.name;
23 this.order = data.order; 23 this.order = data.order;
24 24
25 let services = data.services; 25 let { services } = data;
26 if (data.saving && data.keepLoaded) { 26 if (data.saving && data.keepLoaded) {
27 // Keep workspaces loaded 27 // Keep workspaces loaded
28 services.push(KEEP_WS_LOADED_USID); 28 services.push(KEEP_WS_LOADED_USID);
29 } else if (data.saving && data.services.includes(KEEP_WS_LOADED_USID)) { 29 } else if (data.saving && data.services.includes(KEEP_WS_LOADED_USID)) {
30 // Don't keep loaded 30 // Don't keep loaded
31 services = services.filter(e => e !== KEEP_WS_LOADED_USID); 31 services = services.filter((e) => e !== KEEP_WS_LOADED_USID);
32 } 32 }
33 this.services.replace(services); 33 this.services.replace(services);
34 34
diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js
index 632f3c299..8c73516bc 100644
--- a/src/features/workspaces/store.js
+++ b/src/features/workspaces/store.js
@@ -22,10 +22,6 @@ export default class WorkspacesStore extends FeatureStore {
22 22
23 @observable isFeatureActive = false; 23 @observable isFeatureActive = false;
24 24
25 @observable isPremiumFeature = false;
26
27 @observable isPremiumUpgradeRequired = false;
28
29 @observable activeWorkspace = null; 25 @observable activeWorkspace = null;
30 26
31 @observable nextWorkspace = null; 27 @observable nextWorkspace = null;
@@ -58,7 +54,6 @@ export default class WorkspacesStore extends FeatureStore {
58 54
59 @computed get isUserAllowedToUseFeature() { 55 @computed get isUserAllowedToUseFeature() {
60 return true; 56 return true;
61 // return !this.isPremiumUpgradeRequired;
62 } 57 }
63 58
64 @computed get isAnyWorkspaceActive() { 59 @computed get isAnyWorkspaceActive() {
@@ -69,16 +64,8 @@ export default class WorkspacesStore extends FeatureStore {
69 64
70 _wasDrawerOpenBeforeSettingsRoute = null; 65 _wasDrawerOpenBeforeSettingsRoute = null;
71 66
72 _freeUserActions = [];
73
74 _premiumUserActions = [];
75
76 _allActions = []; 67 _allActions = [];
77 68
78 _freeUserReactions = [];
79
80 _premiumUserReactions = [];
81
82 _allReactions = []; 69 _allReactions = [];
83 70
84 // ========== PUBLIC API ========= // 71 // ========== PUBLIC API ========= //
@@ -90,11 +77,9 @@ export default class WorkspacesStore extends FeatureStore {
90 77
91 // ACTIONS 78 // ACTIONS
92 79
93 this._freeUserActions = createActionBindings([ 80 this._allActions = createActionBindings([
94 [workspaceActions.toggleWorkspaceDrawer, this._toggleWorkspaceDrawer], 81 [workspaceActions.toggleWorkspaceDrawer, this._toggleWorkspaceDrawer],
95 [workspaceActions.openWorkspaceSettings, this._openWorkspaceSettings], 82 [workspaceActions.openWorkspaceSettings, this._openWorkspaceSettings],
96 ]);
97 this._premiumUserActions = createActionBindings([
98 [workspaceActions.edit, this._edit], 83 [workspaceActions.edit, this._edit],
99 [workspaceActions.create, this._create], 84 [workspaceActions.create, this._create],
100 [workspaceActions.delete, this._delete], 85 [workspaceActions.delete, this._delete],
@@ -106,27 +91,18 @@ export default class WorkspacesStore extends FeatureStore {
106 this._toggleKeepAllWorkspacesLoadedSetting, 91 this._toggleKeepAllWorkspacesLoadedSetting,
107 ], 92 ],
108 ]); 93 ]);
109 this._allActions = this._freeUserActions.concat(this._premiumUserActions);
110 this._registerActions(this._allActions); 94 this._registerActions(this._allActions);
111 95
112 // REACTIONS 96 // REACTIONS
113 97
114 this._freeUserReactions = createReactions([ 98 this._allReactions = createReactions([
115 this._disablePremiumFeatures,
116 this._openDrawerWithSettingsReaction, 99 this._openDrawerWithSettingsReaction,
117 this._setFeatureEnabledReaction, 100 this._setFeatureEnabledReaction,
118 this._setIsPremiumFeatureReaction,
119 this._cleanupInvalidServiceReferences, 101 this._cleanupInvalidServiceReferences,
120 ]);
121 this._premiumUserReactions = createReactions([
122 this._setActiveServiceOnWorkspaceSwitchReaction, 102 this._setActiveServiceOnWorkspaceSwitchReaction,
123 this._activateLastUsedWorkspaceReaction, 103 this._activateLastUsedWorkspaceReaction,
124 this._setWorkspaceBeingEditedReaction, 104 this._setWorkspaceBeingEditedReaction,
125 ]); 105 ]);
126 this._allReactions = this._freeUserReactions.concat(
127 this._premiumUserReactions,
128 );
129
130 this._registerReactions(this._allReactions); 106 this._registerReactions(this._allReactions);
131 107
132 getUserWorkspacesRequest.execute(); 108 getUserWorkspacesRequest.execute();
@@ -273,13 +249,6 @@ export default class WorkspacesStore extends FeatureStore {
273 this.isFeatureEnabled = isWorkspaceEnabled; 249 this.isFeatureEnabled = isWorkspaceEnabled;
274 }; 250 };
275 251
276 _setIsPremiumFeatureReaction = () => {
277 // const { features } = this.stores;
278 // const { isWorkspaceIncludedInCurrentPlan } = features.features;
279 // this.isPremiumFeature = !isWorkspaceIncludedInCurrentPlan;
280 // this.isPremiumUpgradeRequired = !isWorkspaceIncludedInCurrentPlan;
281 };
282
283 _setWorkspaceBeingEditedReaction = () => { 252 _setWorkspaceBeingEditedReaction = () => {
284 const { pathname } = this.stores.router.location; 253 const { pathname } = this.stores.router.location;
285 const match = matchRoute('/settings/workspaces/edit/:id', pathname); 254 const match = matchRoute('/settings/workspaces/edit/:id', pathname);
@@ -357,16 +326,4 @@ export default class WorkspacesStore extends FeatureStore {
357 }); 326 });
358 }); 327 });
359 }; 328 };
360
361 _disablePremiumFeatures = () => {
362 if (!this.isUserAllowedToUseFeature) {
363 debug('_disablePremiumFeatures');
364 this._stopActions(this._premiumUserActions);
365 this._stopReactions(this._premiumUserReactions);
366 this.reset();
367 } else {
368 this._startActions(this._premiumUserActions);
369 this._startReactions(this._premiumUserReactions);
370 }
371 };
372} 329}