diff options
Diffstat (limited to 'src/components')
24 files changed, 1328 insertions, 475 deletions
diff --git a/src/components/TrialActivationInfoBar.js b/src/components/TrialActivationInfoBar.js new file mode 100644 index 000000000..77ab97565 --- /dev/null +++ b/src/components/TrialActivationInfoBar.js | |||
@@ -0,0 +1,94 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { defineMessages, intlShape } from 'react-intl'; | ||
4 | import ms from 'ms'; | ||
5 | import injectSheet from 'react-jss'; | ||
6 | import classnames from 'classnames'; | ||
7 | |||
8 | import InfoBar from './ui/InfoBar'; | ||
9 | |||
10 | const messages = defineMessages({ | ||
11 | message: { | ||
12 | id: 'infobar.trialActivated', | ||
13 | defaultMessage: '!!!Your trial was successfully activated. Happy messaging!', | ||
14 | }, | ||
15 | }); | ||
16 | |||
17 | const styles = { | ||
18 | notification: { | ||
19 | height: 'auto', | ||
20 | position: 'absolute', | ||
21 | top: -50, | ||
22 | transition: 'top 0.3s', | ||
23 | zIndex: 500, | ||
24 | width: 'calc(100% - 300px)', | ||
25 | }, | ||
26 | show: { | ||
27 | top: 0, | ||
28 | }, | ||
29 | }; | ||
30 | |||
31 | @injectSheet(styles) | ||
32 | class TrialActivationInfoBar extends Component { | ||
33 | static propTypes = { | ||
34 | // eslint-disable-next-line | ||
35 | classes: PropTypes.object.isRequired, | ||
36 | }; | ||
37 | |||
38 | static contextTypes = { | ||
39 | intl: intlShape, | ||
40 | }; | ||
41 | |||
42 | state = { | ||
43 | showing: false, | ||
44 | removed: false, | ||
45 | } | ||
46 | |||
47 | componentDidMount() { | ||
48 | setTimeout(() => { | ||
49 | this.setState({ | ||
50 | showing: true, | ||
51 | }); | ||
52 | }, 0); | ||
53 | |||
54 | setTimeout(() => { | ||
55 | this.setState({ | ||
56 | showing: false, | ||
57 | }); | ||
58 | }, ms('6s')); | ||
59 | |||
60 | setTimeout(() => { | ||
61 | this.setState({ | ||
62 | removed: true, | ||
63 | }); | ||
64 | }, ms('7s')); | ||
65 | } | ||
66 | |||
67 | render() { | ||
68 | const { classes } = this.props; | ||
69 | const { showing, removed } = this.state; | ||
70 | const { intl } = this.context; | ||
71 | |||
72 | if (removed) return null; | ||
73 | |||
74 | return ( | ||
75 | <div | ||
76 | className={classnames({ | ||
77 | [classes.notification]: true, | ||
78 | [classes.show]: showing, | ||
79 | })} | ||
80 | > | ||
81 | <InfoBar | ||
82 | type="primary" | ||
83 | position="top" | ||
84 | sticky | ||
85 | > | ||
86 | <span className="mdi mdi-information" /> | ||
87 | {intl.formatMessage(messages.message)} | ||
88 | </InfoBar> | ||
89 | </div> | ||
90 | ); | ||
91 | } | ||
92 | } | ||
93 | |||
94 | export default TrialActivationInfoBar; | ||
diff --git a/src/components/auth/Pricing.js b/src/components/auth/Pricing.js index 13a1e2351..cbeaaa5d9 100644 --- a/src/components/auth/Pricing.js +++ b/src/components/auth/Pricing.js | |||
@@ -1,40 +1,107 @@ | |||
1 | import React, { Component } 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 } from 'mobx-react'; | 3 | import { observer } from 'mobx-react'; |
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | // import { Link } from 'react-router'; | 5 | import injectSheet from 'react-jss'; |
6 | import { H2, Loader } from '@meetfranz/ui'; | ||
7 | import classnames from 'classnames'; | ||
8 | |||
9 | import { Button } from '@meetfranz/forms'; | ||
10 | import { FeatureItem } from '../ui/FeatureItem'; | ||
11 | import { FeatureList } from '../ui/FeatureList'; | ||
6 | 12 | ||
7 | // import Button from '../ui/Button'; | ||
8 | import Loader from '../ui/Loader'; | ||
9 | import Appear from '../ui/effects/Appear'; | ||
10 | import SubscriptionForm from '../../containers/subscription/SubscriptionFormScreen'; | ||
11 | 13 | ||
12 | const messages = defineMessages({ | 14 | const messages = defineMessages({ |
13 | headline: { | 15 | headline: { |
14 | id: 'pricing.headline', | 16 | id: 'pricing.trial.headline', |
15 | defaultMessage: '!!!Support Ferdi', | 17 | defaultMessage: '!!!Franz Professional', |
18 | }, | ||
19 | personalOffer: { | ||
20 | id: 'pricing.trial.subheadline', | ||
21 | defaultMessage: '!!!Your personal welcome offer:', | ||
22 | }, | ||
23 | noStringsAttachedHeadline: { | ||
24 | id: 'pricing.trial.terms.headline', | ||
25 | defaultMessage: '!!!No strings attached', | ||
26 | }, | ||
27 | noCreditCard: { | ||
28 | id: 'pricing.trial.terms.noCreditCard', | ||
29 | defaultMessage: '!!!No credit card required', | ||
16 | }, | 30 | }, |
17 | monthlySupportLabel: { | 31 | automaticTrialEnd: { |
18 | id: 'pricing.support.label', | 32 | id: 'pricing.trial.terms.automaticTrialEnd', |
19 | defaultMessage: '!!!Select your support plan', | 33 | defaultMessage: '!!!Your free trial ends automatically after 14 days', |
20 | }, | 34 | }, |
21 | submitButtonLabel: { | 35 | activationError: { |
22 | id: 'pricing.submit.label', | 36 | id: 'pricing.trial.error', |
23 | defaultMessage: '!!!Support the development of Ferdi', | 37 | defaultMessage: '!!!Sorry, we could not activate your trial!', |
24 | }, | 38 | }, |
25 | skipPayment: { | 39 | ctaAccept: { |
26 | id: 'pricing.link.skipPayment', | 40 | id: 'pricing.trial.cta.accept', |
27 | defaultMessage: '!!!I don\'t want to support the development of Ferdi.', | 41 | defaultMessage: '!!!Yes, upgrade my account to Franz Professional', |
42 | }, | ||
43 | ctaSkip: { | ||
44 | id: 'pricing.trial.cta.skip', | ||
45 | defaultMessage: '!!!Continue to Franz', | ||
46 | }, | ||
47 | featuresHeadline: { | ||
48 | id: 'pricing.trial.features.headline', | ||
49 | defaultMessage: '!!!Franz Professional includes:', | ||
28 | }, | 50 | }, |
29 | }); | 51 | }); |
30 | 52 | ||
31 | export default @observer class Signup extends Component { | 53 | const styles = theme => ({ |
54 | container: { | ||
55 | position: 'relative', | ||
56 | marginLeft: -150, | ||
57 | }, | ||
58 | welcomeOffer: { | ||
59 | textAlign: 'center', | ||
60 | fontWeight: 'bold', | ||
61 | }, | ||
62 | keyTerms: { | ||
63 | textAlign: 'center', | ||
64 | }, | ||
65 | content: { | ||
66 | position: 'relative', | ||
67 | zIndex: 20, | ||
68 | }, | ||
69 | featureContainer: { | ||
70 | width: 300, | ||
71 | position: 'absolute', | ||
72 | left: 'calc(100% / 2 + 225px)', | ||
73 | top: 155, | ||
74 | background: theme.signup.pricing.feature.background, | ||
75 | height: 'auto', | ||
76 | padding: 20, | ||
77 | borderTopRightRadius: theme.borderRadius, | ||
78 | borderBottomRightRadius: theme.borderRadius, | ||
79 | zIndex: 10, | ||
80 | }, | ||
81 | featureItem: { | ||
82 | borderBottom: [1, 'solid', theme.signup.pricing.feature.border], | ||
83 | }, | ||
84 | cta: { | ||
85 | marginTop: 40, | ||
86 | width: '100%', | ||
87 | }, | ||
88 | skipLink: { | ||
89 | textAlign: 'center', | ||
90 | marginTop: 10, | ||
91 | }, | ||
92 | error: { | ||
93 | margin: [20, 0, 0], | ||
94 | color: theme.styleTypes.danger.accent, | ||
95 | }, | ||
96 | }); | ||
97 | |||
98 | export default @observer @injectSheet(styles) class Signup extends Component { | ||
32 | static propTypes = { | 99 | static propTypes = { |
33 | donor: MobxPropTypes.objectOrObservableObject.isRequired, | 100 | onSubmit: PropTypes.func.isRequired, |
34 | isLoading: PropTypes.bool.isRequired, | 101 | isLoadingRequiredData: PropTypes.bool.isRequired, |
35 | isLoadingUser: PropTypes.bool.isRequired, | 102 | isActivatingTrial: PropTypes.bool.isRequired, |
36 | onCloseSubscriptionWindow: PropTypes.func.isRequired, | 103 | trialActivationError: PropTypes.bool.isRequired, |
37 | skipAction: PropTypes.func.isRequired, | 104 | classes: PropTypes.object.isRequired, |
38 | }; | 105 | }; |
39 | 106 | ||
40 | static contextTypes = { | 107 | static contextTypes = { |
@@ -43,70 +110,37 @@ export default @observer class Signup extends Component { | |||
43 | 110 | ||
44 | render() { | 111 | render() { |
45 | const { | 112 | const { |
46 | donor, | 113 | onSubmit, |
47 | isLoading, | 114 | isLoadingRequiredData, |
48 | isLoadingUser, | 115 | isActivatingTrial, |
49 | onCloseSubscriptionWindow, | 116 | trialActivationError, |
50 | skipAction, | 117 | classes, |
51 | } = this.props; | 118 | } = this.props; |
52 | const { intl } = this.context; | 119 | const { intl } = this.context; |
53 | 120 | ||
54 | return ( | 121 | return ( |
55 | <div className="auth__scroll-container"> | 122 | <div className={classnames('auth__scroll-container', classes.container)}> |
56 | <div className="auth__container auth__container--signup"> | 123 | <div className={classnames('auth__container', 'auth__container--signup', classes.content)}> |
57 | <form className="Ferdi-form auth__form"> | 124 | <form className="franz-form auth__form"> |
58 | <img | 125 | {isLoadingRequiredData ? <Loader /> : ( |
59 | src="./assets/images/sm.png" | 126 | <img |
60 | className="auth__logo auth__logo--sm" | 127 | src="./assets/images/sm.png" |
61 | alt="" | 128 | className="auth__logo auth__logo--sm" |
62 | /> | 129 | alt="" |
130 | /> | ||
131 | )} | ||
132 | <p className={classes.welcomeOffer}>{intl.formatMessage(messages.personalOffer)}</p> | ||
63 | <h1>{intl.formatMessage(messages.headline)}</h1> | 133 | <h1>{intl.formatMessage(messages.headline)}</h1> |
64 | <div className="auth__letter"> | 134 | <div className="auth__letter"> |
65 | {isLoadingUser && ( | 135 | <p> |
66 | <p>Loading</p> | 136 | We built Franz with a lot of effort, manpower and love, |
67 | )} | 137 | to boost up your messaging experience. |
68 | {!isLoadingUser && ( | 138 | <br /> |
69 | donor.amount ? ( | 139 | </p> |
70 | <span> | 140 | <p> |
71 | <p> | 141 | Get the free 14 day Franz Professional trial and see your communication evolving. |
72 | Thank you so much for your previous donation of | 142 | <br /> |
73 | {' '} | 143 | </p> |
74 | <strong> | ||
75 | $ | ||
76 | {donor.amount} | ||
77 | </strong> | ||
78 | . | ||
79 | <br /> | ||
80 | Your support allowed us to get where we are today. | ||
81 | <br /> | ||
82 | </p> | ||
83 | <p> | ||
84 | As an early supporter, you get | ||
85 | {' '} | ||
86 | <strong>a lifetime premium supporter license</strong> | ||
87 | {' '} | ||
88 | without any | ||
89 | additional charges. | ||
90 | </p> | ||
91 | <p> | ||
92 | However, If you want to keep supporting us, you are more than welcome to subscribe to a plan. | ||
93 | <br /> | ||
94 | <br /> | ||
95 | </p> | ||
96 | </span> | ||
97 | ) : ( | ||
98 | <span> | ||
99 | <p> | ||
100 | We built Ferdi with a lot of effort, manpower and love, | ||
101 | to bring you the best messaging experience. | ||
102 | <br /> | ||
103 | </p> | ||
104 | <p> | ||
105 | Getting a Ferdi Premium Supporter License will allow us to keep improving Ferdi for you. | ||
106 | </p> | ||
107 | </span> | ||
108 | ) | ||
109 | )} | ||
110 | <p> | 144 | <p> |
111 | Thanks for being a hero. | 145 | Thanks for being a hero. |
112 | </p> | 146 | </p> |
@@ -114,20 +148,48 @@ export default @observer class Signup extends Component { | |||
114 | <strong>Stefan Malzner</strong> | 148 | <strong>Stefan Malzner</strong> |
115 | </p> | 149 | </p> |
116 | </div> | 150 | </div> |
117 | <Loader loaded={!isLoading}> | 151 | <div className={classes.keyTerms}> |
118 | <Appear transitionName="slideDown"> | 152 | <H2> |
119 | <span className="label">{intl.formatMessage(messages.monthlySupportLabel)}</span> | 153 | {intl.formatMessage(messages.noStringsAttachedHeadline)} |
120 | <SubscriptionForm | 154 | </H2> |
121 | onCloseWindow={onCloseSubscriptionWindow} | 155 | <ul className={classes.keyTermsList}> |
122 | showSkipOption | 156 | <FeatureItem icon="👉" name={intl.formatMessage(messages.noCreditCard)} /> |
123 | skipAction={skipAction} | 157 | <FeatureItem icon="👉" name={intl.formatMessage(messages.automaticTrialEnd)} /> |
124 | hideInfo={Boolean(donor.amount)} | 158 | </ul> |
125 | skipButtonLabel={intl.formatMessage(messages.skipPayment)} | 159 | </div> |
126 | /> | 160 | {trialActivationError && ( |
127 | </Appear> | 161 | <p className={classes.error}>{intl.formatMessage(messages.activationError)}</p> |
128 | </Loader> | 162 | )} |
163 | <Button | ||
164 | label={intl.formatMessage(messages.ctaAccept)} | ||
165 | className={classes.cta} | ||
166 | onClick={onSubmit} | ||
167 | busy={isActivatingTrial} | ||
168 | disabled={isLoadingRequiredData || isActivatingTrial} | ||
169 | /> | ||
170 | <p className={classes.skipLink}> | ||
171 | <a href="#/">{intl.formatMessage(messages.ctaSkip)}</a> | ||
172 | </p> | ||
129 | </form> | 173 | </form> |
130 | </div> | 174 | </div> |
175 | <div className={classes.featureContainer}> | ||
176 | <H2> | ||
177 | {intl.formatMessage(messages.featuresHeadline)} | ||
178 | </H2> | ||
179 | {/* <ul className={classes.features}> | ||
180 | <FeatureItem name="Add unlimited services" className={classes.featureItem} /> | ||
181 | <FeatureItem name="Spellchecker support" className={classes.featureItem} /> | ||
182 | <FeatureItem name="Workspaces" className={classes.featureItem} /> | ||
183 | <FeatureItem name="Add Custom Websites" className={classes.featureItem} /> | ||
184 | <FeatureItem name="On-premise & other Hosted Services" className={classes.featureItem} /> | ||
185 | <FeatureItem name="Install 3rd party services" className={classes.featureItem} /> | ||
186 | <FeatureItem name="Service Proxies" className={classes.featureItem} /> | ||
187 | <FeatureItem name="Team Management" className={classes.featureItem} /> | ||
188 | <FeatureItem name="No Waiting Screens" className={classes.featureItem} /> | ||
189 | <FeatureItem name="Forever ad-free" className={classes.featureItem} /> | ||
190 | </ul> */} | ||
191 | <FeatureList /> | ||
192 | </div> | ||
131 | </div> | 193 | </div> |
132 | ); | 194 | ); |
133 | } | 195 | } |
diff --git a/src/components/auth/Signup.js b/src/components/auth/Signup.js index 2aedbe6ea..b36e71ce1 100644 --- a/src/components/auth/Signup.js +++ b/src/components/auth/Signup.js | |||
@@ -7,7 +7,6 @@ import { isDevMode, useLiveAPI } from '../../environment'; | |||
7 | import Form from '../../lib/Form'; | 7 | import Form from '../../lib/Form'; |
8 | import { required, email, minLength } from '../../helpers/validation-helpers'; | 8 | import { required, email, minLength } from '../../helpers/validation-helpers'; |
9 | import Input from '../ui/Input'; | 9 | import Input from '../ui/Input'; |
10 | import Radio from '../ui/Radio'; | ||
11 | import Button from '../ui/Button'; | 10 | import Button from '../ui/Button'; |
12 | import Link from '../ui/Link'; | 11 | import Link from '../ui/Link'; |
13 | import Infobox from '../ui/Infobox'; | 12 | import Infobox from '../ui/Infobox'; |
@@ -31,10 +30,10 @@ const messages = defineMessages({ | |||
31 | id: 'signup.email.label', | 30 | id: 'signup.email.label', |
32 | defaultMessage: '!!!Email address', | 31 | defaultMessage: '!!!Email address', |
33 | }, | 32 | }, |
34 | companyLabel: { | 33 | // companyLabel: { |
35 | id: 'signup.company.label', | 34 | // id: 'signup.company.label', |
36 | defaultMessage: '!!!Company', | 35 | // defaultMessage: '!!!Company', |
37 | }, | 36 | // }, |
38 | passwordLabel: { | 37 | passwordLabel: { |
39 | id: 'signup.password.label', | 38 | id: 'signup.password.label', |
40 | defaultMessage: '!!!Password', | 39 | defaultMessage: '!!!Password', |
@@ -79,20 +78,6 @@ export default @observer class Signup extends Component { | |||
79 | 78 | ||
80 | form = new Form({ | 79 | form = new Form({ |
81 | fields: { | 80 | fields: { |
82 | accountType: { | ||
83 | value: 'individual', | ||
84 | validators: [required], | ||
85 | options: [{ | ||
86 | value: 'individual', | ||
87 | label: 'Individual', | ||
88 | }, { | ||
89 | value: 'non-profit', | ||
90 | label: 'Non-Profit', | ||
91 | }, { | ||
92 | value: 'company', | ||
93 | label: 'Company', | ||
94 | }], | ||
95 | }, | ||
96 | firstname: { | 81 | firstname: { |
97 | label: this.context.intl.formatMessage(messages.firstnameLabel), | 82 | label: this.context.intl.formatMessage(messages.firstnameLabel), |
98 | value: '', | 83 | value: '', |
@@ -108,10 +93,6 @@ export default @observer class Signup extends Component { | |||
108 | value: '', | 93 | value: '', |
109 | validators: [required, email], | 94 | validators: [required, email], |
110 | }, | 95 | }, |
111 | organization: { | ||
112 | label: this.context.intl.formatMessage(messages.companyLabel), | ||
113 | value: '', // TODO: make required when accountType: company | ||
114 | }, | ||
115 | password: { | 96 | password: { |
116 | label: this.context.intl.formatMessage(messages.passwordLabel), | 97 | label: this.context.intl.formatMessage(messages.passwordLabel), |
117 | value: '', | 98 | value: '', |
@@ -153,7 +134,6 @@ export default @observer class Signup extends Component { | |||
153 | In Dev Mode your data is not persistent. Please use the live app for accesing the production API. | 134 | In Dev Mode your data is not persistent. Please use the live app for accesing the production API. |
154 | </Infobox> | 135 | </Infobox> |
155 | )} | 136 | )} |
156 | <Radio field={form.$('accountType')} showLabel={false} /> | ||
157 | <div className="grid__row"> | 137 | <div className="grid__row"> |
158 | <Input field={form.$('firstname')} focus /> | 138 | <Input field={form.$('firstname')} focus /> |
159 | <Input field={form.$('lastname')} /> | 139 | <Input field={form.$('lastname')} /> |
@@ -164,9 +144,6 @@ export default @observer class Signup extends Component { | |||
164 | showPasswordToggle | 144 | showPasswordToggle |
165 | scorePassword | 145 | scorePassword |
166 | /> | 146 | /> |
167 | {form.$('accountType').value === 'company' && ( | ||
168 | <Input field={form.$('organization')} /> | ||
169 | )} | ||
170 | {error.code === 'email-duplicate' && ( | 147 | {error.code === 'email-duplicate' && ( |
171 | <p className="error-message center">{intl.formatMessage(messages.emailDuplicate)}</p> | 148 | <p className="error-message center">{intl.formatMessage(messages.emailDuplicate)}</p> |
172 | )} | 149 | )} |
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js index 5c3d301e0..ed004d07e 100644 --- a/src/components/layout/AppLayout.js +++ b/src/components/layout/AppLayout.js | |||
@@ -16,6 +16,8 @@ import { isWindows } from '../../environment'; | |||
16 | import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator'; | 16 | import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator'; |
17 | import { workspaceStore } from '../../features/workspaces'; | 17 | import { workspaceStore } from '../../features/workspaces'; |
18 | import AppUpdateInfoBar from '../AppUpdateInfoBar'; | 18 | import AppUpdateInfoBar from '../AppUpdateInfoBar'; |
19 | import TrialActivationInfoBar from '../TrialActivationInfoBar'; | ||
20 | import Todos from '../../features/todos/containers/TodosScreen'; | ||
19 | 21 | ||
20 | function createMarkup(HTMLString) { | 22 | function createMarkup(HTMLString) { |
21 | return { __html: HTMLString }; | 23 | return { __html: HTMLString }; |
@@ -42,7 +44,8 @@ const messages = defineMessages({ | |||
42 | 44 | ||
43 | const styles = theme => ({ | 45 | const styles = theme => ({ |
44 | appContent: { | 46 | appContent: { |
45 | width: `calc(100% + ${theme.workspaces.drawer.width}px)`, | 47 | // width: `calc(100% + ${theme.workspaces.drawer.width}px)`, |
48 | width: '100%', | ||
46 | transition: 'transform 0.5s ease', | 49 | transition: 'transform 0.5s ease', |
47 | transform() { | 50 | transform() { |
48 | return workspaceStore.isWorkspaceDrawerOpen ? 'translateX(0)' : `translateX(-${theme.workspaces.drawer.width}px)`; | 51 | return workspaceStore.isWorkspaceDrawerOpen ? 'translateX(0)' : `translateX(-${theme.workspaces.drawer.width}px)`; |
@@ -60,7 +63,6 @@ class AppLayout extends Component { | |||
60 | services: PropTypes.element.isRequired, | 63 | services: PropTypes.element.isRequired, |
61 | children: PropTypes.element, | 64 | children: PropTypes.element, |
62 | news: MobxPropTypes.arrayOrObservableArray.isRequired, | 65 | news: MobxPropTypes.arrayOrObservableArray.isRequired, |
63 | // isOnline: PropTypes.bool.isRequired, | ||
64 | showServicesUpdatedInfoBar: PropTypes.bool.isRequired, | 66 | showServicesUpdatedInfoBar: PropTypes.bool.isRequired, |
65 | appUpdateIsDownloaded: PropTypes.bool.isRequired, | 67 | appUpdateIsDownloaded: PropTypes.bool.isRequired, |
66 | nextAppReleaseVersion: PropTypes.string, | 68 | nextAppReleaseVersion: PropTypes.string, |
@@ -72,6 +74,8 @@ class AppLayout extends Component { | |||
72 | areRequiredRequestsSuccessful: PropTypes.bool.isRequired, | 74 | areRequiredRequestsSuccessful: PropTypes.bool.isRequired, |
73 | retryRequiredRequests: PropTypes.func.isRequired, | 75 | retryRequiredRequests: PropTypes.func.isRequired, |
74 | areRequiredRequestsLoading: PropTypes.bool.isRequired, | 76 | areRequiredRequestsLoading: PropTypes.bool.isRequired, |
77 | isDelayAppScreenVisible: PropTypes.bool.isRequired, | ||
78 | hasActivatedTrial: PropTypes.bool.isRequired, | ||
75 | }; | 79 | }; |
76 | 80 | ||
77 | static defaultProps = { | 81 | static defaultProps = { |
@@ -91,7 +95,6 @@ class AppLayout extends Component { | |||
91 | sidebar, | 95 | sidebar, |
92 | services, | 96 | services, |
93 | children, | 97 | children, |
94 | // isOnline, | ||
95 | news, | 98 | news, |
96 | showServicesUpdatedInfoBar, | 99 | showServicesUpdatedInfoBar, |
97 | appUpdateIsDownloaded, | 100 | appUpdateIsDownloaded, |
@@ -104,6 +107,8 @@ class AppLayout extends Component { | |||
104 | areRequiredRequestsSuccessful, | 107 | areRequiredRequestsSuccessful, |
105 | retryRequiredRequests, | 108 | retryRequiredRequests, |
106 | areRequiredRequestsLoading, | 109 | areRequiredRequestsLoading, |
110 | isDelayAppScreenVisible, | ||
111 | hasActivatedTrial, | ||
107 | } = this.props; | 112 | } = this.props; |
108 | 113 | ||
109 | const { intl } = this.context; | 114 | const { intl } = this.context; |
@@ -125,29 +130,31 @@ class AppLayout extends Component { | |||
125 | sticky={item.sticky} | 130 | sticky={item.sticky} |
126 | onHide={() => removeNewsItem({ newsId: item.id })} | 131 | onHide={() => removeNewsItem({ newsId: item.id })} |
127 | > | 132 | > |
128 | <span dangerouslySetInnerHTML={createMarkup(item.message)} /> | 133 | <span |
134 | dangerouslySetInnerHTML={createMarkup(item.message)} | ||
135 | onClick={(event) => { | ||
136 | const { target } = event; | ||
137 | if (target && target.hasAttribute('data-is-news-cta')) { | ||
138 | removeNewsItem({ newsId: item.id }); | ||
139 | } | ||
140 | }} | ||
141 | /> | ||
129 | </InfoBar> | 142 | </InfoBar> |
130 | ))} | 143 | ))} |
131 | {/* {!isOnline && ( | 144 | {hasActivatedTrial && ( |
132 | <InfoBar | 145 | <TrialActivationInfoBar /> |
133 | type="danger" | 146 | )} |
134 | sticky | ||
135 | > | ||
136 | <span className="mdi mdi-flash" /> | ||
137 | {intl.formatMessage(globalMessages.notConnectedToTheInternet)} | ||
138 | </InfoBar> | ||
139 | )} */} | ||
140 | {!areRequiredRequestsSuccessful && showRequiredRequestsError && ( | 147 | {!areRequiredRequestsSuccessful && showRequiredRequestsError && ( |
141 | <InfoBar | 148 | <InfoBar |
142 | type="danger" | 149 | type="danger" |
143 | ctaLabel="Try again" | 150 | ctaLabel="Try again" |
144 | ctaLoading={areRequiredRequestsLoading} | 151 | ctaLoading={areRequiredRequestsLoading} |
145 | sticky | 152 | sticky |
146 | onClick={retryRequiredRequests} | 153 | onClick={retryRequiredRequests} |
147 | > | 154 | > |
148 | <span className="mdi mdi-flash" /> | 155 | <span className="mdi mdi-flash" /> |
149 | {intl.formatMessage(messages.requiredRequestsFailed)} | 156 | {intl.formatMessage(messages.requiredRequestsFailed)} |
150 | </InfoBar> | 157 | </InfoBar> |
151 | )} | 158 | )} |
152 | {authRequestFailed && ( | 159 | {authRequestFailed && ( |
153 | <InfoBar | 160 | <InfoBar |
@@ -183,6 +190,7 @@ class AppLayout extends Component { | |||
183 | {services} | 190 | {services} |
184 | {children} | 191 | {children} |
185 | </div> | 192 | </div> |
193 | <Todos /> | ||
186 | </div> | 194 | </div> |
187 | </div> | 195 | </div> |
188 | </ErrorBoundary> | 196 | </ErrorBoundary> |
diff --git a/src/components/services/content/ServiceRestricted.js b/src/components/services/content/ServiceRestricted.js new file mode 100644 index 000000000..4b8d926aa --- /dev/null +++ b/src/components/services/content/ServiceRestricted.js | |||
@@ -0,0 +1,78 @@ | |||
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 | |||
6 | import { serviceLimitStore } from '../../../features/serviceLimit'; | ||
7 | import Button from '../../ui/Button'; | ||
8 | import { RESTRICTION_TYPES } from '../../../models/Service'; | ||
9 | |||
10 | const messages = defineMessages({ | ||
11 | headlineServiceLimit: { | ||
12 | id: 'service.restrictedHandler.serviceLimit.headline', | ||
13 | defaultMessage: '!!!You have reached your service limit.', | ||
14 | }, | ||
15 | textServiceLimit: { | ||
16 | id: 'service.restrictedHandler.serviceLimit.text', | ||
17 | defaultMessage: '!!!Please upgrade your account to use more than {count} services.', | ||
18 | }, | ||
19 | headlineCustomUrl: { | ||
20 | id: 'service.restrictedHandler.customUrl.headline', | ||
21 | defaultMessage: '!!!Franz Professional Plan required', | ||
22 | }, | ||
23 | textCustomUrl: { | ||
24 | id: 'service.restrictedHandler.customUrl.text', | ||
25 | defaultMessage: '!!!Please upgrade to the Franz Professional plan to use custom urls & self hosted services.', | ||
26 | }, | ||
27 | action: { | ||
28 | id: 'service.restrictedHandler.action', | ||
29 | defaultMessage: '!!!Upgrade Account', | ||
30 | }, | ||
31 | }); | ||
32 | |||
33 | export default @observer class ServiceRestricted extends Component { | ||
34 | static propTypes = { | ||
35 | name: PropTypes.string.isRequired, | ||
36 | upgrade: PropTypes.func.isRequired, | ||
37 | type: PropTypes.number.isRequired, | ||
38 | }; | ||
39 | |||
40 | static contextTypes = { | ||
41 | intl: intlShape, | ||
42 | }; | ||
43 | |||
44 | countdownInterval = null; | ||
45 | |||
46 | countdownIntervalTimeout = 1000; | ||
47 | |||
48 | render() { | ||
49 | const { | ||
50 | name, | ||
51 | upgrade, | ||
52 | type, | ||
53 | } = this.props; | ||
54 | const { intl } = this.context; | ||
55 | |||
56 | return ( | ||
57 | <div className="services__info-layer"> | ||
58 | {type === RESTRICTION_TYPES.SERVICE_LIMIT && ( | ||
59 | <> | ||
60 | <h1>{intl.formatMessage(messages.headlineServiceLimit)}</h1> | ||
61 | <p>{intl.formatMessage(messages.textServiceLimit, { count: serviceLimitStore.serviceLimit })}</p> | ||
62 | </> | ||
63 | )} | ||
64 | {type === RESTRICTION_TYPES.CUSTOM_URL && ( | ||
65 | <> | ||
66 | <h1>{intl.formatMessage(messages.headlineCustomUrl)}</h1> | ||
67 | <p>{intl.formatMessage(messages.textCustomUrl)}</p> | ||
68 | </> | ||
69 | )} | ||
70 | <Button | ||
71 | label={intl.formatMessage(messages.action, { name })} | ||
72 | buttonType="inverted" | ||
73 | onClick={() => upgrade()} | ||
74 | /> | ||
75 | </div> | ||
76 | ); | ||
77 | } | ||
78 | } | ||
diff --git a/src/components/services/content/ServiceView.js b/src/components/services/content/ServiceView.js index 13148b9b3..f65f51346 100644 --- a/src/components/services/content/ServiceView.js +++ b/src/components/services/content/ServiceView.js | |||
@@ -10,6 +10,7 @@ import WebviewLoader from '../../ui/WebviewLoader'; | |||
10 | import WebviewCrashHandler from './WebviewCrashHandler'; | 10 | import WebviewCrashHandler from './WebviewCrashHandler'; |
11 | import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler'; | 11 | import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler'; |
12 | import ServiceDisabled from './ServiceDisabled'; | 12 | import ServiceDisabled from './ServiceDisabled'; |
13 | import ServiceRestricted from './ServiceRestricted'; | ||
13 | import ServiceWebview from './ServiceWebview'; | 14 | import ServiceWebview from './ServiceWebview'; |
14 | 15 | ||
15 | export default @observer class ServiceView extends Component { | 16 | export default @observer class ServiceView extends Component { |
@@ -21,6 +22,7 @@ export default @observer class ServiceView extends Component { | |||
21 | edit: PropTypes.func.isRequired, | 22 | edit: PropTypes.func.isRequired, |
22 | enable: PropTypes.func.isRequired, | 23 | enable: PropTypes.func.isRequired, |
23 | isActive: PropTypes.bool, | 24 | isActive: PropTypes.bool, |
25 | upgrade: PropTypes.func.isRequired, | ||
24 | }; | 26 | }; |
25 | 27 | ||
26 | static defaultProps = { | 28 | static defaultProps = { |
@@ -72,6 +74,7 @@ export default @observer class ServiceView extends Component { | |||
72 | reload, | 74 | reload, |
73 | edit, | 75 | edit, |
74 | enable, | 76 | enable, |
77 | upgrade, | ||
75 | } = this.props; | 78 | } = this.props; |
76 | 79 | ||
77 | const webviewClasses = classnames({ | 80 | const webviewClasses = classnames({ |
@@ -99,7 +102,7 @@ export default @observer class ServiceView extends Component { | |||
99 | reload={reload} | 102 | reload={reload} |
100 | /> | 103 | /> |
101 | )} | 104 | )} |
102 | {service.isEnabled && service.isLoading && service.isFirstLoad && ( | 105 | {service.isEnabled && service.isLoading && service.isFirstLoad && !service.isServiceAccessRestricted && ( |
103 | <WebviewLoader | 106 | <WebviewLoader |
104 | loaded={false} | 107 | loaded={false} |
105 | name={service.name} | 108 | name={service.name} |
@@ -126,11 +129,21 @@ export default @observer class ServiceView extends Component { | |||
126 | )} | 129 | )} |
127 | </Fragment> | 130 | </Fragment> |
128 | ) : ( | 131 | ) : ( |
129 | <ServiceWebview | 132 | <> |
130 | service={service} | 133 | {service.isServiceAccessRestricted ? ( |
131 | setWebviewReference={setWebviewReference} | 134 | <ServiceRestricted |
132 | detachService={detachService} | 135 | name={service.recipe.name} |
133 | /> | 136 | upgrade={upgrade} |
137 | type={service.restrictionType} | ||
138 | /> | ||
139 | ) : ( | ||
140 | <ServiceWebview | ||
141 | service={service} | ||
142 | setWebviewReference={setWebviewReference} | ||
143 | detachService={detachService} | ||
144 | /> | ||
145 | )} | ||
146 | </> | ||
134 | )} | 147 | )} |
135 | {statusBar} | 148 | {statusBar} |
136 | </div> | 149 | </div> |
diff --git a/src/components/services/content/Services.js b/src/components/services/content/Services.js index 5fad070f0..1afbaabc4 100644 --- a/src/components/services/content/Services.js +++ b/src/components/services/content/Services.js | |||
@@ -3,6 +3,9 @@ import PropTypes from 'prop-types'; | |||
3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | 3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; |
4 | import { Link } from 'react-router'; | 4 | import { Link } from 'react-router'; |
5 | import { defineMessages, intlShape } from 'react-intl'; | 5 | import { defineMessages, intlShape } from 'react-intl'; |
6 | import Confetti from 'react-confetti'; | ||
7 | import ms from 'ms'; | ||
8 | import injectSheet from 'react-jss'; | ||
6 | 9 | ||
7 | import ServiceView from './ServiceView'; | 10 | import ServiceView from './ServiceView'; |
8 | import Appear from '../../ui/effects/Appear'; | 11 | import Appear from '../../ui/effects/Appear'; |
@@ -26,7 +29,17 @@ const messages = defineMessages({ | |||
26 | }, | 29 | }, |
27 | }); | 30 | }); |
28 | 31 | ||
29 | export default @observer class Services extends Component { | 32 | |
33 | const styles = { | ||
34 | confettiContainer: { | ||
35 | position: 'absolute', | ||
36 | width: '100%', | ||
37 | zIndex: 9999, | ||
38 | pointerEvents: 'none', | ||
39 | }, | ||
40 | }; | ||
41 | |||
42 | export default @observer @injectSheet(styles) class Services extends Component { | ||
30 | static propTypes = { | 43 | static propTypes = { |
31 | services: MobxPropTypes.arrayOrObservableArray, | 44 | services: MobxPropTypes.arrayOrObservableArray, |
32 | setWebviewReference: PropTypes.func.isRequired, | 45 | setWebviewReference: PropTypes.func.isRequired, |
@@ -36,6 +49,9 @@ export default @observer class Services extends Component { | |||
36 | reload: PropTypes.func.isRequired, | 49 | reload: PropTypes.func.isRequired, |
37 | openSettings: PropTypes.func.isRequired, | 50 | openSettings: PropTypes.func.isRequired, |
38 | update: PropTypes.func.isRequired, | 51 | update: PropTypes.func.isRequired, |
52 | userHasCompletedSignup: PropTypes.bool.isRequired, | ||
53 | hasActivatedTrial: PropTypes.bool.isRequired, | ||
54 | classes: PropTypes.object.isRequired, | ||
39 | }; | 55 | }; |
40 | 56 | ||
41 | static defaultProps = { | 57 | static defaultProps = { |
@@ -46,6 +62,18 @@ export default @observer class Services extends Component { | |||
46 | intl: intlShape, | 62 | intl: intlShape, |
47 | }; | 63 | }; |
48 | 64 | ||
65 | state = { | ||
66 | showConfetti: true, | ||
67 | } | ||
68 | |||
69 | componentDidMount() { | ||
70 | window.setTimeout(() => { | ||
71 | this.setState({ | ||
72 | showConfetti: false, | ||
73 | }); | ||
74 | }, ms('8s')); | ||
75 | } | ||
76 | |||
49 | render() { | 77 | render() { |
50 | const { | 78 | const { |
51 | services, | 79 | services, |
@@ -56,12 +84,29 @@ export default @observer class Services extends Component { | |||
56 | reload, | 84 | reload, |
57 | openSettings, | 85 | openSettings, |
58 | update, | 86 | update, |
87 | userHasCompletedSignup, | ||
88 | hasActivatedTrial, | ||
89 | classes, | ||
59 | } = this.props; | 90 | } = this.props; |
91 | |||
92 | const { | ||
93 | showConfetti, | ||
94 | } = this.state; | ||
95 | |||
60 | const { intl } = this.context; | 96 | const { intl } = this.context; |
61 | const isLoggedIn = Boolean(localStorage.getItem('authToken')); | 97 | const isLoggedIn = Boolean(localStorage.getItem('authToken')); |
62 | 98 | ||
63 | return ( | 99 | return ( |
64 | <div className="services"> | 100 | <div className="services"> |
101 | {(userHasCompletedSignup || hasActivatedTrial) && ( | ||
102 | <div className={classes.confettiContainer}> | ||
103 | <Confetti | ||
104 | width={window.width} | ||
105 | height={window.height} | ||
106 | numberOfPieces={showConfetti ? 200 : 0} | ||
107 | /> | ||
108 | </div> | ||
109 | )} | ||
65 | {services.length === 0 && ( | 110 | {services.length === 0 && ( |
66 | <Appear | 111 | <Appear |
67 | timeout={1500} | 112 | timeout={1500} |
@@ -104,6 +149,7 @@ export default @observer class Services extends Component { | |||
104 | }, | 149 | }, |
105 | redirect: false, | 150 | redirect: false, |
106 | })} | 151 | })} |
152 | upgrade={() => openSettings({ path: 'user' })} | ||
107 | /> | 153 | /> |
108 | ))} | 154 | ))} |
109 | </div> | 155 | </div> |
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js index 4b7637637..f588449f4 100644 --- a/src/components/settings/account/AccountDashboard.js +++ b/src/components/settings/account/AccountDashboard.js | |||
@@ -1,14 +1,18 @@ | |||
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 } from 'mobx-react'; | 3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; |
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | import ReactTooltip from 'react-tooltip'; | 5 | import ReactTooltip from 'react-tooltip'; |
6 | import { ProBadge } from '@meetfranz/ui'; | 6 | import { |
7 | ProBadge, H1, H2, | ||
8 | } from '@meetfranz/ui'; | ||
9 | import moment from 'moment'; | ||
7 | 10 | ||
8 | import Loader from '../../ui/Loader'; | 11 | import Loader from '../../ui/Loader'; |
9 | import Button from '../../ui/Button'; | 12 | import Button from '../../ui/Button'; |
10 | import Infobox from '../../ui/Infobox'; | 13 | import Infobox from '../../ui/Infobox'; |
11 | import SubscriptionForm from '../../../containers/subscription/SubscriptionFormScreen'; | 14 | import SubscriptionForm from '../../../containers/subscription/SubscriptionFormScreen'; |
15 | import { i18nPlanName } from '../../../helpers/plan-helpers'; | ||
12 | 16 | ||
13 | const messages = defineMessages({ | 17 | const messages = defineMessages({ |
14 | headline: { | 18 | headline: { |
@@ -19,10 +23,6 @@ const messages = defineMessages({ | |||
19 | id: 'settings.account.headlineSubscription', | 23 | id: 'settings.account.headlineSubscription', |
20 | defaultMessage: '!!!Your Subscription', | 24 | defaultMessage: '!!!Your Subscription', |
21 | }, | 25 | }, |
22 | headlineUpgrade: { | ||
23 | id: 'settings.account.headlineUpgrade', | ||
24 | defaultMessage: '!!!Upgrade your Account', | ||
25 | }, | ||
26 | headlineDangerZone: { | 26 | headlineDangerZone: { |
27 | id: 'settings.account.headlineDangerZone', | 27 | id: 'settings.account.headlineDangerZone', |
28 | defaultMessage: '!!Danger Zone', | 28 | defaultMessage: '!!Danger Zone', |
@@ -31,6 +31,10 @@ const messages = defineMessages({ | |||
31 | id: 'settings.account.manageSubscription.label', | 31 | id: 'settings.account.manageSubscription.label', |
32 | defaultMessage: '!!!Manage your subscription', | 32 | defaultMessage: '!!!Manage your subscription', |
33 | }, | 33 | }, |
34 | upgradeAccountToPro: { | ||
35 | id: 'settings.account.upgradeToPro.label', | ||
36 | defaultMessage: '!!!Upgrade to Franz Professional', | ||
37 | }, | ||
34 | accountTypeBasic: { | 38 | accountTypeBasic: { |
35 | id: 'settings.account.accountType.basic', | 39 | id: 'settings.account.accountType.basic', |
36 | defaultMessage: '!!!Basic Account', | 40 | defaultMessage: '!!!Basic Account', |
@@ -71,21 +75,39 @@ const messages = defineMessages({ | |||
71 | id: 'settings.account.deleteEmailSent', | 75 | id: 'settings.account.deleteEmailSent', |
72 | defaultMessage: '!!!You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!', | 76 | defaultMessage: '!!!You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!', |
73 | }, | 77 | }, |
78 | trial: { | ||
79 | id: 'settings.account.trial', | ||
80 | defaultMessage: '!!!Free Trial', | ||
81 | }, | ||
82 | yourLicense: { | ||
83 | id: 'settings.account.yourLicense', | ||
84 | defaultMessage: '!!!Your Franz License:', | ||
85 | }, | ||
86 | trialEndsIn: { | ||
87 | id: 'settings.account.trialEndsIn', | ||
88 | defaultMessage: '!!!Your free trial ends in {duration}.', | ||
89 | }, | ||
90 | trialUpdateBillingInformation: { | ||
91 | id: 'settings.account.trialUpdateBillingInfo', | ||
92 | defaultMessage: '!!!Please update your billing info to continue using {license} after your trial period.', | ||
93 | }, | ||
74 | }); | 94 | }); |
75 | 95 | ||
76 | export default @observer class AccountDashboard extends Component { | 96 | @observer |
97 | class AccountDashboard extends Component { | ||
77 | static propTypes = { | 98 | static propTypes = { |
78 | user: MobxPropTypes.observableObject.isRequired, | 99 | user: MobxPropTypes.observableObject.isRequired, |
100 | isPremiumOverrideUser: PropTypes.bool.isRequired, | ||
101 | isProUser: PropTypes.bool.isRequired, | ||
79 | isLoading: PropTypes.bool.isRequired, | 102 | isLoading: PropTypes.bool.isRequired, |
80 | isLoadingPlans: PropTypes.bool.isRequired, | ||
81 | userInfoRequestFailed: PropTypes.bool.isRequired, | 103 | userInfoRequestFailed: PropTypes.bool.isRequired, |
82 | retryUserInfoRequest: PropTypes.func.isRequired, | 104 | retryUserInfoRequest: PropTypes.func.isRequired, |
83 | onCloseSubscriptionWindow: PropTypes.func.isRequired, | ||
84 | deleteAccount: PropTypes.func.isRequired, | 105 | deleteAccount: PropTypes.func.isRequired, |
85 | isLoadingDeleteAccount: PropTypes.bool.isRequired, | 106 | isLoadingDeleteAccount: PropTypes.bool.isRequired, |
86 | isDeleteAccountSuccessful: PropTypes.bool.isRequired, | 107 | isDeleteAccountSuccessful: PropTypes.bool.isRequired, |
87 | openEditAccount: PropTypes.func.isRequired, | 108 | openEditAccount: PropTypes.func.isRequired, |
88 | openBilling: PropTypes.func.isRequired, | 109 | openBilling: PropTypes.func.isRequired, |
110 | upgradeToPro: PropTypes.func.isRequired, | ||
89 | openInvoices: PropTypes.func.isRequired, | 111 | openInvoices: PropTypes.func.isRequired, |
90 | }; | 112 | }; |
91 | 113 | ||
@@ -96,20 +118,27 @@ export default @observer class AccountDashboard extends Component { | |||
96 | render() { | 118 | render() { |
97 | const { | 119 | const { |
98 | user, | 120 | user, |
121 | isPremiumOverrideUser, | ||
122 | isProUser, | ||
99 | isLoading, | 123 | isLoading, |
100 | isLoadingPlans, | ||
101 | userInfoRequestFailed, | 124 | userInfoRequestFailed, |
102 | retryUserInfoRequest, | 125 | retryUserInfoRequest, |
103 | onCloseSubscriptionWindow, | ||
104 | deleteAccount, | 126 | deleteAccount, |
105 | isLoadingDeleteAccount, | 127 | isLoadingDeleteAccount, |
106 | isDeleteAccountSuccessful, | 128 | isDeleteAccountSuccessful, |
107 | openEditAccount, | 129 | openEditAccount, |
108 | openBilling, | 130 | openBilling, |
131 | upgradeToPro, | ||
109 | openInvoices, | 132 | openInvoices, |
110 | } = this.props; | 133 | } = this.props; |
111 | const { intl } = this.context; | 134 | const { intl } = this.context; |
112 | 135 | ||
136 | let planName = ''; | ||
137 | |||
138 | if (user.team && user.team.plan) { | ||
139 | planName = i18nPlanName(user.team.plan, intl); | ||
140 | } | ||
141 | |||
113 | return ( | 142 | return ( |
114 | <div className="settings__main"> | 143 | <div className="settings__main"> |
115 | <div className="settings__header"> | 144 | <div className="settings__header"> |
@@ -135,82 +164,115 @@ export default @observer class AccountDashboard extends Component { | |||
135 | )} | 164 | )} |
136 | 165 | ||
137 | {!userInfoRequestFailed && ( | 166 | {!userInfoRequestFailed && ( |
138 | <Fragment> | 167 | <> |
139 | {!isLoading && ( | 168 | {!isLoading && ( |
140 | <div className="account"> | 169 | <> |
141 | <div className="account__box account__box--flex"> | 170 | <div className="account"> |
142 | <div className="account__avatar"> | 171 | <div className="account__box account__box--flex"> |
143 | <img | 172 | <div className="account__avatar"> |
144 | src="./assets/images/logo.svg" | 173 | <img |
145 | alt="" | 174 | src="./assets/images/logo.svg" |
146 | /> | 175 | alt="" |
147 | </div> | 176 | /> |
148 | <div className="account__info"> | 177 | </div> |
149 | <h2> | 178 | <div className="account__info"> |
150 | <span className="username">{`${user.firstname} ${user.lastname}`}</span> | 179 | <H1> |
180 | <span className="username">{`${user.firstname} ${user.lastname}`}</span> | ||
181 | {user.isPremium && ( | ||
182 | <> | ||
183 | {' '} | ||
184 | <ProBadge /> | ||
185 | </> | ||
186 | )} | ||
187 | </H1> | ||
188 | <p> | ||
189 | {user.organization && `${user.organization}, `} | ||
190 | {user.email} | ||
191 | </p> | ||
151 | {user.isPremium && ( | 192 | {user.isPremium && ( |
193 | <div className="manage-user-links"> | ||
194 | <Button | ||
195 | label={intl.formatMessage(messages.accountEditButton)} | ||
196 | className="franz-form__button--inverted" | ||
197 | onClick={openEditAccount} | ||
198 | /> | ||
199 | </div> | ||
200 | )} | ||
201 | </div> | ||
202 | {!user.isPremium && ( | ||
203 | <Button | ||
204 | label={intl.formatMessage(messages.accountEditButton)} | ||
205 | className="franz-form__button--inverted" | ||
206 | onClick={openEditAccount} | ||
207 | /> | ||
208 | )} | ||
209 | </div> | ||
210 | </div> | ||
211 | {user.isPremium && user.isSubscriptionOwner && ( | ||
212 | <div className="account"> | ||
213 | <div className="account__box"> | ||
214 | <H2> | ||
215 | {intl.formatMessage(messages.yourLicense)} | ||
216 | </H2> | ||
217 | <p> | ||
218 | {isPremiumOverrideUser ? 'Franz Premium' : planName} | ||
219 | {user.team.isTrial && ( | ||
220 | <> | ||
221 | {' – '} | ||
222 | {intl.formatMessage(messages.trial)} | ||
223 | </> | ||
224 | )} | ||
225 | </p> | ||
226 | {user.team.isTrial && ( | ||
152 | <> | 227 | <> |
153 | {' '} | 228 | <br /> |
154 | <ProBadge /> | 229 | <p> |
155 | <span className="badge badge--premium">{intl.formatMessage(messages.accountTypePremium)}</span> | 230 | {intl.formatMessage(messages.trialEndsIn, { |
231 | duration: moment.duration(moment().diff(user.team.trialEnd)).humanize(), | ||
232 | })} | ||
233 | </p> | ||
234 | <p> | ||
235 | {intl.formatMessage(messages.trialUpdateBillingInformation, { | ||
236 | license: planName, | ||
237 | })} | ||
238 | </p> | ||
156 | </> | 239 | </> |
157 | )} | 240 | )} |
158 | </h2> | ||
159 | {user.organization && `${user.organization}, `} | ||
160 | {user.email} | ||
161 | {user.isPremium && ( | ||
162 | <div className="manage-user-links"> | 241 | <div className="manage-user-links"> |
242 | {!isProUser && ( | ||
243 | <Button | ||
244 | label={intl.formatMessage(messages.upgradeAccountToPro)} | ||
245 | className="franz-form__button--primary" | ||
246 | onClick={upgradeToPro} | ||
247 | /> | ||
248 | )} | ||
163 | <Button | 249 | <Button |
164 | label={intl.formatMessage(messages.accountEditButton)} | 250 | label={intl.formatMessage(messages.manageSubscriptionButtonLabel)} |
165 | className="franz-form__button--inverted" | 251 | className="franz-form__button--inverted" |
166 | onClick={openEditAccount} | 252 | onClick={openBilling} |
253 | /> | ||
254 | <Button | ||
255 | label={intl.formatMessage(messages.invoicesButton)} | ||
256 | className="franz-form__button--inverted" | ||
257 | onClick={openInvoices} | ||
167 | /> | 258 | /> |
168 | {user.isSubscriptionOwner && ( | ||
169 | <> | ||
170 | <Button | ||
171 | label={intl.formatMessage(messages.manageSubscriptionButtonLabel)} | ||
172 | className="franz-form__button--inverted" | ||
173 | onClick={openBilling} | ||
174 | /> | ||
175 | <Button | ||
176 | label={intl.formatMessage(messages.invoicesButton)} | ||
177 | className="franz-form__button--inverted" | ||
178 | onClick={openInvoices} | ||
179 | /> | ||
180 | </> | ||
181 | )} | ||
182 | </div> | 259 | </div> |
183 | )} | 260 | </div> |
184 | </div> | 261 | </div> |
185 | {!user.isPremium && ( | 262 | )} |
186 | <Button | 263 | {!user.isPremium && ( |
187 | label={intl.formatMessage(messages.accountEditButton)} | 264 | <div className="account franz-form"> |
188 | className="franz-form__button--inverted" | 265 | <div className="account__box"> |
189 | onClick={openEditAccount} | 266 | <SubscriptionForm /> |
190 | /> | 267 | </div> |
191 | )} | ||
192 | </div> | ||
193 | </div> | ||
194 | )} | ||
195 | |||
196 | {!user.isPremium && ( | ||
197 | isLoadingPlans ? ( | ||
198 | <Loader /> | ||
199 | ) : ( | ||
200 | <div className="account franz-form"> | ||
201 | <div className="account__box"> | ||
202 | <h2>{intl.formatMessage(messages.headlineUpgrade)}</h2> | ||
203 | <SubscriptionForm | ||
204 | onCloseWindow={onCloseSubscriptionWindow} | ||
205 | /> | ||
206 | </div> | 268 | </div> |
207 | </div> | 269 | )} |
208 | ) | 270 | </> |
209 | )} | 271 | )} |
210 | 272 | ||
211 | <div className="account franz-form"> | 273 | <div className="account franz-form"> |
212 | <div className="account__box"> | 274 | <div className="account__box"> |
213 | <h2>{intl.formatMessage(messages.headlineDangerZone)}</h2> | 275 | <H2>{intl.formatMessage(messages.headlineDangerZone)}</H2> |
214 | {!isDeleteAccountSuccessful && ( | 276 | {!isDeleteAccountSuccessful && ( |
215 | <div className="account__subscription"> | 277 | <div className="account__subscription"> |
216 | <p>{intl.formatMessage(messages.deleteInfo)}</p> | 278 | <p>{intl.formatMessage(messages.deleteInfo)}</p> |
@@ -227,7 +289,7 @@ export default @observer class AccountDashboard extends Component { | |||
227 | )} | 289 | )} |
228 | </div> | 290 | </div> |
229 | </div> | 291 | </div> |
230 | </Fragment> | 292 | </> |
231 | )} | 293 | )} |
232 | </div> | 294 | </div> |
233 | <ReactTooltip place="right" type="dark" effect="solid" /> | 295 | <ReactTooltip place="right" type="dark" effect="solid" /> |
@@ -235,3 +297,5 @@ export default @observer class AccountDashboard extends Component { | |||
235 | ); | 297 | ); |
236 | } | 298 | } |
237 | } | 299 | } |
300 | |||
301 | export default AccountDashboard; | ||
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js index 6aa9bda03..201819526 100644 --- a/src/components/settings/navigation/SettingsNavigation.js +++ b/src/components/settings/navigation/SettingsNavigation.js | |||
@@ -8,6 +8,7 @@ import Link from '../../ui/Link'; | |||
8 | import { workspaceStore } from '../../../features/workspaces'; | 8 | import { workspaceStore } from '../../../features/workspaces'; |
9 | import UIStore from '../../../stores/UIStore'; | 9 | import UIStore from '../../../stores/UIStore'; |
10 | import UserStore from '../../../stores/UserStore'; | 10 | import UserStore from '../../../stores/UserStore'; |
11 | import { serviceLimitStore } from '../../../features/serviceLimit'; | ||
11 | 12 | ||
12 | const messages = defineMessages({ | 13 | const messages = defineMessages({ |
13 | availableServices: { | 14 | availableServices: { |
@@ -81,7 +82,12 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp | |||
81 | > | 82 | > |
82 | {intl.formatMessage(messages.yourServices)} | 83 | {intl.formatMessage(messages.yourServices)} |
83 | {' '} | 84 | {' '} |
84 | <span className="badge">{serviceCount}</span> | 85 | <span className="badge"> |
86 | {serviceCount} | ||
87 | {serviceLimitStore.serviceLimit !== 0 && ( | ||
88 | `/${serviceLimitStore.serviceLimit}` | ||
89 | )} | ||
90 | </span> | ||
85 | </Link> | 91 | </Link> |
86 | {workspaceStore.isFeatureEnabled ? ( | 92 | {workspaceStore.isFeatureEnabled ? ( |
87 | <Link | 93 | <Link |
diff --git a/src/components/settings/recipes/RecipeItem.js b/src/components/settings/recipes/RecipeItem.js index 3bb0852b2..12e3775f6 100644 --- a/src/components/settings/recipes/RecipeItem.js +++ b/src/components/settings/recipes/RecipeItem.js | |||
@@ -19,7 +19,7 @@ export default @observer class RecipeItem extends Component { | |||
19 | className="recipe-teaser" | 19 | className="recipe-teaser" |
20 | onClick={onClick} | 20 | onClick={onClick} |
21 | > | 21 | > |
22 | {recipe.local && ( | 22 | {recipe.isDevRecipe && ( |
23 | <span className="recipe-teaser__dev-badge">dev</span> | 23 | <span className="recipe-teaser__dev-badge">dev</span> |
24 | )} | 24 | )} |
25 | <img | 25 | <img |
diff --git a/src/components/settings/recipes/RecipesDashboard.js b/src/components/settings/recipes/RecipesDashboard.js index 00cd725cf..877cbc588 100644 --- a/src/components/settings/recipes/RecipesDashboard.js +++ b/src/components/settings/recipes/RecipesDashboard.js | |||
@@ -4,12 +4,17 @@ import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | |||
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | import { Link } from 'react-router'; | 5 | import { Link } from 'react-router'; |
6 | 6 | ||
7 | import { Button, Input } from '@meetfranz/forms'; | ||
8 | import injectSheet from 'react-jss'; | ||
9 | import { H3, H2, ProBadge } from '@meetfranz/ui'; | ||
7 | import SearchInput from '../../ui/SearchInput'; | 10 | import SearchInput from '../../ui/SearchInput'; |
8 | import Infobox from '../../ui/Infobox'; | 11 | import Infobox from '../../ui/Infobox'; |
9 | import RecipeItem from './RecipeItem'; | 12 | import RecipeItem from './RecipeItem'; |
10 | import Loader from '../../ui/Loader'; | 13 | import Loader from '../../ui/Loader'; |
11 | import Appear from '../../ui/effects/Appear'; | 14 | import Appear from '../../ui/effects/Appear'; |
12 | import { FRANZ_SERVICE_REQUEST } from '../../../config'; | 15 | import { FRANZ_SERVICE_REQUEST } from '../../../config'; |
16 | import LimitReachedInfobox from '../../../features/serviceLimit/components/LimitReachedInfobox'; | ||
17 | import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer'; | ||
13 | 18 | ||
14 | const messages = defineMessages({ | 19 | const messages = defineMessages({ |
15 | headline: { | 20 | headline: { |
@@ -28,9 +33,9 @@ const messages = defineMessages({ | |||
28 | id: 'settings.recipes.all', | 33 | id: 'settings.recipes.all', |
29 | defaultMessage: '!!!All services', | 34 | defaultMessage: '!!!All services', |
30 | }, | 35 | }, |
31 | devRecipes: { | 36 | customRecipes: { |
32 | id: 'settings.recipes.dev', | 37 | id: 'settings.recipes.custom', |
33 | defaultMessage: '!!!Development', | 38 | defaultMessage: '!!!Custom Services', |
34 | }, | 39 | }, |
35 | nothingFound: { | 40 | nothingFound: { |
36 | id: 'settings.recipes.nothingFound', | 41 | id: 'settings.recipes.nothingFound', |
@@ -44,9 +49,61 @@ const messages = defineMessages({ | |||
44 | id: 'settings.recipes.missingService', | 49 | id: 'settings.recipes.missingService', |
45 | defaultMessage: '!!!Missing a service?', | 50 | defaultMessage: '!!!Missing a service?', |
46 | }, | 51 | }, |
52 | customRecipeIntro: { | ||
53 | id: 'settings.recipes.customService.intro', | ||
54 | defaultMessage: '!!!To add a custom service, copy the recipe folder into:', | ||
55 | }, | ||
56 | openFolder: { | ||
57 | id: 'settings.recipes.customService.openFolder', | ||
58 | defaultMessage: '!!!Open directory', | ||
59 | }, | ||
60 | openDevDocs: { | ||
61 | id: 'settings.recipes.customService.openDevDocs', | ||
62 | defaultMessage: '!!!Developer Documentation', | ||
63 | }, | ||
64 | headlineCustomRecipes: { | ||
65 | id: 'settings.recipes.customService.headline.customRecipes', | ||
66 | defaultMessage: '!!!Custom 3rd Party Recipes', | ||
67 | }, | ||
68 | headlineCommunityRecipes: { | ||
69 | id: 'settings.recipes.customService.headline.communityRecipes', | ||
70 | defaultMessage: '!!!Community 3rd Party Recipes', | ||
71 | }, | ||
72 | headlineDevRecipes: { | ||
73 | id: 'settings.recipes.customService.headline.devRecipes', | ||
74 | defaultMessage: '!!!Your Development Service Recipes', | ||
75 | }, | ||
47 | }); | 76 | }); |
48 | 77 | ||
49 | export default @observer class RecipesDashboard extends Component { | 78 | const styles = { |
79 | devRecipeIntroContainer: { | ||
80 | textAlign: 'center', | ||
81 | width: '100%', | ||
82 | height: 'auto', | ||
83 | margin: [40, 0], | ||
84 | }, | ||
85 | path: { | ||
86 | marginTop: 20, | ||
87 | |||
88 | '& > div': { | ||
89 | fontFamily: 'SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace', | ||
90 | }, | ||
91 | }, | ||
92 | actionContainer: { | ||
93 | '& button': { | ||
94 | margin: [0, 10], | ||
95 | }, | ||
96 | }, | ||
97 | devRecipeList: { | ||
98 | marginTop: 20, | ||
99 | height: 'auto', | ||
100 | }, | ||
101 | proBadge: { | ||
102 | marginLeft: '10px !important', | ||
103 | }, | ||
104 | }; | ||
105 | |||
106 | export default @injectSheet(styles) @observer class RecipesDashboard extends Component { | ||
50 | static propTypes = { | 107 | static propTypes = { |
51 | recipes: MobxPropTypes.arrayOrObservableArray.isRequired, | 108 | recipes: MobxPropTypes.arrayOrObservableArray.isRequired, |
52 | isLoading: PropTypes.bool.isRequired, | 109 | isLoading: PropTypes.bool.isRequired, |
@@ -55,12 +112,18 @@ export default @observer class RecipesDashboard extends Component { | |||
55 | searchRecipes: PropTypes.func.isRequired, | 112 | searchRecipes: PropTypes.func.isRequired, |
56 | resetSearch: PropTypes.func.isRequired, | 113 | resetSearch: PropTypes.func.isRequired, |
57 | serviceStatus: MobxPropTypes.arrayOrObservableArray.isRequired, | 114 | serviceStatus: MobxPropTypes.arrayOrObservableArray.isRequired, |
58 | devRecipesCount: PropTypes.number.isRequired, | ||
59 | searchNeedle: PropTypes.string, | 115 | searchNeedle: PropTypes.string, |
116 | recipeFilter: PropTypes.string, | ||
117 | recipeDirectory: PropTypes.string.isRequired, | ||
118 | openRecipeDirectory: PropTypes.func.isRequired, | ||
119 | openDevDocs: PropTypes.func.isRequired, | ||
120 | classes: PropTypes.object.isRequired, | ||
121 | isCommunityRecipesIncludedInCurrentPlan: PropTypes.bool.isRequired, | ||
60 | }; | 122 | }; |
61 | 123 | ||
62 | static defaultProps = { | 124 | static defaultProps = { |
63 | searchNeedle: '', | 125 | searchNeedle: '', |
126 | recipeFilter: 'all', | ||
64 | } | 127 | } |
65 | 128 | ||
66 | static contextTypes = { | 129 | static contextTypes = { |
@@ -76,16 +139,26 @@ export default @observer class RecipesDashboard extends Component { | |||
76 | searchRecipes, | 139 | searchRecipes, |
77 | resetSearch, | 140 | resetSearch, |
78 | serviceStatus, | 141 | serviceStatus, |
79 | devRecipesCount, | ||
80 | searchNeedle, | 142 | searchNeedle, |
143 | recipeFilter, | ||
144 | recipeDirectory, | ||
145 | openRecipeDirectory, | ||
146 | openDevDocs, | ||
147 | classes, | ||
148 | isCommunityRecipesIncludedInCurrentPlan, | ||
81 | } = this.props; | 149 | } = this.props; |
82 | const { intl } = this.context; | 150 | const { intl } = this.context; |
83 | 151 | ||
152 | |||
153 | const communityRecipes = recipes.filter(r => !r.isDevRecipe); | ||
154 | const devRecipes = recipes.filter(r => r.isDevRecipe); | ||
155 | |||
84 | return ( | 156 | return ( |
85 | <div className="settings__main"> | 157 | <div className="settings__main"> |
86 | <div className="settings__header"> | 158 | <div className="settings__header"> |
87 | <h1>{intl.formatMessage(messages.headline)}</h1> | 159 | <h1>{intl.formatMessage(messages.headline)}</h1> |
88 | </div> | 160 | </div> |
161 | <LimitReachedInfobox /> | ||
89 | <div className="settings__body recipes"> | 162 | <div className="settings__body recipes"> |
90 | {serviceStatus.length > 0 && serviceStatus.includes('created') && ( | 163 | {serviceStatus.length > 0 && serviceStatus.includes('created') && ( |
91 | <Appear> | 164 | <Appear> |
@@ -122,20 +195,14 @@ export default @observer class RecipesDashboard extends Component { | |||
122 | > | 195 | > |
123 | {intl.formatMessage(messages.allRecipes)} | 196 | {intl.formatMessage(messages.allRecipes)} |
124 | </Link> | 197 | </Link> |
125 | {devRecipesCount > 0 && ( | 198 | <Link |
126 | <Link | 199 | to="/settings/recipes/dev" |
127 | to="/settings/recipes/dev" | 200 | className="badge" |
128 | className="badge" | 201 | activeClassName={`${!searchNeedle ? 'badge--primary' : ''}`} |
129 | activeClassName={`${!searchNeedle ? 'badge--primary' : ''}`} | 202 | onClick={() => resetSearch()} |
130 | onClick={() => resetSearch()} | 203 | > |
131 | > | 204 | {intl.formatMessage(messages.customRecipes)} |
132 | {intl.formatMessage(messages.devRecipes)} | 205 | </Link> |
133 | {' '} | ||
134 | ( | ||
135 | {devRecipesCount} | ||
136 | ) | ||
137 | </Link> | ||
138 | )} | ||
139 | <a href={FRANZ_SERVICE_REQUEST} target="_blank" className="link recipes__service-request"> | 206 | <a href={FRANZ_SERVICE_REQUEST} target="_blank" className="link recipes__service-request"> |
140 | {intl.formatMessage(messages.missingService)} | 207 | {intl.formatMessage(messages.missingService)} |
141 | {' '} | 208 | {' '} |
@@ -146,23 +213,78 @@ export default @observer class RecipesDashboard extends Component { | |||
146 | {isLoading ? ( | 213 | {isLoading ? ( |
147 | <Loader /> | 214 | <Loader /> |
148 | ) : ( | 215 | ) : ( |
149 | <div className="recipes__list"> | 216 | <> |
150 | {hasLoadedRecipes && recipes.length === 0 && ( | 217 | {recipeFilter === 'dev' && ( |
151 | <p className="align-middle settings__empty-state"> | 218 | <> |
152 | <span className="emoji"> | 219 | <H2> |
153 | <img src="./assets/images/emoji/dontknow.png" alt="" /> | 220 | {intl.formatMessage(messages.headlineCustomRecipes)} |
154 | </span> | 221 | {!isCommunityRecipesIncludedInCurrentPlan && ( |
155 | {intl.formatMessage(messages.nothingFound)} | 222 | <ProBadge className={classes.proBadge} /> |
156 | </p> | 223 | )} |
224 | </H2> | ||
225 | <div className={classes.devRecipeIntroContainer}> | ||
226 | <p> | ||
227 | {intl.formatMessage(messages.customRecipeIntro)} | ||
228 | </p> | ||
229 | <Input | ||
230 | value={recipeDirectory} | ||
231 | className={classes.path} | ||
232 | showLabel={false} | ||
233 | /> | ||
234 | <div className={classes.actionContainer}> | ||
235 | <Button | ||
236 | onClick={openRecipeDirectory} | ||
237 | buttonType="secondary" | ||
238 | label={intl.formatMessage(messages.openFolder)} | ||
239 | /> | ||
240 | <Button | ||
241 | onClick={openDevDocs} | ||
242 | buttonType="secondary" | ||
243 | label={intl.formatMessage(messages.openDevDocs)} | ||
244 | /> | ||
245 | </div> | ||
246 | </div> | ||
247 | </> | ||
248 | )} | ||
249 | <PremiumFeatureContainer | ||
250 | condition={(recipeFilter === 'dev' && communityRecipes.length > 0) && !isCommunityRecipesIncludedInCurrentPlan} | ||
251 | > | ||
252 | {recipeFilter === 'dev' && communityRecipes.length > 0 && ( | ||
253 | <H3>{intl.formatMessage(messages.headlineCommunityRecipes)}</H3> | ||
254 | )} | ||
255 | <div className="recipes__list"> | ||
256 | {hasLoadedRecipes && recipes.length === 0 && recipeFilter !== 'dev' && ( | ||
257 | <p className="align-middle settings__empty-state"> | ||
258 | <span className="emoji"> | ||
259 | <img src="./assets/images/emoji/dontknow.png" alt="" /> | ||
260 | </span> | ||
261 | {intl.formatMessage(messages.nothingFound)} | ||
262 | </p> | ||
263 | )} | ||
264 | {communityRecipes.map(recipe => ( | ||
265 | <RecipeItem | ||
266 | key={recipe.id} | ||
267 | recipe={recipe} | ||
268 | onClick={() => showAddServiceInterface({ recipeId: recipe.id })} | ||
269 | /> | ||
270 | ))} | ||
271 | </div> | ||
272 | </PremiumFeatureContainer> | ||
273 | {recipeFilter === 'dev' && devRecipes.length > 0 && ( | ||
274 | <div className={classes.devRecipeList}> | ||
275 | <H3>{intl.formatMessage(messages.headlineDevRecipes)}</H3> | ||
276 | <div className="recipes__list"> | ||
277 | {devRecipes.map(recipe => ( | ||
278 | <RecipeItem | ||
279 | key={recipe.id} | ||
280 | recipe={recipe} | ||
281 | onClick={() => showAddServiceInterface({ recipeId: recipe.id })} | ||
282 | /> | ||
283 | ))} | ||
284 | </div> | ||
285 | </div> | ||
157 | )} | 286 | )} |
158 | {recipes.map(recipe => ( | 287 | </> |
159 | <RecipeItem | ||
160 | key={recipe.id} | ||
161 | recipe={recipe} | ||
162 | onClick={() => showAddServiceInterface({ recipeId: recipe.id })} | ||
163 | /> | ||
164 | ))} | ||
165 | </div> | ||
166 | )} | 288 | )} |
167 | </div> | 289 | </div> |
168 | </div> | 290 | </div> |
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js index 711b571e2..5fe00cb8b 100644 --- a/src/components/settings/services/EditServiceForm.js +++ b/src/components/settings/services/EditServiceForm.js | |||
@@ -17,6 +17,8 @@ import ImageUpload from '../../ui/ImageUpload'; | |||
17 | import Select from '../../ui/Select'; | 17 | import Select from '../../ui/Select'; |
18 | 18 | ||
19 | import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer'; | 19 | import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer'; |
20 | import LimitReachedInfobox from '../../../features/serviceLimit/components/LimitReachedInfobox'; | ||
21 | import { serviceLimitStore } from '../../../features/serviceLimit'; | ||
20 | 22 | ||
21 | const messages = defineMessages({ | 23 | const messages = defineMessages({ |
22 | saveService: { | 24 | saveService: { |
@@ -128,8 +130,8 @@ export default @observer class EditServiceForm extends Component { | |||
128 | isSaving: PropTypes.bool.isRequired, | 130 | isSaving: PropTypes.bool.isRequired, |
129 | isDeleting: PropTypes.bool.isRequired, | 131 | isDeleting: PropTypes.bool.isRequired, |
130 | isProxyFeatureEnabled: PropTypes.bool.isRequired, | 132 | isProxyFeatureEnabled: PropTypes.bool.isRequired, |
131 | isProxyPremiumFeature: PropTypes.bool.isRequired, | 133 | isServiceProxyIncludedInCurrentPlan: PropTypes.bool.isRequired, |
132 | isSpellcheckerPremiumFeature: PropTypes.bool.isRequired, | 134 | isSpellcheckerIncludedInCurrentPlan: PropTypes.bool.isRequired, |
133 | }; | 135 | }; |
134 | 136 | ||
135 | static defaultProps = { | 137 | static defaultProps = { |
@@ -192,8 +194,8 @@ export default @observer class EditServiceForm extends Component { | |||
192 | isDeleting, | 194 | isDeleting, |
193 | onDelete, | 195 | onDelete, |
194 | isProxyFeatureEnabled, | 196 | isProxyFeatureEnabled, |
195 | isProxyPremiumFeature, | 197 | isServiceProxyIncludedInCurrentPlan, |
196 | isSpellcheckerPremiumFeature, | 198 | isSpellcheckerIncludedInCurrentPlan, |
197 | } = this.props; | 199 | } = this.props; |
198 | const { intl } = this.context; | 200 | const { intl } = this.context; |
199 | 201 | ||
@@ -252,6 +254,7 @@ export default @observer class EditServiceForm extends Component { | |||
252 | )} | 254 | )} |
253 | </span> | 255 | </span> |
254 | </div> | 256 | </div> |
257 | <LimitReachedInfobox /> | ||
255 | <div className="settings__body"> | 258 | <div className="settings__body"> |
256 | <form onSubmit={e => this.submit(e)} id="form"> | 259 | <form onSubmit={e => this.submit(e)} id="form"> |
257 | <div className="service-name"> | 260 | <div className="service-name"> |
@@ -342,7 +345,7 @@ export default @observer class EditServiceForm extends Component { | |||
342 | </div> | 345 | </div> |
343 | 346 | ||
344 | <PremiumFeatureContainer | 347 | <PremiumFeatureContainer |
345 | condition={isSpellcheckerPremiumFeature} | 348 | condition={!isSpellcheckerIncludedInCurrentPlan} |
346 | gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }} | 349 | gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }} |
347 | > | 350 | > |
348 | <div className="settings__settings-group"> | 351 | <div className="settings__settings-group"> |
@@ -352,7 +355,7 @@ export default @observer class EditServiceForm extends Component { | |||
352 | 355 | ||
353 | {isProxyFeatureEnabled && ( | 356 | {isProxyFeatureEnabled && ( |
354 | <PremiumFeatureContainer | 357 | <PremiumFeatureContainer |
355 | condition={isProxyPremiumFeature} | 358 | condition={!isServiceProxyIncludedInCurrentPlan} |
356 | gaEventInfo={{ category: 'User', event: 'upgrade', label: 'proxy' }} | 359 | gaEventInfo={{ category: 'User', event: 'upgrade', label: 'proxy' }} |
357 | > | 360 | > |
358 | <div className="settings__settings-group"> | 361 | <div className="settings__settings-group"> |
@@ -418,7 +421,7 @@ export default @observer class EditServiceForm extends Component { | |||
418 | type="submit" | 421 | type="submit" |
419 | label={intl.formatMessage(messages.saveService)} | 422 | label={intl.formatMessage(messages.saveService)} |
420 | htmlForm="form" | 423 | htmlForm="form" |
421 | disabled={action !== 'edit' && form.isPristine && requiresUserInput} | 424 | disabled={action !== 'edit' && ((form.isPristine && requiresUserInput) || serviceLimitStore.userHasReachedServiceLimit)} |
422 | /> | 425 | /> |
423 | )} | 426 | )} |
424 | </div> | 427 | </div> |
diff --git a/src/components/settings/services/ServicesDashboard.js b/src/components/settings/services/ServicesDashboard.js index 53bae12df..78038e86a 100644 --- a/src/components/settings/services/ServicesDashboard.js +++ b/src/components/settings/services/ServicesDashboard.js | |||
@@ -9,6 +9,7 @@ import Infobox from '../../ui/Infobox'; | |||
9 | import Loader from '../../ui/Loader'; | 9 | import Loader from '../../ui/Loader'; |
10 | import ServiceItem from './ServiceItem'; | 10 | import ServiceItem from './ServiceItem'; |
11 | import Appear from '../../ui/effects/Appear'; | 11 | import Appear from '../../ui/effects/Appear'; |
12 | import LimitReachedInfobox from '../../../features/serviceLimit/components/LimitReachedInfobox'; | ||
12 | 13 | ||
13 | const messages = defineMessages({ | 14 | const messages = defineMessages({ |
14 | headline: { | 15 | headline: { |
@@ -91,6 +92,7 @@ export default @observer class ServicesDashboard extends Component { | |||
91 | <div className="settings__header"> | 92 | <div className="settings__header"> |
92 | <h1>{intl.formatMessage(messages.headline)}</h1> | 93 | <h1>{intl.formatMessage(messages.headline)}</h1> |
93 | </div> | 94 | </div> |
95 | <LimitReachedInfobox /> | ||
94 | <div className="settings__body"> | 96 | <div className="settings__body"> |
95 | {!isLoading && ( | 97 | {!isLoading && ( |
96 | <SearchInput | 98 | <SearchInput |
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js index 660c3c109..19333fdff 100644 --- a/src/components/settings/settings/EditSettingsForm.js +++ b/src/components/settings/settings/EditSettingsForm.js | |||
@@ -105,7 +105,8 @@ export default @observer class EditSettingsForm extends Component { | |||
105 | isClearingAllCache: PropTypes.bool.isRequired, | 105 | isClearingAllCache: PropTypes.bool.isRequired, |
106 | onClearAllCache: PropTypes.func.isRequired, | 106 | onClearAllCache: PropTypes.func.isRequired, |
107 | cacheSize: PropTypes.string.isRequired, | 107 | cacheSize: PropTypes.string.isRequired, |
108 | isSpellcheckerPremiumFeature: PropTypes.bool.isRequired, | 108 | isSpellcheckerIncludedInCurrentPlan: PropTypes.bool.isRequired, |
109 | isTodosEnabled: PropTypes.bool.isRequired, | ||
109 | }; | 110 | }; |
110 | 111 | ||
111 | static contextTypes = { | 112 | static contextTypes = { |
@@ -135,7 +136,8 @@ export default @observer class EditSettingsForm extends Component { | |||
135 | isClearingAllCache, | 136 | isClearingAllCache, |
136 | onClearAllCache, | 137 | onClearAllCache, |
137 | cacheSize, | 138 | cacheSize, |
138 | isSpellcheckerPremiumFeature, | 139 | isSpellcheckerIncludedInCurrentPlan, |
140 | isTodosEnabled, | ||
139 | } = this.props; | 141 | } = this.props; |
140 | const { intl } = this.context; | 142 | const { intl } = this.context; |
141 | 143 | ||
@@ -178,6 +180,9 @@ export default @observer class EditSettingsForm extends Component { | |||
178 | { isLoggedIn && ( | 180 | { isLoggedIn && ( |
179 | <p>{ intl.formatMessage(messages.serverInfo) }</p> | 181 | <p>{ intl.formatMessage(messages.serverInfo) }</p> |
180 | )} | 182 | )} |
183 | {isTodosEnabled && ( | ||
184 | <Toggle field={form.$('enableTodos')} /> | ||
185 | )} | ||
181 | 186 | ||
182 | {/* Appearance */} | 187 | {/* Appearance */} |
183 | <h2 id="apperance">{intl.formatMessage(messages.headlineAppearance)}</h2> | 188 | <h2 id="apperance">{intl.formatMessage(messages.headlineAppearance)}</h2> |
@@ -189,7 +194,7 @@ export default @observer class EditSettingsForm extends Component { | |||
189 | <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2> | 194 | <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2> |
190 | <Select field={form.$('locale')} showLabel={false} /> | 195 | <Select field={form.$('locale')} showLabel={false} /> |
191 | <PremiumFeatureContainer | 196 | <PremiumFeatureContainer |
192 | condition={isSpellcheckerPremiumFeature} | 197 | condition={!isSpellcheckerIncludedInCurrentPlan} |
193 | gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }} | 198 | gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }} |
194 | > | 199 | > |
195 | <Fragment> | 200 | <Fragment> |
diff --git a/src/components/settings/team/TeamDashboard.js b/src/components/settings/team/TeamDashboard.js index 05c942a11..2bf46b48d 100644 --- a/src/components/settings/team/TeamDashboard.js +++ b/src/components/settings/team/TeamDashboard.js | |||
@@ -4,11 +4,14 @@ import { observer } from 'mobx-react'; | |||
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | import ReactTooltip from 'react-tooltip'; | 5 | import ReactTooltip from 'react-tooltip'; |
6 | import injectSheet from 'react-jss'; | 6 | import injectSheet from 'react-jss'; |
7 | import classnames from 'classnames'; | ||
7 | 8 | ||
9 | import { Badge } from '@meetfranz/ui'; | ||
8 | import Loader from '../../ui/Loader'; | 10 | import Loader from '../../ui/Loader'; |
9 | import Button from '../../ui/Button'; | 11 | import Button from '../../ui/Button'; |
10 | import Infobox from '../../ui/Infobox'; | 12 | import Infobox from '../../ui/Infobox'; |
11 | import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer'; | 13 | import globalMessages from '../../../i18n/globalMessages'; |
14 | import UpgradeButton from '../../ui/UpgradeButton'; | ||
12 | 15 | ||
13 | const messages = defineMessages({ | 16 | const messages = defineMessages({ |
14 | headline: { | 17 | headline: { |
@@ -40,6 +43,7 @@ const messages = defineMessages({ | |||
40 | const styles = { | 43 | const styles = { |
41 | cta: { | 44 | cta: { |
42 | margin: [40, 'auto'], | 45 | margin: [40, 'auto'], |
46 | height: 'auto', | ||
43 | }, | 47 | }, |
44 | container: { | 48 | container: { |
45 | display: 'flex', | 49 | display: 'flex', |
@@ -69,6 +73,20 @@ const styles = { | |||
69 | order: 1, | 73 | order: 1, |
70 | }, | 74 | }, |
71 | }, | 75 | }, |
76 | headline: { | ||
77 | marginBottom: 0, | ||
78 | }, | ||
79 | headlineWithSpacing: { | ||
80 | marginBottom: 'inherit', | ||
81 | }, | ||
82 | proRequired: { | ||
83 | margin: [10, 0, 40], | ||
84 | height: 'auto', | ||
85 | }, | ||
86 | buttonContainer: { | ||
87 | display: 'flex', | ||
88 | height: 'auto', | ||
89 | }, | ||
72 | }; | 90 | }; |
73 | 91 | ||
74 | 92 | ||
@@ -79,6 +97,7 @@ export default @injectSheet(styles) @observer class TeamDashboard extends Compon | |||
79 | retryUserInfoRequest: PropTypes.func.isRequired, | 97 | retryUserInfoRequest: PropTypes.func.isRequired, |
80 | openTeamManagement: PropTypes.func.isRequired, | 98 | openTeamManagement: PropTypes.func.isRequired, |
81 | classes: PropTypes.object.isRequired, | 99 | classes: PropTypes.object.isRequired, |
100 | isProUser: PropTypes.bool.isRequired, | ||
82 | }; | 101 | }; |
83 | 102 | ||
84 | static contextTypes = { | 103 | static contextTypes = { |
@@ -91,6 +110,7 @@ export default @injectSheet(styles) @observer class TeamDashboard extends Compon | |||
91 | userInfoRequestFailed, | 110 | userInfoRequestFailed, |
92 | retryUserInfoRequest, | 111 | retryUserInfoRequest, |
93 | openTeamManagement, | 112 | openTeamManagement, |
113 | isProUser, | ||
94 | classes, | 114 | classes, |
95 | } = this.props; | 115 | } = this.props; |
96 | const { intl } = this.context; | 116 | const { intl } = this.context; |
@@ -123,23 +143,42 @@ export default @injectSheet(styles) @observer class TeamDashboard extends Compon | |||
123 | <> | 143 | <> |
124 | {!isLoading && ( | 144 | {!isLoading && ( |
125 | <> | 145 | <> |
126 | <PremiumFeatureContainer> | 146 | <> |
127 | <> | 147 | <h1 className={classnames({ |
128 | <h1>{intl.formatMessage(messages.contentHeadline)}</h1> | 148 | [classes.headline]: true, |
129 | <div className={classes.container}> | 149 | [classes.headlineWithSpacing]: isProUser, |
130 | <div className={classes.content}> | 150 | })} |
131 | <p>{intl.formatMessage(messages.intro)}</p> | 151 | > |
132 | <p>{intl.formatMessage(messages.copy)}</p> | 152 | {intl.formatMessage(messages.contentHeadline)} |
133 | </div> | 153 | |
134 | <img className={classes.image} src="https://cdn.franzinfra.com/announcements/assets/teams.png" alt="Ferdi for Teams" /> | 154 | </h1> |
155 | {!isProUser && ( | ||
156 | <Badge className={classes.proRequired}>{intl.formatMessage(globalMessages.proRequired)}</Badge> | ||
157 | )} | ||
158 | <div className={classes.container}> | ||
159 | <div className={classes.content}> | ||
160 | <p>{intl.formatMessage(messages.intro)}</p> | ||
161 | <p>{intl.formatMessage(messages.copy)}</p> | ||
135 | </div> | 162 | </div> |
136 | <Button | 163 | <img className={classes.image} src="https://cdn.franzinfra.com/announcements/assets/teams.png" alt="Franz for Teams" /> |
137 | label={intl.formatMessage(messages.manageButton)} | 164 | </div> |
138 | onClick={openTeamManagement} | 165 | <div className={classes.buttonContainer}> |
139 | className={classes.cta} | 166 | {!isProUser ? ( |
140 | /> | 167 | <UpgradeButton |
141 | </> | 168 | className={classes.cta} |
142 | </PremiumFeatureContainer> | 169 | gaEventInfo={{ category: 'Todos', event: 'upgrade' }} |
170 | requiresPro | ||
171 | short | ||
172 | /> | ||
173 | ) : ( | ||
174 | <Button | ||
175 | label={intl.formatMessage(messages.manageButton)} | ||
176 | onClick={openTeamManagement} | ||
177 | className={classes.cta} | ||
178 | /> | ||
179 | )} | ||
180 | </div> | ||
181 | </> | ||
143 | </> | 182 | </> |
144 | )} | 183 | )} |
145 | </> | 184 | </> |
diff --git a/src/components/subscription/SubscriptionForm.js b/src/components/subscription/SubscriptionForm.js index 8c7dceece..cdfbbe60d 100644 --- a/src/components/subscription/SubscriptionForm.js +++ b/src/components/subscription/SubscriptionForm.js | |||
@@ -1,214 +1,78 @@ | |||
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 } from 'mobx-react'; | 3 | import { observer } from 'mobx-react'; |
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | import injectSheet from 'react-jss'; | ||
5 | 6 | ||
6 | import Form from '../../lib/Form'; | 7 | import { H3, H2 } from '@meetfranz/ui'; |
7 | import Radio from '../ui/Radio'; | ||
8 | import Button from '../ui/Button'; | ||
9 | import Loader from '../ui/Loader'; | ||
10 | 8 | ||
11 | import { required } from '../../helpers/validation-helpers'; | 9 | import { Button } from '@meetfranz/forms'; |
10 | import { FeatureList } from '../ui/FeatureList'; | ||
12 | 11 | ||
13 | const messages = defineMessages({ | 12 | const messages = defineMessages({ |
14 | submitButtonLabel: { | 13 | submitButtonLabel: { |
15 | id: 'subscription.submit.label', | 14 | id: 'subscription.cta.choosePlan', |
16 | defaultMessage: '!!!Support the development of Franz', | 15 | defaultMessage: '!!!Choose your plan', |
17 | }, | 16 | }, |
18 | paymentSessionError: { | 17 | teaserHeadline: { |
19 | id: 'subscription.paymentSessionError', | 18 | id: 'settings.account.headlineUpgradeAccount', |
20 | defaultMessage: '!!!Could not initialize payment form', | 19 | defaultMessage: '!!!Upgrade your account and get the full Franz experience', |
21 | }, | 20 | }, |
22 | typeFree: { | 21 | teaserText: { |
23 | id: 'subscription.type.free', | 22 | id: 'subscription.teaser.intro', |
24 | defaultMessage: '!!!free', | 23 | defaultMessage: '!!!Franz 5 comes with a wide range of new features to boost up your everyday communication - batteries included. Check out our new plans and find out which one suits you most!', |
25 | }, | ||
26 | typeMonthly: { | ||
27 | id: 'subscription.type.month', | ||
28 | defaultMessage: '!!!month', | ||
29 | }, | ||
30 | typeYearly: { | ||
31 | id: 'subscription.type.year', | ||
32 | defaultMessage: '!!!year', | ||
33 | }, | 24 | }, |
34 | includedFeatures: { | 25 | includedFeatures: { |
35 | id: 'subscription.includedFeatures', | 26 | id: 'subscription.teaser.includedFeatures', |
36 | defaultMessage: '!!!The Ferdi Premium Supporter Account includes', | 27 | defaultMessage: '!!!Paid Franz Plans include:', |
37 | }, | ||
38 | onpremise: { | ||
39 | id: 'subscription.features.onpremise.mattermost', | ||
40 | defaultMessage: '!!!Add on-premise/hosted services like Mattermost', | ||
41 | }, | ||
42 | noInterruptions: { | ||
43 | id: 'subscription.features.noInterruptions', | ||
44 | defaultMessage: '!!!No app delays & nagging to upgrade license', | ||
45 | }, | ||
46 | proxy: { | ||
47 | id: 'subscription.features.proxy', | ||
48 | defaultMessage: '!!!Proxy support for services', | ||
49 | }, | ||
50 | spellchecker: { | ||
51 | id: 'subscription.features.spellchecker', | ||
52 | defaultMessage: '!!!Support for Spellchecker', | ||
53 | }, | 28 | }, |
54 | workspaces: { | 29 | }); |
55 | id: 'subscription.features.workspaces', | 30 | |
56 | defaultMessage: '!!!Organize your services in workspaces', | 31 | const styles = () => ({ |
57 | }, | 32 | activateTrialButton: { |
58 | ads: { | 33 | margin: [40, 0, 50], |
59 | id: 'subscription.features.ads', | ||
60 | defaultMessage: '!!!No ads, ever!', | ||
61 | }, | ||
62 | comingSoon: { | ||
63 | id: 'subscription.features.comingSoon', | ||
64 | defaultMessage: '!!!coming soon', | ||
65 | }, | ||
66 | euTaxInfo: { | ||
67 | id: 'subscription.euTaxInfo', | ||
68 | defaultMessage: '!!!EU residents: local sales tax may apply', | ||
69 | }, | 34 | }, |
70 | }); | 35 | }); |
71 | 36 | ||
72 | export default @observer class SubscriptionForm extends Component { | 37 | export default @observer @injectSheet(styles) class SubscriptionForm extends Component { |
73 | static propTypes = { | 38 | static propTypes = { |
74 | plan: MobxPropTypes.objectOrObservableObject.isRequired, | 39 | selectPlan: PropTypes.func.isRequired, |
75 | isLoading: PropTypes.bool.isRequired, | 40 | isActivatingTrial: PropTypes.bool.isRequired, |
76 | handlePayment: PropTypes.func.isRequired, | 41 | classes: PropTypes.object.isRequired, |
77 | retryPlanRequest: PropTypes.func.isRequired, | ||
78 | isCreatingHostedPage: PropTypes.bool.isRequired, | ||
79 | error: PropTypes.bool.isRequired, | ||
80 | showSkipOption: PropTypes.bool, | ||
81 | skipAction: PropTypes.func, | ||
82 | skipButtonLabel: PropTypes.string, | ||
83 | hideInfo: PropTypes.bool.isRequired, | ||
84 | }; | ||
85 | |||
86 | static defaultProps = { | ||
87 | showSkipOption: false, | ||
88 | skipAction: () => null, | ||
89 | skipButtonLabel: '', | ||
90 | }; | 42 | }; |
91 | 43 | ||
92 | static contextTypes = { | 44 | static contextTypes = { |
93 | intl: intlShape, | 45 | intl: intlShape, |
94 | }; | 46 | }; |
95 | 47 | ||
96 | componentWillMount() { | ||
97 | this.form = this.prepareForm(); | ||
98 | } | ||
99 | |||
100 | prepareForm() { | ||
101 | const { intl } = this.context; | ||
102 | |||
103 | const form = { | ||
104 | fields: { | ||
105 | paymentTier: { | ||
106 | value: 'year', | ||
107 | validators: [required], | ||
108 | options: [{ | ||
109 | value: 'month', | ||
110 | label: `€ ${Object.hasOwnProperty.call(this.props.plan, 'month') | ||
111 | ? `${this.props.plan.month.price} / ${intl.formatMessage(messages.typeMonthly)}` | ||
112 | : 'monthly'}`, | ||
113 | }, { | ||
114 | value: 'year', | ||
115 | label: `€ ${Object.hasOwnProperty.call(this.props.plan, 'year') | ||
116 | ? `${this.props.plan.year.price} / ${intl.formatMessage(messages.typeYearly)}` | ||
117 | : 'yearly'}`, | ||
118 | }], | ||
119 | }, | ||
120 | }, | ||
121 | }; | ||
122 | |||
123 | if (this.props.showSkipOption) { | ||
124 | form.fields.paymentTier.options.unshift({ | ||
125 | value: 'skip', | ||
126 | label: `€ 0 / ${intl.formatMessage(messages.typeFree)}`, | ||
127 | }); | ||
128 | } | ||
129 | |||
130 | return new Form(form, this.context.intl); | ||
131 | } | ||
132 | |||
133 | render() { | 48 | render() { |
134 | const { | 49 | const { |
135 | isLoading, | 50 | isActivatingTrial, |
136 | isCreatingHostedPage, | 51 | selectPlan, |
137 | handlePayment, | 52 | classes, |
138 | retryPlanRequest, | ||
139 | error, | ||
140 | showSkipOption, | ||
141 | skipAction, | ||
142 | skipButtonLabel, | ||
143 | hideInfo, | ||
144 | } = this.props; | 53 | } = this.props; |
145 | const { intl } = this.context; | 54 | const { intl } = this.context; |
146 | 55 | ||
147 | if (error) { | 56 | return ( |
148 | return ( | 57 | <> |
58 | <H2>{intl.formatMessage(messages.teaserHeadline)}</H2> | ||
59 | <p>{intl.formatMessage(messages.teaserText)}</p> | ||
149 | <Button | 60 | <Button |
150 | label="Reload" | 61 | label={intl.formatMessage(messages.submitButtonLabel)} |
151 | onClick={retryPlanRequest} | 62 | className={classes.activateTrialButton} |
152 | isLoaded={!isLoading} | 63 | busy={isActivatingTrial} |
64 | onClick={selectPlan} | ||
65 | stretch | ||
153 | /> | 66 | /> |
154 | ); | 67 | <div className="subscription__premium-info"> |
155 | } | 68 | <H3> |
156 | 69 | {intl.formatMessage(messages.includedFeatures)} | |
157 | return ( | 70 | </H3> |
158 | <Loader loaded={!isLoading}> | 71 | <div className="subscription"> |
159 | <Radio field={this.form.$('paymentTier')} showLabel={false} className="paymentTiers" /> | 72 | <FeatureList /> |
160 | {!hideInfo && ( | ||
161 | <div className="subscription__premium-info"> | ||
162 | <p> | ||
163 | <strong>{intl.formatMessage(messages.includedFeatures)}</strong> | ||
164 | </p> | ||
165 | <div className="subscription"> | ||
166 | <ul className="subscription__premium-features"> | ||
167 | <li>{intl.formatMessage(messages.onpremise)}</li> | ||
168 | <li> | ||
169 | {intl.formatMessage(messages.noInterruptions)} | ||
170 | </li> | ||
171 | <li> | ||
172 | {intl.formatMessage(messages.spellchecker)} | ||
173 | </li> | ||
174 | <li> | ||
175 | {intl.formatMessage(messages.proxy)} | ||
176 | </li> | ||
177 | <li> | ||
178 | {intl.formatMessage(messages.workspaces)} | ||
179 | </li> | ||
180 | <li> | ||
181 | {intl.formatMessage(messages.ads)} | ||
182 | </li> | ||
183 | </ul> | ||
184 | </div> | ||
185 | </div> | 73 | </div> |
186 | )} | 74 | </div> |
187 | <Fragment> | 75 | </> |
188 | {error.code === 'no-payment-session' && ( | ||
189 | <p className="error-message center">{intl.formatMessage(messages.paymentSessionError)}</p> | ||
190 | )} | ||
191 | </Fragment> | ||
192 | {showSkipOption && this.form.$('paymentTier').value === 'skip' ? ( | ||
193 | <Button | ||
194 | label={skipButtonLabel} | ||
195 | className="auth__button" | ||
196 | onClick={skipAction} | ||
197 | /> | ||
198 | ) : ( | ||
199 | <Button | ||
200 | label={intl.formatMessage(messages.submitButtonLabel)} | ||
201 | className="auth__button" | ||
202 | loaded={!isCreatingHostedPage} | ||
203 | onClick={() => handlePayment(this.form.$('paymentTier').value)} | ||
204 | /> | ||
205 | )} | ||
206 | {this.form.$('paymentTier').value !== 'skip' && ( | ||
207 | <p className="legal"> | ||
208 | {intl.formatMessage(messages.euTaxInfo)} | ||
209 | </p> | ||
210 | )} | ||
211 | </Loader> | ||
212 | ); | 76 | ); |
213 | } | 77 | } |
214 | } | 78 | } |
diff --git a/src/components/subscription/SubscriptionPopup.js b/src/components/subscription/SubscriptionPopup.js index 0f6f0260f..12ef8a6e9 100644 --- a/src/components/subscription/SubscriptionPopup.js +++ b/src/components/subscription/SubscriptionPopup.js | |||
@@ -43,7 +43,7 @@ export default @observer class SubscriptionPopup extends Component { | |||
43 | 43 | ||
44 | setTimeout(() => { | 44 | setTimeout(() => { |
45 | this.props.closeWindow(); | 45 | this.props.closeWindow(); |
46 | }, ms('4s')); | 46 | }, ms('1s')); |
47 | } | 47 | } |
48 | 48 | ||
49 | render() { | 49 | render() { |
@@ -61,8 +61,6 @@ export default @observer class SubscriptionPopup extends Component { | |||
61 | autosize | 61 | autosize |
62 | src={encodeURI(url)} | 62 | src={encodeURI(url)} |
63 | onDidNavigate={completeCheck} | 63 | onDidNavigate={completeCheck} |
64 | // onNewWindow={(event, url, frameName, options) => | ||
65 | // openWindow({ event, url, frameName, options })} | ||
66 | /> | 64 | /> |
67 | </div> | 65 | </div> |
68 | <div className="subscription-popup__toolbar franz-form"> | 66 | <div className="subscription-popup__toolbar franz-form"> |
diff --git a/src/components/subscription/TrialForm.js b/src/components/subscription/TrialForm.js new file mode 100644 index 000000000..9ed548f16 --- /dev/null +++ b/src/components/subscription/TrialForm.js | |||
@@ -0,0 +1,115 @@ | |||
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 | |||
7 | import { H3, H2 } from '@meetfranz/ui'; | ||
8 | |||
9 | import { Button } from '@meetfranz/forms'; | ||
10 | import { FeatureList } from '../ui/FeatureList'; | ||
11 | import { FeatureItem } from '../ui/FeatureItem'; | ||
12 | |||
13 | const messages = defineMessages({ | ||
14 | submitButtonLabel: { | ||
15 | id: 'subscription.cta.activateTrial', | ||
16 | defaultMessage: '!!!Yes, start the free Franz Professional trial', | ||
17 | }, | ||
18 | allOptionsButton: { | ||
19 | id: 'subscription.cta.allOptions', | ||
20 | defaultMessage: '!!!See all options', | ||
21 | }, | ||
22 | teaserHeadline: { | ||
23 | id: 'settings.account.headlineTrialUpgrade', | ||
24 | defaultMessage: '!!!Get the free 14 day Franz Professional Trial', | ||
25 | }, | ||
26 | includedFeatures: { | ||
27 | id: 'subscription.includedProFeatures', | ||
28 | defaultMessage: '!!!The Franz Professional Plan includes:', | ||
29 | }, | ||
30 | noStringsAttachedHeadline: { | ||
31 | id: 'pricing.trial.terms.headline', | ||
32 | defaultMessage: '!!!No strings attached', | ||
33 | }, | ||
34 | noCreditCard: { | ||
35 | id: 'pricing.trial.terms.noCreditCard', | ||
36 | defaultMessage: '!!!No credit card required', | ||
37 | }, | ||
38 | automaticTrialEnd: { | ||
39 | id: 'pricing.trial.terms.automaticTrialEnd', | ||
40 | defaultMessage: '!!!Your free trial ends automatically after 14 days', | ||
41 | }, | ||
42 | }); | ||
43 | |||
44 | const styles = theme => ({ | ||
45 | activateTrialButton: { | ||
46 | margin: [40, 0, 10], | ||
47 | }, | ||
48 | allOptionsButton: { | ||
49 | margin: [0, 0, 40], | ||
50 | background: 'none', | ||
51 | border: 'none', | ||
52 | color: theme.colorText, | ||
53 | }, | ||
54 | keyTerms: { | ||
55 | marginTop: 20, | ||
56 | }, | ||
57 | }); | ||
58 | |||
59 | export default @observer @injectSheet(styles) class TrialForm extends Component { | ||
60 | static propTypes = { | ||
61 | activateTrial: PropTypes.func.isRequired, | ||
62 | isActivatingTrial: PropTypes.bool.isRequired, | ||
63 | showAllOptions: PropTypes.func.isRequired, | ||
64 | classes: PropTypes.object.isRequired, | ||
65 | }; | ||
66 | |||
67 | static contextTypes = { | ||
68 | intl: intlShape, | ||
69 | }; | ||
70 | |||
71 | render() { | ||
72 | const { | ||
73 | isActivatingTrial, | ||
74 | activateTrial, | ||
75 | showAllOptions, | ||
76 | classes, | ||
77 | } = this.props; | ||
78 | const { intl } = this.context; | ||
79 | |||
80 | return ( | ||
81 | <> | ||
82 | <H2>{intl.formatMessage(messages.teaserHeadline)}</H2> | ||
83 | <H3 className={classes.keyTerms}> | ||
84 | {intl.formatMessage(messages.noStringsAttachedHeadline)} | ||
85 | </H3> | ||
86 | <ul> | ||
87 | <FeatureItem icon="👉" name={intl.formatMessage(messages.noCreditCard)} /> | ||
88 | <FeatureItem icon="👉" name={intl.formatMessage(messages.automaticTrialEnd)} /> | ||
89 | </ul> | ||
90 | |||
91 | <Button | ||
92 | label={intl.formatMessage(messages.submitButtonLabel)} | ||
93 | className={classes.activateTrialButton} | ||
94 | busy={isActivatingTrial} | ||
95 | onClick={activateTrial} | ||
96 | stretch | ||
97 | /> | ||
98 | <Button | ||
99 | label={intl.formatMessage(messages.allOptionsButton)} | ||
100 | className={classes.allOptionsButton} | ||
101 | onClick={showAllOptions} | ||
102 | stretch | ||
103 | /> | ||
104 | <div className="subscription__premium-info"> | ||
105 | <H3> | ||
106 | {intl.formatMessage(messages.includedFeatures)} | ||
107 | </H3> | ||
108 | <div className="subscription"> | ||
109 | <FeatureList /> | ||
110 | </div> | ||
111 | </div> | ||
112 | </> | ||
113 | ); | ||
114 | } | ||
115 | } | ||
diff --git a/src/components/ui/ActivateTrialButton/index.js b/src/components/ui/ActivateTrialButton/index.js new file mode 100644 index 000000000..e0637da90 --- /dev/null +++ b/src/components/ui/ActivateTrialButton/index.js | |||
@@ -0,0 +1,125 @@ | |||
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 classnames from 'classnames'; | ||
6 | |||
7 | import { Button } from '@meetfranz/forms'; | ||
8 | import { gaEvent } from '../../../lib/analytics'; | ||
9 | |||
10 | import UserStore from '../../../stores/UserStore'; | ||
11 | |||
12 | const messages = defineMessages({ | ||
13 | action: { | ||
14 | id: 'feature.delayApp.upgrade.action', | ||
15 | defaultMessage: '!!!Get a Franz Supporter License', | ||
16 | }, | ||
17 | actionTrial: { | ||
18 | id: 'feature.delayApp.trial.action', | ||
19 | defaultMessage: '!!!Yes, I want the free 14 day trial of Franz Professional', | ||
20 | }, | ||
21 | shortAction: { | ||
22 | id: 'feature.delayApp.upgrade.actionShort', | ||
23 | defaultMessage: '!!!Upgrade account', | ||
24 | }, | ||
25 | shortActionTrial: { | ||
26 | id: 'feature.delayApp.trial.actionShort', | ||
27 | defaultMessage: '!!!Activate the free Franz Professional trial', | ||
28 | }, | ||
29 | noStringsAttachedHeadline: { | ||
30 | id: 'pricing.trial.terms.headline', | ||
31 | defaultMessage: '!!!No strings attached', | ||
32 | }, | ||
33 | noCreditCard: { | ||
34 | id: 'pricing.trial.terms.noCreditCard', | ||
35 | defaultMessage: '!!!No credit card required', | ||
36 | }, | ||
37 | automaticTrialEnd: { | ||
38 | id: 'pricing.trial.terms.automaticTrialEnd', | ||
39 | defaultMessage: '!!!Your free trial ends automatically after 14 days', | ||
40 | }, | ||
41 | }); | ||
42 | |||
43 | @inject('stores', 'actions') @observer | ||
44 | class ActivateTrialButton extends Component { | ||
45 | static propTypes = { | ||
46 | className: PropTypes.string, | ||
47 | short: PropTypes.bool, | ||
48 | gaEventInfo: PropTypes.shape({ | ||
49 | category: PropTypes.string.isRequired, | ||
50 | event: PropTypes.string.isRequired, | ||
51 | label: PropTypes.string, | ||
52 | }), | ||
53 | }; | ||
54 | |||
55 | static defaultProps = { | ||
56 | className: '', | ||
57 | short: false, | ||
58 | gaEventInfo: null, | ||
59 | } | ||
60 | |||
61 | static contextTypes = { | ||
62 | intl: intlShape, | ||
63 | }; | ||
64 | |||
65 | handleCTAClick() { | ||
66 | const { actions, stores, gaEventInfo } = this.props; | ||
67 | const { hadSubscription } = stores.user.data; | ||
68 | // const { defaultTrialPlan } = stores.features.features; | ||
69 | |||
70 | let label = ''; | ||
71 | if (!hadSubscription) { | ||
72 | // actions.user.activateTrial({ planId: defaultTrialPlan }); | ||
73 | |||
74 | label = 'Start Trial'; | ||
75 | } else { | ||
76 | label = 'Upgrade Account'; | ||
77 | } | ||
78 | |||
79 | actions.ui.openSettings({ path: 'user' }); | ||
80 | |||
81 | if (gaEventInfo) { | ||
82 | const { category, event } = gaEventInfo; | ||
83 | gaEvent(category, event, label); | ||
84 | } | ||
85 | } | ||
86 | |||
87 | render() { | ||
88 | const { stores, className, short } = this.props; | ||
89 | const { intl } = this.context; | ||
90 | |||
91 | const { hadSubscription } = stores.user.data; | ||
92 | |||
93 | let label; | ||
94 | if (hadSubscription) { | ||
95 | label = short ? messages.shortAction : messages.action; | ||
96 | } else { | ||
97 | label = short ? messages.shortActionTrial : messages.actionTrial; | ||
98 | } | ||
99 | |||
100 | return ( | ||
101 | <Button | ||
102 | label={intl.formatMessage(label)} | ||
103 | className={classnames({ | ||
104 | [className]: className, | ||
105 | })} | ||
106 | buttonType="inverted" | ||
107 | onClick={this.handleCTAClick.bind(this)} | ||
108 | busy={stores.user.activateTrialRequest.isExecuting} | ||
109 | /> | ||
110 | ); | ||
111 | } | ||
112 | } | ||
113 | |||
114 | export default ActivateTrialButton; | ||
115 | |||
116 | ActivateTrialButton.wrappedComponent.propTypes = { | ||
117 | stores: PropTypes.shape({ | ||
118 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
119 | }).isRequired, | ||
120 | actions: PropTypes.shape({ | ||
121 | ui: PropTypes.shape({ | ||
122 | openSettings: PropTypes.func.isRequired, | ||
123 | }).isRequired, | ||
124 | }).isRequired, | ||
125 | }; | ||
diff --git a/src/components/ui/FeatureItem.js b/src/components/ui/FeatureItem.js new file mode 100644 index 000000000..7c482c4d4 --- /dev/null +++ b/src/components/ui/FeatureItem.js | |||
@@ -0,0 +1,37 @@ | |||
1 | import React from 'react'; | ||
2 | import injectSheet from 'react-jss'; | ||
3 | import { Icon } from '@meetfranz/ui'; | ||
4 | import classnames from 'classnames'; | ||
5 | import { mdiCheckCircle } from '@mdi/js'; | ||
6 | |||
7 | const styles = theme => ({ | ||
8 | featureItem: { | ||
9 | borderBottom: [1, 'solid', theme.defaultContentBorder], | ||
10 | padding: [8, 0], | ||
11 | display: 'flex', | ||
12 | alignItems: 'center', | ||
13 | }, | ||
14 | featureIcon: { | ||
15 | fill: theme.brandSuccess, | ||
16 | marginRight: 10, | ||
17 | }, | ||
18 | }); | ||
19 | |||
20 | export const FeatureItem = injectSheet(styles)(({ | ||
21 | classes, className, name, icon, | ||
22 | }) => ( | ||
23 | <li className={classnames({ | ||
24 | [classes.featureItem]: true, | ||
25 | [className]: className, | ||
26 | })} | ||
27 | > | ||
28 | {icon ? ( | ||
29 | <span className={classes.featureIcon}>{icon}</span> | ||
30 | ) : ( | ||
31 | <Icon icon={mdiCheckCircle} className={classes.featureIcon} size={1.5} /> | ||
32 | )} | ||
33 | {name} | ||
34 | </li> | ||
35 | )); | ||
36 | |||
37 | export default FeatureItem; | ||
diff --git a/src/components/ui/FeatureList.js b/src/components/ui/FeatureList.js new file mode 100644 index 000000000..62944ad75 --- /dev/null +++ b/src/components/ui/FeatureList.js | |||
@@ -0,0 +1,89 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { defineMessages, intlShape } from 'react-intl'; | ||
4 | |||
5 | import { FeatureItem } from './FeatureItem'; | ||
6 | |||
7 | const messages = defineMessages({ | ||
8 | unlimitedServices: { | ||
9 | id: 'pricing.features.unlimitedServices', | ||
10 | defaultMessage: '!!!Add unlimited services', | ||
11 | }, | ||
12 | spellchecker: { | ||
13 | id: 'pricing.features.spellchecker', | ||
14 | defaultMessage: '!!!Spellchecker support', | ||
15 | }, | ||
16 | workspaces: { | ||
17 | id: 'pricing.features.workspaces', | ||
18 | defaultMessage: '!!!Workspaces', | ||
19 | }, | ||
20 | customWebsites: { | ||
21 | id: 'pricing.features.customWebsites', | ||
22 | defaultMessage: '!!!Add Custom Websites', | ||
23 | }, | ||
24 | onPremise: { | ||
25 | id: 'pricing.features.onPremise', | ||
26 | defaultMessage: '!!!On-premise & other Hosted Services', | ||
27 | }, | ||
28 | thirdPartyServices: { | ||
29 | id: 'pricing.features.thirdPartyServices', | ||
30 | defaultMessage: '!!!Install 3rd party services', | ||
31 | }, | ||
32 | serviceProxies: { | ||
33 | id: 'pricing.features.serviceProxies', | ||
34 | defaultMessage: '!!!Service Proxies', | ||
35 | }, | ||
36 | teamManagement: { | ||
37 | id: 'pricing.features.teamManagement', | ||
38 | defaultMessage: '!!!Team Management', | ||
39 | }, | ||
40 | appDelays: { | ||
41 | id: 'pricing.features.appDelays', | ||
42 | defaultMessage: '!!!No Waiting Screens', | ||
43 | }, | ||
44 | adFree: { | ||
45 | id: 'pricing.features.adFree', | ||
46 | defaultMessage: '!!!Forever ad-free', | ||
47 | }, | ||
48 | }); | ||
49 | |||
50 | export class FeatureList extends Component { | ||
51 | static propTypes = { | ||
52 | className: PropTypes.string, | ||
53 | featureClassName: PropTypes.string, | ||
54 | }; | ||
55 | |||
56 | static defaultProps = { | ||
57 | className: '', | ||
58 | featureClassName: '', | ||
59 | } | ||
60 | |||
61 | static contextTypes = { | ||
62 | intl: intlShape, | ||
63 | }; | ||
64 | |||
65 | render() { | ||
66 | const { | ||
67 | className, | ||
68 | featureClassName, | ||
69 | } = this.props; | ||
70 | const { intl } = this.context; | ||
71 | |||
72 | return ( | ||
73 | <ul className={className}> | ||
74 | <FeatureItem name={intl.formatMessage(messages.unlimitedServices)} className={featureClassName} /> | ||
75 | <FeatureItem name={intl.formatMessage(messages.spellchecker)} className={featureClassName} /> | ||
76 | <FeatureItem name={intl.formatMessage(messages.workspaces)} className={featureClassName} /> | ||
77 | <FeatureItem name={intl.formatMessage(messages.customWebsites)} className={featureClassName} /> | ||
78 | <FeatureItem name={intl.formatMessage(messages.onPremise)} className={featureClassName} /> | ||
79 | <FeatureItem name={intl.formatMessage(messages.thirdPartyServices)} className={featureClassName} /> | ||
80 | <FeatureItem name={intl.formatMessage(messages.serviceProxies)} className={featureClassName} /> | ||
81 | <FeatureItem name={intl.formatMessage(messages.teamManagement)} className={featureClassName} /> | ||
82 | <FeatureItem name={intl.formatMessage(messages.appDelays)} className={featureClassName} /> | ||
83 | <FeatureItem name={intl.formatMessage(messages.adFree)} className={featureClassName} /> | ||
84 | </ul> | ||
85 | ); | ||
86 | } | ||
87 | } | ||
88 | |||
89 | export default FeatureList; | ||
diff --git a/src/components/ui/Modal/index.js b/src/components/ui/Modal/index.js index 0b7154760..63d858c47 100644 --- a/src/components/ui/Modal/index.js +++ b/src/components/ui/Modal/index.js | |||
@@ -5,6 +5,7 @@ import classnames from 'classnames'; | |||
5 | import injectCSS from 'react-jss'; | 5 | import injectCSS from 'react-jss'; |
6 | import { Icon } from '@meetfranz/ui'; | 6 | import { Icon } from '@meetfranz/ui'; |
7 | 7 | ||
8 | import { mdiClose } from '@mdi/js'; | ||
8 | import styles from './styles'; | 9 | import styles from './styles'; |
9 | 10 | ||
10 | // ReactModal.setAppElement('#root'); | 11 | // ReactModal.setAppElement('#root'); |
@@ -59,7 +60,7 @@ export default @injectCSS(styles) class Modal extends Component { | |||
59 | className={classes.close} | 60 | className={classes.close} |
60 | onClick={close} | 61 | onClick={close} |
61 | > | 62 | > |
62 | <Icon icon="mdiClose" size={1.5} /> | 63 | <Icon icon={mdiClose} size={1.5} /> |
63 | </button> | 64 | </button> |
64 | )} | 65 | )} |
65 | <div className={classes.content}> | 66 | <div className={classes.content}> |
diff --git a/src/components/ui/PremiumFeatureContainer/index.js b/src/components/ui/PremiumFeatureContainer/index.js index b890b09ab..c53d345a0 100644 --- a/src/components/ui/PremiumFeatureContainer/index.js +++ b/src/components/ui/PremiumFeatureContainer/index.js | |||
@@ -9,6 +9,7 @@ import { oneOrManyChildElements } from '../../../prop-types'; | |||
9 | import UserStore from '../../../stores/UserStore'; | 9 | import UserStore from '../../../stores/UserStore'; |
10 | 10 | ||
11 | import styles from './styles'; | 11 | import styles from './styles'; |
12 | import { FeatureStore } from '../../../features/utils/FeatureStore'; | ||
12 | 13 | ||
13 | const messages = defineMessages({ | 14 | const messages = defineMessages({ |
14 | action: { | 15 | action: { |
@@ -21,7 +22,10 @@ const messages = defineMessages({ | |||
21 | class PremiumFeatureContainer extends Component { | 22 | class PremiumFeatureContainer extends Component { |
22 | static propTypes = { | 23 | static propTypes = { |
23 | classes: PropTypes.object.isRequired, | 24 | classes: PropTypes.object.isRequired, |
24 | condition: PropTypes.bool, | 25 | condition: PropTypes.oneOfType([ |
26 | PropTypes.bool, | ||
27 | PropTypes.func, | ||
28 | ]), | ||
25 | gaEventInfo: PropTypes.shape({ | 29 | gaEventInfo: PropTypes.shape({ |
26 | category: PropTypes.string.isRequired, | 30 | category: PropTypes.string.isRequired, |
27 | event: PropTypes.string.isRequired, | 31 | event: PropTypes.string.isRequired, |
@@ -30,7 +34,7 @@ class PremiumFeatureContainer extends Component { | |||
30 | }; | 34 | }; |
31 | 35 | ||
32 | static defaultProps = { | 36 | static defaultProps = { |
33 | condition: true, | 37 | condition: null, |
34 | gaEventInfo: null, | 38 | gaEventInfo: null, |
35 | }; | 39 | }; |
36 | 40 | ||
@@ -49,7 +53,18 @@ class PremiumFeatureContainer extends Component { | |||
49 | 53 | ||
50 | const { intl } = this.context; | 54 | const { intl } = this.context; |
51 | 55 | ||
52 | return !stores.user.data.isPremium && !!condition ? ( | 56 | let showWrapper = !!condition; |
57 | |||
58 | if (condition === null) { | ||
59 | showWrapper = !stores.user.data.isPremium; | ||
60 | } else if (typeof condition === 'function') { | ||
61 | showWrapper = condition({ | ||
62 | isPremium: stores.user.data.isPremium, | ||
63 | features: stores.features.features, | ||
64 | }); | ||
65 | } | ||
66 | |||
67 | return showWrapper ? ( | ||
53 | <div className={classes.container}> | 68 | <div className={classes.container}> |
54 | <div className={classes.titleContainer}> | 69 | <div className={classes.titleContainer}> |
55 | <p className={classes.title}>Premium Feature</p> | 70 | <p className={classes.title}>Premium Feature</p> |
@@ -75,6 +90,7 @@ PremiumFeatureContainer.wrappedComponent.propTypes = { | |||
75 | children: oneOrManyChildElements.isRequired, | 90 | children: oneOrManyChildElements.isRequired, |
76 | stores: PropTypes.shape({ | 91 | stores: PropTypes.shape({ |
77 | user: PropTypes.instanceOf(UserStore).isRequired, | 92 | user: PropTypes.instanceOf(UserStore).isRequired, |
93 | features: PropTypes.instanceOf(FeatureStore).isRequired, | ||
78 | }).isRequired, | 94 | }).isRequired, |
79 | actions: PropTypes.shape({ | 95 | actions: PropTypes.shape({ |
80 | ui: PropTypes.shape({ | 96 | ui: PropTypes.shape({ |
diff --git a/src/components/ui/UpgradeButton/index.js b/src/components/ui/UpgradeButton/index.js new file mode 100644 index 000000000..73762f0bf --- /dev/null +++ b/src/components/ui/UpgradeButton/index.js | |||
@@ -0,0 +1,89 @@ | |||
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 | |||
6 | import { Button } from '@meetfranz/forms'; | ||
7 | import { gaEvent } from '../../../lib/analytics'; | ||
8 | |||
9 | import UserStore from '../../../stores/UserStore'; | ||
10 | import ActivateTrialButton from '../ActivateTrialButton'; | ||
11 | |||
12 | const messages = defineMessages({ | ||
13 | upgradeToPro: { | ||
14 | id: 'global.upgradeButton.upgradeToPro', | ||
15 | defaultMessage: '!!!Upgrade to Franz Professional', | ||
16 | }, | ||
17 | }); | ||
18 | |||
19 | @inject('stores', 'actions') @observer | ||
20 | class UpgradeButton extends Component { | ||
21 | static propTypes = { | ||
22 | // eslint-disable-next-line | ||
23 | classes: PropTypes.object.isRequired, | ||
24 | className: PropTypes.string, | ||
25 | gaEventInfo: PropTypes.shape({ | ||
26 | category: PropTypes.string.isRequired, | ||
27 | event: PropTypes.string.isRequired, | ||
28 | label: PropTypes.string, | ||
29 | }), | ||
30 | requiresPro: PropTypes.bool, | ||
31 | }; | ||
32 | |||
33 | static defaultProps = { | ||
34 | className: '', | ||
35 | gaEventInfo: null, | ||
36 | requiresPro: false, | ||
37 | } | ||
38 | |||
39 | static contextTypes = { | ||
40 | intl: intlShape, | ||
41 | }; | ||
42 | |||
43 | handleCTAClick() { | ||
44 | const { actions, gaEventInfo } = this.props; | ||
45 | |||
46 | actions.ui.openSettings({ path: 'user' }); | ||
47 | if (gaEventInfo) { | ||
48 | const { category, event } = gaEventInfo; | ||
49 | gaEvent(category, event, 'Upgrade Account'); | ||
50 | } | ||
51 | } | ||
52 | |||
53 | render() { | ||
54 | const { stores, requiresPro } = this.props; | ||
55 | const { intl } = this.context; | ||
56 | |||
57 | const { isPremium, isPersonal } = stores.user; | ||
58 | |||
59 | if (isPremium && isPersonal && requiresPro) { | ||
60 | return ( | ||
61 | <Button | ||
62 | label={intl.formatMessage(messages.upgradeToPro)} | ||
63 | onClick={this.handleCTAClick.bind(this)} | ||
64 | className={this.props.className} | ||
65 | buttonType="inverted" | ||
66 | /> | ||
67 | ); | ||
68 | } | ||
69 | |||
70 | if (!isPremium) { | ||
71 | return <ActivateTrialButton {...this.props} />; | ||
72 | } | ||
73 | |||
74 | return null; | ||
75 | } | ||
76 | } | ||
77 | |||
78 | export default UpgradeButton; | ||
79 | |||
80 | UpgradeButton.wrappedComponent.propTypes = { | ||
81 | stores: PropTypes.shape({ | ||
82 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
83 | }).isRequired, | ||
84 | actions: PropTypes.shape({ | ||
85 | ui: PropTypes.shape({ | ||
86 | openSettings: PropTypes.func.isRequired, | ||
87 | }).isRequired, | ||
88 | }).isRequired, | ||
89 | }; | ||