aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/features')
-rw-r--r--src/features/basicAuth/Component.js1
-rw-r--r--src/features/communityRecipes/index.js28
-rw-r--r--src/features/communityRecipes/store.js31
-rw-r--r--src/features/delayApp/Component.js43
-rw-r--r--src/features/delayApp/index.js2
-rw-r--r--src/features/serviceLimit/components/LimitReachedInfobox.js78
-rw-r--r--src/features/serviceLimit/index.js33
-rw-r--r--src/features/serviceLimit/store.js41
-rw-r--r--src/features/serviceProxy/index.js8
-rw-r--r--src/features/shareFranz/Component.js13
-rw-r--r--src/features/spellchecker/index.js8
-rw-r--r--src/features/todos/components/TodosWebview.js105
-rw-r--r--src/features/todos/containers/TodosScreen.js13
-rw-r--r--src/features/todos/store.js2
-rw-r--r--src/features/workspaces/components/WorkspaceDrawer.js7
-rw-r--r--src/features/workspaces/components/WorkspaceSwitchingIndicator.js8
-rw-r--r--src/features/workspaces/components/WorkspacesDashboard.js50
-rw-r--r--src/features/workspaces/store.js9
18 files changed, 411 insertions, 69 deletions
diff --git a/src/features/basicAuth/Component.js b/src/features/basicAuth/Component.js
index a8252acb7..ba9ae2273 100644
--- a/src/features/basicAuth/Component.js
+++ b/src/features/basicAuth/Component.js
@@ -27,7 +27,6 @@ export default @injectSheet(styles) @observer class BasicAuthModal extends Compo
27 e.preventDefault(); 27 e.preventDefault();
28 28
29 const values = Form.values(); 29 const values = Form.values();
30 console.log('form submit', values);
31 30
32 sendCredentials(values.user, values.password); 31 sendCredentials(values.user, values.password);
33 resetState(); 32 resetState();
diff --git a/src/features/communityRecipes/index.js b/src/features/communityRecipes/index.js
new file mode 100644
index 000000000..4d050f90e
--- /dev/null
+++ b/src/features/communityRecipes/index.js
@@ -0,0 +1,28 @@
1import { reaction } from 'mobx';
2import { CommunityRecipesStore } from './store';
3
4const debug = require('debug')('Franz:feature:communityRecipes');
5
6export const DEFAULT_SERVICE_LIMIT = 3;
7
8export const communityRecipesStore = new CommunityRecipesStore();
9
10export default function initCommunityRecipes(stores, actions) {
11 const { features } = stores;
12
13 communityRecipesStore.start(stores, actions);
14
15 // Toggle communityRecipe premium status
16 reaction(
17 () => (
18 features.features.isCommunityRecipesIncludedInCurrentPlan
19 ),
20 (isPremiumFeature) => {
21 debug('Community recipes is premium feature: ', isPremiumFeature);
22 communityRecipesStore.isCommunityRecipesIncludedInCurrentPlan = isPremiumFeature;
23 },
24 {
25 fireImmediately: true,
26 },
27 );
28}
diff --git a/src/features/communityRecipes/store.js b/src/features/communityRecipes/store.js
new file mode 100644
index 000000000..4d45c3b33
--- /dev/null
+++ b/src/features/communityRecipes/store.js
@@ -0,0 +1,31 @@
1import { computed, observable } from 'mobx';
2import { FeatureStore } from '../utils/FeatureStore';
3
4const debug = require('debug')('Franz:feature:communityRecipes:store');
5
6export class CommunityRecipesStore extends FeatureStore {
7 @observable isCommunityRecipesIncludedInCurrentPlan = false;
8
9 start(stores, actions) {
10 debug('start');
11 this.stores = stores;
12 this.actions = actions;
13 }
14
15 stop() {
16 debug('stop');
17 super.stop();
18 }
19
20 @computed get communityRecipes() {
21 if (!this.stores) return [];
22
23 return this.stores.recipePreviews.dev.map((r) => {
24 r.isDevRecipe = !!r.author.find(a => a.email === this.stores.user.data.email);
25
26 return r;
27 });
28 }
29}
30
31export default CommunityRecipesStore;
diff --git a/src/features/delayApp/Component.js b/src/features/delayApp/Component.js
index ff0f1f2f8..de5653f04 100644
--- a/src/features/delayApp/Component.js
+++ b/src/features/delayApp/Component.js
@@ -4,29 +4,39 @@ import { inject, observer } 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';
6 6
7import { Button } from '@meetfranz/forms';
7import { gaEvent } from '../../lib/analytics'; 8import { gaEvent } from '../../lib/analytics';
8 9
9import Button from '../../components/ui/Button'; 10// import Button from '../../components/ui/Button';
10 11
11import { config } from '.'; 12import { config } from '.';
12import styles from './styles'; 13import styles from './styles';
14import UserStore from '../../stores/UserStore';
13 15
14const messages = defineMessages({ 16const messages = defineMessages({
15 headline: { 17 headline: {
16 id: 'feature.delayApp.headline', 18 id: 'feature.delayApp.headline',
17 defaultMessage: '!!!Please purchase license to skip waiting', 19 defaultMessage: '!!!Please purchase license to skip waiting',
18 }, 20 },
21 headlineTrial: {
22 id: 'feature.delayApp.trial.headline',
23 defaultMessage: '!!!Get the free Franz Professional 14 day trial and skip the line',
24 },
19 action: { 25 action: {
20 id: 'feature.delayApp.action', 26 id: 'feature.delayApp.upgrade.action',
21 defaultMessage: '!!!Get a Franz Supporter License', 27 defaultMessage: '!!!Get a Franz Supporter License',
22 }, 28 },
29 actionTrial: {
30 id: 'feature.delayApp.trial.action',
31 defaultMessage: '!!!Yes, I want the free 14 day trial of Franz Professional',
32 },
23 text: { 33 text: {
24 id: 'feature.delayApp.text', 34 id: 'feature.delayApp.text',
25 defaultMessage: '!!!Franz will continue in {seconds} seconds.', 35 defaultMessage: '!!!Franz will continue in {seconds} seconds.',
26 }, 36 },
27}); 37});
28 38
29export default @inject('actions') @injectSheet(styles) @observer class DelayApp extends Component { 39export default @inject('stores', 'actions') @injectSheet(styles) @observer class DelayApp extends Component {
30 static propTypes = { 40 static propTypes = {
31 // eslint-disable-next-line 41 // eslint-disable-next-line
32 classes: PropTypes.object.isRequired, 42 classes: PropTypes.object.isRequired,
@@ -62,25 +72,37 @@ export default @inject('actions') @injectSheet(styles) @observer class DelayApp
62 } 72 }
63 73
64 handleCTAClick() { 74 handleCTAClick() {
65 const { actions } = this.props; 75 const { actions, stores } = this.props;
76 const { hadSubscription } = stores.user.data;
77 const { defaultTrialPlan } = stores.features.features;
78
79 if (!hadSubscription) {
80 console.log('directly activate trial');
81 actions.user.activateTrial({ planId: defaultTrialPlan });
66 82
67 actions.ui.openSettings({ path: 'user' }); 83 gaEvent('DelayApp', 'subscribe_click', 'Delay App Feature');
84 } else {
85 actions.ui.openSettings({ path: 'user' });
68 86
69 gaEvent('DelayApp', 'subscribe_click', 'Delay App Feature'); 87 gaEvent('DelayApp', 'subscribe_click', 'Delay App Feature');
88 }
70 } 89 }
71 90
72 render() { 91 render() {
73 const { classes } = this.props; 92 const { classes, stores } = this.props;
74 const { intl } = this.context; 93 const { intl } = this.context;
75 94
95 const { hadSubscription } = stores.user.data;
96
76 return ( 97 return (
77 <div className={`${classes.container}`}> 98 <div className={`${classes.container}`}>
78 <h1 className={classes.headline}>{intl.formatMessage(messages.headline)}</h1> 99 <h1 className={classes.headline}>{intl.formatMessage(hadSubscription ? messages.headline : messages.headlineTrial)}</h1>
79 <Button 100 <Button
80 label={intl.formatMessage(messages.action)} 101 label={intl.formatMessage(hadSubscription ? messages.action : messages.actionTrial)}
81 className={classes.button} 102 className={classes.button}
82 buttonType="inverted" 103 buttonType="inverted"
83 onClick={this.handleCTAClick.bind(this)} 104 onClick={this.handleCTAClick.bind(this)}
105 busy={stores.user.activateTrialRequest.isExecuting}
84 /> 106 />
85 <p className="footnote"> 107 <p className="footnote">
86 {intl.formatMessage(messages.text, { 108 {intl.formatMessage(messages.text, {
@@ -93,6 +115,9 @@ export default @inject('actions') @injectSheet(styles) @observer class DelayApp
93} 115}
94 116
95DelayApp.wrappedComponent.propTypes = { 117DelayApp.wrappedComponent.propTypes = {
118 stores: PropTypes.shape({
119 user: PropTypes.instanceOf(UserStore).isRequired,
120 }).isRequired,
96 actions: PropTypes.shape({ 121 actions: PropTypes.shape({
97 ui: PropTypes.shape({ 122 ui: PropTypes.shape({
98 openSettings: PropTypes.func.isRequired, 123 openSettings: PropTypes.func.isRequired,
diff --git a/src/features/delayApp/index.js b/src/features/delayApp/index.js
index 39fae3b20..627537de7 100644
--- a/src/features/delayApp/index.js
+++ b/src/features/delayApp/index.js
@@ -44,7 +44,7 @@ export default function init(stores) {
44 config.delayDuration = globalConfig.wait !== undefined ? globalConfig.wait : DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.wait; 44 config.delayDuration = globalConfig.wait !== undefined ? globalConfig.wait : DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.wait;
45 45
46 autorun(() => { 46 autorun(() => {
47 if (stores.services.all.length === 0) { 47 if (stores.services.allDisplayed.length === 0) {
48 debug('seas', stores.services.all.length); 48 debug('seas', stores.services.all.length);
49 shownAfterLaunch = true; 49 shownAfterLaunch = true;
50 return; 50 return;
diff --git a/src/features/serviceLimit/components/LimitReachedInfobox.js b/src/features/serviceLimit/components/LimitReachedInfobox.js
new file mode 100644
index 000000000..fc54dcf85
--- /dev/null
+++ b/src/features/serviceLimit/components/LimitReachedInfobox.js
@@ -0,0 +1,78 @@
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
8import { gaEvent } from '../../../lib/analytics';
9
10const messages = defineMessages({
11 limitReached: {
12 id: 'feature.serviceLimit.limitReached',
13 defaultMessage: '!!!You have added {amount} of {limit} services. Please upgrade your account to add more services.',
14 },
15 action: {
16 id: 'premiumFeature.button.upgradeAccount',
17 defaultMessage: '!!!Upgrade account',
18 },
19});
20
21const styles = theme => ({
22 container: {
23 height: 'auto',
24 background: theme.styleTypes.primary.accent,
25 color: theme.styleTypes.primary.contrast,
26 borderRadius: 0,
27 marginBottom: 0,
28
29 '& > div': {
30 marginBottom: 0,
31 },
32
33 '& button': {
34 color: theme.styleTypes.primary.contrast,
35 },
36 },
37});
38
39
40@inject('stores', 'actions') @injectSheet(styles) @observer
41class LimitReachedInfobox extends Component {
42 static propTypes = {
43 classes: PropTypes.object.isRequired,
44 stores: PropTypes.object.isRequired,
45 actions: PropTypes.object.isRequired,
46 };
47
48 static contextTypes = {
49 intl: intlShape,
50 };
51
52 render() {
53 const { classes, stores, actions } = this.props;
54 const { intl } = this.context;
55
56 const {
57 serviceLimit,
58 } = stores;
59
60 if (!serviceLimit.userHasReachedServiceLimit) return null;
61
62 return (
63 <Infobox
64 icon="mdiInformation"
65 className={classes.container}
66 ctaLabel={intl.formatMessage(messages.action)}
67 ctaOnClick={() => {
68 actions.ui.openSettings({ path: 'user' });
69 gaEvent('Service Limit', 'upgrade', 'Upgrade account');
70 }}
71 >
72 {intl.formatMessage(messages.limitReached, { amount: serviceLimit.serviceCount, limit: serviceLimit.serviceLimit })}
73 </Infobox>
74 );
75 }
76}
77
78export default LimitReachedInfobox;
diff --git a/src/features/serviceLimit/index.js b/src/features/serviceLimit/index.js
new file mode 100644
index 000000000..92ad8bb98
--- /dev/null
+++ b/src/features/serviceLimit/index.js
@@ -0,0 +1,33 @@
1import { reaction } from 'mobx';
2import { ServiceLimitStore } from './store';
3
4const debug = require('debug')('Franz:feature:serviceLimit');
5
6export const DEFAULT_SERVICE_LIMIT = 3;
7
8let store = null;
9
10export const serviceLimitStore = new ServiceLimitStore();
11
12export default function initServiceLimit(stores, actions) {
13 const { features } = stores;
14
15 // Toggle serviceLimit feature
16 reaction(
17 () => (
18 features.features.isServiceLimitEnabled
19 ),
20 (isEnabled) => {
21 if (isEnabled) {
22 debug('Initializing `serviceLimit` feature');
23 store = serviceLimitStore.start(stores, actions);
24 } else if (store) {
25 debug('Disabling `serviceLimit` feature');
26 serviceLimitStore.stop();
27 }
28 },
29 {
30 fireImmediately: true,
31 },
32 );
33}
diff --git a/src/features/serviceLimit/store.js b/src/features/serviceLimit/store.js
new file mode 100644
index 000000000..9836c5f51
--- /dev/null
+++ b/src/features/serviceLimit/store.js
@@ -0,0 +1,41 @@
1import { computed, observable } from 'mobx';
2import { FeatureStore } from '../utils/FeatureStore';
3import { DEFAULT_SERVICE_LIMIT } from '.';
4
5const debug = require('debug')('Franz: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 = true;
16 }
17
18 stop() {
19 super.stop();
20
21 this.isServiceLimitEnabled = false;
22 }
23
24 @computed get userHasReachedServiceLimit() {
25 if (!this.isServiceLimitEnabled) return false;
26
27 return this.serviceLimit !== 0 && this.serviceCount >= this.serviceLimit;
28 }
29
30 @computed get serviceLimit() {
31 if (!this.isServiceLimitEnabled || this.stores.features.features.serviceLimitCount === 0) return 0;
32
33 return this.stores.features.features.serviceLimitCount || DEFAULT_SERVICE_LIMIT;
34 }
35
36 @computed get serviceCount() {
37 return this.stores.services.all.length;
38 }
39}
40
41export default ServiceLimitStore;
diff --git a/src/features/serviceProxy/index.js b/src/features/serviceProxy/index.js
index 4bea327ad..55c600de4 100644
--- a/src/features/serviceProxy/index.js
+++ b/src/features/serviceProxy/index.js
@@ -9,17 +9,17 @@ const debug = require('debug')('Franz:feature:serviceProxy');
9 9
10export const config = observable({ 10export const config = observable({
11 isEnabled: DEFAULT_FEATURES_CONFIG.isServiceProxyEnabled, 11 isEnabled: DEFAULT_FEATURES_CONFIG.isServiceProxyEnabled,
12 isPremium: DEFAULT_FEATURES_CONFIG.isServiceProxyPremiumFeature, 12 isPremium: DEFAULT_FEATURES_CONFIG.isServiceProxyIncludedInCurrentPlan,
13}); 13});
14 14
15export default function init(stores) { 15export default function init(stores) {
16 debug('Initializing `serviceProxy` feature'); 16 debug('Initializing `serviceProxy` feature');
17 17
18 autorun(() => { 18 autorun(() => {
19 const { isServiceProxyEnabled, isServiceProxyPremiumFeature } = stores.features.features; 19 const { isServiceProxyEnabled, isServiceProxyIncludedInCurrentPlan } = stores.features.features;
20 20
21 config.isEnabled = isServiceProxyEnabled !== undefined ? isServiceProxyEnabled : DEFAULT_FEATURES_CONFIG.isServiceProxyEnabled; 21 config.isEnabled = isServiceProxyEnabled !== undefined ? isServiceProxyEnabled : DEFAULT_FEATURES_CONFIG.isServiceProxyEnabled;
22 config.isPremium = isServiceProxyPremiumFeature !== undefined ? isServiceProxyPremiumFeature : DEFAULT_FEATURES_CONFIG.isServiceProxyPremiumFeature; 22 config.isIncludedInCurrentPlan = isServiceProxyIncludedInCurrentPlan !== undefined ? isServiceProxyIncludedInCurrentPlan : DEFAULT_FEATURES_CONFIG.isServiceProxyIncludedInCurrentPlan;
23 23
24 const services = stores.services.enabled; 24 const services = stores.services.enabled;
25 const isPremiumUser = stores.user.data.isPremium; 25 const isPremiumUser = stores.user.data.isPremium;
@@ -30,7 +30,7 @@ export default function init(stores) {
30 services.forEach((service) => { 30 services.forEach((service) => {
31 const s = session.fromPartition(`persist:service-${service.id}`); 31 const s = session.fromPartition(`persist:service-${service.id}`);
32 32
33 if (config.isEnabled && (isPremiumUser || !config.isPremium)) { 33 if (config.isEnabled && (isPremiumUser || !config.isIncludedInCurrentPlan)) {
34 const serviceProxyConfig = proxySettings[service.id]; 34 const serviceProxyConfig = proxySettings[service.id];
35 35
36 if (serviceProxyConfig && serviceProxyConfig.isEnabled && serviceProxyConfig.host) { 36 if (serviceProxyConfig && serviceProxyConfig.isEnabled && serviceProxyConfig.host) {
diff --git a/src/features/shareFranz/Component.js b/src/features/shareFranz/Component.js
index 8d1d595c5..a33315e17 100644
--- a/src/features/shareFranz/Component.js
+++ b/src/features/shareFranz/Component.js
@@ -6,6 +6,9 @@ import { defineMessages, intlShape } from 'react-intl';
6import { Button } from '@meetfranz/forms'; 6import { Button } from '@meetfranz/forms';
7import { H1, Icon } from '@meetfranz/ui'; 7import { H1, Icon } from '@meetfranz/ui';
8 8
9import {
10 mdiHeart, mdiEmail, mdiFacebookBox, mdiTwitter,
11} from '@mdi/js';
9import Modal from '../../components/ui/Modal'; 12import Modal from '../../components/ui/Modal';
10import { state } from '.'; 13import { state } from '.';
11import { gaEvent } from '../../lib/analytics'; 14import { gaEvent } from '../../lib/analytics';
@@ -75,7 +78,7 @@ const styles = theme => ({
75 }, 78 },
76 cta: { 79 cta: {
77 background: theme.styleTypes.primary.contrast, 80 background: theme.styleTypes.primary.contrast,
78 color: theme.styleTypes.primary.accent, 81 color: `${theme.styleTypes.primary.accent} !important`,
79 82
80 '& svg': { 83 '& svg': {
81 fill: theme.styleTypes.primary.accent, 84 fill: theme.styleTypes.primary.accent,
@@ -116,7 +119,7 @@ export default @injectSheet(styles) @inject('stores') @observer class ShareFranz
116 close={this.close.bind(this)} 119 close={this.close.bind(this)}
117 > 120 >
118 <div className={classes.heartContainer}> 121 <div className={classes.heartContainer}>
119 <Icon icon="mdiHeart" className={classes.heart} size={4} /> 122 <Icon icon={mdiHeart} className={classes.heart} size={4} />
120 </div> 123 </div>
121 <H1 className={classes.headline}> 124 <H1 className={classes.headline}>
122 {intl.formatMessage(messages.headline)} 125 {intl.formatMessage(messages.headline)}
@@ -126,7 +129,7 @@ export default @injectSheet(styles) @inject('stores') @observer class ShareFranz
126 <Button 129 <Button
127 label={intl.formatMessage(messages.actionsEmail)} 130 label={intl.formatMessage(messages.actionsEmail)}
128 className={classes.cta} 131 className={classes.cta}
129 icon="mdiEmail" 132 icon={mdiEmail}
130 href={`mailto:?subject=Meet the cool app Franz&body=${intl.formatMessage(messages.shareTextEmail, { count: serviceCount })}}`} 133 href={`mailto:?subject=Meet the cool app Franz&body=${intl.formatMessage(messages.shareTextEmail, { count: serviceCount })}}`}
131 target="_blank" 134 target="_blank"
132 onClick={() => { 135 onClick={() => {
@@ -136,7 +139,7 @@ export default @injectSheet(styles) @inject('stores') @observer class ShareFranz
136 <Button 139 <Button
137 label={intl.formatMessage(messages.actionsFacebook)} 140 label={intl.formatMessage(messages.actionsFacebook)}
138 className={classes.cta} 141 className={classes.cta}
139 icon="mdiFacebookBox" 142 icon={mdiFacebookBox}
140 href="https://www.facebook.com/sharer/sharer.php?u=https://www.meetfranz.com?utm_source=facebook&utm_medium=referral&utm_campaign=share-button" 143 href="https://www.facebook.com/sharer/sharer.php?u=https://www.meetfranz.com?utm_source=facebook&utm_medium=referral&utm_campaign=share-button"
141 target="_blank" 144 target="_blank"
142 onClick={() => { 145 onClick={() => {
@@ -146,7 +149,7 @@ export default @injectSheet(styles) @inject('stores') @observer class ShareFranz
146 <Button 149 <Button
147 label={intl.formatMessage(messages.actionsTwitter)} 150 label={intl.formatMessage(messages.actionsTwitter)}
148 className={classes.cta} 151 className={classes.cta}
149 icon="mdiTwitter" 152 icon={mdiTwitter}
150 href={`http://twitter.com/intent/tweet?status=${intl.formatMessage(messages.shareTextTwitter, { count: serviceCount })}`} 153 href={`http://twitter.com/intent/tweet?status=${intl.formatMessage(messages.shareTextTwitter, { count: serviceCount })}`}
151 target="_blank" 154 target="_blank"
152 onClick={() => { 155 onClick={() => {
diff --git a/src/features/spellchecker/index.js b/src/features/spellchecker/index.js
index 79a2172b4..a07f9f63a 100644
--- a/src/features/spellchecker/index.js
+++ b/src/features/spellchecker/index.js
@@ -5,18 +5,18 @@ import { DEFAULT_FEATURES_CONFIG } from '../../config';
5const debug = require('debug')('Franz:feature:spellchecker'); 5const debug = require('debug')('Franz:feature:spellchecker');
6 6
7export const config = observable({ 7export const config = observable({
8 isPremium: DEFAULT_FEATURES_CONFIG.isSpellcheckerPremiumFeature, 8 isIncludedInCurrentPlan: DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan,
9}); 9});
10 10
11export default function init(stores) { 11export default function init(stores) {
12 debug('Initializing `spellchecker` feature'); 12 debug('Initializing `spellchecker` feature');
13 13
14 autorun(() => { 14 autorun(() => {
15 const { isSpellcheckerPremiumFeature } = stores.features.features; 15 const { isSpellcheckerIncludedInCurrentPlan } = stores.features.features;
16 16
17 config.isPremium = isSpellcheckerPremiumFeature !== undefined ? isSpellcheckerPremiumFeature : DEFAULT_FEATURES_CONFIG.isSpellcheckerPremiumFeature; 17 config.isIncludedInCurrentPlan = isSpellcheckerIncludedInCurrentPlan !== undefined ? isSpellcheckerIncludedInCurrentPlan : DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan;
18 18
19 if (!stores.user.data.isPremium && config.isPremium && stores.settings.app.enableSpellchecking) { 19 if (!stores.user.data.isPremium && config.isIncludedInCurrentPlan && stores.settings.app.enableSpellchecking) {
20 debug('Override settings.spellcheckerEnabled flag to false'); 20 debug('Override settings.spellcheckerEnabled flag to false');
21 21
22 Object.assign(stores.settings.app, { 22 Object.assign(stores.settings.app, {
diff --git a/src/features/todos/components/TodosWebview.js b/src/features/todos/components/TodosWebview.js
index 9dd313109..143955a7b 100644
--- a/src/features/todos/components/TodosWebview.js
+++ b/src/features/todos/components/TodosWebview.js
@@ -4,12 +4,31 @@ import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 4import injectSheet from 'react-jss';
5import Webview from 'react-electron-web-view'; 5import Webview from 'react-electron-web-view';
6import { Icon } from '@meetfranz/ui'; 6import { Icon } from '@meetfranz/ui';
7import { defineMessages, intlShape } from 'react-intl';
7 8
9import { mdiChevronRight, mdiCheckAll } from '@mdi/js';
8import * as environment from '../../../environment'; 10import * as environment from '../../../environment';
11import Appear from '../../../components/ui/effects/Appear';
12import ActivateTrialButton from '../../../components/ui/ActivateTrialButton';
9 13
10const OPEN_TODOS_BUTTON_SIZE = 45; 14const OPEN_TODOS_BUTTON_SIZE = 45;
11const CLOSE_TODOS_BUTTON_SIZE = 35; 15const CLOSE_TODOS_BUTTON_SIZE = 35;
12 16
17const messages = defineMessages({
18 premiumInfo: {
19 id: 'feature.todos.premium.info',
20 defaultMessage: '!!!The Franz Todos Preview is currently only available for Franz Premium accounts.',
21 },
22 upgradeCTA: {
23 id: 'feature.todos.premium.upgrade',
24 defaultMessage: '!!!Upgrade Account',
25 },
26 rolloutInfo: {
27 id: 'feature.todos.premium.rollout',
28 defaultMessage: '!!!Franz Todos will be available to everyone soon.',
29 },
30});
31
13const styles = theme => ({ 32const styles = theme => ({
14 root: { 33 root: {
15 background: theme.colorBackground, 34 background: theme.colorBackground,
@@ -47,7 +66,7 @@ const styles = theme => ({
47 height: OPEN_TODOS_BUTTON_SIZE, 66 height: OPEN_TODOS_BUTTON_SIZE,
48 background: theme.todos.toggleButton.background, 67 background: theme.todos.toggleButton.background,
49 position: 'absolute', 68 position: 'absolute',
50 bottom: 80, 69 bottom: 120,
51 right: props => (props.width + (props.isVisible ? -OPEN_TODOS_BUTTON_SIZE / 2 : 0)), 70 right: props => (props.width + (props.isVisible ? -OPEN_TODOS_BUTTON_SIZE / 2 : 0)),
52 borderRadius: OPEN_TODOS_BUTTON_SIZE / 2, 71 borderRadius: OPEN_TODOS_BUTTON_SIZE / 2,
53 opacity: props => (props.isVisible ? 0 : 1), 72 opacity: props => (props.isVisible ? 0 : 1),
@@ -71,10 +90,10 @@ const styles = theme => ({
71 height: CLOSE_TODOS_BUTTON_SIZE, 90 height: CLOSE_TODOS_BUTTON_SIZE,
72 background: theme.todos.toggleButton.background, 91 background: theme.todos.toggleButton.background,
73 position: 'absolute', 92 position: 'absolute',
74 bottom: 80, 93 bottom: 120,
75 right: ({ width }) => (width + -CLOSE_TODOS_BUTTON_SIZE / 2), 94 right: ({ width }) => (width + -CLOSE_TODOS_BUTTON_SIZE / 2),
76 borderRadius: CLOSE_TODOS_BUTTON_SIZE / 2, 95 borderRadius: CLOSE_TODOS_BUTTON_SIZE / 2,
77 opacity: 0, 96 opacity: ({ isTodosIncludedInCurrentPlan }) => (!isTodosIncludedInCurrentPlan ? 1 : 0),
78 transition: 'opacity 0.5s', 97 transition: 'opacity 0.5s',
79 zIndex: 600, 98 zIndex: 600,
80 display: 'flex', 99 display: 'flex',
@@ -86,6 +105,26 @@ const styles = theme => ({
86 fill: theme.todos.toggleButton.textColor, 105 fill: theme.todos.toggleButton.textColor,
87 }, 106 },
88 }, 107 },
108 premiumContainer: {
109 display: 'flex',
110 flexDirection: 'column',
111 justifyContent: 'center',
112 alignItems: 'center',
113 width: '80%',
114 maxWidth: 300,
115 margin: [-50, 'auto', 0],
116 textAlign: 'center',
117 },
118 premiumIcon: {
119 marginBottom: 40,
120 background: theme.styleTypes.primary.accent,
121 fill: theme.styleTypes.primary.contrast,
122 padding: 10,
123 borderRadius: 10,
124 },
125 premiumCTA: {
126 marginTop: 40,
127 },
89}); 128});
90 129
91@injectSheet(styles) @observer 130@injectSheet(styles) @observer
@@ -99,6 +138,7 @@ class TodosWebview extends Component {
99 resize: PropTypes.func.isRequired, 138 resize: PropTypes.func.isRequired,
100 width: PropTypes.number.isRequired, 139 width: PropTypes.number.isRequired,
101 minWidth: PropTypes.number.isRequired, 140 minWidth: PropTypes.number.isRequired,
141 isTodosIncludedInCurrentPlan: PropTypes.bool.isRequired,
102 }; 142 };
103 143
104 state = { 144 state = {
@@ -106,6 +146,10 @@ class TodosWebview extends Component {
106 width: 300, 146 width: 300,
107 }; 147 };
108 148
149 static contextTypes = {
150 intl: intlShape,
151 };
152
109 componentWillMount() { 153 componentWillMount() {
110 const { width } = this.props; 154 const { width } = this.props;
111 155
@@ -182,9 +226,19 @@ class TodosWebview extends Component {
182 226
183 render() { 227 render() {
184 const { 228 const {
185 classes, isVisible, togglePanel, 229 classes,
230 isVisible,
231 togglePanel,
232 isTodosIncludedInCurrentPlan,
186 } = this.props; 233 } = this.props;
187 const { width, delta, isDragging } = this.state; 234
235 const {
236 width,
237 delta,
238 isDragging,
239 } = this.state;
240
241 const { intl } = this.context;
188 242
189 return ( 243 return (
190 <div 244 <div
@@ -198,7 +252,7 @@ class TodosWebview extends Component {
198 className={isVisible ? classes.closeTodosButton : classes.openTodosButton} 252 className={isVisible ? classes.closeTodosButton : classes.openTodosButton}
199 type="button" 253 type="button"
200 > 254 >
201 <Icon icon={isVisible ? 'mdiChevronRight' : 'mdiCheckAll'} size={2} /> 255 <Icon icon={isVisible ? mdiChevronRight : mdiCheckAll} size={2} />
202 </button> 256 </button>
203 <div 257 <div
204 className={classes.resizeHandler} 258 className={classes.resizeHandler}
@@ -211,18 +265,33 @@ class TodosWebview extends Component {
211 style={{ left: delta }} // This hack is required as resizing with webviews beneath behaves quite bad 265 style={{ left: delta }} // This hack is required as resizing with webviews beneath behaves quite bad
212 /> 266 />
213 )} 267 )}
214 <Webview 268 {isTodosIncludedInCurrentPlan ? (
215 className={classes.webview} 269 <Webview
216 onDidAttach={() => { 270 className={classes.webview}
217 const { setTodosWebview } = this.props; 271 onDidAttach={() => {
218 setTodosWebview(this.webview); 272 const { setTodosWebview } = this.props;
219 this.startListeningToIpcMessages(); 273 setTodosWebview(this.webview);
220 }} 274 this.startListeningToIpcMessages();
221 partition="persist:todos" 275 }}
222 preload="./features/todos/preload.js" 276 partition="persist:todos"
223 ref={(webview) => { this.webview = webview ? webview.view : null; }} 277 preload="./features/todos/preload.js"
224 src={environment.TODOS_FRONTEND} 278 ref={(webview) => { this.webview = webview ? webview.view : null; }}
225 /> 279 src={environment.TODOS_FRONTEND}
280 />
281 ) : (
282 <Appear>
283 <div className={classes.premiumContainer}>
284 <Icon icon={mdiCheckAll} className={classes.premiumIcon} size={5} />
285 <p>{intl.formatMessage(messages.premiumInfo)}</p>
286 <p>{intl.formatMessage(messages.rolloutInfo)}</p>
287 <ActivateTrialButton
288 className={classes.premiumCTA}
289 gaEventInfo={{ category: 'Todos', event: 'upgrade' }}
290 short
291 />
292 </div>
293 </Appear>
294 )}
226 </div> 295 </div>
227 ); 296 );
228 } 297 }
diff --git a/src/features/todos/containers/TodosScreen.js b/src/features/todos/containers/TodosScreen.js
index d071d0677..65afc985b 100644
--- a/src/features/todos/containers/TodosScreen.js
+++ b/src/features/todos/containers/TodosScreen.js
@@ -1,12 +1,14 @@
1import React, { Component } from 'react'; 1import React, { Component } from 'react';
2import { observer } from 'mobx-react'; 2import { observer, inject } from 'mobx-react';
3import PropTypes from 'prop-types';
3 4
5import FeaturesStore from '../../../stores/FeaturesStore';
4import TodosWebview from '../components/TodosWebview'; 6import TodosWebview from '../components/TodosWebview';
5import ErrorBoundary from '../../../components/util/ErrorBoundary'; 7import ErrorBoundary from '../../../components/util/ErrorBoundary';
6import { TODOS_MIN_WIDTH, todosStore } from '..'; 8import { TODOS_MIN_WIDTH, todosStore } from '..';
7import { todoActions } from '../actions'; 9import { todoActions } from '../actions';
8 10
9@observer 11@inject('stores', 'actions') @observer
10class TodosScreen extends Component { 12class TodosScreen extends Component {
11 render() { 13 render() {
12 if (!todosStore || !todosStore.isFeatureActive) { 14 if (!todosStore || !todosStore.isFeatureActive) {
@@ -23,6 +25,7 @@ class TodosScreen extends Component {
23 width={todosStore.width} 25 width={todosStore.width}
24 minWidth={TODOS_MIN_WIDTH} 26 minWidth={TODOS_MIN_WIDTH}
25 resize={width => todoActions.resize({ width })} 27 resize={width => todoActions.resize({ width })}
28 isTodosIncludedInCurrentPlan={this.props.stores.features.features.isTodosIncludedInCurrentPlan || false}
26 /> 29 />
27 </ErrorBoundary> 30 </ErrorBoundary>
28 ); 31 );
@@ -30,3 +33,9 @@ class TodosScreen extends Component {
30} 33}
31 34
32export default TodosScreen; 35export default TodosScreen;
36
37TodosScreen.wrappedComponent.propTypes = {
38 stores: PropTypes.shape({
39 features: PropTypes.instanceOf(FeaturesStore).isRequired,
40 }).isRequired,
41};
diff --git a/src/features/todos/store.js b/src/features/todos/store.js
index 242b38bf7..170408ebb 100644
--- a/src/features/todos/store.js
+++ b/src/features/todos/store.js
@@ -12,6 +12,7 @@ import { createReactions } from '../../stores/lib/Reaction';
12import { createActionBindings } from '../utils/ActionBinding'; 12import { createActionBindings } from '../utils/ActionBinding';
13import { DEFAULT_TODOS_WIDTH, TODOS_MIN_WIDTH, DEFAULT_TODOS_VISIBLE } from '.'; 13import { DEFAULT_TODOS_WIDTH, TODOS_MIN_WIDTH, DEFAULT_TODOS_VISIBLE } from '.';
14import { IPC } from './constants'; 14import { IPC } from './constants';
15import { state as delayAppState } from '../delayApp';
15 16
16const debug = require('debug')('Franz:feature:todos:store'); 17const debug = require('debug')('Franz:feature:todos:store');
17 18
@@ -29,6 +30,7 @@ export default class TodoStore extends FeatureStore {
29 } 30 }
30 31
31 @computed get isTodosPanelVisible() { 32 @computed get isTodosPanelVisible() {
33 if (this.stores.services.all.length === 0 || delayAppState.isDelayAppScreenVisible) return false;
32 if (this.settings.isTodosPanelVisible === undefined) return DEFAULT_TODOS_VISIBLE; 34 if (this.settings.isTodosPanelVisible === undefined) return DEFAULT_TODOS_VISIBLE;
33 35
34 return this.settings.isTodosPanelVisible; 36 return this.settings.isTodosPanelVisible;
diff --git a/src/features/workspaces/components/WorkspaceDrawer.js b/src/features/workspaces/components/WorkspaceDrawer.js
index 684e50dd0..e7bc0b157 100644
--- a/src/features/workspaces/components/WorkspaceDrawer.js
+++ b/src/features/workspaces/components/WorkspaceDrawer.js
@@ -7,6 +7,7 @@ import { H1, Icon, ProBadge } from '@meetfranz/ui';
7import { Button } from '@meetfranz/forms/lib'; 7import { Button } from '@meetfranz/forms/lib';
8import ReactTooltip from 'react-tooltip'; 8import ReactTooltip from 'react-tooltip';
9 9
10import { mdiPlusBox, mdiSettings } from '@mdi/js';
10import WorkspaceDrawerItem from './WorkspaceDrawerItem'; 11import WorkspaceDrawerItem from './WorkspaceDrawerItem';
11import { workspaceActions } from '../actions'; 12import { workspaceActions } from '../actions';
12import { GA_CATEGORY_WORKSPACES, workspaceStore } from '../index'; 13import { GA_CATEGORY_WORKSPACES, workspaceStore } from '../index';
@@ -159,7 +160,7 @@ class WorkspaceDrawer extends Component {
159 data-tip={`${intl.formatMessage(messages.workspacesSettingsTooltip)}`} 160 data-tip={`${intl.formatMessage(messages.workspacesSettingsTooltip)}`}
160 > 161 >
161 <Icon 162 <Icon
162 icon="mdiSettings" 163 icon={mdiSettings}
163 size={1.5} 164 size={1.5}
164 className={classes.workspacesSettingsButtonIcon} 165 className={classes.workspacesSettingsButtonIcon}
165 /> 166 />
@@ -184,7 +185,7 @@ class WorkspaceDrawer extends Component {
184 className={classes.premiumCtaButton} 185 className={classes.premiumCtaButton}
185 buttonType="primary" 186 buttonType="primary"
186 label={intl.formatMessage(messages.premiumCtaButtonLabel)} 187 label={intl.formatMessage(messages.premiumCtaButtonLabel)}
187 icon="mdiPlusBox" 188 icon={mdiPlusBox}
188 onClick={() => { 189 onClick={() => {
189 workspaceActions.openWorkspaceSettings(); 190 workspaceActions.openWorkspaceSettings();
190 gaEvent(GA_CATEGORY_WORKSPACES, 'add', 'drawerPremiumCta'); 191 gaEvent(GA_CATEGORY_WORKSPACES, 'add', 'drawerPremiumCta');
@@ -227,7 +228,7 @@ class WorkspaceDrawer extends Component {
227 }} 228 }}
228 > 229 >
229 <Icon 230 <Icon
230 icon="mdiPlusBox" 231 icon={mdiPlusBox}
231 size={1} 232 size={1}
232 className={classes.workspacesSettingsButtonIcon} 233 className={classes.workspacesSettingsButtonIcon}
233 /> 234 />
diff --git a/src/features/workspaces/components/WorkspaceSwitchingIndicator.js b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
index c4a800a7b..a70d1d66f 100644
--- a/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
+++ b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
@@ -21,11 +21,8 @@ const styles = theme => ({
21 alignItems: 'flex-start', 21 alignItems: 'flex-start',
22 position: 'absolute', 22 position: 'absolute',
23 transition: 'width 0.5s ease', 23 transition: 'width 0.5s ease',
24 width: '100%',
25 marginTop: '20px',
26 },
27 wrapperWhenDrawerIsOpen: {
28 width: `calc(100% - ${theme.workspaces.drawer.width}px)`, 24 width: `calc(100% - ${theme.workspaces.drawer.width}px)`,
25 marginTop: '20px',
29 }, 26 },
30 component: { 27 component: {
31 background: 'rgba(20, 20, 20, 0.4)', 28 background: 'rgba(20, 20, 20, 0.4)',
@@ -64,14 +61,13 @@ class WorkspaceSwitchingIndicator extends Component {
64 render() { 61 render() {
65 const { classes, theme } = this.props; 62 const { classes, theme } = this.props;
66 const { intl } = this.context; 63 const { intl } = this.context;
67 const { isSwitchingWorkspace, isWorkspaceDrawerOpen, nextWorkspace } = workspaceStore; 64 const { isSwitchingWorkspace, nextWorkspace } = workspaceStore;
68 if (!isSwitchingWorkspace) return null; 65 if (!isSwitchingWorkspace) return null;
69 const nextWorkspaceName = nextWorkspace ? nextWorkspace.name : 'All services'; 66 const nextWorkspaceName = nextWorkspace ? nextWorkspace.name : 'All services';
70 return ( 67 return (
71 <div 68 <div
72 className={classnames([ 69 className={classnames([
73 classes.wrapper, 70 classes.wrapper,
74 isWorkspaceDrawerOpen ? classes.wrapperWhenDrawerIsOpen : null,
75 ])} 71 ])}
76 > 72 >
77 <div className={classes.component}> 73 <div className={classes.component}>
diff --git a/src/features/workspaces/components/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.js
index 09c98ab8c..059a681de 100644
--- a/src/features/workspaces/components/WorkspacesDashboard.js
+++ b/src/features/workspaces/components/WorkspacesDashboard.js
@@ -1,6 +1,6 @@
1import React, { Component, Fragment } from 'react'; 1import React, { Component, Fragment } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } 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 } from '@meetfranz/ui'; 6import { Infobox } from '@meetfranz/ui';
@@ -12,6 +12,8 @@ import Request from '../../../stores/lib/Request';
12import Appear from '../../../components/ui/effects/Appear'; 12import Appear from '../../../components/ui/effects/Appear';
13import { workspaceStore } from '../index'; 13import { workspaceStore } from '../index';
14import PremiumFeatureContainer from '../../../components/ui/PremiumFeatureContainer'; 14import PremiumFeatureContainer from '../../../components/ui/PremiumFeatureContainer';
15import UIStore from '../../../stores/UIStore';
16import ActivateTrialButton from '../../../components/ui/ActivateTrialButton';
15 17
16const messages = defineMessages({ 18const messages = defineMessages({
17 headline: { 19 headline: {
@@ -62,17 +64,27 @@ const styles = theme => ({
62 height: 'auto', 64 height: 'auto',
63 }, 65 },
64 premiumAnnouncement: { 66 premiumAnnouncement: {
65 padding: '20px', 67 padding: 20,
66 backgroundColor: '#3498db', 68 // backgroundColor: '#3498db',
67 marginLeft: '-20px', 69 marginLeft: -20,
68 marginBottom: '20px', 70 marginBottom: 40,
71 paddingBottom: 40,
69 height: 'auto', 72 height: 'auto',
70 color: 'white', 73 display: 'flex',
71 borderRadius: theme.borderRadius, 74 borderBottom: [1, 'solid', theme.inputBackground],
75 },
76 teaserImage: {
77 width: 200,
78 height: '100%',
79 float: 'left',
80 margin: [-8, 0, 0, -20],
81 },
82 upgradeCTA: {
83 marginTop: 20,
72 }, 84 },
73}); 85});
74 86
75@injectSheet(styles) @observer 87@inject('stores') @injectSheet(styles) @observer
76class WorkspacesDashboard extends Component { 88class WorkspacesDashboard extends Component {
77 static propTypes = { 89 static propTypes = {
78 classes: PropTypes.object.isRequired, 90 classes: PropTypes.object.isRequired,
@@ -100,7 +112,9 @@ class WorkspacesDashboard extends Component {
100 onWorkspaceClick, 112 onWorkspaceClick,
101 workspaces, 113 workspaces,
102 } = this.props; 114 } = this.props;
115
103 const { intl } = this.context; 116 const { intl } = this.context;
117
104 return ( 118 return (
105 <div className="settings__main"> 119 <div className="settings__main">
106 <div className="settings__header"> 120 <div className="settings__header">
@@ -138,13 +152,21 @@ class WorkspacesDashboard extends Component {
138 152
139 {workspaceStore.isPremiumUpgradeRequired && ( 153 {workspaceStore.isPremiumUpgradeRequired && (
140 <div className={classes.premiumAnnouncement}> 154 <div className={classes.premiumAnnouncement}>
141 <h2>{intl.formatMessage(messages.workspaceFeatureHeadline)}</h2> 155 <img src={`./assets/images/workspaces/teaser_${this.props.stores.ui.isDarkThemeActive ? 'dark' : 'light'}.png`} className={classes.teaserImage} alt="" />
142 <p>{intl.formatMessage(messages.workspaceFeatureInfo)}</p> 156 <div>
157 <h2>{intl.formatMessage(messages.workspaceFeatureHeadline)}</h2>
158 <p>{intl.formatMessage(messages.workspaceFeatureInfo)}</p>
159 <ActivateTrialButton
160 className={classes.upgradeCTA}
161 gaEventInfo={{ category: 'Workspaces', event: 'upgrade' }}
162 short
163 />
164 </div>
143 </div> 165 </div>
144 )} 166 )}
145 167
146 <PremiumFeatureContainer 168 <PremiumFeatureContainer
147 condition={workspaceStore.isPremiumFeature} 169 condition={() => workspaceStore.isPremiumUpgradeRequired}
148 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'workspaces' }} 170 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'workspaces' }}
149 > 171 >
150 {/* ===== Create workspace form ===== */} 172 {/* ===== Create workspace form ===== */}
@@ -207,3 +229,9 @@ class WorkspacesDashboard extends Component {
207} 229}
208 230
209export default WorkspacesDashboard; 231export default WorkspacesDashboard;
232
233WorkspacesDashboard.wrappedComponent.propTypes = {
234 stores: PropTypes.shape({
235 ui: PropTypes.instanceOf(UIStore).isRequired,
236 }).isRequired,
237};
diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js
index a82f6895c..4a1f80b4e 100644
--- a/src/features/workspaces/store.js
+++ b/src/features/workspaces/store.js
@@ -253,11 +253,10 @@ export default class WorkspacesStore extends FeatureStore {
253 }; 253 };
254 254
255 _setIsPremiumFeatureReaction = () => { 255 _setIsPremiumFeatureReaction = () => {
256 const { features, user } = this.stores; 256 const { features } = this.stores;
257 const { isPremium } = user.data; 257 const { isWorkspaceIncludedInCurrentPlan } = features.features;
258 const { isWorkspacePremiumFeature } = features.features; 258 this.isPremiumFeature = !isWorkspaceIncludedInCurrentPlan;
259 this.isPremiumFeature = isWorkspacePremiumFeature; 259 this.isPremiumUpgradeRequired = !isWorkspaceIncludedInCurrentPlan;
260 this.isPremiumUpgradeRequired = isWorkspacePremiumFeature && !isPremium;
261 }; 260 };
262 261
263 _setWorkspaceBeingEditedReaction = () => { 262 _setWorkspaceBeingEditedReaction = () => {