diff options
Diffstat (limited to 'src/features')
24 files changed, 1028 insertions, 112 deletions
diff --git a/src/features/announcements/store.js b/src/features/announcements/store.js index 2884fb06f..91348029f 100644 --- a/src/features/announcements/store.js +++ b/src/features/announcements/store.js | |||
@@ -62,6 +62,11 @@ export class AnnouncementsStore extends FeatureStore { | |||
62 | return this.stores.settings.stats.appStarts <= 1; | 62 | return this.stores.settings.stats.appStarts <= 1; |
63 | } | 63 | } |
64 | 64 | ||
65 | @computed get isAnnouncementShown() { | ||
66 | const { router } = this.stores; | ||
67 | return router.location.pathname.includes('/announcements'); | ||
68 | } | ||
69 | |||
65 | async start(stores, actions) { | 70 | async start(stores, actions) { |
66 | debug('AnnouncementsStore::start'); | 71 | debug('AnnouncementsStore::start'); |
67 | this.stores = stores; | 72 | this.stores = stores; |
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 ba50652e8..c61cb06c9 100644 --- a/src/features/delayApp/Component.js +++ b/src/features/delayApp/Component.js | |||
@@ -4,19 +4,28 @@ 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 '../../components/ui/Button'; | 7 | import { Button } from '@meetfranz/forms'; |
8 | 8 | ||
9 | import { config } from '.'; | 9 | import { config } from '.'; |
10 | import styles from './styles'; | 10 | import styles from './styles'; |
11 | import UserStore from '../../stores/UserStore'; | ||
11 | 12 | ||
12 | const messages = defineMessages({ | 13 | const messages = defineMessages({ |
13 | headline: { | 14 | headline: { |
14 | id: 'feature.delayApp.headline', | 15 | id: 'feature.delayApp.headline', |
15 | defaultMessage: '!!!Please purchase license to skip waiting', | 16 | defaultMessage: '!!!Please purchase license to skip waiting', |
16 | }, | 17 | }, |
18 | headlineTrial: { | ||
19 | id: 'feature.delayApp.trial.headline', | ||
20 | defaultMessage: '!!!Get the free Franz Professional 14 day trial and skip the line', | ||
21 | }, | ||
17 | action: { | 22 | action: { |
18 | id: 'feature.delayApp.action', | 23 | id: 'feature.delayApp.upgrade.action', |
19 | defaultMessage: '!!!Get a Ferdi Supporter License', | 24 | defaultMessage: '!!!Get a Franz Supporter License', |
25 | }, | ||
26 | actionTrial: { | ||
27 | id: 'feature.delayApp.trial.action', | ||
28 | defaultMessage: '!!!Yes, I want the free 14 day trial of Franz Professional', | ||
20 | }, | 29 | }, |
21 | text: { | 30 | text: { |
22 | id: 'feature.delayApp.text', | 31 | id: 'feature.delayApp.text', |
@@ -24,7 +33,7 @@ const messages = defineMessages({ | |||
24 | }, | 33 | }, |
25 | }); | 34 | }); |
26 | 35 | ||
27 | export default @inject('actions') @injectSheet(styles) @observer class DelayApp extends Component { | 36 | export default @inject('stores', 'actions') @injectSheet(styles) @observer class DelayApp extends Component { |
28 | static propTypes = { | 37 | static propTypes = { |
29 | // eslint-disable-next-line | 38 | // eslint-disable-next-line |
30 | classes: PropTypes.object.isRequired, | 39 | classes: PropTypes.object.isRequired, |
@@ -60,23 +69,32 @@ export default @inject('actions') @injectSheet(styles) @observer class DelayApp | |||
60 | } | 69 | } |
61 | 70 | ||
62 | handleCTAClick() { | 71 | handleCTAClick() { |
63 | const { actions } = this.props; | 72 | const { actions, stores } = this.props; |
64 | 73 | const { hadSubscription } = stores.user.data; | |
65 | actions.ui.openSettings({ path: 'user' }); | 74 | const { defaultTrialPlan } = stores.features.features; |
75 | |||
76 | if (!hadSubscription) { | ||
77 | actions.user.activateTrial({ planId: defaultTrialPlan }); | ||
78 | } else { | ||
79 | actions.ui.openSettings({ path: 'user' }); | ||
80 | } | ||
66 | } | 81 | } |
67 | 82 | ||
68 | render() { | 83 | render() { |
69 | const { classes } = this.props; | 84 | const { classes, stores } = this.props; |
70 | const { intl } = this.context; | 85 | const { intl } = this.context; |
71 | 86 | ||
87 | const { hadSubscription } = stores.user.data; | ||
88 | |||
72 | return ( | 89 | return ( |
73 | <div className={`${classes.container}`}> | 90 | <div className={`${classes.container}`}> |
74 | <h1 className={classes.headline}>{intl.formatMessage(messages.headline)}</h1> | 91 | <h1 className={classes.headline}>{intl.formatMessage(hadSubscription ? messages.headline : messages.headlineTrial)}</h1> |
75 | <Button | 92 | <Button |
76 | label={intl.formatMessage(messages.action)} | 93 | label={intl.formatMessage(hadSubscription ? messages.action : messages.actionTrial)} |
77 | className={classes.button} | 94 | className={classes.button} |
78 | buttonType="inverted" | 95 | buttonType="inverted" |
79 | onClick={this.handleCTAClick.bind(this)} | 96 | onClick={this.handleCTAClick.bind(this)} |
97 | busy={stores.user.activateTrialRequest.isExecuting} | ||
80 | /> | 98 | /> |
81 | <p className="footnote"> | 99 | <p className="footnote"> |
82 | {intl.formatMessage(messages.text, { | 100 | {intl.formatMessage(messages.text, { |
@@ -89,6 +107,9 @@ export default @inject('actions') @injectSheet(styles) @observer class DelayApp | |||
89 | } | 107 | } |
90 | 108 | ||
91 | DelayApp.wrappedComponent.propTypes = { | 109 | DelayApp.wrappedComponent.propTypes = { |
110 | stores: PropTypes.shape({ | ||
111 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
112 | }).isRequired, | ||
92 | actions: PropTypes.shape({ | 113 | actions: PropTypes.shape({ |
93 | ui: PropTypes.shape({ | 114 | ui: PropTypes.shape({ |
94 | openSettings: PropTypes.func.isRequired, | 115 | openSettings: PropTypes.func.isRequired, |
diff --git a/src/features/delayApp/index.js b/src/features/delayApp/index.js index c753eeffe..4f793f16c 100644 --- a/src/features/delayApp/index.js +++ b/src/features/delayApp/index.js | |||
@@ -43,14 +43,16 @@ export default function init(stores) { | |||
43 | config.delayDuration = globalConfig.wait !== undefined ? globalConfig.wait : DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.wait; | 43 | config.delayDuration = globalConfig.wait !== undefined ? globalConfig.wait : DEFAULT_FEATURES_CONFIG.needToWaitToProceedConfig.wait; |
44 | 44 | ||
45 | autorun(() => { | 45 | autorun(() => { |
46 | if (stores.services.all.length === 0) { | 46 | const { isAnnouncementShown } = stores.announcements; |
47 | debug('seas', stores.services.all.length); | 47 | if (stores.services.allDisplayed.length === 0 || isAnnouncementShown) { |
48 | shownAfterLaunch = true; | 48 | shownAfterLaunch = true; |
49 | setVisibility(false); | ||
49 | return; | 50 | return; |
50 | } | 51 | } |
51 | 52 | ||
52 | const diff = moment().diff(timeLastDelay); | 53 | const diff = moment().diff(timeLastDelay); |
53 | if ((stores.app.isFocused && diff >= config.delayOffset) || !shownAfterLaunch) { | 54 | const itsTimeToWait = diff >= config.delayOffset; |
55 | if (!isAnnouncementShown && ((stores.app.isFocused && itsTimeToWait) || !shownAfterLaunch)) { | ||
54 | debug(`App will be delayed for ${config.delayDuration / 1000}s`); | 56 | debug(`App will be delayed for ${config.delayDuration / 1000}s`); |
55 | 57 | ||
56 | setVisibility(true); | 58 | setVisibility(true); |
@@ -63,6 +65,8 @@ export default function init(stores) { | |||
63 | 65 | ||
64 | setVisibility(false); | 66 | setVisibility(false); |
65 | }, config.delayDuration + 1000); // timer needs to be able to hit 0 | 67 | }, config.delayDuration + 1000); // timer needs to be able to hit 0 |
68 | } else { | ||
69 | setVisibility(false); | ||
66 | } | 70 | } |
67 | }); | 71 | }); |
68 | } else { | 72 | } else { |
diff --git a/src/features/serviceLimit/components/LimitReachedInfobox.js b/src/features/serviceLimit/components/LimitReachedInfobox.js new file mode 100644 index 000000000..19285a4eb --- /dev/null +++ b/src/features/serviceLimit/components/LimitReachedInfobox.js | |||
@@ -0,0 +1,79 @@ | |||
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.warning.accent, | ||
25 | color: theme.styleTypes.warning.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 | type="warning" | ||
66 | className={classes.container} | ||
67 | ctaLabel={intl.formatMessage(messages.action)} | ||
68 | ctaOnClick={() => { | ||
69 | actions.ui.openSettings({ path: 'user' }); | ||
70 | gaEvent('Service Limit', 'upgrade', 'Upgrade account'); | ||
71 | }} | ||
72 | > | ||
73 | {intl.formatMessage(messages.limitReached, { amount: serviceLimit.serviceCount, limit: serviceLimit.serviceLimit })} | ||
74 | </Infobox> | ||
75 | ); | ||
76 | } | ||
77 | } | ||
78 | |||
79 | 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 2e66acaf3..405fb0ab5 100644 --- a/src/features/shareFranz/Component.js +++ b/src/features/shareFranz/Component.js | |||
@@ -6,6 +6,9 @@ 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 { | ||
10 | mdiHeart, mdiEmail, mdiFacebookBox, mdiTwitter, | ||
11 | } from '@mdi/js'; | ||
9 | import Modal from '../../components/ui/Modal'; | 12 | import Modal from '../../components/ui/Modal'; |
10 | import { state } from '.'; | 13 | import { state } from '.'; |
11 | import ServicesStore from '../../stores/ServicesStore'; | 14 | import ServicesStore from '../../stores/ServicesStore'; |
@@ -74,7 +77,7 @@ const styles = theme => ({ | |||
74 | }, | 77 | }, |
75 | cta: { | 78 | cta: { |
76 | background: theme.styleTypes.primary.contrast, | 79 | background: theme.styleTypes.primary.contrast, |
77 | color: theme.styleTypes.primary.accent, | 80 | color: `${theme.styleTypes.primary.accent} !important`, |
78 | 81 | ||
79 | '& svg': { | 82 | '& svg': { |
80 | fill: theme.styleTypes.primary.accent, | 83 | fill: theme.styleTypes.primary.accent, |
@@ -115,7 +118,7 @@ export default @injectSheet(styles) @inject('stores') @observer class ShareFranz | |||
115 | close={this.close.bind(this)} | 118 | close={this.close.bind(this)} |
116 | > | 119 | > |
117 | <div className={classes.heartContainer}> | 120 | <div className={classes.heartContainer}> |
118 | <Icon icon="mdiHeart" className={classes.heart} size={4} /> | 121 | <Icon icon={mdiHeart} className={classes.heart} size={4} /> |
119 | </div> | 122 | </div> |
120 | <H1 className={classes.headline}> | 123 | <H1 className={classes.headline}> |
121 | {intl.formatMessage(messages.headline)} | 124 | {intl.formatMessage(messages.headline)} |
@@ -125,21 +128,21 @@ export default @injectSheet(styles) @inject('stores') @observer class ShareFranz | |||
125 | <Button | 128 | <Button |
126 | label={intl.formatMessage(messages.actionsEmail)} | 129 | label={intl.formatMessage(messages.actionsEmail)} |
127 | className={classes.cta} | 130 | className={classes.cta} |
128 | icon="mdiEmail" | 131 | icon={mdiEmail} |
129 | href={`mailto:?subject=Meet the cool app Franz&body=${intl.formatMessage(messages.shareTextEmail, { count: serviceCount })}}`} | 132 | href={`mailto:?subject=Meet the cool app Franz&body=${intl.formatMessage(messages.shareTextEmail, { count: serviceCount })}}`} |
130 | target="_blank" | 133 | target="_blank" |
131 | /> | 134 | /> |
132 | <Button | 135 | <Button |
133 | label={intl.formatMessage(messages.actionsFacebook)} | 136 | label={intl.formatMessage(messages.actionsFacebook)} |
134 | className={classes.cta} | 137 | className={classes.cta} |
135 | icon="mdiFacebookBox" | 138 | icon={mdiFacebookBox} |
136 | href="https://www.facebook.com/sharer/sharer.php?u=https://www.meetfranz.com?utm_source=facebook&utm_medium=referral&utm_campaign=share-button" | 139 | href="https://www.facebook.com/sharer/sharer.php?u=https://www.meetfranz.com?utm_source=facebook&utm_medium=referral&utm_campaign=share-button" |
137 | target="_blank" | 140 | target="_blank" |
138 | /> | 141 | /> |
139 | <Button | 142 | <Button |
140 | label={intl.formatMessage(messages.actionsTwitter)} | 143 | label={intl.formatMessage(messages.actionsTwitter)} |
141 | className={classes.cta} | 144 | className={classes.cta} |
142 | icon="mdiTwitter" | 145 | icon={mdiTwitter} |
143 | href={`http://twitter.com/intent/tweet?status=${intl.formatMessage(messages.shareTextTwitter, { count: serviceCount })}`} | 146 | href={`http://twitter.com/intent/tweet?status=${intl.formatMessage(messages.shareTextTwitter, { count: serviceCount })}`} |
144 | target="_blank" | 147 | target="_blank" |
145 | /> | 148 | /> |
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..1ccc9a592 --- /dev/null +++ b/src/features/todos/actions.js | |||
@@ -0,0 +1,23 @@ | |||
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 | toggleTodosFeatureVisibility: {}, | ||
10 | setTodosWebview: { | ||
11 | webview: PropTypes.instanceOf(Element).isRequired, | ||
12 | }, | ||
13 | handleHostMessage: { | ||
14 | action: PropTypes.string.isRequired, | ||
15 | data: PropTypes.object, | ||
16 | }, | ||
17 | handleClientMessage: { | ||
18 | action: PropTypes.string.isRequired, | ||
19 | data: PropTypes.object, | ||
20 | }, | ||
21 | }, PropTypes.checkPropTypes); | ||
22 | |||
23 | 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..c06183e37 --- /dev/null +++ b/src/features/todos/components/TodosWebview.js | |||
@@ -0,0 +1,300 @@ | |||
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 | import { defineMessages, intlShape } from 'react-intl'; | ||
8 | |||
9 | import { mdiChevronRight, mdiCheckAll } from '@mdi/js'; | ||
10 | import * as environment from '../../../environment'; | ||
11 | import Appear from '../../../components/ui/effects/Appear'; | ||
12 | import UpgradeButton from '../../../components/ui/UpgradeButton'; | ||
13 | |||
14 | const OPEN_TODOS_BUTTON_SIZE = 45; | ||
15 | const CLOSE_TODOS_BUTTON_SIZE = 35; | ||
16 | |||
17 | const messages = defineMessages({ | ||
18 | premiumInfo: { | ||
19 | id: 'feature.todos.premium.info', | ||
20 | defaultMessage: '!!!Franz Todos are available to premium users now!', | ||
21 | }, | ||
22 | upgradeCTA: { | ||
23 | id: 'feature.todos.premium.upgrade', | ||
24 | defaultMessage: '!!!Upgrade Account', | ||
25 | }, | ||
26 | rolloutInfo: { | ||
27 | id: 'feature.todos.premium.rollout', | ||
28 | defaultMessage: '!!!Everyone else will have to wait a little longer.', | ||
29 | }, | ||
30 | }); | ||
31 | |||
32 | const styles = theme => ({ | ||
33 | root: { | ||
34 | background: theme.colorBackground, | ||
35 | position: 'relative', | ||
36 | borderLeft: [1, 'solid', theme.todos.todosLayer.borderLeftColor], | ||
37 | zIndex: 300, | ||
38 | |||
39 | transform: ({ isVisible, width }) => `translateX(${isVisible ? 0 : width}px)`, | ||
40 | |||
41 | '&:hover $closeTodosButton': { | ||
42 | opacity: 1, | ||
43 | }, | ||
44 | '& webview': { | ||
45 | height: '100%', | ||
46 | }, | ||
47 | }, | ||
48 | resizeHandler: { | ||
49 | position: 'absolute', | ||
50 | left: 0, | ||
51 | marginLeft: -5, | ||
52 | width: 10, | ||
53 | zIndex: 400, | ||
54 | cursor: 'col-resize', | ||
55 | }, | ||
56 | dragIndicator: { | ||
57 | position: 'absolute', | ||
58 | left: 0, | ||
59 | width: 5, | ||
60 | zIndex: 400, | ||
61 | background: theme.todos.dragIndicator.background, | ||
62 | |||
63 | }, | ||
64 | openTodosButton: { | ||
65 | width: OPEN_TODOS_BUTTON_SIZE, | ||
66 | height: OPEN_TODOS_BUTTON_SIZE, | ||
67 | background: theme.todos.toggleButton.background, | ||
68 | position: 'absolute', | ||
69 | bottom: 120, | ||
70 | right: props => (props.width + (props.isVisible ? -OPEN_TODOS_BUTTON_SIZE / 2 : 0)), | ||
71 | borderRadius: OPEN_TODOS_BUTTON_SIZE / 2, | ||
72 | opacity: props => (props.isVisible ? 0 : 1), | ||
73 | transition: 'right 0.5s', | ||
74 | zIndex: 600, | ||
75 | display: 'flex', | ||
76 | alignItems: 'center', | ||
77 | justifyContent: 'center', | ||
78 | boxShadow: [0, 0, 10, theme.todos.toggleButton.shadowColor], | ||
79 | |||
80 | borderTopRightRadius: props => (props.isVisible ? null : 0), | ||
81 | borderBottomRightRadius: props => (props.isVisible ? null : 0), | ||
82 | |||
83 | '& svg': { | ||
84 | fill: theme.todos.toggleButton.textColor, | ||
85 | transition: 'all 0.5s', | ||
86 | }, | ||
87 | }, | ||
88 | closeTodosButton: { | ||
89 | width: CLOSE_TODOS_BUTTON_SIZE, | ||
90 | height: CLOSE_TODOS_BUTTON_SIZE, | ||
91 | background: theme.todos.toggleButton.background, | ||
92 | position: 'absolute', | ||
93 | bottom: 120, | ||
94 | right: ({ width }) => (width + -CLOSE_TODOS_BUTTON_SIZE / 2), | ||
95 | borderRadius: CLOSE_TODOS_BUTTON_SIZE / 2, | ||
96 | opacity: ({ isTodosIncludedInCurrentPlan }) => (!isTodosIncludedInCurrentPlan ? 1 : 0), | ||
97 | transition: 'opacity 0.5s', | ||
98 | zIndex: 600, | ||
99 | display: 'flex', | ||
100 | alignItems: 'center', | ||
101 | justifyContent: 'center', | ||
102 | boxShadow: [0, 0, 10, theme.todos.toggleButton.shadowColor], | ||
103 | |||
104 | '& svg': { | ||
105 | fill: theme.todos.toggleButton.textColor, | ||
106 | }, | ||
107 | }, | ||
108 | premiumContainer: { | ||
109 | display: 'flex', | ||
110 | flexDirection: 'column', | ||
111 | justifyContent: 'center', | ||
112 | alignItems: 'center', | ||
113 | width: '80%', | ||
114 | maxWidth: 300, | ||
115 | margin: [0, 'auto'], | ||
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 | }, | ||
128 | }); | ||
129 | |||
130 | @injectSheet(styles) @observer | ||
131 | class TodosWebview extends Component { | ||
132 | static propTypes = { | ||
133 | classes: PropTypes.object.isRequired, | ||
134 | isVisible: PropTypes.bool.isRequired, | ||
135 | togglePanel: PropTypes.func.isRequired, | ||
136 | handleClientMessage: PropTypes.func.isRequired, | ||
137 | setTodosWebview: PropTypes.func.isRequired, | ||
138 | resize: PropTypes.func.isRequired, | ||
139 | width: PropTypes.number.isRequired, | ||
140 | minWidth: PropTypes.number.isRequired, | ||
141 | isTodosIncludedInCurrentPlan: PropTypes.bool.isRequired, | ||
142 | }; | ||
143 | |||
144 | state = { | ||
145 | isDragging: false, | ||
146 | width: 300, | ||
147 | }; | ||
148 | |||
149 | static contextTypes = { | ||
150 | intl: intlShape, | ||
151 | }; | ||
152 | |||
153 | componentWillMount() { | ||
154 | const { width } = this.props; | ||
155 | |||
156 | this.setState({ | ||
157 | width, | ||
158 | }); | ||
159 | } | ||
160 | |||
161 | componentDidMount() { | ||
162 | this.node.addEventListener('mousemove', this.resizePanel.bind(this)); | ||
163 | this.node.addEventListener('mouseup', this.stopResize.bind(this)); | ||
164 | this.node.addEventListener('mouseleave', this.stopResize.bind(this)); | ||
165 | } | ||
166 | |||
167 | startResize = (event) => { | ||
168 | this.setState({ | ||
169 | isDragging: true, | ||
170 | initialPos: event.clientX, | ||
171 | delta: 0, | ||
172 | }); | ||
173 | }; | ||
174 | |||
175 | resizePanel(e) { | ||
176 | const { minWidth } = this.props; | ||
177 | |||
178 | const { | ||
179 | isDragging, | ||
180 | initialPos, | ||
181 | } = this.state; | ||
182 | |||
183 | if (isDragging && Math.abs(e.clientX - window.innerWidth) > minWidth) { | ||
184 | const delta = e.clientX - initialPos; | ||
185 | |||
186 | this.setState({ | ||
187 | delta, | ||
188 | }); | ||
189 | } | ||
190 | } | ||
191 | |||
192 | stopResize() { | ||
193 | const { | ||
194 | resize, | ||
195 | minWidth, | ||
196 | } = this.props; | ||
197 | |||
198 | const { | ||
199 | isDragging, | ||
200 | delta, | ||
201 | width, | ||
202 | } = this.state; | ||
203 | |||
204 | if (isDragging) { | ||
205 | let newWidth = width + (delta < 0 ? Math.abs(delta) : -Math.abs(delta)); | ||
206 | |||
207 | if (newWidth < minWidth) { | ||
208 | newWidth = minWidth; | ||
209 | } | ||
210 | |||
211 | this.setState({ | ||
212 | isDragging: false, | ||
213 | delta: 0, | ||
214 | width: newWidth, | ||
215 | }); | ||
216 | |||
217 | resize(newWidth); | ||
218 | } | ||
219 | } | ||
220 | |||
221 | startListeningToIpcMessages() { | ||
222 | const { handleClientMessage } = this.props; | ||
223 | if (!this.webview) return; | ||
224 | this.webview.addEventListener('ipc-message', e => handleClientMessage(e.args[0])); | ||
225 | } | ||
226 | |||
227 | render() { | ||
228 | const { | ||
229 | classes, | ||
230 | isVisible, | ||
231 | togglePanel, | ||
232 | isTodosIncludedInCurrentPlan, | ||
233 | } = this.props; | ||
234 | |||
235 | const { | ||
236 | width, | ||
237 | delta, | ||
238 | isDragging, | ||
239 | } = this.state; | ||
240 | |||
241 | const { intl } = this.context; | ||
242 | |||
243 | return ( | ||
244 | <div | ||
245 | className={classes.root} | ||
246 | style={{ width: isVisible ? width : 0 }} | ||
247 | onMouseUp={() => this.stopResize()} | ||
248 | ref={(node) => { this.node = node; }} | ||
249 | > | ||
250 | <button | ||
251 | onClick={() => togglePanel()} | ||
252 | className={isVisible ? classes.closeTodosButton : classes.openTodosButton} | ||
253 | type="button" | ||
254 | > | ||
255 | <Icon icon={isVisible ? mdiChevronRight : mdiCheckAll} size={2} /> | ||
256 | </button> | ||
257 | <div | ||
258 | className={classes.resizeHandler} | ||
259 | style={Object.assign({ left: delta }, isDragging ? { width: 600, marginLeft: -200 } : {})} // This hack is required as resizing with webviews beneath behaves quite bad | ||
260 | onMouseDown={e => this.startResize(e)} | ||
261 | /> | ||
262 | {isDragging && ( | ||
263 | <div | ||
264 | className={classes.dragIndicator} | ||
265 | style={{ left: delta }} // This hack is required as resizing with webviews beneath behaves quite bad | ||
266 | /> | ||
267 | )} | ||
268 | {isTodosIncludedInCurrentPlan ? ( | ||
269 | <Webview | ||
270 | className={classes.webview} | ||
271 | onDidAttach={() => { | ||
272 | const { setTodosWebview } = this.props; | ||
273 | setTodosWebview(this.webview); | ||
274 | this.startListeningToIpcMessages(); | ||
275 | }} | ||
276 | partition="persist:todos" | ||
277 | preload="./features/todos/preload.js" | ||
278 | ref={(webview) => { this.webview = webview ? webview.view : null; }} | ||
279 | src={environment.TODOS_FRONTEND} | ||
280 | /> | ||
281 | ) : ( | ||
282 | <Appear> | ||
283 | <div className={classes.premiumContainer}> | ||
284 | <Icon icon={mdiCheckAll} className={classes.premiumIcon} size={4} /> | ||
285 | <p>{intl.formatMessage(messages.premiumInfo)}</p> | ||
286 | <p>{intl.formatMessage(messages.rolloutInfo)}</p> | ||
287 | <UpgradeButton | ||
288 | className={classes.premiumCTA} | ||
289 | gaEventInfo={{ category: 'Todos', event: 'upgrade' }} | ||
290 | short | ||
291 | /> | ||
292 | </div> | ||
293 | </Appear> | ||
294 | )} | ||
295 | </div> | ||
296 | ); | ||
297 | } | ||
298 | } | ||
299 | |||
300 | 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..a5da0b014 --- /dev/null +++ b/src/features/todos/containers/TodosScreen.js | |||
@@ -0,0 +1,41 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import { observer, inject } from 'mobx-react'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | |||
5 | import FeaturesStore from '../../../stores/FeaturesStore'; | ||
6 | import TodosWebview from '../components/TodosWebview'; | ||
7 | import ErrorBoundary from '../../../components/util/ErrorBoundary'; | ||
8 | import { TODOS_MIN_WIDTH, todosStore } from '..'; | ||
9 | import { todoActions } from '../actions'; | ||
10 | |||
11 | @inject('stores', 'actions') @observer | ||
12 | class TodosScreen extends Component { | ||
13 | render() { | ||
14 | if (!todosStore || !todosStore.isFeatureActive || todosStore.isTodosPanelForceHidden) { | ||
15 | return null; | ||
16 | } | ||
17 | |||
18 | return ( | ||
19 | <ErrorBoundary> | ||
20 | <TodosWebview | ||
21 | isVisible={todosStore.isTodosPanelVisible} | ||
22 | togglePanel={todoActions.toggleTodosPanel} | ||
23 | handleClientMessage={todoActions.handleClientMessage} | ||
24 | setTodosWebview={webview => todoActions.setTodosWebview({ webview })} | ||
25 | width={todosStore.width} | ||
26 | minWidth={TODOS_MIN_WIDTH} | ||
27 | resize={width => todoActions.resize({ width })} | ||
28 | isTodosIncludedInCurrentPlan={this.props.stores.features.features.isTodosIncludedInCurrentPlan || false} | ||
29 | /> | ||
30 | </ErrorBoundary> | ||
31 | ); | ||
32 | } | ||
33 | } | ||
34 | |||
35 | export default TodosScreen; | ||
36 | |||
37 | TodosScreen.wrappedComponent.propTypes = { | ||
38 | stores: PropTypes.shape({ | ||
39 | features: PropTypes.instanceOf(FeaturesStore).isRequired, | ||
40 | }).isRequired, | ||
41 | }; | ||
diff --git a/src/features/todos/index.js b/src/features/todos/index.js new file mode 100644 index 000000000..7388aebaf --- /dev/null +++ b/src/features/todos/index.js | |||
@@ -0,0 +1,39 @@ | |||
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 | export const DEFAULT_IS_FEATURE_ENABLED_BY_USER = true; | ||
12 | |||
13 | export const TODOS_ROUTES = { | ||
14 | TARGET: '/todos', | ||
15 | }; | ||
16 | |||
17 | export const todosStore = new TodoStore(); | ||
18 | |||
19 | export default function initTodos(stores, actions) { | ||
20 | stores.todos = todosStore; | ||
21 | const { features } = stores; | ||
22 | |||
23 | // Toggle todos feature | ||
24 | reaction( | ||
25 | () => features.features.isTodosEnabled, | ||
26 | (isEnabled) => { | ||
27 | if (isEnabled) { | ||
28 | debug('Initializing `todos` feature'); | ||
29 | todosStore.start(stores, actions); | ||
30 | } else if (todosStore.isFeatureActive) { | ||
31 | debug('Disabling `todos` feature'); | ||
32 | todosStore.stop(); | ||
33 | } | ||
34 | }, | ||
35 | { | ||
36 | fireImmediately: true, | ||
37 | }, | ||
38 | ); | ||
39 | } | ||
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..05eef4ec1 --- /dev/null +++ b/src/features/todos/store.js | |||
@@ -0,0 +1,213 @@ | |||
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 { | ||
14 | DEFAULT_TODOS_WIDTH, TODOS_MIN_WIDTH, DEFAULT_TODOS_VISIBLE, TODOS_ROUTES, DEFAULT_IS_FEATURE_ENABLED_BY_USER, | ||
15 | } from '.'; | ||
16 | import { IPC } from './constants'; | ||
17 | import { state as delayAppState } from '../delayApp'; | ||
18 | |||
19 | const debug = require('debug')('Franz:feature:todos:store'); | ||
20 | |||
21 | export default class TodoStore extends FeatureStore { | ||
22 | @observable isFeatureEnabled = false; | ||
23 | |||
24 | @observable isFeatureActive = false; | ||
25 | |||
26 | webview = null; | ||
27 | |||
28 | @computed get width() { | ||
29 | const width = this.settings.width || DEFAULT_TODOS_WIDTH; | ||
30 | |||
31 | return width < TODOS_MIN_WIDTH ? TODOS_MIN_WIDTH : width; | ||
32 | } | ||
33 | |||
34 | @computed get isTodosPanelForceHidden() { | ||
35 | const { isAnnouncementShown } = this.stores.announcements; | ||
36 | return delayAppState.isDelayAppScreenVisible || !this.settings.isFeatureEnabledByUser || isAnnouncementShown; | ||
37 | } | ||
38 | |||
39 | @computed get isTodosPanelVisible() { | ||
40 | if (this.settings.isTodosPanelVisible === undefined) return DEFAULT_TODOS_VISIBLE; | ||
41 | return this.settings.isTodosPanelVisible; | ||
42 | } | ||
43 | |||
44 | @computed get settings() { | ||
45 | return localStorage.getItem('todos') || {}; | ||
46 | } | ||
47 | |||
48 | // ========== PUBLIC API ========= // | ||
49 | |||
50 | @action start(stores, actions) { | ||
51 | debug('TodoStore::start'); | ||
52 | this.stores = stores; | ||
53 | this.actions = actions; | ||
54 | |||
55 | // ACTIONS | ||
56 | |||
57 | this._registerActions(createActionBindings([ | ||
58 | [todoActions.resize, this._resize], | ||
59 | [todoActions.toggleTodosPanel, this._toggleTodosPanel], | ||
60 | [todoActions.setTodosWebview, this._setTodosWebview], | ||
61 | [todoActions.handleHostMessage, this._handleHostMessage], | ||
62 | [todoActions.handleClientMessage, this._handleClientMessage], | ||
63 | [todoActions.toggleTodosFeatureVisibility, this._toggleTodosFeatureVisibility], | ||
64 | ])); | ||
65 | |||
66 | // REACTIONS | ||
67 | |||
68 | this._allReactions = createReactions([ | ||
69 | this._setFeatureEnabledReaction, | ||
70 | this._updateTodosConfig, | ||
71 | this._firstLaunchReaction, | ||
72 | this._routeCheckReaction, | ||
73 | ]); | ||
74 | |||
75 | this._registerReactions(this._allReactions); | ||
76 | |||
77 | this.isFeatureActive = true; | ||
78 | |||
79 | if (this.settings.isFeatureEnabledByUser === undefined) { | ||
80 | this._updateSettings({ | ||
81 | isFeatureEnabledByUser: DEFAULT_IS_FEATURE_ENABLED_BY_USER, | ||
82 | }); | ||
83 | } | ||
84 | } | ||
85 | |||
86 | @action stop() { | ||
87 | super.stop(); | ||
88 | debug('TodoStore::stop'); | ||
89 | this.reset(); | ||
90 | this.isFeatureActive = false; | ||
91 | } | ||
92 | |||
93 | // ========== PRIVATE METHODS ========= // | ||
94 | |||
95 | _updateSettings = (changes) => { | ||
96 | localStorage.setItem('todos', { | ||
97 | ...this.settings, | ||
98 | ...changes, | ||
99 | }); | ||
100 | }; | ||
101 | |||
102 | // Actions | ||
103 | |||
104 | @action _resize = ({ width }) => { | ||
105 | this._updateSettings({ | ||
106 | width, | ||
107 | }); | ||
108 | }; | ||
109 | |||
110 | @action _toggleTodosPanel = () => { | ||
111 | this._updateSettings({ | ||
112 | isTodosPanelVisible: !this.isTodosPanelVisible, | ||
113 | }); | ||
114 | }; | ||
115 | |||
116 | @action _setTodosWebview = ({ webview }) => { | ||
117 | debug('_setTodosWebview', webview); | ||
118 | this.webview = webview; | ||
119 | }; | ||
120 | |||
121 | @action _handleHostMessage = (message) => { | ||
122 | debug('_handleHostMessage', message); | ||
123 | if (message.action === 'todos:create') { | ||
124 | this.webview.send(IPC.TODOS_HOST_CHANNEL, message); | ||
125 | } | ||
126 | }; | ||
127 | |||
128 | @action _handleClientMessage = (message) => { | ||
129 | debug('_handleClientMessage', message); | ||
130 | switch (message.action) { | ||
131 | case 'todos:initialized': this._onTodosClientInitialized(); break; | ||
132 | case 'todos:goToService': this._goToService(message.data); break; | ||
133 | default: | ||
134 | debug('Unknown client message reiceived', message); | ||
135 | } | ||
136 | }; | ||
137 | |||
138 | @action _toggleTodosFeatureVisibility = () => { | ||
139 | debug('_toggleTodosFeatureVisibility'); | ||
140 | |||
141 | this._updateSettings({ | ||
142 | isFeatureEnabledByUser: !this.settings.isFeatureEnabledByUser, | ||
143 | }); | ||
144 | }; | ||
145 | |||
146 | // Todos client message handlers | ||
147 | |||
148 | _onTodosClientInitialized = () => { | ||
149 | const { authToken } = this.stores.user; | ||
150 | const { isDarkThemeActive } = this.stores.ui; | ||
151 | const { locale } = this.stores.app; | ||
152 | if (!this.webview) return; | ||
153 | this.webview.send(IPC.TODOS_HOST_CHANNEL, { | ||
154 | action: 'todos:configure', | ||
155 | data: { | ||
156 | authToken, | ||
157 | locale, | ||
158 | theme: isDarkThemeActive ? ThemeType.dark : ThemeType.default, | ||
159 | }, | ||
160 | }); | ||
161 | }; | ||
162 | |||
163 | _goToService = ({ url, serviceId }) => { | ||
164 | if (url) { | ||
165 | this.stores.services.one(serviceId).webview.loadURL(url); | ||
166 | } | ||
167 | this.actions.service.setActive({ serviceId }); | ||
168 | }; | ||
169 | |||
170 | // Reactions | ||
171 | |||
172 | _setFeatureEnabledReaction = () => { | ||
173 | const { isTodosEnabled } = this.stores.features.features; | ||
174 | |||
175 | this.isFeatureEnabled = isTodosEnabled; | ||
176 | }; | ||
177 | |||
178 | _updateTodosConfig = () => { | ||
179 | // Resend the config if any part changes in Franz: | ||
180 | this._onTodosClientInitialized(); | ||
181 | }; | ||
182 | |||
183 | _firstLaunchReaction = () => { | ||
184 | const { stats } = this.stores.settings.all; | ||
185 | |||
186 | // Hide todos layer on first app start but show on second | ||
187 | if (stats.appStarts <= 1) { | ||
188 | this._updateSettings({ | ||
189 | isTodosPanelVisible: false, | ||
190 | }); | ||
191 | } else if (stats.appStarts <= 2) { | ||
192 | this._updateSettings({ | ||
193 | isTodosPanelVisible: true, | ||
194 | }); | ||
195 | } | ||
196 | }; | ||
197 | |||
198 | _routeCheckReaction = () => { | ||
199 | const { pathname } = this.stores.router.location; | ||
200 | |||
201 | if (pathname === TODOS_ROUTES.TARGET) { | ||
202 | debug('Router is on todos route, show todos panel'); | ||
203 | // todosStore.start(stores, actions); | ||
204 | this.stores.router.push('/'); | ||
205 | |||
206 | if (!this.isTodosPanelVisible) { | ||
207 | this._updateSettings({ | ||
208 | isTodosPanelVisible: true, | ||
209 | }); | ||
210 | } | ||
211 | } | ||
212 | } | ||
213 | } | ||
diff --git a/src/features/workspaces/components/WorkspaceDrawer.js b/src/features/workspaces/components/WorkspaceDrawer.js index cbc7372ca..f4ee89a14 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'; | |||
7 | import { Button } from '@meetfranz/forms/lib'; | 7 | import { Button } from '@meetfranz/forms/lib'; |
8 | import ReactTooltip from 'react-tooltip'; | 8 | import ReactTooltip from 'react-tooltip'; |
9 | 9 | ||
10 | import { mdiPlusBox, mdiSettings } from '@mdi/js'; | ||
10 | import WorkspaceDrawerItem from './WorkspaceDrawerItem'; | 11 | import WorkspaceDrawerItem from './WorkspaceDrawerItem'; |
11 | import { workspaceActions } from '../actions'; | 12 | import { workspaceActions } from '../actions'; |
12 | import { workspaceStore } from '../index'; | 13 | import { workspaceStore } from '../index'; |
@@ -157,7 +158,7 @@ class WorkspaceDrawer extends Component { | |||
157 | data-tip={`${intl.formatMessage(messages.workspacesSettingsTooltip)}`} | 158 | data-tip={`${intl.formatMessage(messages.workspacesSettingsTooltip)}`} |
158 | > | 159 | > |
159 | <Icon | 160 | <Icon |
160 | icon="mdiSettings" | 161 | icon={mdiSettings} |
161 | size={1.5} | 162 | size={1.5} |
162 | className={classes.workspacesSettingsButtonIcon} | 163 | className={classes.workspacesSettingsButtonIcon} |
163 | /> | 164 | /> |
@@ -181,7 +182,7 @@ class WorkspaceDrawer extends Component { | |||
181 | className={classes.premiumCtaButton} | 182 | className={classes.premiumCtaButton} |
182 | buttonType="primary" | 183 | buttonType="primary" |
183 | label={intl.formatMessage(messages.premiumCtaButtonLabel)} | 184 | label={intl.formatMessage(messages.premiumCtaButtonLabel)} |
184 | icon="mdiPlusBox" | 185 | icon={mdiPlusBox} |
185 | onClick={() => { | 186 | onClick={() => { |
186 | workspaceActions.openWorkspaceSettings(); | 187 | workspaceActions.openWorkspaceSettings(); |
187 | }} | 188 | }} |
@@ -220,7 +221,7 @@ class WorkspaceDrawer extends Component { | |||
220 | }} | 221 | }} |
221 | > | 222 | > |
222 | <Icon | 223 | <Icon |
223 | icon="mdiPlusBox" | 224 | icon={mdiPlusBox} |
224 | size={1} | 225 | size={1} |
225 | className={classes.workspacesSettingsButtonIcon} | 226 | className={classes.workspacesSettingsButtonIcon} |
226 | /> | 227 | /> |
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 9b51f2602..977b23999 100644 --- a/src/features/workspaces/components/WorkspacesDashboard.js +++ b/src/features/workspaces/components/WorkspacesDashboard.js | |||
@@ -1,9 +1,9 @@ | |||
1 | import React, { Component, Fragment } from 'react'; | 1 | import React, { Component, Fragment } from 'react'; |
2 | import PropTypes from 'prop-types'; | 2 | import PropTypes from 'prop-types'; |
3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | 3 | import { observer, PropTypes as MobxPropTypes, inject } 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 | import { Infobox } from '@meetfranz/ui'; | 6 | import { Infobox, Badge } from '@meetfranz/ui'; |
7 | 7 | ||
8 | import Loader from '../../../components/ui/Loader'; | 8 | import Loader from '../../../components/ui/Loader'; |
9 | import WorkspaceItem from './WorkspaceItem'; | 9 | import WorkspaceItem from './WorkspaceItem'; |
@@ -11,7 +11,9 @@ import CreateWorkspaceForm from './CreateWorkspaceForm'; | |||
11 | import Request from '../../../stores/lib/Request'; | 11 | import Request from '../../../stores/lib/Request'; |
12 | import Appear from '../../../components/ui/effects/Appear'; | 12 | import Appear from '../../../components/ui/effects/Appear'; |
13 | import { workspaceStore } from '../index'; | 13 | import { workspaceStore } from '../index'; |
14 | import PremiumFeatureContainer from '../../../components/ui/PremiumFeatureContainer'; | 14 | import UIStore from '../../../stores/UIStore'; |
15 | import globalMessages from '../../../i18n/globalMessages'; | ||
16 | import UpgradeButton from '../../../components/ui/UpgradeButton'; | ||
15 | 17 | ||
16 | const messages = defineMessages({ | 18 | const messages = defineMessages({ |
17 | headline: { | 19 | headline: { |
@@ -48,7 +50,7 @@ const messages = defineMessages({ | |||
48 | }, | 50 | }, |
49 | }); | 51 | }); |
50 | 52 | ||
51 | const styles = theme => ({ | 53 | const styles = () => ({ |
52 | table: { | 54 | table: { |
53 | width: '100%', | 55 | width: '100%', |
54 | '& td': { | 56 | '& td': { |
@@ -62,17 +64,28 @@ const styles = theme => ({ | |||
62 | height: 'auto', | 64 | height: 'auto', |
63 | }, | 65 | }, |
64 | premiumAnnouncement: { | 66 | premiumAnnouncement: { |
65 | padding: '20px', | ||
66 | backgroundColor: '#3498db', | ||
67 | marginLeft: '-20px', | ||
68 | marginBottom: '20px', | ||
69 | height: 'auto', | 67 | height: 'auto', |
70 | color: 'white', | 68 | }, |
71 | borderRadius: theme.borderRadius, | 69 | premiumAnnouncementContainer: { |
70 | display: 'flex', | ||
71 | }, | ||
72 | announcementHeadline: { | ||
73 | marginBottom: 0, | ||
74 | }, | ||
75 | teaserImage: { | ||
76 | width: 250, | ||
77 | margin: [-8, 0, 0, 20], | ||
78 | alignSelf: 'center', | ||
79 | }, | ||
80 | upgradeCTA: { | ||
81 | margin: [40, 'auto'], | ||
82 | }, | ||
83 | proRequired: { | ||
84 | margin: [10, 0, 40], | ||
72 | }, | 85 | }, |
73 | }); | 86 | }); |
74 | 87 | ||
75 | @injectSheet(styles) @observer | 88 | @inject('stores') @injectSheet(styles) @observer |
76 | class WorkspacesDashboard extends Component { | 89 | class WorkspacesDashboard extends Component { |
77 | static propTypes = { | 90 | static propTypes = { |
78 | classes: PropTypes.object.isRequired, | 91 | classes: PropTypes.object.isRequired, |
@@ -100,7 +113,9 @@ class WorkspacesDashboard extends Component { | |||
100 | onWorkspaceClick, | 113 | onWorkspaceClick, |
101 | workspaces, | 114 | workspaces, |
102 | } = this.props; | 115 | } = this.props; |
116 | |||
103 | const { intl } = this.context; | 117 | const { intl } = this.context; |
118 | |||
104 | return ( | 119 | return ( |
105 | <div className="settings__main"> | 120 | <div className="settings__main"> |
106 | <div className="settings__header"> | 121 | <div className="settings__header"> |
@@ -138,68 +153,80 @@ class WorkspacesDashboard extends Component { | |||
138 | 153 | ||
139 | {workspaceStore.isPremiumUpgradeRequired && ( | 154 | {workspaceStore.isPremiumUpgradeRequired && ( |
140 | <div className={classes.premiumAnnouncement}> | 155 | <div className={classes.premiumAnnouncement}> |
141 | <h2>{intl.formatMessage(messages.workspaceFeatureHeadline)}</h2> | 156 | |
142 | <p>{intl.formatMessage(messages.workspaceFeatureInfo)}</p> | 157 | <h1 className={classes.announcementHeadline}>{intl.formatMessage(messages.workspaceFeatureHeadline)}</h1> |
158 | <Badge className={classes.proRequired}>{intl.formatMessage(globalMessages.proRequired)}</Badge> | ||
159 | <div className={classes.premiumAnnouncementContainer}> | ||
160 | <div className={classes.premiumAnnouncementContent}> | ||
161 | <p>{intl.formatMessage(messages.workspaceFeatureInfo)}</p> | ||
162 | <UpgradeButton | ||
163 | className={classes.upgradeCTA} | ||
164 | gaEventInfo={{ category: 'Workspaces', event: 'upgrade' }} | ||
165 | short | ||
166 | requiresPro | ||
167 | /> | ||
168 | </div> | ||
169 | <img src={`https://cdn.franzinfra.com/announcements/assets/workspaces_${this.props.stores.ui.isDarkThemeActive ? 'dark' : 'light'}.png`} className={classes.teaserImage} alt="" /> | ||
170 | </div> | ||
143 | </div> | 171 | </div> |
144 | )} | 172 | )} |
145 | 173 | ||
146 | <PremiumFeatureContainer | 174 | {!workspaceStore.isPremiumUpgradeRequired && ( |
147 | condition={workspaceStore.isPremiumFeature} | 175 | <> |
148 | gaEventInfo={{ category: 'User', event: 'upgrade', label: 'workspaces' }} | 176 | {/* ===== Create workspace form ===== */} |
149 | > | 177 | <div className={classes.createForm}> |
150 | {/* ===== Create workspace form ===== */} | 178 | <CreateWorkspaceForm |
151 | <div className={classes.createForm}> | 179 | isSubmitting={createWorkspaceRequest.isExecuting} |
152 | <CreateWorkspaceForm | 180 | onSubmit={onCreateWorkspaceSubmit} |
153 | isSubmitting={createWorkspaceRequest.isExecuting} | 181 | /> |
154 | onSubmit={onCreateWorkspaceSubmit} | 182 | </div> |
155 | /> | 183 | {getUserWorkspacesRequest.isExecuting ? ( |
156 | </div> | 184 | <Loader /> |
157 | {getUserWorkspacesRequest.isExecuting ? ( | 185 | ) : ( |
158 | <Loader /> | 186 | <Fragment> |
159 | ) : ( | 187 | {/* ===== Workspace could not be loaded error ===== */} |
160 | <Fragment> | 188 | {getUserWorkspacesRequest.error ? ( |
161 | {/* ===== Workspace could not be loaded error ===== */} | 189 | <Infobox |
162 | {getUserWorkspacesRequest.error ? ( | 190 | icon="alert" |
163 | <Infobox | 191 | type="danger" |
164 | icon="alert" | 192 | ctaLabel={intl.formatMessage(messages.tryReloadWorkspaces)} |
165 | type="danger" | 193 | ctaLoading={getUserWorkspacesRequest.isExecuting} |
166 | ctaLabel={intl.formatMessage(messages.tryReloadWorkspaces)} | 194 | ctaOnClick={getUserWorkspacesRequest.retry} |
167 | ctaLoading={getUserWorkspacesRequest.isExecuting} | 195 | > |
168 | ctaOnClick={getUserWorkspacesRequest.retry} | 196 | {intl.formatMessage(messages.workspacesRequestFailed)} |
169 | > | 197 | </Infobox> |
170 | {intl.formatMessage(messages.workspacesRequestFailed)} | 198 | ) : ( |
171 | </Infobox> | 199 | <Fragment> |
172 | ) : ( | 200 | {workspaces.length === 0 ? ( |
173 | <Fragment> | 201 | <div className="align-middle settings__empty-state"> |
174 | {workspaces.length === 0 ? ( | 202 | {/* ===== Workspaces empty state ===== */} |
175 | <div className="align-middle settings__empty-state"> | 203 | <p className="settings__empty-text"> |
176 | {/* ===== Workspaces empty state ===== */} | 204 | <span className="emoji"> |
177 | <p className="settings__empty-text"> | 205 | <img src="./assets/images/emoji/sad.png" alt="" /> |
178 | <span className="emoji"> | 206 | </span> |
179 | <img src="./assets/images/emoji/sad.png" alt="" /> | 207 | {intl.formatMessage(messages.noServicesAdded)} |
180 | </span> | 208 | </p> |
181 | {intl.formatMessage(messages.noServicesAdded)} | 209 | </div> |
182 | </p> | 210 | ) : ( |
183 | </div> | 211 | <table className={classes.table}> |
184 | ) : ( | 212 | {/* ===== Workspaces list ===== */} |
185 | <table className={classes.table}> | 213 | <tbody> |
186 | {/* ===== Workspaces list ===== */} | 214 | {workspaces.map(workspace => ( |
187 | <tbody> | 215 | <WorkspaceItem |
188 | {workspaces.map(workspace => ( | 216 | key={workspace.id} |
189 | <WorkspaceItem | 217 | workspace={workspace} |
190 | key={workspace.id} | 218 | onItemClick={w => onWorkspaceClick(w)} |
191 | workspace={workspace} | 219 | /> |
192 | onItemClick={w => onWorkspaceClick(w)} | 220 | ))} |
193 | /> | 221 | </tbody> |
194 | ))} | 222 | </table> |
195 | </tbody> | 223 | )} |
196 | </table> | 224 | </Fragment> |
197 | )} | 225 | )} |
198 | </Fragment> | 226 | </Fragment> |
199 | )} | 227 | )} |
200 | </Fragment> | 228 | </> |
201 | )} | 229 | )} |
202 | </PremiumFeatureContainer> | ||
203 | </div> | 230 | </div> |
204 | </div> | 231 | </div> |
205 | ); | 232 | ); |
@@ -207,3 +234,9 @@ class WorkspacesDashboard extends Component { | |||
207 | } | 234 | } |
208 | 235 | ||
209 | export default WorkspacesDashboard; | 236 | export default WorkspacesDashboard; |
237 | |||
238 | WorkspacesDashboard.wrappedComponent.propTypes = { | ||
239 | stores: PropTypes.shape({ | ||
240 | ui: PropTypes.instanceOf(UIStore).isRequired, | ||
241 | }).isRequired, | ||
242 | }; | ||
diff --git a/src/features/workspaces/containers/WorkspacesScreen.js b/src/features/workspaces/containers/WorkspacesScreen.js index 2ab565fa1..affbd230d 100644 --- a/src/features/workspaces/containers/WorkspacesScreen.js +++ b/src/features/workspaces/containers/WorkspacesScreen.js | |||
@@ -11,7 +11,7 @@ import { | |||
11 | updateWorkspaceRequest, | 11 | updateWorkspaceRequest, |
12 | } from '../api'; | 12 | } from '../api'; |
13 | 13 | ||
14 | @inject('actions') @observer | 14 | @inject('stores', 'actions') @observer |
15 | class WorkspacesScreen extends Component { | 15 | class WorkspacesScreen extends Component { |
16 | static propTypes = { | 16 | static propTypes = { |
17 | actions: PropTypes.shape({ | 17 | actions: PropTypes.shape({ |
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 = () => { |