summaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2019-09-03 20:33:00 +0200
committerLibravatar Stefan Malzner <stefan@adlk.io>2019-09-03 20:33:00 +0200
commit3ca3e36cf7fce6b241e242a07cb4731760ee4265 (patch)
tree0796a3712aebbf695545892be19fc677e7acec8e /src/components
parentAutomatic i18n update (i18n.meetfranz.com) (diff)
parentUpdate en-US.json (diff)
downloadferdium-app-3ca3e36cf7fce6b241e242a07cb4731760ee4265.tar.gz
ferdium-app-3ca3e36cf7fce6b241e242a07cb4731760ee4265.tar.zst
ferdium-app-3ca3e36cf7fce6b241e242a07cb4731760ee4265.zip
Merge branch 'develop' into i18n
Diffstat (limited to 'src/components')
-rw-r--r--src/components/TrialActivationInfoBar.js94
-rw-r--r--src/components/auth/AuthLayout.js6
-rw-r--r--src/components/auth/Pricing.js246
-rw-r--r--src/components/auth/Signup.js31
-rw-r--r--src/components/layout/AppLayout.js130
-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.js197
-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.js6
-rw-r--r--src/components/settings/team/TeamDashboard.js10
-rw-r--r--src/components/subscription/SubscriptionForm.js226
-rw-r--r--src/components/subscription/SubscriptionPopup.js4
-rw-r--r--src/components/subscription/TrialForm.js114
-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
22 files changed, 1060 insertions, 507 deletions
diff --git a/src/components/TrialActivationInfoBar.js b/src/components/TrialActivationInfoBar.js
new file mode 100644
index 000000000..acdf51d08
--- /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: 300,
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/AuthLayout.js b/src/components/auth/AuthLayout.js
index 3d43d4e5c..75a8cfc61 100644
--- a/src/components/auth/AuthLayout.js
+++ b/src/components/auth/AuthLayout.js
@@ -22,7 +22,6 @@ export default @observer class AuthLayout extends Component {
22 retryHealthCheck: PropTypes.func.isRequired, 22 retryHealthCheck: PropTypes.func.isRequired,
23 isHealthCheckLoading: PropTypes.bool.isRequired, 23 isHealthCheckLoading: PropTypes.bool.isRequired,
24 isFullScreen: PropTypes.bool.isRequired, 24 isFullScreen: PropTypes.bool.isRequired,
25 darkMode: PropTypes.bool.isRequired,
26 nextAppReleaseVersion: PropTypes.string, 25 nextAppReleaseVersion: PropTypes.string,
27 installAppUpdate: PropTypes.func.isRequired, 26 installAppUpdate: PropTypes.func.isRequired,
28 appUpdateIsDownloaded: PropTypes.bool.isRequired, 27 appUpdateIsDownloaded: PropTypes.bool.isRequired,
@@ -45,7 +44,6 @@ export default @observer class AuthLayout extends Component {
45 retryHealthCheck, 44 retryHealthCheck,
46 isHealthCheckLoading, 45 isHealthCheckLoading,
47 isFullScreen, 46 isFullScreen,
48 darkMode,
49 nextAppReleaseVersion, 47 nextAppReleaseVersion,
50 installAppUpdate, 48 installAppUpdate,
51 appUpdateIsDownloaded, 49 appUpdateIsDownloaded,
@@ -53,7 +51,7 @@ export default @observer class AuthLayout extends Component {
53 const { intl } = this.context; 51 const { intl } = this.context;
54 52
55 return ( 53 return (
56 <div className={darkMode ? 'theme__dark' : ''}> 54 <>
57 {isWindows && !isFullScreen && <TitleBar menu={window.franz.menu.template} icon="assets/images/logo.svg" />} 55 {isWindows && !isFullScreen && <TitleBar menu={window.franz.menu.template} icon="assets/images/logo.svg" />}
58 <div className="auth"> 56 <div className="auth">
59 {!isOnline && ( 57 {!isOnline && (
@@ -93,7 +91,7 @@ export default @observer class AuthLayout extends Component {
93 <img src="./assets/images/adlk.svg" alt="" /> 91 <img src="./assets/images/adlk.svg" alt="" />
94 </Link> 92 </Link>
95 </div> 93 </div>
96 </div> 94 </>
97 ); 95 );
98 } 96 }
99} 97}
diff --git a/src/components/auth/Pricing.js b/src/components/auth/Pricing.js
index 7ab14f429..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 Franz', 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 Franz', 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 Franz.', 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="franz-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 Franz 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 Franz Premium Supporter License will allow us to keep improving Franz 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 d9b83eeb8..0499d764b 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: '',
@@ -151,7 +132,6 @@ export default @observer class Signup extends Component {
151 In Dev Mode your data is not persistent. Please use the live app for accesing the production API. 132 In Dev Mode your data is not persistent. Please use the live app for accesing the production API.
152 </Infobox> 133 </Infobox>
153 )} 134 )}
154 <Radio field={form.$('accountType')} showLabel={false} />
155 <div className="grid__row"> 135 <div className="grid__row">
156 <Input field={form.$('firstname')} focus /> 136 <Input field={form.$('firstname')} focus />
157 <Input field={form.$('lastname')} /> 137 <Input field={form.$('lastname')} />
@@ -162,9 +142,6 @@ export default @observer class Signup extends Component {
162 showPasswordToggle 142 showPasswordToggle
163 scorePassword 143 scorePassword
164 /> 144 />
165 {form.$('accountType').value === 'company' && (
166 <Input field={form.$('organization')} />
167 )}
168 {error.code === 'email-duplicate' && ( 145 {error.code === 'email-duplicate' && (
169 <p className="error-message center">{intl.formatMessage(messages.emailDuplicate)}</p> 146 <p className="error-message center">{intl.formatMessage(messages.emailDuplicate)}</p>
170 )} 147 )}
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index 499bc097a..941e60bfd 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -17,6 +17,8 @@ import { isWindows } from '../../environment';
17import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator'; 17import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator';
18import { workspaceStore } from '../../features/workspaces'; 18import { workspaceStore } from '../../features/workspaces';
19import AppUpdateInfoBar from '../AppUpdateInfoBar'; 19import AppUpdateInfoBar from '../AppUpdateInfoBar';
20import TrialActivationInfoBar from '../TrialActivationInfoBar';
21import Todos from '../../features/todos/containers/TodosScreen';
20 22
21function createMarkup(HTMLString) { 23function createMarkup(HTMLString) {
22 return { __html: HTMLString }; 24 return { __html: HTMLString };
@@ -39,7 +41,8 @@ const messages = defineMessages({
39 41
40const styles = theme => ({ 42const styles = theme => ({
41 appContent: { 43 appContent: {
42 width: `calc(100% + ${theme.workspaces.drawer.width}px)`, 44 // width: `calc(100% + ${theme.workspaces.drawer.width}px)`,
45 width: '100%',
43 transition: 'transform 0.5s ease', 46 transition: 'transform 0.5s ease',
44 transform() { 47 transform() {
45 return workspaceStore.isWorkspaceDrawerOpen ? 'translateX(0)' : `translateX(-${theme.workspaces.drawer.width}px)`; 48 return workspaceStore.isWorkspaceDrawerOpen ? 'translateX(0)' : `translateX(-${theme.workspaces.drawer.width}px)`;
@@ -57,7 +60,6 @@ class AppLayout extends Component {
57 services: PropTypes.element.isRequired, 60 services: PropTypes.element.isRequired,
58 children: PropTypes.element, 61 children: PropTypes.element,
59 news: MobxPropTypes.arrayOrObservableArray.isRequired, 62 news: MobxPropTypes.arrayOrObservableArray.isRequired,
60 // isOnline: PropTypes.bool.isRequired,
61 showServicesUpdatedInfoBar: PropTypes.bool.isRequired, 63 showServicesUpdatedInfoBar: PropTypes.bool.isRequired,
62 appUpdateIsDownloaded: PropTypes.bool.isRequired, 64 appUpdateIsDownloaded: PropTypes.bool.isRequired,
63 nextAppReleaseVersion: PropTypes.string, 65 nextAppReleaseVersion: PropTypes.string,
@@ -68,8 +70,8 @@ class AppLayout extends Component {
68 areRequiredRequestsSuccessful: PropTypes.bool.isRequired, 70 areRequiredRequestsSuccessful: PropTypes.bool.isRequired,
69 retryRequiredRequests: PropTypes.func.isRequired, 71 retryRequiredRequests: PropTypes.func.isRequired,
70 areRequiredRequestsLoading: PropTypes.bool.isRequired, 72 areRequiredRequestsLoading: PropTypes.bool.isRequired,
71 darkMode: PropTypes.bool.isRequired,
72 isDelayAppScreenVisible: PropTypes.bool.isRequired, 73 isDelayAppScreenVisible: PropTypes.bool.isRequired,
74 hasActivatedTrial: PropTypes.bool.isRequired,
73 }; 75 };
74 76
75 static defaultProps = { 77 static defaultProps = {
@@ -89,7 +91,6 @@ class AppLayout extends Component {
89 sidebar, 91 sidebar,
90 services, 92 services,
91 children, 93 children,
92 // isOnline,
93 news, 94 news,
94 showServicesUpdatedInfoBar, 95 showServicesUpdatedInfoBar,
95 appUpdateIsDownloaded, 96 appUpdateIsDownloaded,
@@ -101,78 +102,71 @@ class AppLayout extends Component {
101 areRequiredRequestsSuccessful, 102 areRequiredRequestsSuccessful,
102 retryRequiredRequests, 103 retryRequiredRequests,
103 areRequiredRequestsLoading, 104 areRequiredRequestsLoading,
104 darkMode,
105 isDelayAppScreenVisible, 105 isDelayAppScreenVisible,
106 hasActivatedTrial,
106 } = this.props; 107 } = this.props;
107 108
108 const { intl } = this.context; 109 const { intl } = this.context;
109 110
110 return ( 111 return (
111 <ErrorBoundary> 112 <ErrorBoundary>
112 <div className={(darkMode ? 'theme__dark' : '')}> 113 <div className="app">
113 <div className="app"> 114 {isWindows && !isFullScreen && <TitleBar menu={window.franz.menu.template} icon="assets/images/logo.svg" />}
114 {isWindows && !isFullScreen && <TitleBar menu={window.franz.menu.template} icon="assets/images/logo.svg" />} 115 <div className={`app__content ${classes.appContent}`}>
115 <div className={`app__content ${classes.appContent}`}> 116 {workspacesDrawer}
116 {workspacesDrawer} 117 {sidebar}
117 {sidebar} 118 <div className="app__service">
118 <div className="app__service"> 119 <WorkspaceSwitchingIndicator />
119 <WorkspaceSwitchingIndicator /> 120 {news.length > 0 && news.map(item => (
120 {news.length > 0 && news.map(item => ( 121 <InfoBar
121 <InfoBar 122 key={item.id}
122 key={item.id} 123 position="top"
123 position="top" 124 type={item.type}
124 type={item.type} 125 sticky={item.sticky}
125 sticky={item.sticky} 126 onHide={() => removeNewsItem({ newsId: item.id })}
126 onHide={() => removeNewsItem({ newsId: item.id })} 127 >
127 > 128 <span dangerouslySetInnerHTML={createMarkup(item.message)} />
128 <span dangerouslySetInnerHTML={createMarkup(item.message)} /> 129 </InfoBar>
129 </InfoBar> 130 ))}
130 ))} 131 {hasActivatedTrial && (
131 {/* {!isOnline && ( 132 <TrialActivationInfoBar />
132 <InfoBar 133 )}
133 type="danger" 134 {!areRequiredRequestsSuccessful && showRequiredRequestsError && (
134 sticky 135 <InfoBar
135 > 136 type="danger"
136 <span className="mdi mdi-flash" /> 137 ctaLabel="Try again"
137 {intl.formatMessage(globalMessages.notConnectedToTheInternet)} 138 ctaLoading={areRequiredRequestsLoading}
138 </InfoBar> 139 sticky
139 )} */} 140 onClick={retryRequiredRequests}
140 {!areRequiredRequestsSuccessful && showRequiredRequestsError && ( 141 >
141 <InfoBar 142 <span className="mdi mdi-flash" />
142 type="danger" 143 {intl.formatMessage(messages.requiredRequestsFailed)}
143 ctaLabel="Try again" 144 </InfoBar>
144 ctaLoading={areRequiredRequestsLoading} 145 )}
145 sticky 146 {showServicesUpdatedInfoBar && (
146 onClick={retryRequiredRequests} 147 <InfoBar
147 > 148 type="primary"
148 <span className="mdi mdi-flash" /> 149 ctaLabel={intl.formatMessage(messages.buttonReloadServices)}
149 {intl.formatMessage(messages.requiredRequestsFailed)} 150 onClick={reloadServicesAfterUpdate}
150 </InfoBar> 151 sticky
151 )} 152 >
152 {showServicesUpdatedInfoBar && ( 153 <span className="mdi mdi-power-plug" />
153 <InfoBar 154 {intl.formatMessage(messages.servicesUpdated)}
154 type="primary" 155 </InfoBar>
155 ctaLabel={intl.formatMessage(messages.buttonReloadServices)} 156 )}
156 onClick={reloadServicesAfterUpdate} 157 {appUpdateIsDownloaded && (
157 sticky 158 <AppUpdateInfoBar
158 > 159 nextAppReleaseVersion={nextAppReleaseVersion}
159 <span className="mdi mdi-power-plug" /> 160 onInstallUpdate={installAppUpdate}
160 {intl.formatMessage(messages.servicesUpdated)} 161 />
161 </InfoBar> 162 )}
162 )} 163 {isDelayAppScreenVisible && (<DelayApp />)}
163 {appUpdateIsDownloaded && ( 164 <BasicAuth />
164 <AppUpdateInfoBar 165 <ShareFranz />
165 nextAppReleaseVersion={nextAppReleaseVersion} 166 {services}
166 onInstallUpdate={installAppUpdate} 167 {children}
167 />
168 )}
169 {isDelayAppScreenVisible && (<DelayApp />)}
170 <BasicAuth />
171 <ShareFranz />
172 {services}
173 {children}
174 </div>
175 </div> 168 </div>
169 <Todos />
176 </div> 170 </div>
177 </div> 171 </div>
178 </ErrorBoundary> 172 </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 8f8c38a11..73c27bfb6 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';
@@ -18,7 +21,17 @@ const messages = defineMessages({
18 }, 21 },
19}); 22});
20 23
21export default @observer class Services extends Component { 24
25const styles = {
26 confettiContainer: {
27 position: 'absolute',
28 width: '100%',
29 zIndex: 9999,
30 pointerEvents: 'none',
31 },
32};
33
34export default @observer @injectSheet(styles) class Services extends Component {
22 static propTypes = { 35 static propTypes = {
23 services: MobxPropTypes.arrayOrObservableArray, 36 services: MobxPropTypes.arrayOrObservableArray,
24 setWebviewReference: PropTypes.func.isRequired, 37 setWebviewReference: PropTypes.func.isRequired,
@@ -28,6 +41,9 @@ export default @observer class Services extends Component {
28 reload: PropTypes.func.isRequired, 41 reload: PropTypes.func.isRequired,
29 openSettings: PropTypes.func.isRequired, 42 openSettings: PropTypes.func.isRequired,
30 update: PropTypes.func.isRequired, 43 update: PropTypes.func.isRequired,
44 userHasCompletedSignup: PropTypes.bool.isRequired,
45 hasActivatedTrial: PropTypes.bool.isRequired,
46 classes: PropTypes.object.isRequired,
31 }; 47 };
32 48
33 static defaultProps = { 49 static defaultProps = {
@@ -38,6 +54,18 @@ export default @observer class Services extends Component {
38 intl: intlShape, 54 intl: intlShape,
39 }; 55 };
40 56
57 state = {
58 showConfetti: true,
59 }
60
61 componentDidMount() {
62 window.setTimeout(() => {
63 this.setState({
64 showConfetti: false,
65 });
66 }, ms('8s'));
67 }
68
41 render() { 69 render() {
42 const { 70 const {
43 services, 71 services,
@@ -48,11 +76,28 @@ export default @observer class Services extends Component {
48 reload, 76 reload,
49 openSettings, 77 openSettings,
50 update, 78 update,
79 userHasCompletedSignup,
80 hasActivatedTrial,
81 classes,
51 } = this.props; 82 } = this.props;
83
84 const {
85 showConfetti,
86 } = this.state;
87
52 const { intl } = this.context; 88 const { intl } = this.context;
53 89
54 return ( 90 return (
55 <div className="services"> 91 <div className="services">
92 {(userHasCompletedSignup || hasActivatedTrial) && (
93 <div className={classes.confettiContainer}>
94 <Confetti
95 width={window.width}
96 height={window.height}
97 numberOfPieces={showConfetti ? 200 : 0}
98 />
99 </div>
100 )}
56 {services.length === 0 && ( 101 {services.length === 0 && (
57 <Appear 102 <Appear
58 timeout={1500} 103 timeout={1500}
@@ -89,6 +134,7 @@ export default @observer class Services extends Component {
89 }, 134 },
90 redirect: false, 135 redirect: false,
91 })} 136 })}
137 upgrade={() => openSettings({ path: 'user' })}
92 /> 138 />
93 ))} 139 ))}
94 </div> 140 </div>
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js
index 3f6964b6b..900a83a78 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,36 @@ 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 { 96export default @observer class AccountDashboard extends Component {
77 static propTypes = { 97 static propTypes = {
78 user: MobxPropTypes.observableObject.isRequired, 98 user: MobxPropTypes.observableObject.isRequired,
79 isLoading: PropTypes.bool.isRequired, 99 isLoading: PropTypes.bool.isRequired,
80 isLoadingPlans: PropTypes.bool.isRequired,
81 userInfoRequestFailed: PropTypes.bool.isRequired, 100 userInfoRequestFailed: PropTypes.bool.isRequired,
82 retryUserInfoRequest: PropTypes.func.isRequired, 101 retryUserInfoRequest: PropTypes.func.isRequired,
83 onCloseSubscriptionWindow: PropTypes.func.isRequired,
84 deleteAccount: PropTypes.func.isRequired, 102 deleteAccount: PropTypes.func.isRequired,
85 isLoadingDeleteAccount: PropTypes.bool.isRequired, 103 isLoadingDeleteAccount: PropTypes.bool.isRequired,
86 isDeleteAccountSuccessful: PropTypes.bool.isRequired, 104 isDeleteAccountSuccessful: PropTypes.bool.isRequired,
87 openEditAccount: PropTypes.func.isRequired, 105 openEditAccount: PropTypes.func.isRequired,
88 openBilling: PropTypes.func.isRequired, 106 openBilling: PropTypes.func.isRequired,
107 upgradeToPro: PropTypes.func.isRequired,
89 openInvoices: PropTypes.func.isRequired, 108 openInvoices: PropTypes.func.isRequired,
90 }; 109 };
91 110
@@ -97,19 +116,24 @@ export default @observer class AccountDashboard extends Component {
97 const { 116 const {
98 user, 117 user,
99 isLoading, 118 isLoading,
100 isLoadingPlans,
101 userInfoRequestFailed, 119 userInfoRequestFailed,
102 retryUserInfoRequest, 120 retryUserInfoRequest,
103 onCloseSubscriptionWindow,
104 deleteAccount, 121 deleteAccount,
105 isLoadingDeleteAccount, 122 isLoadingDeleteAccount,
106 isDeleteAccountSuccessful, 123 isDeleteAccountSuccessful,
107 openEditAccount, 124 openEditAccount,
108 openBilling, 125 openBilling,
126 upgradeToPro,
109 openInvoices, 127 openInvoices,
110 } = this.props; 128 } = this.props;
111 const { intl } = this.context; 129 const { intl } = this.context;
112 130
131 let planName = '';
132
133 if (user.team && user.team.plan) {
134 planName = i18nPlanName(user.team.plan, intl);
135 }
136
113 return ( 137 return (
114 <div className="settings__main"> 138 <div className="settings__main">
115 <div className="settings__header"> 139 <div className="settings__header">
@@ -135,82 +159,113 @@ export default @observer class AccountDashboard extends Component {
135 )} 159 )}
136 160
137 {!userInfoRequestFailed && ( 161 {!userInfoRequestFailed && (
138 <Fragment> 162 <>
139 {!isLoading && ( 163 {!isLoading && (
140 <div className="account"> 164 <>
141 <div className="account__box account__box--flex"> 165 <div className="account">
142 <div className="account__avatar"> 166 <div className="account__box account__box--flex">
143 <img 167 <div className="account__avatar">
144 src="./assets/images/logo.svg" 168 <img
145 alt="" 169 src="./assets/images/logo.svg"
146 /> 170 alt=""
147 </div> 171 />
148 <div className="account__info"> 172 </div>
149 <h2> 173 <div className="account__info">
150 <span className="username">{`${user.firstname} ${user.lastname}`}</span> 174 <H1>
175 <span className="username">{`${user.firstname} ${user.lastname}`}</span>
176 {user.isPremium && (
177 <>
178 {' '}
179 <ProBadge />
180 </>
181 )}
182 </H1>
183 <p>
184 {user.organization && `${user.organization}, `}
185 {user.email}
186 </p>
151 {user.isPremium && ( 187 {user.isPremium && (
188 <div className="manage-user-links">
189 <Button
190 label={intl.formatMessage(messages.accountEditButton)}
191 className="franz-form__button--inverted"
192 onClick={openEditAccount}
193 />
194 </div>
195 )}
196 </div>
197 {!user.isPremium && (
198 <Button
199 label={intl.formatMessage(messages.accountEditButton)}
200 className="franz-form__button--inverted"
201 onClick={openEditAccount}
202 />
203 )}
204 </div>
205 </div>
206 {user.isPremium && user.isSubscriptionOwner && (
207 <div className="account">
208 <div className="account__box">
209 <H2>
210 {intl.formatMessage(messages.yourLicense)}
211 </H2>
212 <p>
213 {planName}
214 {user.team.isTrial && (
215 <>
216 {' – '}
217 {intl.formatMessage(messages.trial)}
218 </>
219 )}
220 </p>
221 {user.team.isTrial && (
152 <> 222 <>
153 {' '} 223 <br />
154 <ProBadge /> 224 <p>
155 <span className="badge badge--premium">{intl.formatMessage(messages.accountTypePremium)}</span> 225 {intl.formatMessage(messages.trialEndsIn, {
226 duration: moment.duration(moment().diff(user.team.trialEnd)).humanize(),
227 })}
228 </p>
229 <p>
230 {intl.formatMessage(messages.trialUpdateBillingInformation, {
231 license: planName,
232 })}
233 </p>
156 </> 234 </>
157 )} 235 )}
158 </h2>
159 {user.organization && `${user.organization}, `}
160 {user.email}
161 {user.isPremium && (
162 <div className="manage-user-links"> 236 <div className="manage-user-links">
163 <Button 237 <Button
164 label={intl.formatMessage(messages.accountEditButton)} 238 label={intl.formatMessage(messages.upgradeAccountToPro)}
239 className="franz-form__button--primary"
240 onClick={upgradeToPro}
241 />
242 <Button
243 label={intl.formatMessage(messages.manageSubscriptionButtonLabel)}
165 className="franz-form__button--inverted" 244 className="franz-form__button--inverted"
166 onClick={openEditAccount} 245 onClick={openBilling}
246 />
247 <Button
248 label={intl.formatMessage(messages.invoicesButton)}
249 className="franz-form__button--inverted"
250 onClick={openInvoices}
167 /> 251 />
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> 252 </div>
183 )} 253 </div>
184 </div> 254 </div>
185 {!user.isPremium && ( 255 )}
186 <Button 256 {!user.isPremium && (
187 label={intl.formatMessage(messages.accountEditButton)} 257 <div className="account franz-form">
188 className="franz-form__button--inverted" 258 <div className="account__box">
189 onClick={openEditAccount} 259 <SubscriptionForm />
190 /> 260 </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> 261 </div>
207 </div> 262 )}
208 ) 263 </>
209 )} 264 )}
210 265
211 <div className="account franz-form"> 266 <div className="account franz-form">
212 <div className="account__box"> 267 <div className="account__box">
213 <h2>{intl.formatMessage(messages.headlineDangerZone)}</h2> 268 <H2>{intl.formatMessage(messages.headlineDangerZone)}</H2>
214 {!isDeleteAccountSuccessful && ( 269 {!isDeleteAccountSuccessful && (
215 <div className="account__subscription"> 270 <div className="account__subscription">
216 <p>{intl.formatMessage(messages.deleteInfo)}</p> 271 <p>{intl.formatMessage(messages.deleteInfo)}</p>
@@ -227,7 +282,7 @@ export default @observer class AccountDashboard extends Component {
227 )} 282 )}
228 </div> 283 </div>
229 </div> 284 </div>
230 </Fragment> 285 </>
231 )} 286 )}
232 </div> 287 </div>
233 <ReactTooltip place="right" type="dark" effect="solid" /> 288 <ReactTooltip place="right" type="dark" effect="solid" />
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js
index df4b3b3b2..4696b82eb 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: {
@@ -80,7 +81,12 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp
80 > 81 >
81 {intl.formatMessage(messages.yourServices)} 82 {intl.formatMessage(messages.yourServices)}
82 {' '} 83 {' '}
83 <span className="badge">{serviceCount}</span> 84 <span className="badge">
85 {serviceCount}
86 {serviceLimitStore.serviceLimit !== 0 && (
87 `/${serviceLimitStore.serviceLimit}`
88 )}
89 </span>
84 </Link> 90 </Link>
85 {workspaceStore.isFeatureEnabled ? ( 91 {workspaceStore.isFeatureEnabled ? (
86 <Link 92 <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..75e60b7ec 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 Service Recipes',
67 },
68 headlineCommunityRecipes: {
69 id: 'settings.recipes.customService.headline.communityRecipes',
70 defaultMessage: '!!!Community Services',
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 4ba2eb844..5cde0db8e 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 efd453356..3f9e0a6bc 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -100,7 +100,7 @@ export default @observer class EditSettingsForm extends Component {
100 isClearingAllCache: PropTypes.bool.isRequired, 100 isClearingAllCache: PropTypes.bool.isRequired,
101 onClearAllCache: PropTypes.func.isRequired, 101 onClearAllCache: PropTypes.func.isRequired,
102 cacheSize: PropTypes.string.isRequired, 102 cacheSize: PropTypes.string.isRequired,
103 isSpellcheckerPremiumFeature: PropTypes.bool.isRequired, 103 isSpellcheckerIncludedInCurrentPlan: PropTypes.bool.isRequired,
104 }; 104 };
105 105
106 static contextTypes = { 106 static contextTypes = {
@@ -130,7 +130,7 @@ export default @observer class EditSettingsForm extends Component {
130 isClearingAllCache, 130 isClearingAllCache,
131 onClearAllCache, 131 onClearAllCache,
132 cacheSize, 132 cacheSize,
133 isSpellcheckerPremiumFeature, 133 isSpellcheckerIncludedInCurrentPlan,
134 } = this.props; 134 } = this.props;
135 const { intl } = this.context; 135 const { intl } = this.context;
136 136
@@ -173,7 +173,7 @@ export default @observer class EditSettingsForm extends Component {
173 <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2> 173 <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2>
174 <Select field={form.$('locale')} showLabel={false} /> 174 <Select field={form.$('locale')} showLabel={false} />
175 <PremiumFeatureContainer 175 <PremiumFeatureContainer
176 condition={isSpellcheckerPremiumFeature} 176 condition={!isSpellcheckerIncludedInCurrentPlan}
177 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }} 177 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }}
178 > 178 >
179 <Fragment> 179 <Fragment>
diff --git a/src/components/settings/team/TeamDashboard.js b/src/components/settings/team/TeamDashboard.js
index 82c517fcb..990ee52e7 100644
--- a/src/components/settings/team/TeamDashboard.js
+++ b/src/components/settings/team/TeamDashboard.js
@@ -133,13 +133,13 @@ export default @injectSheet(styles) @observer class TeamDashboard extends Compon
133 </div> 133 </div>
134 <img className={classes.image} src="https://cdn.franzinfra.com/announcements/assets/teams.png" alt="Franz for Teams" /> 134 <img className={classes.image} src="https://cdn.franzinfra.com/announcements/assets/teams.png" alt="Franz for Teams" />
135 </div> 135 </div>
136 <Button
137 label={intl.formatMessage(messages.manageButton)}
138 onClick={openTeamManagement}
139 className={classes.cta}
140 />
141 </> 136 </>
142 </PremiumFeatureContainer> 137 </PremiumFeatureContainer>
138 <Button
139 label={intl.formatMessage(messages.manageButton)}
140 onClick={openTeamManagement}
141 className={classes.cta}
142 />
143 </> 143 </>
144 )} 144 )}
145 </> 145 </>
diff --git a/src/components/subscription/SubscriptionForm.js b/src/components/subscription/SubscriptionForm.js
index 50f1e0522..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 Franz 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..9fe1c93b7
--- /dev/null
+++ b/src/components/subscription/TrialForm.js
@@ -0,0 +1,114 @@
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 = () => ({
45 activateTrialButton: {
46 margin: [40, 0, 10],
47 },
48 allOptionsButton: {
49 margin: [0, 0, 40],
50 background: 'none',
51 border: 'none',
52 },
53 keyTerms: {
54 marginTop: 20,
55 },
56});
57
58export default @observer @injectSheet(styles) class TrialForm extends Component {
59 static propTypes = {
60 activateTrial: PropTypes.func.isRequired,
61 isActivatingTrial: PropTypes.bool.isRequired,
62 showAllOptions: PropTypes.func.isRequired,
63 classes: PropTypes.object.isRequired,
64 };
65
66 static contextTypes = {
67 intl: intlShape,
68 };
69
70 render() {
71 const {
72 isActivatingTrial,
73 activateTrial,
74 showAllOptions,
75 classes,
76 } = this.props;
77 const { intl } = this.context;
78
79 return (
80 <>
81 <H2>{intl.formatMessage(messages.teaserHeadline)}</H2>
82 <H3 className={classes.keyTerms}>
83 {intl.formatMessage(messages.noStringsAttachedHeadline)}
84 </H3>
85 <ul>
86 <FeatureItem icon="👉" name={intl.formatMessage(messages.noCreditCard)} />
87 <FeatureItem icon="👉" name={intl.formatMessage(messages.automaticTrialEnd)} />
88 </ul>
89
90 <Button
91 label={intl.formatMessage(messages.submitButtonLabel)}
92 className={classes.activateTrialButton}
93 busy={isActivatingTrial}
94 onClick={activateTrial}
95 stretch
96 />
97 <Button
98 label={intl.formatMessage(messages.allOptionsButton)}
99 className={classes.allOptionsButton}
100 onClick={showAllOptions}
101 stretch
102 />
103 <div className="subscription__premium-info">
104 <H3>
105 {intl.formatMessage(messages.includedFeatures)}
106 </H3>
107 <div className="subscription">
108 <FeatureList />
109 </div>
110 </div>
111 </>
112 );
113 }
114}
diff --git a/src/components/ui/FeatureItem.js b/src/components/ui/FeatureItem.js
new file mode 100644
index 000000000..53616f2eb
--- /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.legacyStyles.themeGrayDark],
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}>