diff options
Diffstat (limited to 'src/features')
21 files changed, 776 insertions, 34 deletions
diff --git a/src/features/announcements/components/AnnouncementScreen.js b/src/features/announcements/components/AnnouncementScreen.js index e7c5fe395..03bd5ba41 100644 --- a/src/features/announcements/components/AnnouncementScreen.js +++ b/src/features/announcements/components/AnnouncementScreen.js | |||
@@ -28,7 +28,7 @@ const smallScreen = '1000px'; | |||
28 | const styles = theme => ({ | 28 | const styles = theme => ({ |
29 | container: { | 29 | container: { |
30 | background: theme.colorBackground, | 30 | background: theme.colorBackground, |
31 | position: 'absolute', | 31 | position: 'relative', |
32 | top: 0, | 32 | top: 0, |
33 | zIndex: 140, | 33 | zIndex: 140, |
34 | width: '100%', | 34 | width: '100%', |
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 @@ | |||
1 | import { reaction } from 'mobx'; | ||
2 | import { CommunityRecipesStore } from './store'; | ||
3 | |||
4 | const debug = require('debug')('Franz:feature:communityRecipes'); | ||
5 | |||
6 | export const DEFAULT_SERVICE_LIMIT = 3; | ||
7 | |||
8 | export const communityRecipesStore = new CommunityRecipesStore(); | ||
9 | |||
10 | export 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 @@ | |||
1 | import { computed, observable } from 'mobx'; | ||
2 | import { FeatureStore } from '../utils/FeatureStore'; | ||
3 | |||
4 | const debug = require('debug')('Franz:feature:communityRecipes:store'); | ||
5 | |||
6 | export 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 | |||
31 | export 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'; | |||
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | import injectSheet from 'react-jss'; | 5 | import injectSheet from 'react-jss'; |
6 | 6 | ||
7 | import { Button } from '@meetfranz/forms'; | ||
7 | import { gaEvent } from '../../lib/analytics'; | 8 | import { gaEvent } from '../../lib/analytics'; |
8 | 9 | ||
9 | import Button from '../../components/ui/Button'; | 10 | // import Button from '../../components/ui/Button'; |
10 | 11 | ||
11 | import { config } from '.'; | 12 | import { config } from '.'; |
12 | import styles from './styles'; | 13 | import styles from './styles'; |
14 | import UserStore from '../../stores/UserStore'; | ||
13 | 15 | ||
14 | const messages = defineMessages({ | 16 | const 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 | ||
29 | export default @inject('actions') @injectSheet(styles) @observer class DelayApp extends Component { | 39 | export 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 | ||
95 | DelayApp.wrappedComponent.propTypes = { | 117 | DelayApp.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 67f0fc5e6..627537de7 100644 --- a/src/features/delayApp/index.js +++ b/src/features/delayApp/index.js | |||
@@ -33,7 +33,7 @@ export default function init(stores) { | |||
33 | }; | 33 | }; |
34 | 34 | ||
35 | reaction( | 35 | reaction( |
36 | () => stores.user.isLoggedIn && stores.features.features.needToWaitToProceed && !stores.user.data.isPremium, | 36 | () => stores.user.isLoggedIn && stores.services.allServicesRequest.wasExecuted && stores.features.features.needToWaitToProceed && !stores.user.data.isPremium, |
37 | (isEnabled) => { | 37 | (isEnabled) => { |
38 | if (isEnabled) { | 38 | if (isEnabled) { |
39 | debug('Enabling `delayApp` feature'); | 39 | debug('Enabling `delayApp` feature'); |
@@ -44,7 +44,8 @@ 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 | shownAfterLaunch = true; | 49 | shownAfterLaunch = true; |
49 | return; | 50 | return; |
50 | } | 51 | } |
@@ -64,7 +65,7 @@ export default function init(stores) { | |||
64 | debug('Resetting app delay'); | 65 | debug('Resetting app delay'); |
65 | 66 | ||
66 | setVisibility(false); | 67 | setVisibility(false); |
67 | }, DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.wait + 1000); // timer needs to be able to hit 0 | 68 | }, config.delayDuration + 1000); // timer needs to be able to hit 0 |
68 | } | 69 | } |
69 | }); | 70 | }); |
70 | } else { | 71 | } else { |
diff --git a/src/features/delayApp/styles.js b/src/features/delayApp/styles.js index 5c214cfdf..69c3c7a27 100644 --- a/src/features/delayApp/styles.js +++ b/src/features/delayApp/styles.js | |||
@@ -1,7 +1,6 @@ | |||
1 | export default theme => ({ | 1 | export default theme => ({ |
2 | container: { | 2 | container: { |
3 | background: theme.colorBackground, | 3 | background: theme.colorBackground, |
4 | position: 'absolute', | ||
5 | top: 0, | 4 | top: 0, |
6 | width: '100%', | 5 | width: '100%', |
7 | display: 'flex', | 6 | display: 'flex', |
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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | import injectSheet from 'react-jss'; | ||
6 | import { Infobox } from '@meetfranz/ui'; | ||
7 | |||
8 | import { gaEvent } from '../../../lib/analytics'; | ||
9 | |||
10 | const 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 | |||
21 | const 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 | ||
41 | class 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 | |||
78 | export 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 @@ | |||
1 | import { reaction } from 'mobx'; | ||
2 | import { ServiceLimitStore } from './store'; | ||
3 | |||
4 | const debug = require('debug')('Franz:feature:serviceLimit'); | ||
5 | |||
6 | export const DEFAULT_SERVICE_LIMIT = 3; | ||
7 | |||
8 | let store = null; | ||
9 | |||
10 | export const serviceLimitStore = new ServiceLimitStore(); | ||
11 | |||
12 | export 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 @@ | |||
1 | import { computed, observable } from 'mobx'; | ||
2 | import { FeatureStore } from '../utils/FeatureStore'; | ||
3 | import { DEFAULT_SERVICE_LIMIT } from '.'; | ||
4 | |||
5 | const debug = require('debug')('Franz:feature:serviceLimit:store'); | ||
6 | |||
7 | export 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 | |||
41 | export 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 | ||
10 | export const config = observable({ | 10 | export 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 | ||
15 | export default function init(stores) { | 15 | export 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..859c0ebe9 100644 --- a/src/features/shareFranz/Component.js +++ b/src/features/shareFranz/Component.js | |||
@@ -6,6 +6,7 @@ import { defineMessages, intlShape } from 'react-intl'; | |||
6 | import { Button } from '@meetfranz/forms'; | 6 | import { Button } from '@meetfranz/forms'; |
7 | import { H1, Icon } from '@meetfranz/ui'; | 7 | import { H1, Icon } from '@meetfranz/ui'; |
8 | 8 | ||
9 | import { mdiHeart } from '@mdi/js'; | ||
9 | import Modal from '../../components/ui/Modal'; | 10 | import Modal from '../../components/ui/Modal'; |
10 | import { state } from '.'; | 11 | import { state } from '.'; |
11 | import { gaEvent } from '../../lib/analytics'; | 12 | import { gaEvent } from '../../lib/analytics'; |
@@ -116,7 +117,7 @@ export default @injectSheet(styles) @inject('stores') @observer class ShareFranz | |||
116 | close={this.close.bind(this)} | 117 | close={this.close.bind(this)} |
117 | > | 118 | > |
118 | <div className={classes.heartContainer}> | 119 | <div className={classes.heartContainer}> |
119 | <Icon icon="mdiHeart" className={classes.heart} size={4} /> | 120 | <Icon icon={mdiHeart} className={classes.heart} size={4} /> |
120 | </div> | 121 | </div> |
121 | <H1 className={classes.headline}> | 122 | <H1 className={classes.headline}> |
122 | {intl.formatMessage(messages.headline)} | 123 | {intl.formatMessage(messages.headline)} |
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'; | |||
5 | const debug = require('debug')('Franz:feature:spellchecker'); | 5 | const debug = require('debug')('Franz:feature:spellchecker'); |
6 | 6 | ||
7 | export const config = observable({ | 7 | export const config = observable({ |
8 | isPremium: DEFAULT_FEATURES_CONFIG.isSpellcheckerPremiumFeature, | 8 | isIncludedInCurrentPlan: DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan, |
9 | }); | 9 | }); |
10 | 10 | ||
11 | export default function init(stores) { | 11 | export 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/actions.js b/src/features/todos/actions.js new file mode 100644 index 000000000..dc63d5fcd --- /dev/null +++ b/src/features/todos/actions.js | |||
@@ -0,0 +1,22 @@ | |||
1 | import PropTypes from 'prop-types'; | ||
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions'; | ||
3 | |||
4 | export const todoActions = createActionsFromDefinitions({ | ||
5 | resize: { | ||
6 | width: PropTypes.number.isRequired, | ||
7 | }, | ||
8 | toggleTodosPanel: {}, | ||
9 | setTodosWebview: { | ||
10 | webview: PropTypes.instanceOf(Element).isRequired, | ||
11 | }, | ||
12 | handleHostMessage: { | ||
13 | action: PropTypes.string.isRequired, | ||
14 | data: PropTypes.object, | ||
15 | }, | ||
16 | handleClientMessage: { | ||
17 | action: PropTypes.string.isRequired, | ||
18 | data: PropTypes.object, | ||
19 | }, | ||
20 | }, PropTypes.checkPropTypes); | ||
21 | |||
22 | export default todoActions; | ||
diff --git a/src/features/todos/components/TodosWebview.js b/src/features/todos/components/TodosWebview.js new file mode 100644 index 000000000..288c1906f --- /dev/null +++ b/src/features/todos/components/TodosWebview.js | |||
@@ -0,0 +1,237 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import injectSheet from 'react-jss'; | ||
5 | import Webview from 'react-electron-web-view'; | ||
6 | import { Icon } from '@meetfranz/ui'; | ||
7 | |||
8 | import * as environment from '../../../environment'; | ||
9 | |||
10 | const OPEN_TODOS_BUTTON_SIZE = 45; | ||
11 | const CLOSE_TODOS_BUTTON_SIZE = 35; | ||
12 | |||
13 | const styles = theme => ({ | ||
14 | root: { | ||
15 | background: theme.colorBackground, | ||
16 | position: 'relative', | ||
17 | borderLeft: [1, 'solid', theme.todos.todosLayer.borderLeftColor], | ||
18 | zIndex: 300, | ||
19 | |||
20 | transform: ({ isVisible, width }) => `translateX(${isVisible ? 0 : width}px)`, | ||
21 | |||
22 | '&:hover $closeTodosButton': { | ||
23 | opacity: 1, | ||
24 | }, | ||
25 | }, | ||
26 | webview: { | ||
27 | height: '100%', | ||
28 | |||
29 | '& webview': { | ||
30 | height: '100%', | ||
31 | }, | ||
32 | }, | ||
33 | resizeHandler: { | ||
34 | position: 'absolute', | ||
35 | left: 0, | ||
36 | marginLeft: -5, | ||
37 | width: 10, | ||
38 | zIndex: 400, | ||
39 | cursor: 'col-resize', | ||
40 | }, | ||
41 | dragIndicator: { | ||
42 | position: 'absolute', | ||
43 | left: 0, | ||
44 | width: 5, | ||
45 | zIndex: 400, | ||
46 | background: theme.todos.dragIndicator.background, | ||
47 | |||
48 | }, | ||
49 | openTodosButton: { | ||
50 | width: OPEN_TODOS_BUTTON_SIZE, | ||
51 | height: OPEN_TODOS_BUTTON_SIZE, | ||
52 | background: theme.todos.toggleButton.background, | ||
53 | position: 'absolute', | ||
54 | bottom: 80, | ||
55 | right: props => (props.width + (props.isVisible ? -OPEN_TODOS_BUTTON_SIZE / 2 : 0)), | ||
56 | borderRadius: OPEN_TODOS_BUTTON_SIZE / 2, | ||
57 | opacity: props => (props.isVisible ? 0 : 1), | ||
58 | transition: 'right 0.5s', | ||
59 | zIndex: 600, | ||
60 | display: 'flex', | ||
61 | alignItems: 'center', | ||
62 | justifyContent: 'center', | ||
63 | boxShadow: [0, 0, 10, theme.todos.toggleButton.shadowColor], | ||
64 | |||
65 | borderTopRightRadius: props => (props.isVisible ? null : 0), | ||
66 | borderBottomRightRadius: props => (props.isVisible ? null : 0), | ||
67 | |||
68 | '& svg': { | ||
69 | fill: theme.todos.toggleButton.textColor, | ||
70 | transition: 'all 0.5s', | ||
71 | }, | ||
72 | }, | ||
73 | closeTodosButton: { | ||
74 | width: CLOSE_TODOS_BUTTON_SIZE, | ||
75 | height: CLOSE_TODOS_BUTTON_SIZE, | ||
76 | background: theme.todos.toggleButton.background, | ||
77 | position: 'absolute', | ||
78 | bottom: 80, | ||
79 | right: ({ width }) => (width + -CLOSE_TODOS_BUTTON_SIZE / 2), | ||
80 | borderRadius: CLOSE_TODOS_BUTTON_SIZE / 2, | ||
81 | opacity: 0, | ||
82 | transition: 'opacity 0.5s', | ||
83 | zIndex: 600, | ||
84 | display: 'flex', | ||
85 | alignItems: 'center', | ||
86 | justifyContent: 'center', | ||
87 | boxShadow: [0, 0, 10, theme.todos.toggleButton.shadowColor], | ||
88 | |||
89 | '& svg': { | ||
90 | fill: theme.todos.toggleButton.textColor, | ||
91 | }, | ||
92 | }, | ||
93 | }); | ||
94 | |||
95 | @injectSheet(styles) @observer | ||
96 | class TodosWebview extends Component { | ||
97 | static propTypes = { | ||
98 | classes: PropTypes.object.isRequired, | ||
99 | isVisible: PropTypes.bool.isRequired, | ||
100 | togglePanel: PropTypes.func.isRequired, | ||
101 | handleClientMessage: PropTypes.func.isRequired, | ||
102 | setTodosWebview: PropTypes.func.isRequired, | ||
103 | resize: PropTypes.func.isRequired, | ||
104 | width: PropTypes.number.isRequired, | ||
105 | minWidth: PropTypes.number.isRequired, | ||
106 | }; | ||
107 | |||
108 | state = { | ||
109 | isDragging: false, | ||
110 | width: 300, | ||
111 | }; | ||
112 | |||
113 | componentWillMount() { | ||
114 | const { width } = this.props; | ||
115 | |||
116 | this.setState({ | ||
117 | width, | ||
118 | }); | ||
119 | } | ||
120 | |||
121 | componentDidMount() { | ||
122 | this.node.addEventListener('mousemove', this.resizePanel.bind(this)); | ||
123 | this.node.addEventListener('mouseup', this.stopResize.bind(this)); | ||
124 | this.node.addEventListener('mouseleave', this.stopResize.bind(this)); | ||
125 | } | ||
126 | |||
127 | startResize = (event) => { | ||
128 | this.setState({ | ||
129 | isDragging: true, | ||
130 | initialPos: event.clientX, | ||
131 | delta: 0, | ||
132 | }); | ||
133 | }; | ||
134 | |||
135 | resizePanel(e) { | ||
136 | const { minWidth } = this.props; | ||
137 | |||
138 | const { | ||
139 | isDragging, | ||
140 | initialPos, | ||
141 | } = this.state; | ||
142 | |||
143 | if (isDragging && Math.abs(e.clientX - window.innerWidth) > minWidth) { | ||
144 | const delta = e.clientX - initialPos; | ||
145 | |||
146 | this.setState({ | ||
147 | delta, | ||
148 | }); | ||
149 | } | ||
150 | } | ||
151 | |||
152 | stopResize() { | ||
153 | const { | ||
154 | resize, | ||
155 | minWidth, | ||
156 | } = this.props; | ||
157 | |||
158 | const { | ||
159 | isDragging, | ||
160 | delta, | ||
161 | width, | ||
162 | } = this.state; | ||
163 | |||
164 | if (isDragging) { | ||
165 | let newWidth = width + (delta < 0 ? Math.abs(delta) : -Math.abs(delta)); | ||
166 | |||
167 | if (newWidth < minWidth) { | ||
168 | newWidth = minWidth; | ||
169 | } | ||
170 | |||
171 | this.setState({ | ||
172 | isDragging: false, | ||
173 | delta: 0, | ||
174 | width: newWidth, | ||
175 | }); | ||
176 | |||
177 | resize(newWidth); | ||
178 | } | ||
179 | } | ||
180 | |||
181 | startListeningToIpcMessages() { | ||
182 | const { handleClientMessage } = this.props; | ||
183 | if (!this.webview) return; | ||
184 | this.webview.addEventListener('ipc-message', e => handleClientMessage(e.args[0])); | ||
185 | } | ||
186 | |||
187 | render() { | ||
188 | const { | ||
189 | classes, isVisible, togglePanel, | ||
190 | } = this.props; | ||
191 | const { width, delta, isDragging } = this.state; | ||
192 | |||
193 | return ( | ||
194 | <> | ||
195 | <div | ||
196 | className={classes.root} | ||
197 | style={{ width: isVisible ? width : 0 }} | ||
198 | onMouseUp={() => this.stopResize()} | ||
199 | ref={(node) => { this.node = node; }} | ||
200 | > | ||
201 | <button | ||
202 | onClick={() => togglePanel()} | ||
203 | className={isVisible ? classes.closeTodosButton : classes.openTodosButton} | ||
204 | type="button" | ||
205 | > | ||
206 | <Icon icon={isVisible ? 'mdiChevronRight' : 'mdiCheckAll'} size={2} /> | ||
207 | </button> | ||
208 | <div | ||
209 | className={classes.resizeHandler} | ||
210 | style={Object.assign({ left: delta }, isDragging ? { width: 600, marginLeft: -200 } : {})} // This hack is required as resizing with webviews beneath behaves quite bad | ||
211 | onMouseDown={e => this.startResize(e)} | ||
212 | /> | ||
213 | {isDragging && ( | ||
214 | <div | ||
215 | className={classes.dragIndicator} | ||
216 | style={{ left: delta }} // This hack is required as resizing with webviews beneath behaves quite bad | ||
217 | /> | ||
218 | )} | ||
219 | <Webview | ||
220 | className={classes.webview} | ||
221 | onDidAttach={() => { | ||
222 | const { setTodosWebview } = this.props; | ||
223 | setTodosWebview(this.webview); | ||
224 | this.startListeningToIpcMessages(); | ||
225 | }} | ||
226 | partition="persist:todos" | ||
227 | preload="./features/todos/preload.js" | ||
228 | ref={(webview) => { this.webview = webview ? webview.view : null; }} | ||
229 | src={environment.TODOS_FRONTEND} | ||
230 | /> | ||
231 | </div> | ||
232 | </> | ||
233 | ); | ||
234 | } | ||
235 | } | ||
236 | |||
237 | export default TodosWebview; | ||
diff --git a/src/features/todos/constants.js b/src/features/todos/constants.js new file mode 100644 index 000000000..2e8a431cc --- /dev/null +++ b/src/features/todos/constants.js | |||
@@ -0,0 +1,4 @@ | |||
1 | export const IPC = { | ||
2 | TODOS_HOST_CHANNEL: 'TODOS_HOST_CHANNEL', | ||
3 | TODOS_CLIENT_CHANNEL: 'TODOS_CLIENT_CHANNEL', | ||
4 | }; | ||
diff --git a/src/features/todos/containers/TodosScreen.js b/src/features/todos/containers/TodosScreen.js new file mode 100644 index 000000000..d071d0677 --- /dev/null +++ b/src/features/todos/containers/TodosScreen.js | |||
@@ -0,0 +1,32 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import { observer } from 'mobx-react'; | ||
3 | |||
4 | import TodosWebview from '../components/TodosWebview'; | ||
5 | import ErrorBoundary from '../../../components/util/ErrorBoundary'; | ||
6 | import { TODOS_MIN_WIDTH, todosStore } from '..'; | ||
7 | import { todoActions } from '../actions'; | ||
8 | |||
9 | @observer | ||
10 | class TodosScreen extends Component { | ||
11 | render() { | ||
12 | if (!todosStore || !todosStore.isFeatureActive) { | ||
13 | return null; | ||
14 | } | ||
15 | |||
16 | return ( | ||
17 | <ErrorBoundary> | ||
18 | <TodosWebview | ||
19 | isVisible={todosStore.isTodosPanelVisible} | ||
20 | togglePanel={todoActions.toggleTodosPanel} | ||
21 | handleClientMessage={todoActions.handleClientMessage} | ||
22 | setTodosWebview={webview => todoActions.setTodosWebview({ webview })} | ||
23 | width={todosStore.width} | ||
24 | minWidth={TODOS_MIN_WIDTH} | ||
25 | resize={width => todoActions.resize({ width })} | ||
26 | /> | ||
27 | </ErrorBoundary> | ||
28 | ); | ||
29 | } | ||
30 | } | ||
31 | |||
32 | export default TodosScreen; | ||
diff --git a/src/features/todos/index.js b/src/features/todos/index.js new file mode 100644 index 000000000..00b165cc5 --- /dev/null +++ b/src/features/todos/index.js | |||
@@ -0,0 +1,34 @@ | |||
1 | import { reaction } from 'mobx'; | ||
2 | import TodoStore from './store'; | ||
3 | |||
4 | const debug = require('debug')('Franz:feature:todos'); | ||
5 | |||
6 | export const GA_CATEGORY_TODOS = 'Todos'; | ||
7 | |||
8 | export const DEFAULT_TODOS_WIDTH = 300; | ||
9 | export const TODOS_MIN_WIDTH = 200; | ||
10 | export const DEFAULT_TODOS_VISIBLE = true; | ||
11 | |||
12 | export const todosStore = new TodoStore(); | ||
13 | |||
14 | export default function initTodos(stores, actions) { | ||
15 | stores.todos = todosStore; | ||
16 | const { features } = stores; | ||
17 | |||
18 | // Toggle todos feature | ||
19 | reaction( | ||
20 | () => features.features.isTodosEnabled, | ||
21 | (isEnabled) => { | ||
22 | if (isEnabled) { | ||
23 | debug('Initializing `todos` feature'); | ||
24 | todosStore.start(stores, actions); | ||
25 | } else if (todosStore.isFeatureActive) { | ||
26 | debug('Disabling `todos` feature'); | ||
27 | todosStore.stop(); | ||
28 | } | ||
29 | }, | ||
30 | { | ||
31 | fireImmediately: true, | ||
32 | }, | ||
33 | ); | ||
34 | } | ||
diff --git a/src/features/todos/preload.js b/src/features/todos/preload.js new file mode 100644 index 000000000..6e38a2ef3 --- /dev/null +++ b/src/features/todos/preload.js | |||
@@ -0,0 +1,23 @@ | |||
1 | import { ipcRenderer } from 'electron'; | ||
2 | import { IPC } from './constants'; | ||
3 | |||
4 | const debug = require('debug')('Franz:feature:todos:preload'); | ||
5 | |||
6 | debug('Preloading Todos Webview'); | ||
7 | |||
8 | let hostMessageListener = () => {}; | ||
9 | |||
10 | window.franz = { | ||
11 | onInitialize(ipcHostMessageListener) { | ||
12 | hostMessageListener = ipcHostMessageListener; | ||
13 | ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, { action: 'todos:initialized' }); | ||
14 | }, | ||
15 | sendToHost(message) { | ||
16 | ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, message); | ||
17 | }, | ||
18 | }; | ||
19 | |||
20 | ipcRenderer.on(IPC.TODOS_HOST_CHANNEL, (event, message) => { | ||
21 | debug('Received host message', event, message); | ||
22 | hostMessageListener(message); | ||
23 | }); | ||
diff --git a/src/features/todos/store.js b/src/features/todos/store.js new file mode 100644 index 000000000..acf95df0d --- /dev/null +++ b/src/features/todos/store.js | |||
@@ -0,0 +1,147 @@ | |||
1 | import { ThemeType } from '@meetfranz/theme'; | ||
2 | import { | ||
3 | computed, | ||
4 | action, | ||
5 | observable, | ||
6 | } from 'mobx'; | ||
7 | import localStorage from 'mobx-localstorage'; | ||
8 | |||
9 | import { todoActions } from './actions'; | ||
10 | import { FeatureStore } from '../utils/FeatureStore'; | ||
11 | import { createReactions } from '../../stores/lib/Reaction'; | ||
12 | import { createActionBindings } from '../utils/ActionBinding'; | ||
13 | import { DEFAULT_TODOS_WIDTH, TODOS_MIN_WIDTH, DEFAULT_TODOS_VISIBLE } from '.'; | ||
14 | import { IPC } from './constants'; | ||
15 | |||
16 | const debug = require('debug')('Franz:feature:todos:store'); | ||
17 | |||
18 | export default class TodoStore extends FeatureStore { | ||
19 | @observable isFeatureEnabled = false; | ||
20 | |||
21 | @observable isFeatureActive = false; | ||
22 | |||
23 | webview = null; | ||
24 | |||
25 | @computed get width() { | ||
26 | const width = this.settings.width || DEFAULT_TODOS_WIDTH; | ||
27 | |||
28 | return width < TODOS_MIN_WIDTH ? TODOS_MIN_WIDTH : width; | ||
29 | } | ||
30 | |||
31 | @computed get isTodosPanelVisible() { | ||
32 | if (this.settings.isTodosPanelVisible === undefined) return DEFAULT_TODOS_VISIBLE; | ||
33 | |||
34 | return this.settings.isTodosPanelVisible; | ||
35 | } | ||
36 | |||
37 | @computed get settings() { | ||
38 | return localStorage.getItem('todos') || {}; | ||
39 | } | ||
40 | |||
41 | // ========== PUBLIC API ========= // | ||
42 | |||
43 | @action start(stores, actions) { | ||
44 | debug('TodoStore::start'); | ||
45 | this.stores = stores; | ||
46 | this.actions = actions; | ||
47 | |||
48 | // ACTIONS | ||
49 | |||
50 | this._registerActions(createActionBindings([ | ||
51 | [todoActions.resize, this._resize], | ||
52 | [todoActions.toggleTodosPanel, this._toggleTodosPanel], | ||
53 | [todoActions.setTodosWebview, this._setTodosWebview], | ||
54 | [todoActions.handleHostMessage, this._handleHostMessage], | ||
55 | [todoActions.handleClientMessage, this._handleClientMessage], | ||
56 | ])); | ||
57 | |||
58 | // REACTIONS | ||
59 | |||
60 | this._allReactions = createReactions([ | ||
61 | this._setFeatureEnabledReaction, | ||
62 | ]); | ||
63 | |||
64 | this._registerReactions(this._allReactions); | ||
65 | |||
66 | this.isFeatureActive = true; | ||
67 | } | ||
68 | |||
69 | @action stop() { | ||
70 | super.stop(); | ||
71 | debug('TodoStore::stop'); | ||
72 | this.reset(); | ||
73 | this.isFeatureActive = false; | ||
74 | } | ||
75 | |||
76 | // ========== PRIVATE METHODS ========= // | ||
77 | |||
78 | _updateSettings = (changes) => { | ||
79 | localStorage.setItem('todos', { | ||
80 | ...this.settings, | ||
81 | ...changes, | ||
82 | }); | ||
83 | }; | ||
84 | |||
85 | // Actions | ||
86 | |||
87 | @action _resize = ({ width }) => { | ||
88 | this._updateSettings({ | ||
89 | width, | ||
90 | }); | ||
91 | }; | ||
92 | |||
93 | @action _toggleTodosPanel = () => { | ||
94 | this._updateSettings({ | ||
95 | isTodosPanelVisible: !this.isTodosPanelVisible, | ||
96 | }); | ||
97 | }; | ||
98 | |||
99 | @action _setTodosWebview = ({ webview }) => { | ||
100 | debug('_setTodosWebview', webview); | ||
101 | this.webview = webview; | ||
102 | }; | ||
103 | |||
104 | @action _handleHostMessage = (message) => { | ||
105 | debug('_handleHostMessage', message); | ||
106 | if (message.action === 'todos:create') { | ||
107 | this.webview.send(IPC.TODOS_HOST_CHANNEL, message); | ||
108 | } | ||
109 | }; | ||
110 | |||
111 | @action _handleClientMessage = (message) => { | ||
112 | debug('_handleClientMessage', message); | ||
113 | switch (message.action) { | ||
114 | case 'todos:initialized': this._onTodosClientInitialized(); break; | ||
115 | case 'todos:goToService': this._goToService(message.data); break; | ||
116 | default: | ||
117 | debug('Unknown client message reiceived', message); | ||
118 | } | ||
119 | }; | ||
120 | |||
121 | // Todos client message handlers | ||
122 | |||
123 | _onTodosClientInitialized = () => { | ||
124 | this.webview.send(IPC.TODOS_HOST_CHANNEL, { | ||
125 | action: 'todos:configure', | ||
126 | data: { | ||
127 | authToken: this.stores.user.authToken, | ||
128 | theme: this.stores.ui.isDarkThemeActive ? ThemeType.dark : ThemeType.default, | ||
129 | }, | ||
130 | }); | ||
131 | }; | ||
132 | |||
133 | _goToService = ({ url, serviceId }) => { | ||
134 | if (url) { | ||
135 | this.stores.services.one(serviceId).webview.loadURL(url); | ||
136 | } | ||
137 | this.actions.service.setActive({ serviceId }); | ||
138 | }; | ||
139 | |||
140 | // Reactions | ||
141 | |||
142 | _setFeatureEnabledReaction = () => { | ||
143 | const { isTodosEnabled } = this.stores.features.features; | ||
144 | |||
145 | this.isFeatureEnabled = isTodosEnabled; | ||
146 | }; | ||
147 | } | ||
diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js index 07b16ff23..e44569be9 100644 --- a/src/features/workspaces/store.js +++ b/src/features/workspaces/store.js | |||
@@ -79,7 +79,7 @@ export default class WorkspacesStore extends FeatureStore { | |||
79 | 79 | ||
80 | // ========== PUBLIC API ========= // | 80 | // ========== PUBLIC API ========= // |
81 | 81 | ||
82 | start(stores, actions) { | 82 | @action start(stores, actions) { |
83 | debug('WorkspacesStore::start'); | 83 | debug('WorkspacesStore::start'); |
84 | this.stores = stores; | 84 | this.stores = stores; |
85 | this.actions = actions; | 85 | this.actions = actions; |
@@ -104,7 +104,7 @@ export default class WorkspacesStore extends FeatureStore { | |||
104 | // REACTIONS | 104 | // REACTIONS |
105 | 105 | ||
106 | this._freeUserReactions = createReactions([ | 106 | this._freeUserReactions = createReactions([ |
107 | this._stopPremiumActionsAndReactions, | 107 | this._disablePremiumFeatures, |
108 | this._openDrawerWithSettingsReaction, | 108 | this._openDrawerWithSettingsReaction, |
109 | this._setFeatureEnabledReaction, | 109 | this._setFeatureEnabledReaction, |
110 | this._setIsPremiumFeatureReaction, | 110 | this._setIsPremiumFeatureReaction, |
@@ -123,10 +123,7 @@ export default class WorkspacesStore extends FeatureStore { | |||
123 | this.isFeatureActive = true; | 123 | this.isFeatureActive = true; |
124 | } | 124 | } |
125 | 125 | ||
126 | stop() { | 126 | @action reset() { |
127 | super.stop(); | ||
128 | debug('WorkspacesStore::stop'); | ||
129 | this.isFeatureActive = false; | ||
130 | this.activeWorkspace = null; | 127 | this.activeWorkspace = null; |
131 | this.nextWorkspace = null; | 128 | this.nextWorkspace = null; |
132 | this.workspaceBeingEdited = null; | 129 | this.workspaceBeingEdited = null; |
@@ -134,6 +131,13 @@ export default class WorkspacesStore extends FeatureStore { | |||
134 | this.isWorkspaceDrawerOpen = false; | 131 | this.isWorkspaceDrawerOpen = false; |
135 | } | 132 | } |
136 | 133 | ||
134 | @action stop() { | ||
135 | super.stop(); | ||
136 | debug('WorkspacesStore::stop'); | ||
137 | this.reset(); | ||
138 | this.isFeatureActive = false; | ||
139 | } | ||
140 | |||
137 | filterServicesByActiveWorkspace = (services) => { | 141 | filterServicesByActiveWorkspace = (services) => { |
138 | const { activeWorkspace, isFeatureActive } = this; | 142 | const { activeWorkspace, isFeatureActive } = this; |
139 | if (isFeatureActive && activeWorkspace) { | 143 | if (isFeatureActive && activeWorkspace) { |
@@ -251,9 +255,9 @@ export default class WorkspacesStore extends FeatureStore { | |||
251 | _setIsPremiumFeatureReaction = () => { | 255 | _setIsPremiumFeatureReaction = () => { |
252 | const { features, user } = this.stores; | 256 | const { features, user } = this.stores; |
253 | const { isPremium } = user.data; | 257 | const { isPremium } = user.data; |
254 | const { isWorkspacePremiumFeature } = features.features; | 258 | const { isWorkspaceIncludedInCurrentPlan } = features.features; |
255 | this.isPremiumFeature = isWorkspacePremiumFeature; | 259 | this.isPremiumFeature = !isWorkspaceIncludedInCurrentPlan; |
256 | this.isPremiumUpgradeRequired = isWorkspacePremiumFeature && !isPremium; | 260 | this.isPremiumUpgradeRequired = !isWorkspaceIncludedInCurrentPlan && !isPremium; |
257 | }; | 261 | }; |
258 | 262 | ||
259 | _setWorkspaceBeingEditedReaction = () => { | 263 | _setWorkspaceBeingEditedReaction = () => { |
@@ -281,6 +285,7 @@ export default class WorkspacesStore extends FeatureStore { | |||
281 | }; | 285 | }; |
282 | 286 | ||
283 | _activateLastUsedWorkspaceReaction = () => { | 287 | _activateLastUsedWorkspaceReaction = () => { |
288 | debug('_activateLastUsedWorkspaceReaction'); | ||
284 | if (!this.activeWorkspace && this.userHasWorkspaces) { | 289 | if (!this.activeWorkspace && this.userHasWorkspaces) { |
285 | const { lastActiveWorkspace } = this.settings; | 290 | const { lastActiveWorkspace } = this.settings; |
286 | if (lastActiveWorkspace) { | 291 | if (lastActiveWorkspace) { |
@@ -324,10 +329,12 @@ export default class WorkspacesStore extends FeatureStore { | |||
324 | }); | 329 | }); |
325 | }; | 330 | }; |
326 | 331 | ||
327 | _stopPremiumActionsAndReactions = () => { | 332 | _disablePremiumFeatures = () => { |
328 | if (!this.isUserAllowedToUseFeature) { | 333 | if (!this.isUserAllowedToUseFeature) { |
334 | debug('_disablePremiumFeatures'); | ||
329 | this._stopActions(this._premiumUserActions); | 335 | this._stopActions(this._premiumUserActions); |
330 | this._stopReactions(this._premiumUserReactions); | 336 | this._stopReactions(this._premiumUserReactions); |
337 | this.reset(); | ||
331 | } else { | 338 | } else { |
332 | this._startActions(this._premiumUserActions); | 339 | this._startActions(this._premiumUserActions); |
333 | this._startReactions(this._premiumUserReactions); | 340 | this._startReactions(this._premiumUserReactions); |