aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/settings
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/settings
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/settings')
-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
8 files changed, 378 insertions, 137 deletions
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 </>