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