aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
authorLibravatar vantezzen <properly@protonmail.com>2019-09-07 15:50:23 +0200
committerLibravatar vantezzen <properly@protonmail.com>2019-09-07 15:50:23 +0200
commite7a74514c1e7c3833dfdcf5900cb87f9e6e8354e (patch)
treeb8314e4155503b135dcb07e8b4a0e847e25c19cf /src/components
parentUpdate CHANGELOG.md (diff)
parentUpdate CHANGELOG.md (diff)
downloadferdium-app-e7a74514c1e7c3833dfdcf5900cb87f9e6e8354e.tar.gz
ferdium-app-e7a74514c1e7c3833dfdcf5900cb87f9e6e8354e.tar.zst
ferdium-app-e7a74514c1e7c3833dfdcf5900cb87f9e6e8354e.zip
Merge branch 'master' of https://github.com/meetfranz/franz into franz-5.3.0
Diffstat (limited to 'src/components')
-rw-r--r--src/components/TrialActivationInfoBar.js94
-rw-r--r--src/components/auth/Pricing.js248
-rw-r--r--src/components/auth/Signup.js31
-rw-r--r--src/components/layout/AppLayout.js54
-rw-r--r--src/components/services/content/ServiceRestricted.js78
-rw-r--r--src/components/services/content/ServiceView.js25
-rw-r--r--src/components/services/content/Services.js48
-rw-r--r--src/components/settings/account/AccountDashboard.js208
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js8
-rw-r--r--src/components/settings/recipes/RecipeItem.js2
-rw-r--r--src/components/settings/recipes/RecipesDashboard.js194
-rw-r--r--src/components/settings/services/EditServiceForm.js17
-rw-r--r--src/components/settings/services/ServicesDashboard.js2
-rw-r--r--src/components/settings/settings/EditSettingsForm.js11
-rw-r--r--src/components/settings/team/TeamDashboard.js73
-rw-r--r--src/components/subscription/SubscriptionForm.js226
-rw-r--r--src/components/subscription/SubscriptionPopup.js4
-rw-r--r--src/components/subscription/TrialForm.js115
-rw-r--r--src/components/ui/ActivateTrialButton/index.js125
-rw-r--r--src/components/ui/FeatureItem.js37
-rw-r--r--src/components/ui/FeatureList.js89
-rw-r--r--src/components/ui/Modal/index.js3
-rw-r--r--src/components/ui/PremiumFeatureContainer/index.js22
-rw-r--r--src/components/ui/UpgradeButton/index.js89
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 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { defineMessages, intlShape } from 'react-intl';
4import ms from 'ms';
5import injectSheet from 'react-jss';
6import classnames from 'classnames';
7
8import InfoBar from './ui/InfoBar';
9
10const messages = defineMessages({
11 message: {
12 id: 'infobar.trialActivated',
13 defaultMessage: '!!!Your trial was successfully activated. Happy messaging!',
14 },
15});
16
17const 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)
32class 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
94export 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 @@
1import React, { Component } from 'react'; 1import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5// import { Link } from 'react-router'; 5import injectSheet from 'react-jss';
6import { H2, Loader } from '@meetfranz/ui';
7import classnames from 'classnames';
8
9import { Button } from '@meetfranz/forms';
10import { FeatureItem } from '../ui/FeatureItem';
11import { FeatureList } from '../ui/FeatureList';
6 12
7// import Button from '../ui/Button';
8import Loader from '../ui/Loader';
9import Appear from '../ui/effects/Appear';
10import SubscriptionForm from '../../containers/subscription/SubscriptionFormScreen';
11 13
12const messages = defineMessages({ 14const 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
31export default @observer class Signup extends Component { 53const 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
98export 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';
7import Form from '../../lib/Form'; 7import Form from '../../lib/Form';
8import { required, email, minLength } from '../../helpers/validation-helpers'; 8import { required, email, minLength } from '../../helpers/validation-helpers';
9import Input from '../ui/Input'; 9import Input from '../ui/Input';
10import Radio from '../ui/Radio';
11import Button from '../ui/Button'; 10import Button from '../ui/Button';
12import Link from '../ui/Link'; 11import Link from '../ui/Link';
13import Infobox from '../ui/Infobox'; 12import 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';
16import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator'; 16import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator';
17import { workspaceStore } from '../../features/workspaces'; 17import { workspaceStore } from '../../features/workspaces';
18import AppUpdateInfoBar from '../AppUpdateInfoBar'; 18import AppUpdateInfoBar from '../AppUpdateInfoBar';
19import TrialActivationInfoBar from '../TrialActivationInfoBar';
20import Todos from '../../features/todos/containers/TodosScreen';
19 21
20function createMarkup(HTMLString) { 22function createMarkup(HTMLString) {
21 return { __html: HTMLString }; 23 return { __html: HTMLString };
@@ -42,7 +44,8 @@ const messages = defineMessages({
42 44
43const styles = theme => ({ 45const 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 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5
6import { serviceLimitStore } from '../../../features/serviceLimit';
7import Button from '../../ui/Button';
8import { RESTRICTION_TYPES } from '../../../models/Service';
9
10const 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
33export 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';
10import WebviewCrashHandler from './WebviewCrashHandler'; 10import WebviewCrashHandler from './WebviewCrashHandler';
11import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler'; 11import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler';
12import ServiceDisabled from './ServiceDisabled'; 12import ServiceDisabled from './ServiceDisabled';
13import ServiceRestricted from './ServiceRestricted';
13import ServiceWebview from './ServiceWebview'; 14import ServiceWebview from './ServiceWebview';
14 15
15export default @observer class ServiceView extends Component { 16export 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';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { Link } from 'react-router'; 4import { Link } from 'react-router';
5import { defineMessages, intlShape } from 'react-intl'; 5import { defineMessages, intlShape } from 'react-intl';
6import Confetti from 'react-confetti';
7import ms from 'ms';
8import injectSheet from 'react-jss';
6 9
7import ServiceView from './ServiceView'; 10import ServiceView from './ServiceView';
8import Appear from '../../ui/effects/Appear'; 11import Appear from '../../ui/effects/Appear';
@@ -26,7 +29,17 @@ const messages = defineMessages({
26 }, 29 },
27}); 30});
28 31
29export default @observer class Services extends Component { 32
33const styles = {
34 confettiContainer: {
35 position: 'absolute',
36 width: '100%',
37 zIndex: 9999,
38 pointerEvents: 'none',
39 },
40};
41
42export 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 @@
1import React, { Component, Fragment } from 'react'; 1import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import ReactTooltip from 'react-tooltip'; 5import ReactTooltip from 'react-tooltip';
6import { ProBadge } from '@meetfranz/ui'; 6import {
7 ProBadge, H1, H2,
8} from '@meetfranz/ui';
9import moment from 'moment';
7 10
8import Loader from '../../ui/Loader'; 11import Loader from '../../ui/Loader';
9import Button from '../../ui/Button'; 12import Button from '../../ui/Button';
10import Infobox from '../../ui/Infobox'; 13import Infobox from '../../ui/Infobox';
11import SubscriptionForm from '../../../containers/subscription/SubscriptionFormScreen'; 14import SubscriptionForm from '../../../containers/subscription/SubscriptionFormScreen';
15import { i18nPlanName } from '../../../helpers/plan-helpers';
12 16
13const messages = defineMessages({ 17const 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
76export default @observer class AccountDashboard extends Component { 96@observer
97class 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
301export 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';
8import { workspaceStore } from '../../../features/workspaces'; 8import { workspaceStore } from '../../../features/workspaces';
9import UIStore from '../../../stores/UIStore'; 9import UIStore from '../../../stores/UIStore';
10import UserStore from '../../../stores/UserStore'; 10import UserStore from '../../../stores/UserStore';
11import { serviceLimitStore } from '../../../features/serviceLimit';
11 12
12const messages = defineMessages({ 13const 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';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import { Link } from 'react-router'; 5import { Link } from 'react-router';
6 6
7import { Button, Input } from '@meetfranz/forms';
8import injectSheet from 'react-jss';
9import { H3, H2, ProBadge } from '@meetfranz/ui';
7import SearchInput from '../../ui/SearchInput'; 10import SearchInput from '../../ui/SearchInput';
8import Infobox from '../../ui/Infobox'; 11import Infobox from '../../ui/Infobox';
9import RecipeItem from './RecipeItem'; 12import RecipeItem from './RecipeItem';
10import Loader from '../../ui/Loader'; 13import Loader from '../../ui/Loader';
11import Appear from '../../ui/effects/Appear'; 14import Appear from '../../ui/effects/Appear';
12import { FRANZ_SERVICE_REQUEST } from '../../../config'; 15import { FRANZ_SERVICE_REQUEST } from '../../../config';
16import LimitReachedInfobox from '../../../features/serviceLimit/components/LimitReachedInfobox';
17import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
13 18
14const messages = defineMessages({ 19const 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
49export default @observer class RecipesDashboard extends Component { 78const 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
106export 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';
17import Select from '../../ui/Select'; 17import Select from '../../ui/Select';
18 18
19import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer'; 19import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
20import LimitReachedInfobox from '../../../features/serviceLimit/components/LimitReachedInfobox';
21import { serviceLimitStore } from '../../../features/serviceLimit';
20 22
21const messages = defineMessages({ 23const 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';
9import Loader from '../../ui/Loader'; 9import Loader from '../../ui/Loader';
10import ServiceItem from './ServiceItem'; 10import ServiceItem from './ServiceItem';
11import Appear from '../../ui/effects/Appear'; 11import Appear from '../../ui/effects/Appear';
12import LimitReachedInfobox from '../../../features/serviceLimit/components/LimitReachedInfobox';
12 13
13const messages = defineMessages({ 14const 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';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import ReactTooltip from 'react-tooltip'; 5import ReactTooltip from 'react-tooltip';
6import injectSheet from 'react-jss'; 6import injectSheet from 'react-jss';
7import classnames from 'classnames';
7 8
9import { Badge } from '@meetfranz/ui';
8import Loader from '../../ui/Loader'; 10import Loader from '../../ui/Loader';
9import Button from '../../ui/Button'; 11import Button from '../../ui/Button';
10import Infobox from '../../ui/Infobox'; 12import Infobox from '../../ui/Infobox';
11import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer'; 13import globalMessages from '../../../i18n/globalMessages';
14import UpgradeButton from '../../ui/UpgradeButton';
12 15
13const messages = defineMessages({ 16const messages = defineMessages({
14 headline: { 17 headline: {
@@ -40,6 +43,7 @@ const messages = defineMessages({
40const styles = { 43const 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 @@
1import React, { Component, Fragment } from 'react'; 1import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss';
5 6
6import Form from '../../lib/Form'; 7import { H3, H2 } from '@meetfranz/ui';
7import Radio from '../ui/Radio';
8import Button from '../ui/Button';
9import Loader from '../ui/Loader';
10 8
11import { required } from '../../helpers/validation-helpers'; 9import { Button } from '@meetfranz/forms';
10import { FeatureList } from '../ui/FeatureList';
12 11
13const messages = defineMessages({ 12const 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', 31const 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
72export default @observer class SubscriptionForm extends Component { 37export 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 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss';
6
7import { H3, H2 } from '@meetfranz/ui';
8
9import { Button } from '@meetfranz/forms';
10import { FeatureList } from '../ui/FeatureList';
11import { FeatureItem } from '../ui/FeatureItem';
12
13const 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
44const 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
59export 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 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import classnames from 'classnames';
6
7import { Button } from '@meetfranz/forms';
8import { gaEvent } from '../../../lib/analytics';
9
10import UserStore from '../../../stores/UserStore';
11
12const 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
44class 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
114export default ActivateTrialButton;
115
116ActivateTrialButton.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 @@
1import React from 'react';
2import injectSheet from 'react-jss';
3import { Icon } from '@meetfranz/ui';
4import classnames from 'classnames';
5import { mdiCheckCircle } from '@mdi/js';
6
7const 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
20export 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
37export 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 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { defineMessages, intlShape } from 'react-intl';
4
5import { FeatureItem } from './FeatureItem';
6
7const 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
50export 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
89export 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';
5import injectCSS from 'react-jss'; 5import injectCSS from 'react-jss';
6import { Icon } from '@meetfranz/ui'; 6import { Icon } from '@meetfranz/ui';
7 7
8import { mdiClose } from '@mdi/js';
8import styles from './styles'; 9import 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';
9import UserStore from '../../../stores/UserStore'; 9import UserStore from '../../../stores/UserStore';
10 10
11import styles from './styles'; 11import styles from './styles';
12import { FeatureStore } from '../../../features/utils/FeatureStore';
12 13
13const messages = defineMessages({ 14const messages = defineMessages({
14 action: { 15 action: {
@@ -21,7 +22,10 @@ const messages = defineMessages({
21class PremiumFeatureContainer extends Component { 22class 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 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5
6import { Button } from '@meetfranz/forms';
7import { gaEvent } from '../../../lib/analytics';
8
9import UserStore from '../../../stores/UserStore';
10import ActivateTrialButton from '../ActivateTrialButton';
11
12const messages = defineMessages({
13 upgradeToPro: {
14 id: 'global.upgradeButton.upgradeToPro',
15 defaultMessage: '!!!Upgrade to Franz Professional',
16 },
17});
18
19@inject('stores', 'actions') @observer
20class 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
78export default UpgradeButton;
79
80UpgradeButton.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};