aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/features')
-rw-r--r--src/features/communityRecipes/index.js19
-rw-r--r--src/features/communityRecipes/store.js4
-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/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.js4
-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/store.js3
-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/workspaces/components/WorkspaceDrawer.js141
-rw-r--r--src/features/workspaces/components/WorkspacesDashboard.js129
-rw-r--r--src/features/workspaces/store.js47
31 files changed, 92 insertions, 1835 deletions
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..acc1e52f6 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;
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/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..b66375453 100644
--- a/src/features/shareFranz/Component.js
+++ b/src/features/shareFranz/Component.js
@@ -36,11 +36,11 @@ 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
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/store.js b/src/features/todos/store.js
index 429507927..abaec677f 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() {
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/workspaces/components/WorkspaceDrawer.js b/src/features/workspaces/components/WorkspaceDrawer.js
index bf7016e2f..3ddc9cf16 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,22 +28,10 @@ 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 => ({
@@ -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/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.js
index cfaacd56e..0c2588c42 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/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}