aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar vantezzen <hello@vantezzen.io>2019-10-24 15:55:22 +0200
committerLibravatar vantezzen <hello@vantezzen.io>2019-10-24 15:55:22 +0200
commitbacb5b940333f7e3af9f9d978d1d72c75f1aa321 (patch)
tree50c8ecb3d08e997106e48d1de5b904a2dba30991 /src
parentMerge translations (diff)
parentSwitch to beta version (diff)
downloadferdium-app-bacb5b940333f7e3af9f9d978d1d72c75f1aa321.tar.gz
ferdium-app-bacb5b940333f7e3af9f9d978d1d72c75f1aa321.tar.zst
ferdium-app-bacb5b940333f7e3af9f9d978d1d72c75f1aa321.zip
Merge branch 'develop' into l10n_develop
Diffstat (limited to 'src')
-rw-r--r--src/actions/index.js4
-rw-r--r--src/actions/payment.js4
-rw-r--r--src/actions/user.js4
-rw-r--r--src/api/server/ServerApi.js2
-rw-r--r--src/components/auth/Login.js2
-rw-r--r--src/components/auth/Pricing.js122
-rw-r--r--src/components/auth/Signup.js2
-rw-r--r--src/components/auth/Welcome.js2
-rw-r--r--src/components/layout/AppLayout.js4
-rw-r--r--src/components/settings/account/AccountDashboard.js18
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js2
-rw-r--r--src/components/settings/services/EditServiceForm.js19
-rw-r--r--src/components/subscription/SubscriptionForm.js6
-rw-r--r--src/components/subscription/SubscriptionPopup.js2
-rw-r--r--src/components/subscription/TrialForm.js4
-rw-r--r--src/components/ui/FeatureItem.js1
-rw-r--r--src/components/ui/FeatureList.js74
-rw-r--r--src/components/ui/PremiumFeatureContainer/index.js4
-rw-r--r--src/config.js8
-rw-r--r--src/containers/auth/PricingScreen.js32
-rw-r--r--src/containers/auth/SignupScreen.js22
-rw-r--r--src/containers/layout/AppLayoutContainer.js5
-rw-r--r--src/containers/settings/AccountScreen.js17
-rw-r--r--src/containers/subscription/SubscriptionFormScreen.js32
-rw-r--r--src/electron/Settings.js4
-rw-r--r--src/electron/ipc-api/localServer.js3
-rw-r--r--src/features/delayApp/Component.js2
-rw-r--r--src/features/delayApp/index.js9
-rw-r--r--src/features/planSelection/actions.js9
-rw-r--r--src/features/planSelection/api.js26
-rw-r--r--src/features/planSelection/components/PlanItem.js207
-rw-r--r--src/features/planSelection/components/PlanSelection.js279
-rw-r--r--src/features/planSelection/containers/PlanSelectionScreen.js123
-rw-r--r--src/features/planSelection/index.js28
-rw-r--r--src/features/planSelection/store.js68
-rw-r--r--src/features/shareFranz/index.js3
-rw-r--r--src/features/trialStatusBar/actions.js13
-rw-r--r--src/features/trialStatusBar/components/ProgressBar.js45
-rw-r--r--src/features/trialStatusBar/components/TrialStatusBar.js135
-rw-r--r--src/features/trialStatusBar/containers/TrialStatusBarScreen.js109
-rw-r--r--src/features/trialStatusBar/index.js30
-rw-r--r--src/features/trialStatusBar/store.js72
-rw-r--r--src/features/workspaces/components/WorkspaceDrawer.js9
-rw-r--r--src/features/workspaces/components/WorkspacesDashboard.js5
-rw-r--r--src/features/workspaces/store.js5
-rw-r--r--src/helpers/plan-helpers.js8
-rw-r--r--src/i18n/locales/defaultMessages.json649
-rw-r--r--src/i18n/locales/en-US.json41
-rw-r--r--src/i18n/locales/zh-Hant.json485
-rw-r--r--src/i18n/messages/src/components/auth/Pricing.json92
-rw-r--r--src/i18n/messages/src/components/layout/AppLayout.json16
-rw-r--r--src/i18n/messages/src/components/ui/FeatureList.json105
-rw-r--r--src/i18n/messages/src/features/delayApp/Component.json2
-rw-r--r--src/i18n/messages/src/features/planSelection/components/PlanItem.json41
-rw-r--r--src/i18n/messages/src/features/planSelection/components/PlanSelection.json158
-rw-r--r--src/i18n/messages/src/features/planSelection/components/PlanTeaser.json28
-rw-r--r--src/i18n/messages/src/features/planSelection/containers/PlanSelectionScreen.json54
-rw-r--r--src/i18n/messages/src/features/trialStatusBar/components/TrialStatusBar.json41
-rw-r--r--src/i18n/messages/src/features/trialStatusBar/containers/TrialStatusBarScreen.json54
-rw-r--r--src/i18n/messages/src/features/workspaces/components/WorkspacesDashboard.json32
-rw-r--r--src/i18n/messages/src/helpers/plan-helpers.json8
-rw-r--r--src/index.js13
-rw-r--r--src/lib/Menu.js4
-rw-r--r--src/lib/TouchBar.js4
-rw-r--r--src/stores/AppStore.js53
-rw-r--r--src/stores/FeaturesStore.js5
-rw-r--r--src/stores/PaymentStore.js38
-rw-r--r--src/stores/ServicesStore.js8
-rw-r--r--src/stores/UserStore.js8
-rw-r--r--src/stores/index.js2
-rw-r--r--src/styles/settings.scss2
-rw-r--r--src/styles/subscription-popup.scss5
-rw-r--r--src/webview/lib/RecipeWebview.js10
-rw-r--r--src/webview/recipe.js4
74 files changed, 2808 insertions, 738 deletions
diff --git a/src/actions/index.js b/src/actions/index.js
index 336344d76..9d3684edc 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -14,6 +14,8 @@ import requests from './requests';
14import announcements from '../features/announcements/actions'; 14import announcements from '../features/announcements/actions';
15import workspaces from '../features/workspaces/actions'; 15import workspaces from '../features/workspaces/actions';
16import todos from '../features/todos/actions'; 16import todos from '../features/todos/actions';
17import planSelection from '../features/planSelection/actions';
18import trialStatusBar from '../features/trialStatusBar/actions';
17 19
18const actions = Object.assign({}, { 20const actions = Object.assign({}, {
19 service, 21 service,
@@ -33,4 +35,6 @@ export default Object.assign(
33 { announcements }, 35 { announcements },
34 { workspaces }, 36 { workspaces },
35 { todos }, 37 { todos },
38 { planSelection },
39 { trialStatusBar },
36); 40);
diff --git a/src/actions/payment.js b/src/actions/payment.js
index 2aaefc025..f61faf197 100644
--- a/src/actions/payment.js
+++ b/src/actions/payment.js
@@ -4,5 +4,9 @@ export default {
4 createHostedPage: { 4 createHostedPage: {
5 planId: PropTypes.string.isRequired, 5 planId: PropTypes.string.isRequired,
6 }, 6 },
7 upgradeAccount: {
8 planId: PropTypes.string.isRequired,
9 onCloseWindow: PropTypes.func,
10 },
7 createDashboardUrl: {}, 11 createDashboardUrl: {},
8}; 12};
diff --git a/src/actions/user.js b/src/actions/user.js
index 5d7d9a899..7061a367a 100644
--- a/src/actions/user.js
+++ b/src/actions/user.js
@@ -11,8 +11,10 @@ export default {
11 lastname: PropTypes.string.isRequired, 11 lastname: PropTypes.string.isRequired,
12 email: PropTypes.string.isRequired, 12 email: PropTypes.string.isRequired,
13 password: PropTypes.string.isRequired, 13 password: PropTypes.string.isRequired,
14 accountType: PropTypes.string.isRequired, 14 accountType: PropTypes.string,
15 company: PropTypes.string, 15 company: PropTypes.string,
16 plan: PropTypes.string,
17 currency: PropTypes.string,
16 }, 18 },
17 retrievePassword: { 19 retrievePassword: {
18 email: PropTypes.string.isRequired, 20 email: PropTypes.string.isRequired,
diff --git a/src/api/server/ServerApi.js b/src/api/server/ServerApi.js
index 02f6b389d..a5d636b4e 100644
--- a/src/api/server/ServerApi.js
+++ b/src/api/server/ServerApi.js
@@ -87,7 +87,7 @@ export default class ServerApi {
87 } 87 }
88 const trial = await request.json(); 88 const trial = await request.json();
89 89
90 debug('ServerApi::signup resolves', trial); 90 debug('ServerApi::activateTrial resolves', trial);
91 return true; 91 return true;
92 } 92 }
93 93
diff --git a/src/components/auth/Login.js b/src/components/auth/Login.js
index e58016e25..e25121de0 100644
--- a/src/components/auth/Login.js
+++ b/src/components/auth/Login.js
@@ -70,7 +70,7 @@ const messages = defineMessages({
70 }, 70 },
71}); 71});
72 72
73export default @observer @inject('actions') class Login extends Component { 73export default @inject('actions') @observer class Login extends Component {
74 static propTypes = { 74 static propTypes = {
75 onSubmit: PropTypes.func.isRequired, 75 onSubmit: PropTypes.func.isRequired,
76 isSubmitting: PropTypes.bool.isRequired, 76 isSubmitting: PropTypes.bool.isRequired,
diff --git a/src/components/auth/Pricing.js b/src/components/auth/Pricing.js
index aadb18d91..593cb9c4b 100644
--- a/src/components/auth/Pricing.js
+++ b/src/components/auth/Pricing.js
@@ -13,12 +13,20 @@ import { FeatureList } from '../ui/FeatureList';
13 13
14const messages = defineMessages({ 14const messages = defineMessages({
15 headline: { 15 headline: {
16 id: 'pricing.trial.headline', 16 id: 'pricing.trial.headline.pro',
17 defaultMessage: '!!!Franz Professional', 17 defaultMessage: '!!!Hi {name}, welcome to Franz',
18 }, 18 },
19 personalOffer: { 19 specialTreat: {
20 id: 'pricing.trial.subheadline', 20 id: 'pricing.trial.intro.specialTreat',
21 defaultMessage: '!!!Your personal welcome offer:', 21 defaultMessage: '!!!We have a special treat for you.',
22 },
23 tryPro: {
24 id: 'pricing.trial.intro.tryPro',
25 defaultMessage: '!!!Enjoy the full Franz Professional experience completely free for 14 days.',
26 },
27 happyMessaging: {
28 id: 'pricing.trial.intro.happyMessaging',
29 defaultMessage: '!!!Happy messaging,',
22 }, 30 },
23 noStringsAttachedHeadline: { 31 noStringsAttachedHeadline: {
24 id: 'pricing.trial.terms.headline', 32 id: 'pricing.trial.terms.headline',
@@ -32,13 +40,21 @@ const messages = defineMessages({
32 id: 'pricing.trial.terms.automaticTrialEnd', 40 id: 'pricing.trial.terms.automaticTrialEnd',
33 defaultMessage: '!!!Your free trial ends automatically after 14 days', 41 defaultMessage: '!!!Your free trial ends automatically after 14 days',
34 }, 42 },
43 trialWorth: {
44 id: 'pricing.trial.terms.trialWorth',
45 defaultMessage: '!!!Free trial (normally {currency}{price} per month)',
46 },
35 activationError: { 47 activationError: {
36 id: 'pricing.trial.error', 48 id: 'pricing.trial.error',
37 defaultMessage: '!!!Sorry, we could not activate your trial!', 49 defaultMessage: '!!!Sorry, we could not activate your trial!',
38 }, 50 },
39 ctaAccept: { 51 ctaAccept: {
40 id: 'pricing.trial.cta.accept', 52 id: 'pricing.trial.cta.accept',
41 defaultMessage: '!!!Yes, upgrade my account to Franz Professional', 53 defaultMessage: '!!!Start my 14-day Franz Professional Trial ',
54 },
55 ctaStart: {
56 id: 'pricing.trial.cta.start',
57 defaultMessage: '!!!Start using Franz',
42 }, 58 },
43 ctaSkip: { 59 ctaSkip: {
44 id: 'pricing.trial.cta.skip', 60 id: 'pricing.trial.cta.skip',
@@ -58,6 +74,7 @@ const styles = theme => ({
58 welcomeOffer: { 74 welcomeOffer: {
59 textAlign: 'center', 75 textAlign: 'center',
60 fontWeight: 'bold', 76 fontWeight: 'bold',
77 marginBottom: '6 !important',
61 }, 78 },
62 keyTerms: { 79 keyTerms: {
63 textAlign: 'center', 80 textAlign: 'center',
@@ -93,6 +110,34 @@ const styles = theme => ({
93 margin: [20, 0, 0], 110 margin: [20, 0, 0],
94 color: theme.styleTypes.danger.accent, 111 color: theme.styleTypes.danger.accent,
95 }, 112 },
113 priceContainer: {
114 display: 'flex',
115 justifyContent: 'space-evenly',
116 margin: [10, 0, 15],
117 },
118 price: {
119 '& sup': {
120 verticalAlign: 14,
121 fontSize: 20,
122 },
123 },
124 figure: {
125 fontSize: 40,
126 },
127 regularPrice: {
128 position: 'relative',
129
130 '&:before': {
131 content: '" "',
132 position: 'absolute',
133 width: '130%',
134 height: 1,
135 top: 14,
136 left: -12,
137 borderBottom: [3, 'solid', 'red'],
138 transform: 'rotateZ(-20deg)',
139 },
140 },
96}); 141});
97 142
98export default @injectSheet(styles) @observer class Signup extends Component { 143export default @injectSheet(styles) @observer class Signup extends Component {
@@ -101,7 +146,11 @@ export default @injectSheet(styles) @observer class Signup extends Component {
101 isLoadingRequiredData: PropTypes.bool.isRequired, 146 isLoadingRequiredData: PropTypes.bool.isRequired,
102 isActivatingTrial: PropTypes.bool.isRequired, 147 isActivatingTrial: PropTypes.bool.isRequired,
103 trialActivationError: PropTypes.bool.isRequired, 148 trialActivationError: PropTypes.bool.isRequired,
149 canSkipTrial: PropTypes.bool.isRequired,
104 classes: PropTypes.object.isRequired, 150 classes: PropTypes.object.isRequired,
151 currency: PropTypes.string.isRequired,
152 price: PropTypes.number.isRequired,
153 name: PropTypes.string.isRequired,
105 }; 154 };
106 155
107 static contextTypes = { 156 static contextTypes = {
@@ -114,10 +163,16 @@ export default @injectSheet(styles) @observer class Signup extends Component {
114 isLoadingRequiredData, 163 isLoadingRequiredData,
115 isActivatingTrial, 164 isActivatingTrial,
116 trialActivationError, 165 trialActivationError,
166 canSkipTrial,
117 classes, 167 classes,
168 currency,
169 price,
170 name,
118 } = this.props; 171 } = this.props;
119 const { intl } = this.context; 172 const { intl } = this.context;
120 173
174 const [intPart, fractionPart] = (price).toString().split('.');
175
121 return ( 176 return (
122 <div className={classnames('auth__scroll-container', classes.container)}> 177 <div className={classnames('auth__scroll-container', classes.container)}>
123 <div className={classnames('auth__container', 'auth__container--signup', classes.content)}> 178 <div className={classnames('auth__container', 'auth__container--signup', classes.content)}>
@@ -129,30 +184,51 @@ export default @injectSheet(styles) @observer class Signup extends Component {
129 alt="" 184 alt=""
130 /> 185 />
131 )} 186 )}
132 <p className={classes.welcomeOffer}>{intl.formatMessage(messages.personalOffer)}</p> 187 <h1>{intl.formatMessage(messages.headline, { name })}</h1>
133 <h1>{intl.formatMessage(messages.headline)}</h1>
134 <div className="auth__letter"> 188 <div className="auth__letter">
135 <p> 189 <p>
136 We built Franz with a lot of effort, manpower and love, 190 {intl.formatMessage(messages.specialTreat)}
137 to boost up your messaging experience.
138 <br /> 191 <br />
139 </p> 192 </p>
140 <p> 193 <p>
141 Get the free 14 day Franz Professional trial and see your communication evolving. 194 {intl.formatMessage(messages.tryPro)}
142 <br /> 195 <br />
143 </p> 196 </p>
144 <p> 197 <p>
145 Thanks for being a hero. 198 {intl.formatMessage(messages.happyMessaging)}
146 </p> 199 </p>
147 <p> 200 <p>
148 <strong>Stefan Malzner</strong> 201 <strong>Stefan Malzner</strong>
149 </p> 202 </p>
150 </div> 203 </div>
204 <div className={classes.priceContainer}>
205 <p className={classnames(classes.price, classes.regularPrice)}>
206 <span className={classes.figure}>
207 {currency}
208 {intPart}
209 </span>
210 <sup>{fractionPart}</sup>
211 </p>
212 <p className={classnames(classes.price, classes.trialPrice)}>
213 <span className={classes.figure}>
214 {currency}
215 0
216 </span>
217 <sup>00</sup>
218 </p>
219 </div>
151 <div className={classes.keyTerms}> 220 <div className={classes.keyTerms}>
152 <H2> 221 <H2>
153 {intl.formatMessage(messages.noStringsAttachedHeadline)} 222 {intl.formatMessage(messages.noStringsAttachedHeadline)}
154 </H2> 223 </H2>
155 <ul className={classes.keyTermsList}> 224 <ul className={classes.keyTermsList}>
225 <FeatureItem
226 icon="👉"
227 name={intl.formatMessage(messages.trialWorth, {
228 currency,
229 price,
230 })}
231 />
156 <FeatureItem icon="👉" name={intl.formatMessage(messages.noCreditCard)} /> 232 <FeatureItem icon="👉" name={intl.formatMessage(messages.noCreditCard)} />
157 <FeatureItem icon="👉" name={intl.formatMessage(messages.automaticTrialEnd)} /> 233 <FeatureItem icon="👉" name={intl.formatMessage(messages.automaticTrialEnd)} />
158 </ul> 234 </ul>
@@ -161,33 +237,23 @@ export default @injectSheet(styles) @observer class Signup extends Component {
161 <p className={classes.error}>{intl.formatMessage(messages.activationError)}</p> 237 <p className={classes.error}>{intl.formatMessage(messages.activationError)}</p>
162 )} 238 )}
163 <Button 239 <Button
164 label={intl.formatMessage(messages.ctaAccept)} 240 label={intl.formatMessage(!canSkipTrial ? messages.ctaStart : messages.ctaAccept)}
165 className={classes.cta} 241 className={classes.cta}
166 onClick={onSubmit} 242 onClick={onSubmit}
167 busy={isActivatingTrial} 243 busy={isActivatingTrial}
168 disabled={isLoadingRequiredData || isActivatingTrial} 244 disabled={isLoadingRequiredData || isActivatingTrial}
169 /> 245 />
170 <p className={classes.skipLink}> 246 {canSkipTrial && (
171 <a href="#/">{intl.formatMessage(messages.ctaSkip)}</a> 247 <p className={classes.skipLink}>
172 </p> 248 <a href="#/">{intl.formatMessage(messages.ctaSkip)}</a>
249 </p>
250 )}
173 </form> 251 </form>
174 </div> 252 </div>
175 <div className={classes.featureContainer}> 253 <div className={classes.featureContainer}>
176 <H2> 254 <H2>
177 {intl.formatMessage(messages.featuresHeadline)} 255 {intl.formatMessage(messages.featuresHeadline)}
178 </H2> 256 </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 /> 257 <FeatureList />
192 </div> 258 </div>
193 </div> 259 </div>
diff --git a/src/components/auth/Signup.js b/src/components/auth/Signup.js
index 47e9daf18..a166155a7 100644
--- a/src/components/auth/Signup.js
+++ b/src/components/auth/Signup.js
@@ -74,7 +74,7 @@ const messages = defineMessages({
74 }, 74 },
75}); 75});
76 76
77export default @observer @inject('actions') class Signup extends Component { 77export default @inject('actions') @observer class Signup extends Component {
78 static propTypes = { 78 static propTypes = {
79 onSubmit: PropTypes.func.isRequired, 79 onSubmit: PropTypes.func.isRequired,
80 isSubmitting: PropTypes.bool.isRequired, 80 isSubmitting: PropTypes.bool.isRequired,
diff --git a/src/components/auth/Welcome.js b/src/components/auth/Welcome.js
index 2ca8b430f..1453c1d7c 100644
--- a/src/components/auth/Welcome.js
+++ b/src/components/auth/Welcome.js
@@ -22,7 +22,7 @@ const messages = defineMessages({
22 }, 22 },
23}); 23});
24 24
25export default @observer @inject('actions') class Login extends Component { 25export default @inject('actions') @observer class Login extends Component {
26 static propTypes = { 26 static propTypes = {
27 loginRoute: PropTypes.string.isRequired, 27 loginRoute: PropTypes.string.isRequired,
28 signupRoute: PropTypes.string.isRequired, 28 signupRoute: PropTypes.string.isRequired,
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index 2b0719f92..80e6daf19 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -19,6 +19,8 @@ import { workspaceStore } from '../../features/workspaces';
19import AppUpdateInfoBar from '../AppUpdateInfoBar'; 19import AppUpdateInfoBar from '../AppUpdateInfoBar';
20import TrialActivationInfoBar from '../TrialActivationInfoBar'; 20import TrialActivationInfoBar from '../TrialActivationInfoBar';
21import Todos from '../../features/todos/containers/TodosScreen'; 21import Todos from '../../features/todos/containers/TodosScreen';
22import PlanSelection from '../../features/planSelection/containers/PlanSelectionScreen';
23import TrialStatusBar from '../../features/trialStatusBar/containers/TrialStatusBarScreen';
22 24
23function createMarkup(HTMLString) { 25function createMarkup(HTMLString) {
24 return { __html: HTMLString }; 26 return { __html: HTMLString };
@@ -189,9 +191,11 @@ class AppLayout extends Component {
189 <QuickSwitch /> 191 <QuickSwitch />
190 {services} 192 {services}
191 {children} 193 {children}
194 <TrialStatusBar />
192 </div> 195 </div>
193 <Todos /> 196 <Todos />
194 </div> 197 </div>
198 <PlanSelection />
195 </div> 199 </div>
196 </ErrorBoundary> 200 </ErrorBoundary>
197 ); 201 );
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js
index f588449f4..83dc34a52 100644
--- a/src/components/settings/account/AccountDashboard.js
+++ b/src/components/settings/account/AccountDashboard.js
@@ -109,6 +109,7 @@ class AccountDashboard extends Component {
109 openBilling: PropTypes.func.isRequired, 109 openBilling: PropTypes.func.isRequired,
110 upgradeToPro: PropTypes.func.isRequired, 110 upgradeToPro: PropTypes.func.isRequired,
111 openInvoices: PropTypes.func.isRequired, 111 openInvoices: PropTypes.func.isRequired,
112 onCloseSubscriptionWindow: PropTypes.func.isRequired,
112 }; 113 };
113 114
114 static contextTypes = { 115 static contextTypes = {
@@ -130,6 +131,7 @@ class AccountDashboard extends Component {
130 openBilling, 131 openBilling,
131 upgradeToPro, 132 upgradeToPro,
132 openInvoices, 133 openInvoices,
134 onCloseSubscriptionWindow,
133 } = this.props; 135 } = this.props;
134 const { intl } = this.context; 136 const { intl } = this.context;
135 137
@@ -215,7 +217,9 @@ class AccountDashboard extends Component {
215 {intl.formatMessage(messages.yourLicense)} 217 {intl.formatMessage(messages.yourLicense)}
216 </H2> 218 </H2>
217 <p> 219 <p>
218 {isPremiumOverrideUser ? 'Franz Premium' : planName} 220 Franz
221 {' '}
222 {isPremiumOverrideUser ? 'Premium' : planName}
219 {user.team.isTrial && ( 223 {user.team.isTrial && (
220 <> 224 <>
221 {' – '} 225 {' – '}
@@ -238,14 +242,16 @@ class AccountDashboard extends Component {
238 </p> 242 </p>
239 </> 243 </>
240 )} 244 )}
241 <div className="manage-user-links"> 245 {!isProUser && (
242 {!isProUser && ( 246 <div className="manage-user-links">
243 <Button 247 <Button
244 label={intl.formatMessage(messages.upgradeAccountToPro)} 248 label={intl.formatMessage(messages.upgradeAccountToPro)}
245 className="franz-form__button--primary" 249 className="franz-form__button--primary"
246 onClick={upgradeToPro} 250 onClick={upgradeToPro}
247 /> 251 />
248 )} 252 </div>
253 )}
254 <div className="manage-user-links">
249 <Button 255 <Button
250 label={intl.formatMessage(messages.manageSubscriptionButtonLabel)} 256 label={intl.formatMessage(messages.manageSubscriptionButtonLabel)}
251 className="franz-form__button--inverted" 257 className="franz-form__button--inverted"
@@ -263,7 +269,9 @@ class AccountDashboard extends Component {
263 {!user.isPremium && ( 269 {!user.isPremium && (
264 <div className="account franz-form"> 270 <div className="account franz-form">
265 <div className="account__box"> 271 <div className="account__box">
266 <SubscriptionForm /> 272 <SubscriptionForm
273 onCloseWindow={onCloseSubscriptionWindow}
274 />
267 </div> 275 </div>
268 </div> 276 </div>
269 )} 277 )}
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js
index 49e73e569..192cfde7a 100644
--- a/src/components/settings/navigation/SettingsNavigation.js
+++ b/src/components/settings/navigation/SettingsNavigation.js
@@ -86,9 +86,9 @@ export default @inject('stores', 'actions') @observer class SettingsNavigation e
86 }, 86 },
87 }); 87 });
88 } 88 }
89 this.props.stores.user.isLoggingOut = true;
89 } 90 }
90 91
91 this.props.stores.user.isLoggingOut = true;
92 this.props.stores.router.push(isLoggedIn ? '/auth/logout' : '/auth/welcome'); 92 this.props.stores.router.push(isLoggedIn ? '/auth/logout' : '/auth/welcome');
93 93
94 if (isLoggedIn) { 94 if (isLoggedIn) {
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js
index 76138aa15..fa34ac60b 100644
--- a/src/components/settings/services/EditServiceForm.js
+++ b/src/components/settings/services/EditServiceForm.js
@@ -328,6 +328,18 @@ export default @observer class EditServiceForm extends Component {
328 )} 328 )}
329 </Tabs> 329 </Tabs>
330 )} 330 )}
331
332 {recipe.message && (
333 <p
334 className="settings__message"
335 style={{
336 marginTop: 0,
337 }}
338 >
339 <span className="mdi mdi-information" />
340 {recipe.message}
341 </p>
342 )}
331 <div className="service-flex-grid"> 343 <div className="service-flex-grid">
332 <div className="settings__options"> 344 <div className="settings__options">
333 <div className="settings__settings-group"> 345 <div className="settings__settings-group">
@@ -417,13 +429,6 @@ export default @observer class EditServiceForm extends Component {
417 </div> 429 </div>
418 </PremiumFeatureContainer> 430 </PremiumFeatureContainer>
419 )} 431 )}
420
421 {recipe.message && (
422 <p className="settings__message">
423 <span className="mdi mdi-information" />
424 {recipe.message}
425 </p>
426 )}
427 </form> 432 </form>
428 </div> 433 </div>
429 <div className="settings__controls"> 434 <div className="settings__controls">
diff --git a/src/components/subscription/SubscriptionForm.js b/src/components/subscription/SubscriptionForm.js
index cdfbbe60d..ec486e5d0 100644
--- a/src/components/subscription/SubscriptionForm.js
+++ b/src/components/subscription/SubscriptionForm.js
@@ -30,11 +30,12 @@ const messages = defineMessages({
30 30
31const styles = () => ({ 31const styles = () => ({
32 activateTrialButton: { 32 activateTrialButton: {
33 margin: [40, 0, 50], 33 margin: [40, 'auto', 50],
34 display: 'flex',
34 }, 35 },
35}); 36});
36 37
37export default @observer @injectSheet(styles) class SubscriptionForm extends Component { 38export default @injectSheet(styles) @observer class SubscriptionForm extends Component {
38 static propTypes = { 39 static propTypes = {
39 selectPlan: PropTypes.func.isRequired, 40 selectPlan: PropTypes.func.isRequired,
40 isActivatingTrial: PropTypes.bool.isRequired, 41 isActivatingTrial: PropTypes.bool.isRequired,
@@ -62,7 +63,6 @@ export default @observer @injectSheet(styles) class SubscriptionForm extends Com
62 className={classes.activateTrialButton} 63 className={classes.activateTrialButton}
63 busy={isActivatingTrial} 64 busy={isActivatingTrial}
64 onClick={selectPlan} 65 onClick={selectPlan}
65 stretch
66 /> 66 />
67 <div className="subscription__premium-info"> 67 <div className="subscription__premium-info">
68 <H3> 68 <H3>
diff --git a/src/components/subscription/SubscriptionPopup.js b/src/components/subscription/SubscriptionPopup.js
index 12ef8a6e9..12a51ad7b 100644
--- a/src/components/subscription/SubscriptionPopup.js
+++ b/src/components/subscription/SubscriptionPopup.js
@@ -59,8 +59,10 @@ export default @observer class SubscriptionPopup extends Component {
59 className="subscription-popup__webview" 59 className="subscription-popup__webview"
60 60
61 autosize 61 autosize
62 allowpopups
62 src={encodeURI(url)} 63 src={encodeURI(url)}
63 onDidNavigate={completeCheck} 64 onDidNavigate={completeCheck}
65 onDidNavigateInPage={completeCheck}
64 /> 66 />
65 </div> 67 </div>
66 <div className="subscription-popup__toolbar franz-form"> 68 <div className="subscription-popup__toolbar franz-form">
diff --git a/src/components/subscription/TrialForm.js b/src/components/subscription/TrialForm.js
index 6ccdf20ae..d61b779ed 100644
--- a/src/components/subscription/TrialForm.js
+++ b/src/components/subscription/TrialForm.js
@@ -43,7 +43,8 @@ const messages = defineMessages({
43 43
44const styles = theme => ({ 44const styles = theme => ({
45 activateTrialButton: { 45 activateTrialButton: {
46 margin: [40, 0, 10], 46 margin: [40, 'auto', 10],
47 display: 'flex',
47 }, 48 },
48 allOptionsButton: { 49 allOptionsButton: {
49 margin: [0, 0, 40], 50 margin: [0, 0, 40],
@@ -93,7 +94,6 @@ export default @injectSheet(styles) @observer class TrialForm extends Component
93 className={classes.activateTrialButton} 94 className={classes.activateTrialButton}
94 busy={isActivatingTrial} 95 busy={isActivatingTrial}
95 onClick={activateTrial} 96 onClick={activateTrial}
96 stretch
97 /> 97 />
98 <Button 98 <Button
99 label={intl.formatMessage(messages.allOptionsButton)} 99 label={intl.formatMessage(messages.allOptionsButton)}
diff --git a/src/components/ui/FeatureItem.js b/src/components/ui/FeatureItem.js
index 7c482c4d4..4926df470 100644
--- a/src/components/ui/FeatureItem.js
+++ b/src/components/ui/FeatureItem.js
@@ -10,6 +10,7 @@ const styles = theme => ({
10 padding: [8, 0], 10 padding: [8, 0],
11 display: 'flex', 11 display: 'flex',
12 alignItems: 'center', 12 alignItems: 'center',
13 textAlign: 'left',
13 }, 14 },
14 featureIcon: { 15 featureIcon: {
15 fill: theme.brandSuccess, 16 fill: theme.brandSuccess,
diff --git a/src/components/ui/FeatureList.js b/src/components/ui/FeatureList.js
index 62944ad75..7ba8b54d7 100644
--- a/src/components/ui/FeatureList.js
+++ b/src/components/ui/FeatureList.js
@@ -3,12 +3,33 @@ import PropTypes from 'prop-types';
3import { defineMessages, intlShape } from 'react-intl'; 3import { defineMessages, intlShape } from 'react-intl';
4 4
5import { FeatureItem } from './FeatureItem'; 5import { FeatureItem } from './FeatureItem';
6import { PLANS } from '../../config';
6 7
7const messages = defineMessages({ 8const messages = defineMessages({
9 availableRecipes: {
10 id: 'pricing.features.recipes',
11 defaultMessage: '!!!Choose from more than 70 Services',
12 },
13 accountSync: {
14 id: 'pricing.features.accountSync',
15 defaultMessage: '!!!Account Synchronisation',
16 },
17 desktopNotifications: {
18 id: 'pricing.features.desktopNotifications',
19 defaultMessage: '!!!Desktop Notifications',
20 },
8 unlimitedServices: { 21 unlimitedServices: {
9 id: 'pricing.features.unlimitedServices', 22 id: 'pricing.features.unlimitedServices',
10 defaultMessage: '!!!Add unlimited services', 23 defaultMessage: '!!!Add unlimited services',
11 }, 24 },
25 upToThreeServices: {
26 id: 'pricing.features.upToThreeServices',
27 defaultMessage: '!!!Add up to 3 services',
28 },
29 upToSixServices: {
30 id: 'pricing.features.upToSixServices',
31 defaultMessage: '!!!Add up to 6 services',
32 },
12 spellchecker: { 33 spellchecker: {
13 id: 'pricing.features.spellchecker', 34 id: 'pricing.features.spellchecker',
14 defaultMessage: '!!!Spellchecker support', 35 defaultMessage: '!!!Spellchecker support',
@@ -51,11 +72,13 @@ export class FeatureList extends Component {
51 static propTypes = { 72 static propTypes = {
52 className: PropTypes.string, 73 className: PropTypes.string,
53 featureClassName: PropTypes.string, 74 featureClassName: PropTypes.string,
75 plan: PropTypes.oneOf(PLANS),
54 }; 76 };
55 77
56 static defaultProps = { 78 static defaultProps = {
57 className: '', 79 className: '',
58 featureClassName: '', 80 featureClassName: '',
81 plan: false,
59 } 82 }
60 83
61 static contextTypes = { 84 static contextTypes = {
@@ -66,21 +89,52 @@ export class FeatureList extends Component {
66 const { 89 const {
67 className, 90 className,
68 featureClassName, 91 featureClassName,
92 plan,
69 } = this.props; 93 } = this.props;
70 const { intl } = this.context; 94 const { intl } = this.context;
71 95
96 const features = [];
97 if (plan === PLANS.FREE) {
98 features.push(
99 messages.upToThreeServices,
100 messages.availableRecipes,
101 messages.accountSync,
102 messages.desktopNotifications,
103 );
104 } else if (plan === PLANS.PERSONAL) {
105 features.push(
106 messages.upToSixServices,
107 messages.spellchecker,
108 messages.appDelays,
109 messages.adFree,
110 );
111 } else if (plan === PLANS.PRO) {
112 features.push(
113 messages.unlimitedServices,
114 messages.workspaces,
115 messages.customWebsites,
116 // messages.onPremise,
117 messages.thirdPartyServices,
118 // messages.serviceProxies,
119 );
120 } else {
121 features.push(
122 messages.unlimitedServices,
123 messages.spellchecker,
124 messages.workspaces,
125 messages.customWebsites,
126 messages.onPremise,
127 messages.thirdPartyServices,
128 messages.serviceProxies,
129 messages.teamManagement,
130 messages.appDelays,
131 messages.adFree,
132 );
133 }
134
72 return ( 135 return (
73 <ul className={className}> 136 <ul className={className}>
74 <FeatureItem name={intl.formatMessage(messages.unlimitedServices)} className={featureClassName} /> 137 {features.map(feature => <FeatureItem name={intl.formatMessage(feature)} 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> 138 </ul>
85 ); 139 );
86 } 140 }
diff --git a/src/components/ui/PremiumFeatureContainer/index.js b/src/components/ui/PremiumFeatureContainer/index.js
index 7ba353be3..36bf38c98 100644
--- a/src/components/ui/PremiumFeatureContainer/index.js
+++ b/src/components/ui/PremiumFeatureContainer/index.js
@@ -9,7 +9,7 @@ import { oneOrManyChildElements } from '../../../prop-types';
9import UserStore from '../../../stores/UserStore'; 9import UserStore from '../../../stores/UserStore';
10 10
11import styles from './styles'; 11import styles from './styles';
12import FeatureStore from '../../../stores/FeaturesStore'; 12import FeaturesStore from '../../../stores/FeaturesStore';
13 13
14const messages = defineMessages({ 14const messages = defineMessages({
15 action: { 15 action: {
@@ -90,7 +90,7 @@ PremiumFeatureContainer.wrappedComponent.propTypes = {
90 children: oneOrManyChildElements.isRequired, 90 children: oneOrManyChildElements.isRequired,
91 stores: PropTypes.shape({ 91 stores: PropTypes.shape({
92 user: PropTypes.instanceOf(UserStore).isRequired, 92 user: PropTypes.instanceOf(UserStore).isRequired,
93 features: PropTypes.instanceOf(FeatureStore).isRequired, 93 features: PropTypes.instanceOf(FeaturesStore).isRequired,
94 }).isRequired, 94 }).isRequired,
95 actions: PropTypes.shape({ 95 actions: PropTypes.shape({
96 ui: PropTypes.shape({ 96 ui: PropTypes.shape({
diff --git a/src/config.js b/src/config.js
index e762f879f..761d26eea 100644
--- a/src/config.js
+++ b/src/config.js
@@ -125,10 +125,10 @@ export const ALLOWED_PROTOCOLS = [
125]; 125];
126 126
127export const PLANS = { 127export const PLANS = {
128 PERSONAL: 'PERSONAL', 128 PERSONAL: 'personal',
129 PRO: 'PRO', 129 PRO: 'pro',
130 LEGACY: 'LEGACY', 130 LEGACY: 'legacy',
131 FREE: 'FREE', 131 FREE: 'free',
132}; 132};
133 133
134export const PLANS_MAPPING = { 134export const PLANS_MAPPING = {
diff --git a/src/containers/auth/PricingScreen.js b/src/containers/auth/PricingScreen.js
index af1651931..21c859c12 100644
--- a/src/containers/auth/PricingScreen.js
+++ b/src/containers/auth/PricingScreen.js
@@ -20,14 +20,19 @@ export default @inject('stores', 'actions') @observer class PricingScreen extend
20 } = this.props; 20 } = this.props;
21 21
22 const { activateTrialRequest } = stores.user; 22 const { activateTrialRequest } = stores.user;
23 const { defaultTrialPlan } = stores.features.features; 23 const { defaultTrialPlan, canSkipTrial } = stores.features.anonymousFeatures;
24 24
25 actions.user.activateTrial({ planId: defaultTrialPlan }); 25 if (!canSkipTrial) {
26 await activateTrialRequest._promise;
27
28 if (!activateTrialRequest.isError) {
29 stores.router.push('/'); 26 stores.router.push('/');
30 stores.user.hasCompletedSignup = true; 27 stores.user.hasCompletedSignup = true;
28 } else {
29 actions.user.activateTrial({ planId: defaultTrialPlan });
30 await activateTrialRequest._promise;
31
32 if (!activateTrialRequest.isError) {
33 stores.router.push('/');
34 stores.user.hasCompletedSignup = true;
35 }
31 } 36 }
32 } 37 }
33 38
@@ -37,8 +42,17 @@ export default @inject('stores', 'actions') @observer class PricingScreen extend
37 stores, 42 stores,
38 } = this.props; 43 } = this.props;
39 44
40 const { getUserInfoRequest, activateTrialRequest } = stores.user; 45 const { getUserInfoRequest, activateTrialRequest, data } = stores.user;
41 const { featuresRequest } = stores.features; 46 const { featuresRequest, features } = stores.features;
47
48 const { pricingConfig } = features;
49
50 let currency = '$';
51 let price = 5.99;
52 if (pricingConfig) {
53 ({ currency } = pricingConfig);
54 ({ price } = pricingConfig.plans.pro.yearly);
55 }
42 56
43 return ( 57 return (
44 <Pricing 58 <Pricing
@@ -46,7 +60,11 @@ export default @inject('stores', 'actions') @observer class PricingScreen extend
46 isLoadingRequiredData={(getUserInfoRequest.isExecuting || !getUserInfoRequest.wasExecuted) || (featuresRequest.isExecuting || !featuresRequest.wasExecuted)} 60 isLoadingRequiredData={(getUserInfoRequest.isExecuting || !getUserInfoRequest.wasExecuted) || (featuresRequest.isExecuting || !featuresRequest.wasExecuted)}
47 isActivatingTrial={activateTrialRequest.isExecuting} 61 isActivatingTrial={activateTrialRequest.isExecuting}
48 trialActivationError={activateTrialRequest.isError} 62 trialActivationError={activateTrialRequest.isError}
63 canSkipTrial={features.canSkipTrial}
49 error={error} 64 error={error}
65 currency={currency}
66 price={price}
67 name={data.firstname}
50 /> 68 />
51 ); 69 );
52 } 70 }
diff --git a/src/containers/auth/SignupScreen.js b/src/containers/auth/SignupScreen.js
index efc7ea4c1..f93498be2 100644
--- a/src/containers/auth/SignupScreen.js
+++ b/src/containers/auth/SignupScreen.js
@@ -4,6 +4,7 @@ import { inject, observer } from 'mobx-react';
4 4
5import Signup from '../../components/auth/Signup'; 5import Signup from '../../components/auth/Signup';
6import UserStore from '../../stores/UserStore'; 6import UserStore from '../../stores/UserStore';
7import FeaturesStore from '../../stores/FeaturesStore';
7 8
8import { globalError as globalErrorPropType } from '../../prop-types'; 9import { globalError as globalErrorPropType } from '../../prop-types';
9 10
@@ -12,11 +13,27 @@ export default @inject('stores', 'actions') @observer class SignupScreen extends
12 error: globalErrorPropType.isRequired, 13 error: globalErrorPropType.isRequired,
13 }; 14 };
14 15
16 onSignup(values) {
17 const { actions, stores } = this.props;
18
19 const { canSkipTrial, defaultTrialPlan, pricingConfig } = stores.features.anonymousFeatures;
20
21 if (!canSkipTrial) {
22 Object.assign(values, {
23 plan: defaultTrialPlan,
24 currency: pricingConfig.currencyID,
25 });
26 }
27
28 actions.user.signup(values);
29 }
30
15 render() { 31 render() {
16 const { actions, stores, error } = this.props; 32 const { stores, error } = this.props;
33
17 return ( 34 return (
18 <Signup 35 <Signup
19 onSubmit={actions.user.signup} 36 onSubmit={values => this.onSignup(values)}
20 isSubmitting={stores.user.signupRequest.isExecuting} 37 isSubmitting={stores.user.signupRequest.isExecuting}
21 loginRoute={stores.user.loginRoute} 38 loginRoute={stores.user.loginRoute}
22 error={error} 39 error={error}
@@ -33,5 +50,6 @@ SignupScreen.wrappedComponent.propTypes = {
33 }).isRequired, 50 }).isRequired,
34 stores: PropTypes.shape({ 51 stores: PropTypes.shape({
35 user: PropTypes.instanceOf(UserStore).isRequired, 52 user: PropTypes.instanceOf(UserStore).isRequired,
53 features: PropTypes.instanceOf(FeaturesStore).isRequired,
36 }).isRequired, 54 }).isRequired,
37}; 55};
diff --git a/src/containers/layout/AppLayoutContainer.js b/src/containers/layout/AppLayoutContainer.js
index 95fbd109f..a4312d9de 100644
--- a/src/containers/layout/AppLayoutContainer.js
+++ b/src/containers/layout/AppLayoutContainer.js
@@ -24,6 +24,7 @@ import { state as delayAppState } from '../../features/delayApp';
24import { workspaceActions } from '../../features/workspaces/actions'; 24import { workspaceActions } from '../../features/workspaces/actions';
25import WorkspaceDrawer from '../../features/workspaces/components/WorkspaceDrawer'; 25import WorkspaceDrawer from '../../features/workspaces/components/WorkspaceDrawer';
26import { workspaceStore } from '../../features/workspaces'; 26import { workspaceStore } from '../../features/workspaces';
27import WorkspacesStore from '../../features/workspaces/store';
27 28
28export default @inject('stores', 'actions') @observer class AppLayoutContainer extends Component { 29export default @inject('stores', 'actions') @observer class AppLayoutContainer extends Component {
29 static defaultProps = { 30 static defaultProps = {
@@ -41,6 +42,7 @@ export default @inject('stores', 'actions') @observer class AppLayoutContainer e
41 globalError, 42 globalError,
42 requests, 43 requests,
43 user, 44 user,
45 workspaces,
44 } = this.props.stores; 46 } = this.props.stores;
45 47
46 const { 48 const {
@@ -79,7 +81,7 @@ export default @inject('stores', 'actions') @observer class AppLayoutContainer e
79 const isLoadingServices = services.allServicesRequest.isExecuting 81 const isLoadingServices = services.allServicesRequest.isExecuting
80 && services.allServicesRequest.isExecutingFirstTime; 82 && services.allServicesRequest.isExecutingFirstTime;
81 83
82 if (isLoadingFeatures || isLoadingServices) { 84 if (isLoadingFeatures || isLoadingServices || workspaces.isLoadingWorkspaces) {
83 return ( 85 return (
84 <ThemeProvider theme={ui.theme}> 86 <ThemeProvider theme={ui.theme}>
85 <AppLoader /> 87 <AppLoader />
@@ -175,6 +177,7 @@ AppLayoutContainer.wrappedComponent.propTypes = {
175 user: PropTypes.instanceOf(UserStore).isRequired, 177 user: PropTypes.instanceOf(UserStore).isRequired,
176 requests: PropTypes.instanceOf(RequestStore).isRequired, 178 requests: PropTypes.instanceOf(RequestStore).isRequired,
177 globalError: PropTypes.instanceOf(GlobalErrorStore).isRequired, 179 globalError: PropTypes.instanceOf(GlobalErrorStore).isRequired,
180 workspaces: PropTypes.instanceOf(WorkspacesStore).isRequired,
178 }).isRequired, 181 }).isRequired,
179 actions: PropTypes.shape({ 182 actions: PropTypes.shape({
180 service: PropTypes.shape({ 183 service: PropTypes.shape({
diff --git a/src/containers/settings/AccountScreen.js b/src/containers/settings/AccountScreen.js
index 41cdeb79a..93ab44690 100644
--- a/src/containers/settings/AccountScreen.js
+++ b/src/containers/settings/AccountScreen.js
@@ -5,6 +5,7 @@ import { inject, observer } from 'mobx-react';
5import PaymentStore from '../../stores/PaymentStore'; 5import PaymentStore from '../../stores/PaymentStore';
6import UserStore from '../../stores/UserStore'; 6import UserStore from '../../stores/UserStore';
7import AppStore from '../../stores/AppStore'; 7import AppStore from '../../stores/AppStore';
8import FeaturesStore from '../../stores/FeaturesStore';
8 9
9import AccountDashboard from '../../components/settings/account/AccountDashboard'; 10import AccountDashboard from '../../components/settings/account/AccountDashboard';
10import ErrorBoundary from '../../components/util/ErrorBoundary'; 11import ErrorBoundary from '../../components/util/ErrorBoundary';
@@ -12,8 +13,9 @@ import { WEBSITE } from '../../environment';
12 13
13export default @inject('stores', 'actions') @observer class AccountScreen extends Component { 14export default @inject('stores', 'actions') @observer class AccountScreen extends Component {
14 onCloseWindow() { 15 onCloseWindow() {
15 const { user } = this.props.stores; 16 const { user, features } = this.props.stores;
16 user.getUserInfoRequest.invalidate({ immediately: true }); 17 user.getUserInfoRequest.invalidate({ immediately: true });
18 features.featuresRequest.invalidate({ immediately: true });
17 } 19 }
18 20
19 reloadData() { 21 reloadData() {
@@ -39,12 +41,17 @@ export default @inject('stores', 'actions') @observer class AccountScreen extend
39 } 41 }
40 42
41 render() { 43 render() {
42 const { user, payment } = this.props.stores; 44 const { user, payment, features } = this.props.stores;
43 const { user: userActions } = this.props.actions; 45 const {
46 user: userActions,
47 payment: paymentActions,
48 } = this.props.actions;
44 49
45 const isLoadingUserInfo = user.getUserInfoRequest.isExecuting; 50 const isLoadingUserInfo = user.getUserInfoRequest.isExecuting;
46 const isLoadingPlans = payment.plansRequest.isExecuting; 51 const isLoadingPlans = payment.plansRequest.isExecuting;
47 52
53 const { upgradeAccount } = paymentActions;
54
48 return ( 55 return (
49 <ErrorBoundary> 56 <ErrorBoundary>
50 <AccountDashboard 57 <AccountDashboard
@@ -60,7 +67,7 @@ export default @inject('stores', 'actions') @observer class AccountScreen extend
60 isLoadingDeleteAccount={user.deleteAccountRequest.isExecuting} 67 isLoadingDeleteAccount={user.deleteAccountRequest.isExecuting}
61 isDeleteAccountSuccessful={user.deleteAccountRequest.wasExecuted && !user.deleteAccountRequest.isError} 68 isDeleteAccountSuccessful={user.deleteAccountRequest.wasExecuted && !user.deleteAccountRequest.isError}
62 openEditAccount={() => this.handleWebsiteLink('/user/profile')} 69 openEditAccount={() => this.handleWebsiteLink('/user/profile')}
63 upgradeToPro={() => this.handleWebsiteLink('/inapp/user/licenses')} 70 upgradeToPro={() => upgradeAccount({ planId: features.features.pricingConfig.plans.pro.yearly.id })}
64 openBilling={() => this.handleWebsiteLink('/user/billing')} 71 openBilling={() => this.handleWebsiteLink('/user/billing')}
65 openInvoices={() => this.handleWebsiteLink('/user/invoices')} 72 openInvoices={() => this.handleWebsiteLink('/user/invoices')}
66 /> 73 />
@@ -72,12 +79,14 @@ export default @inject('stores', 'actions') @observer class AccountScreen extend
72AccountScreen.wrappedComponent.propTypes = { 79AccountScreen.wrappedComponent.propTypes = {
73 stores: PropTypes.shape({ 80 stores: PropTypes.shape({
74 user: PropTypes.instanceOf(UserStore).isRequired, 81 user: PropTypes.instanceOf(UserStore).isRequired,
82 features: PropTypes.instanceOf(FeaturesStore).isRequired,
75 payment: PropTypes.instanceOf(PaymentStore).isRequired, 83 payment: PropTypes.instanceOf(PaymentStore).isRequired,
76 app: PropTypes.instanceOf(AppStore).isRequired, 84 app: PropTypes.instanceOf(AppStore).isRequired,
77 }).isRequired, 85 }).isRequired,
78 actions: PropTypes.shape({ 86 actions: PropTypes.shape({
79 payment: PropTypes.shape({ 87 payment: PropTypes.shape({
80 createDashboardUrl: PropTypes.func.isRequired, 88 createDashboardUrl: PropTypes.func.isRequired,
89 upgradeAccount: PropTypes.func.isRequired,
81 }).isRequired, 90 }).isRequired,
82 app: PropTypes.shape({ 91 app: PropTypes.shape({
83 openExternalUrl: PropTypes.func.isRequired, 92 openExternalUrl: PropTypes.func.isRequired,
diff --git a/src/containers/subscription/SubscriptionFormScreen.js b/src/containers/subscription/SubscriptionFormScreen.js
index 726b10628..38e46a7ba 100644
--- a/src/containers/subscription/SubscriptionFormScreen.js
+++ b/src/containers/subscription/SubscriptionFormScreen.js
@@ -1,4 +1,5 @@
1import React, { Component } from 'react'; 1import React, { Component } from 'react';
2import { remote } from 'electron';
2import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 4import { inject, observer } from 'mobx-react';
4 5
@@ -7,11 +8,21 @@ import PaymentStore from '../../stores/PaymentStore';
7import SubscriptionForm from '../../components/subscription/SubscriptionForm'; 8import SubscriptionForm from '../../components/subscription/SubscriptionForm';
8import TrialForm from '../../components/subscription/TrialForm'; 9import TrialForm from '../../components/subscription/TrialForm';
9 10
11const { BrowserWindow } = remote;
12
10export default @inject('stores', 'actions') @observer class SubscriptionFormScreen extends Component { 13export default @inject('stores', 'actions') @observer class SubscriptionFormScreen extends Component {
14 static propTypes = {
15 onCloseWindow: PropTypes.func,
16 }
17
18 static defaultProps = {
19 onCloseWindow: () => null,
20 }
21
11 async openBrowser() { 22 async openBrowser() {
12 const { 23 const {
13 actions,
14 stores, 24 stores,
25 onCloseWindow,
15 } = this.props; 26 } = this.props;
16 27
17 const { 28 const {
@@ -22,7 +33,24 @@ export default @inject('stores', 'actions') @observer class SubscriptionFormScre
22 let hostedPageURL = features.features.planSelectionURL; 33 let hostedPageURL = features.features.planSelectionURL;
23 hostedPageURL = user.getAuthURL(hostedPageURL); 34 hostedPageURL = user.getAuthURL(hostedPageURL);
24 35
25 actions.app.openExternalUrl({ url: hostedPageURL }); 36 const paymentWindow = new BrowserWindow({
37 parent: remote.getCurrentWindow(),
38 modal: true,
39 title: '🔒 Franz Supporter License',
40 width: 800,
41 height: window.innerHeight - 100,
42 maxWidth: 800,
43 minWidth: 600,
44 webPreferences: {
45 nodeIntegration: true,
46 webviewTag: true,
47 },
48 });
49 paymentWindow.loadURL(`file://${__dirname}/../../index.html#/payment/${encodeURIComponent(hostedPageURL)}`);
50
51 paymentWindow.on('closed', () => {
52 onCloseWindow();
53 });
26 } 54 }
27 55
28 render() { 56 render() {
diff --git a/src/electron/Settings.js b/src/electron/Settings.js
index 95e3dcfa4..d4f0d25bf 100644
--- a/src/electron/Settings.js
+++ b/src/electron/Settings.js
@@ -47,7 +47,9 @@ export default class Settings {
47 } 47 }
48 48
49 _writeFile() { 49 _writeFile() {
50 outputJsonSync(this.settingsFile, this.store); 50 outputJsonSync(this.settingsFile, this.store, {
51 spaces: 2,
52 });
51 debug('Write settings file', this.type, toJS(this.store)); 53 debug('Write settings file', this.type, toJS(this.store));
52 } 54 }
53 55
diff --git a/src/electron/ipc-api/localServer.js b/src/electron/ipc-api/localServer.js
index 2f8f1020a..d12fb5708 100644
--- a/src/electron/ipc-api/localServer.js
+++ b/src/electron/ipc-api/localServer.js
@@ -1,5 +1,4 @@
1import { ipcMain, app } from 'electron'; 1import { ipcMain, app } from 'electron';
2import path from 'path';
3import net from 'net'; 2import net from 'net';
4import startServer from '../../server/start'; 3import startServer from '../../server/start';
5 4
@@ -38,7 +37,7 @@ export default (params) => {
38 console.log('Starting local server on port', port); 37 console.log('Starting local server on port', port);
39 38
40 startServer( 39 startServer(
41 path.join(app.getPath('userData'), 'server.sqlite'), 40 app.getPath('userData'),
42 port, 41 port,
43 ); 42 );
44 43
diff --git a/src/features/delayApp/Component.js b/src/features/delayApp/Component.js
index c61cb06c9..81f89bc52 100644
--- a/src/features/delayApp/Component.js
+++ b/src/features/delayApp/Component.js
@@ -21,7 +21,7 @@ const messages = defineMessages({
21 }, 21 },
22 action: { 22 action: {
23 id: 'feature.delayApp.upgrade.action', 23 id: 'feature.delayApp.upgrade.action',
24 defaultMessage: '!!!Get a Franz Supporter License', 24 defaultMessage: '!!!Upgrade Franz',
25 }, 25 },
26 actionTrial: { 26 actionTrial: {
27 id: 'feature.delayApp.trial.action', 27 id: 'feature.delayApp.trial.action',
diff --git a/src/features/delayApp/index.js b/src/features/delayApp/index.js
index 5cc6c9506..51bd887a2 100644
--- a/src/features/delayApp/index.js
+++ b/src/features/delayApp/index.js
@@ -3,6 +3,7 @@ import moment from 'moment';
3import DelayAppComponent from './Component'; 3import DelayAppComponent from './Component';
4 4
5import { DEFAULT_FEATURES_CONFIG } from '../../config'; 5import { DEFAULT_FEATURES_CONFIG } from '../../config';
6import { getUserWorkspacesRequest } from '../workspaces/api';
6 7
7const debug = require('debug')('Ferdi:feature:delayApp'); 8const debug = require('debug')('Ferdi:feature:delayApp');
8 9
@@ -32,7 +33,13 @@ export default function init(stores) {
32 }; 33 };
33 34
34 reaction( 35 reaction(
35 () => stores.user.isLoggedIn && stores.services.allServicesRequest.wasExecuted && stores.features.features.needToWaitToProceed && !stores.user.data.isPremium, 36 () => (
37 stores.user.isLoggedIn
38 && stores.services.allServicesRequest.wasExecuted
39 && getUserWorkspacesRequest.wasExecuted
40 && stores.features.features.needToWaitToProceed
41 && !stores.user.data.isPremium
42 ),
36 (isEnabled) => { 43 (isEnabled) => {
37 if (isEnabled) { 44 if (isEnabled) {
38 debug('Enabling `delayApp` feature'); 45 debug('Enabling `delayApp` feature');
diff --git a/src/features/planSelection/actions.js b/src/features/planSelection/actions.js
new file mode 100644
index 000000000..83f58bfd7
--- /dev/null
+++ b/src/features/planSelection/actions.js
@@ -0,0 +1,9 @@
1import PropTypes from 'prop-types';
2import { createActionsFromDefinitions } from '../../actions/lib/actions';
3
4export const planSelectionActions = createActionsFromDefinitions({
5 downgradeAccount: {},
6 hideOverlay: {},
7}, PropTypes.checkPropTypes);
8
9export default planSelectionActions;
diff --git a/src/features/planSelection/api.js b/src/features/planSelection/api.js
new file mode 100644
index 000000000..734643f10
--- /dev/null
+++ b/src/features/planSelection/api.js
@@ -0,0 +1,26 @@
1import { sendAuthRequest } from '../../api/utils/auth';
2import { API, API_VERSION } from '../../environment';
3import Request from '../../stores/lib/Request';
4
5const debug = require('debug')('Franz:feature:planSelection:api');
6
7export const planSelectionApi = {
8 downgrade: async () => {
9 const url = `${API}/${API_VERSION}/payment/downgrade`;
10 const options = {
11 method: 'PUT',
12 };
13 debug('downgrade UPDATE', url, options);
14 const result = await sendAuthRequest(url, options);
15 debug('downgrade RESULT', result);
16 if (!result.ok) throw result;
17
18 return result.ok;
19 },
20};
21
22export const downgradeUserRequest = new Request(planSelectionApi, 'downgrade');
23
24export const resetApiRequests = () => {
25 downgradeUserRequest.reset();
26};
diff --git a/src/features/planSelection/components/PlanItem.js b/src/features/planSelection/components/PlanItem.js
new file mode 100644
index 000000000..ec061377b
--- /dev/null
+++ b/src/features/planSelection/components/PlanItem.js
@@ -0,0 +1,207 @@
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';
6import classnames from 'classnames';
7import color from 'color';
8
9import { H2 } from '@meetfranz/ui';
10
11import { Button } from '@meetfranz/forms';
12import { mdiArrowRight } from '@mdi/js';
13
14const messages = defineMessages({
15 perMonth: {
16 id: 'subscription.interval.perMonth',
17 defaultMessage: '!!!per month',
18 },
19 perMonthPerUser: {
20 id: 'subscription.interval.perMonthPerUser',
21 defaultMessage: '!!!per month & user',
22 },
23 bestValue: {
24 id: 'subscription.bestValue',
25 defaultMessage: '!!!Best value',
26 },
27});
28
29const styles = theme => ({
30 root: {
31 display: 'flex',
32 flexDirection: 'column',
33 borderRadius: theme.borderRadius,
34 flex: 1,
35 color: theme.styleTypes.primary.accent,
36 overflow: 'hidden',
37 textAlign: 'center',
38
39 '& h2': {
40 textAlign: 'center',
41 marginBottom: 10,
42 fontSize: 30,
43 color: theme.styleTypes.primary.contrast,
44 },
45 },
46 currency: {
47 fontSize: 35,
48 },
49 priceWrapper: {
50 height: 50,
51 marginBottom: 0,
52 },
53 price: {
54 fontSize: 50,
55
56 '& sup': {
57 fontSize: 20,
58 verticalAlign: 20,
59 },
60 },
61 text: {
62 marginBottom: 'auto',
63 },
64 cta: {
65 background: theme.styleTypes.primary.accent,
66 color: theme.styleTypes.primary.contrast,
67 margin: [40, 'auto', 0, 'auto'],
68 },
69 divider: {
70 width: 40,
71 border: 0,
72 borderTop: [1, 'solid', theme.styleTypes.primary.contrast],
73 margin: [15, 'auto', 20],
74 },
75 header: {
76 padding: 20,
77 background: color(theme.styleTypes.primary.accent).darken(0.25).hex(),
78 color: theme.styleTypes.primary.contrast,
79 position: 'relative',
80 },
81 content: {
82 padding: [10, 20, 20],
83 background: '#EFEFEF',
84 },
85 simpleCTA: {
86 background: 'none',
87 color: theme.styleTypes.primary.accent,
88
89 '& svg': {
90 fill: theme.styleTypes.primary.accent,
91 },
92 },
93 bestValue: {
94 background: theme.styleTypes.success.accent,
95 color: theme.styleTypes.success.contrast,
96 right: -66,
97 top: -40,
98 height: 'auto',
99 position: 'absolute',
100 transform: 'rotateZ(45deg)',
101 textAlign: 'center',
102 padding: [5, 50],
103 transformOrigin: 'left bottom',
104 fontSize: 12,
105 boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
106 },
107});
108
109
110export default @observer @injectSheet(styles) class PlanItem extends Component {
111 static propTypes = {
112 name: PropTypes.string.isRequired,
113 text: PropTypes.string.isRequired,
114 price: PropTypes.number.isRequired,
115 currency: PropTypes.string.isRequired,
116 upgrade: PropTypes.func.isRequired,
117 ctaLabel: PropTypes.string.isRequired,
118 simpleCTA: PropTypes.bool,
119 perUser: PropTypes.bool,
120 classes: PropTypes.object.isRequired,
121 bestValue: PropTypes.bool,
122 className: PropTypes.string,
123 children: PropTypes.element,
124 };
125
126 static defaultProps = {
127 simpleCTA: false,
128 perUser: false,
129 children: null,
130 bestValue: false,
131 className: '',
132 }
133
134 static contextTypes = {
135 intl: intlShape,
136 };
137
138 render() {
139 const {
140 name,
141 text,
142 price,
143 currency,
144 classes,
145 upgrade,
146 ctaLabel,
147 simpleCTA,
148 perUser,
149 bestValue,
150 className,
151 children,
152 } = this.props;
153 const { intl } = this.context;
154
155 const priceParts = `${price}`.split('.');
156
157 return (
158 <div className={classnames({
159 [classes.root]: true,
160 [className]: className,
161 })}
162 >
163 <div className={classes.header}>
164 {bestValue && (
165 <div className={classes.bestValue}>
166 {intl.formatMessage(messages.bestValue)}
167 </div>
168 )}
169 <H2 className={classes.planName}>{name}</H2>
170 <p className={classes.text}>
171 {text}
172 </p>
173 <hr className={classes.divider} />
174 <p className={classes.priceWrapper}>
175 <span className={classes.currency}>{currency}</span>
176 <span className={classes.price}>
177 {priceParts[0]}
178 <sup>{priceParts[1]}</sup>
179 </span>
180 </p>
181 <p className={classes.interval}>
182 {intl.formatMessage(perUser ? messages.perMonthPerUser : messages.perMonth)}
183 </p>
184 </div>
185
186 <div className={classes.content}>
187 {children}
188
189 <Button
190 className={classnames({
191 [classes.cta]: true,
192 [classes.simpleCTA]: simpleCTA,
193 })}
194 icon={simpleCTA ? mdiArrowRight : null}
195 label={(
196 <>
197 {ctaLabel}
198 </>
199 )}
200 onClick={upgrade}
201 />
202 </div>
203
204 </div>
205 );
206 }
207}
diff --git a/src/features/planSelection/components/PlanSelection.js b/src/features/planSelection/components/PlanSelection.js
new file mode 100644
index 000000000..4bf5238dd
--- /dev/null
+++ b/src/features/planSelection/components/PlanSelection.js
@@ -0,0 +1,279 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import { defineMessages, intlShape } from 'react-intl';
6import { H1, H2, Icon } from '@meetfranz/ui';
7import color from 'color';
8
9import { mdiRocket, mdiArrowRight } from '@mdi/js';
10import PlanItem from './PlanItem';
11import { i18nPlanName } from '../../../helpers/plan-helpers';
12import { PLANS } from '../../../config';
13import { FeatureList } from '../../../components/ui/FeatureList';
14import Appear from '../../../components/ui/effects/Appear';
15
16const messages = defineMessages({
17 welcome: {
18 id: 'feature.planSelection.fullscreen.welcome',
19 defaultMessage: '!!!Are you ready to choose, {name}',
20 },
21 subheadline: {
22 id: 'feature.planSelection.fullscreen.subheadline',
23 defaultMessage: '!!!It\'s time to make a choice. Franz works best on our Personal and Professional plans. Please have a look and choose the best one for you.',
24 },
25 textFree: {
26 id: 'feature.planSelection.free.text',
27 defaultMessage: '!!!Basic functionality',
28 },
29 textPersonal: {
30 id: 'feature.planSelection.personal.text',
31 defaultMessage: '!!!More services, no waiting - ideal for personal use.',
32 },
33 textProfessional: {
34 id: 'feature.planSelection.pro.text',
35 defaultMessage: '!!!Unlimited services and professional features for you - and your team.',
36 },
37 ctaStayOnFree: {
38 id: 'feature.planSelection.cta.stayOnFree',
39 defaultMessage: '!!!Stay on Free',
40 },
41 ctaDowngradeFree: {
42 id: 'feature.planSelection.cta.ctaDowngradeFree',
43 defaultMessage: '!!!Downgrade to Free',
44 },
45 actionTrial: {
46 id: 'feature.planSelection.cta.trial',
47 defaultMessage: '!!!Start my free 14-days Trial',
48 },
49 shortActionPersonal: {
50 id: 'feature.planSelection.cta.upgradePersonal',
51 defaultMessage: '!!!Choose Personal',
52 },
53 shortActionPro: {
54 id: 'feature.planSelection.cta.upgradePro',
55 defaultMessage: '!!!Choose Professional',
56 },
57 fullFeatureList: {
58 id: 'feature.planSelection.fullFeatureList',
59 defaultMessage: '!!!Complete comparison of all plans',
60 },
61 pricesBasedOnAnnualPayment: {
62 id: 'feature.planSelection.pricesBasedOnAnnualPayment',
63 defaultMessage: '!!!All prices based on yearly payment',
64 },
65});
66
67const styles = theme => ({
68 root: {
69 background: theme.colorModalOverlayBackground,
70 width: '100%',
71 height: '100%',
72 position: 'absolute',
73 top: 0,
74 left: 0,
75 display: 'flex',
76 justifyContent: 'center',
77 alignItems: 'center',
78 zIndex: 999999,
79 overflowY: 'scroll',
80 },
81 container: {
82 width: '80%',
83 height: 'auto',
84 background: theme.styleTypes.primary.accent,
85 padding: 40,
86 borderRadius: theme.borderRadius,
87 maxWidth: 1000,
88
89 '& h1, & h2': {
90 textAlign: 'center',
91 color: theme.styleTypes.primary.contrast,
92 },
93 },
94 plans: {
95 display: 'flex',
96 margin: [40, 0, 0],
97 height: 'auto',
98
99 '& > div': {
100 margin: [0, 15],
101 flex: 1,
102 height: 'auto',
103 background: theme.styleTypes.primary.contrast,
104 boxShadow: [0, 2, 30, color('#000').alpha(0.1).rgb().string()],
105 },
106 },
107 bigIcon: {
108 background: theme.styleTypes.danger.accent,
109 width: 120,
110 height: 120,
111 display: 'flex',
112 alignItems: 'center',
113 borderRadius: '100%',
114 justifyContent: 'center',
115 margin: [-100, 'auto', 20],
116
117 '& svg': {
118 width: '80px !important',
119 height: '80px !important',
120 filter: 'drop-shadow( 0px 2px 3px rgba(0, 0, 0, 0.3))',
121 fill: theme.styleTypes.danger.contrast,
122 },
123 },
124 headline: {
125 fontSize: 40,
126 },
127 subheadline: {
128 maxWidth: 660,
129 fontSize: 22,
130 lineHeight: 1.1,
131 margin: [0, 'auto'],
132 },
133 featureList: {
134 '& li': {
135 borderBottom: [1, 'solid', '#CECECE'],
136 },
137 },
138 footer: {
139 display: 'flex',
140 color: theme.styleTypes.primary.contrast,
141 marginTop: 20,
142 padding: [0, 15],
143 },
144 fullFeatureList: {
145 marginRight: 'auto',
146 textAlign: 'center',
147 display: 'flex',
148 justifyContent: 'center',
149 alignItems: 'center',
150 color: `${theme.styleTypes.primary.contrast} !important`,
151
152 '& svg': {
153 marginRight: 5,
154 },
155 },
156 scrollContainer: {
157 border: '1px solid red',
158 overflow: 'scroll-x',
159 },
160 featuredPlan: {
161 transform: 'scale(1.05)',
162 },
163 disclaimer: {
164 textAlign: 'right',
165 margin: [10, 15, 0, 0],
166 },
167});
168
169@injectSheet(styles) @observer
170class PlanSelection extends Component {
171 static propTypes = {
172 classes: PropTypes.object.isRequired,
173 firstname: PropTypes.string.isRequired,
174 plans: PropTypes.object.isRequired,
175 currency: PropTypes.string.isRequired,
176 subscriptionExpired: PropTypes.bool.isRequired,
177 upgradeAccount: PropTypes.func.isRequired,
178 stayOnFree: PropTypes.func.isRequired,
179 hadSubscription: PropTypes.bool.isRequired,
180 };
181
182 static contextTypes = {
183 intl: intlShape,
184 };
185
186 componentDidMount() {
187 }
188
189 render() {
190 const {
191 classes,
192 firstname,
193 plans,
194 currency,
195 subscriptionExpired,
196 upgradeAccount,
197 stayOnFree,
198 hadSubscription,
199 } = this.props;
200
201 const { intl } = this.context;
202
203 return (
204 <Appear>
205 <div
206 className={classes.root}
207 >
208 <div className={classes.container}>
209 <div className={classes.bigIcon}>
210 <Icon icon={mdiRocket} />
211 </div>
212 <H1 className={classes.headline}>{intl.formatMessage(messages.welcome, { name: firstname })}</H1>
213 <H2 className={classes.subheadline}>{intl.formatMessage(messages.subheadline)}</H2>
214 <div className={classes.plans}>
215 <PlanItem
216 name={i18nPlanName(PLANS.FREE, intl)}
217 text={intl.formatMessage(messages.textFree)}
218 price={0}
219 currency={currency}
220 ctaLabel={intl.formatMessage(subscriptionExpired ? messages.ctaDowngradeFree : messages.ctaStayOnFree)}
221 upgrade={() => stayOnFree()}
222 simpleCTA
223 >
224 <FeatureList
225 plan={PLANS.FREE}
226 className={classes.featureList}
227 />
228 </PlanItem>
229 <PlanItem
230 name={i18nPlanName(plans.pro.yearly.id, intl)}
231 text={intl.formatMessage(messages.textProfessional)}
232 price={plans.pro.yearly.price}
233 currency={currency}
234 ctaLabel={intl.formatMessage(hadSubscription ? messages.shortActionPro : messages.actionTrial)}
235 upgrade={() => upgradeAccount(plans.pro.yearly.id)}
236 className={classes.featuredPlan}
237 perUser
238 bestValue
239 >
240 <FeatureList
241 plan={PLANS.PRO}
242 className={classes.featureList}
243 />
244 </PlanItem>
245 <PlanItem
246 name={i18nPlanName(plans.personal.yearly.id, intl)}
247 text={intl.formatMessage(messages.textPersonal)}
248 price={plans.personal.yearly.price}
249 currency={currency}
250 ctaLabel={intl.formatMessage(hadSubscription ? messages.shortActionPersonal : messages.actionTrial)}
251 upgrade={() => upgradeAccount(plans.personal.yearly.id)}
252 >
253 <FeatureList
254 plan={PLANS.PERSONAL}
255 className={classes.featureList}
256 />
257 </PlanItem>
258 </div>
259 <div className={classes.footer}>
260 <a
261 href="https://meetfranz.com/pricing"
262 target="_blank"
263 className={classes.fullFeatureList}
264 >
265 <Icon icon={mdiArrowRight} />
266 {intl.formatMessage(messages.fullFeatureList)}
267 </a>
268 {/* <p className={classes.disclaimer}> */}
269 {intl.formatMessage(messages.pricesBasedOnAnnualPayment)}
270 {/* </p> */}
271 </div>
272 </div>
273 </div>
274 </Appear>
275 );
276 }
277}
278
279export default PlanSelection;
diff --git a/src/features/planSelection/containers/PlanSelectionScreen.js b/src/features/planSelection/containers/PlanSelectionScreen.js
new file mode 100644
index 000000000..d202c924e
--- /dev/null
+++ b/src/features/planSelection/containers/PlanSelectionScreen.js
@@ -0,0 +1,123 @@
1import React, { Component } from 'react';
2import { observer, inject } from 'mobx-react';
3import PropTypes from 'prop-types';
4import { remote } from 'electron';
5import { defineMessages, intlShape } from 'react-intl';
6
7import FeaturesStore from '../../../stores/FeaturesStore';
8import UserStore from '../../../stores/UserStore';
9import PlanSelection from '../components/PlanSelection';
10import ErrorBoundary from '../../../components/util/ErrorBoundary';
11import { planSelectionStore } from '..';
12
13const { dialog, app } = remote;
14
15const messages = defineMessages({
16 dialogTitle: {
17 id: 'feature.planSelection.fullscreen.dialog.title',
18 defaultMessage: '!!!Downgrade your Franz Plan',
19 },
20 dialogMessage: {
21 id: 'feature.planSelection.fullscreen.dialog.message',
22 defaultMessage: '!!!You\'re about to downgrade to our Free account. Are you sure? Click here instead to get more services and functionality for just {currency}{price} a month.',
23 },
24 dialogCTADowngrade: {
25 id: 'feature.planSelection.fullscreen.dialog.cta.downgrade',
26 defaultMessage: '!!!Downgrade to Free',
27 },
28 dialogCTAUpgrade: {
29 id: 'feature.planSelection.fullscreen.dialog.cta.upgrade',
30 defaultMessage: '!!!Choose Personal',
31 },
32});
33
34@inject('stores', 'actions') @observer
35class PlanSelectionScreen extends Component {
36 static contextTypes = {
37 intl: intlShape,
38 };
39
40 upgradeAccount(planId) {
41 const { upgradeAccount } = this.props.actions.payment;
42
43 upgradeAccount({
44 planId,
45 });
46 }
47
48 render() {
49 if (!planSelectionStore || !planSelectionStore.isFeatureActive || !planSelectionStore.showPlanSelectionOverlay) {
50 return null;
51 }
52
53 const { intl } = this.context;
54
55 const { user, features } = this.props.stores;
56 const { plans, currency } = features.features.pricingConfig;
57 const { activateTrial } = this.props.actions.user;
58 const { downgradeAccount, hideOverlay } = this.props.actions.planSelection;
59
60 return (
61 <ErrorBoundary>
62 <PlanSelection
63 firstname={user.data.firstname}
64 plans={plans}
65 currency={currency}
66 upgradeAccount={(planId) => {
67 if (user.data.hadSubscription) {
68 this.upgradeAccount(planId);
69 } else {
70 activateTrial({
71 planId,
72 });
73 }
74 }}
75 stayOnFree={() => {
76 const selection = dialog.showMessageBoxSync(app.mainWindow, {
77 type: 'question',
78 message: intl.formatMessage(messages.dialogTitle),
79 detail: intl.formatMessage(messages.dialogMessage, {
80 currency,
81 price: plans.personal.yearly.price,
82 }),
83 buttons: [
84 intl.formatMessage(messages.dialogCTADowngrade),
85 intl.formatMessage(messages.dialogCTAUpgrade),
86 ],
87 });
88
89 if (selection === 0) {
90 downgradeAccount();
91 hideOverlay();
92 } else {
93 this.upgradeAccount(plans.personal.yearly.id);
94 }
95 }}
96 subscriptionExpired={user.team && user.team.state === 'expired' && !user.team.userHasDowngraded}
97 hadSubscription={user.data.hadSubscription}
98 />
99 </ErrorBoundary>
100 );
101 }
102}
103
104export default PlanSelectionScreen;
105
106PlanSelectionScreen.wrappedComponent.propTypes = {
107 stores: PropTypes.shape({
108 features: PropTypes.instanceOf(FeaturesStore).isRequired,
109 user: PropTypes.instanceOf(UserStore).isRequired,
110 }).isRequired,
111 actions: PropTypes.shape({
112 payment: PropTypes.shape({
113 upgradeAccount: PropTypes.func.isRequired,
114 }),
115 planSelection: PropTypes.shape({
116 downgradeAccount: PropTypes.func.isRequired,
117 hideOverlay: PropTypes.func.isRequired,
118 }),
119 user: PropTypes.shape({
120 activateTrial: PropTypes.func.isRequired,
121 }),
122 }).isRequired,
123};
diff --git a/src/features/planSelection/index.js b/src/features/planSelection/index.js
new file mode 100644
index 000000000..890be8871
--- /dev/null
+++ b/src/features/planSelection/index.js
@@ -0,0 +1,28 @@
1import { reaction } from 'mobx';
2import PlanSelectionStore from './store';
3
4const debug = require('debug')('Franz:feature:planSelection');
5
6export const planSelectionStore = new PlanSelectionStore();
7
8export default function initPlanSelection(stores, actions) {
9 stores.planSelection = planSelectionStore;
10 const { features } = stores;
11
12 // Toggle planSelection feature
13 reaction(
14 () => features.features.isPlanSelectionEnabled,
15 (isEnabled) => {
16 if (isEnabled) {
17 debug('Initializing `planSelection` feature');
18 planSelectionStore.start(stores, actions);
19 } else if (planSelectionStore.isFeatureActive) {
20 debug('Disabling `planSelection` feature');
21 planSelectionStore.stop();
22 }
23 },
24 {
25 fireImmediately: true,
26 },
27 );
28}
diff --git a/src/features/planSelection/store.js b/src/features/planSelection/store.js
new file mode 100644
index 000000000..448e323ff
--- /dev/null
+++ b/src/features/planSelection/store.js
@@ -0,0 +1,68 @@
1import {
2 action,
3 observable,
4 computed,
5} from 'mobx';
6
7import { planSelectionActions } from './actions';
8import { FeatureStore } from '../utils/FeatureStore';
9import { createActionBindings } from '../utils/ActionBinding';
10import { downgradeUserRequest } from './api';
11
12const debug = require('debug')('Franz:feature:planSelection:store');
13
14export default class PlanSelectionStore extends FeatureStore {
15 @observable isFeatureEnabled = false;
16
17 @observable isFeatureActive = false;
18
19 @observable hideOverlay = false;
20
21 @computed get showPlanSelectionOverlay() {
22 const { team, isPremium } = this.stores.user;
23 if (team && !this.hideOverlay && !isPremium) {
24 return team.state === 'expired' && !team.userHasDowngraded;
25 }
26
27 return false;
28 }
29
30 // ========== PUBLIC API ========= //
31
32 @action start(stores, actions, api) {
33 debug('PlanSelectionStore::start');
34 this.stores = stores;
35 this.actions = actions;
36 this.api = api;
37
38 // ACTIONS
39
40 this._registerActions(createActionBindings([
41 [planSelectionActions.downgradeAccount, this._downgradeAccount],
42 [planSelectionActions.hideOverlay, this._hideOverlay],
43 ]));
44
45 this.isFeatureActive = true;
46 }
47
48 @action stop() {
49 super.stop();
50 debug('PlanSelectionStore::stop');
51 this.isFeatureActive = false;
52 }
53
54 // ========== PRIVATE METHODS ========= //
55
56 // Actions
57 @action _downgradeAccount = () => {
58 downgradeUserRequest.execute();
59 }
60
61 @action _hideOverlay = () => {
62 this.hideOverlay = true;
63 }
64
65 @action _showOverlay = () => {
66 this.hideOverlay = false;
67 }
68}
diff --git a/src/features/shareFranz/index.js b/src/features/shareFranz/index.js
index 217e926f9..04e3684ae 100644
--- a/src/features/shareFranz/index.js
+++ b/src/features/shareFranz/index.js
@@ -2,6 +2,7 @@ import { observable, reaction } from 'mobx';
2import ms from 'ms'; 2import ms from 'ms';
3 3
4import { state as delayAppState } from '../delayApp'; 4import { state as delayAppState } from '../delayApp';
5import { planSelectionStore } from '../planSelection';
5 6
6export { default as Component } from './Component'; 7export { default as Component } from './Component';
7 8
@@ -31,7 +32,7 @@ export default function initialize(stores) {
31 () => stores.user.isLoggedIn, 32 () => stores.user.isLoggedIn,
32 () => { 33 () => {
33 setTimeout(() => { 34 setTimeout(() => {
34 if (stores.settings.stats.appStarts % 50 === 0) { 35 if (stores.settings.stats.appStarts % 50 === 0 && !planSelectionStore.showPlanSelectionOverlay) {
35 if (delayAppState.isDelayAppScreenVisible) { 36 if (delayAppState.isDelayAppScreenVisible) {
36 debug('Delaying share modal by 5 minutes'); 37 debug('Delaying share modal by 5 minutes');
37 setTimeout(() => showModal(), ms('5m')); 38 setTimeout(() => showModal(), ms('5m'));
diff --git a/src/features/trialStatusBar/actions.js b/src/features/trialStatusBar/actions.js
new file mode 100644
index 000000000..38df76458
--- /dev/null
+++ b/src/features/trialStatusBar/actions.js
@@ -0,0 +1,13 @@
1import PropTypes from 'prop-types';
2import { createActionsFromDefinitions } from '../../actions/lib/actions';
3
4export const trialStatusBarActions = createActionsFromDefinitions({
5 upgradeAccount: {
6 planId: PropTypes.string.isRequired,
7 onCloseWindow: PropTypes.func.isRequired,
8 },
9 downgradeAccount: {},
10 hideOverlay: {},
11}, PropTypes.checkPropTypes);
12
13export default trialStatusBarActions;
diff --git a/src/features/trialStatusBar/components/ProgressBar.js b/src/features/trialStatusBar/components/ProgressBar.js
new file mode 100644
index 000000000..41b74d396
--- /dev/null
+++ b/src/features/trialStatusBar/components/ProgressBar.js
@@ -0,0 +1,45 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5
6const styles = theme => ({
7 root: {
8 background: theme.trialStatusBar.progressBar.background,
9 width: '25%',
10 maxWidth: 200,
11 height: 8,
12 display: 'flex',
13 alignItems: 'center',
14 borderRadius: theme.borderRadius,
15 overflow: 'hidden',
16 },
17 progress: {
18 background: theme.trialStatusBar.progressBar.progressIndicator,
19 width: ({ percent }) => `${percent}%`,
20 height: '100%',
21 },
22});
23
24@injectSheet(styles) @observer
25class ProgressBar extends Component {
26 static propTypes = {
27 classes: PropTypes.object.isRequired,
28 };
29
30 render() {
31 const {
32 classes,
33 } = this.props;
34
35 return (
36 <div
37 className={classes.root}
38 >
39 <div className={classes.progress} />
40 </div>
41 );
42 }
43}
44
45export default ProgressBar;
diff --git a/src/features/trialStatusBar/components/TrialStatusBar.js b/src/features/trialStatusBar/components/TrialStatusBar.js
new file mode 100644
index 000000000..b8fe4acc9
--- /dev/null
+++ b/src/features/trialStatusBar/components/TrialStatusBar.js
@@ -0,0 +1,135 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import { defineMessages, intlShape } from 'react-intl';
6import { Icon } from '@meetfranz/ui';
7import { mdiArrowRight, mdiWindowClose } from '@mdi/js';
8import classnames from 'classnames';
9
10import ProgressBar from './ProgressBar';
11
12const messages = defineMessages({
13 restTime: {
14 id: 'feature.trialStatusBar.restTime',
15 defaultMessage: '!!!Your Free Franz {plan} Trial ends in {time}.',
16 },
17 expired: {
18 id: 'feature.trialStatusBar.expired',
19 defaultMessage: '!!!Your free Franz {plan} Trial has expired, please upgrade your account.',
20 },
21 cta: {
22 id: 'feature.trialStatusBar.cta',
23 defaultMessage: '!!!Upgrade now',
24 },
25});
26
27const styles = theme => ({
28 root: {
29 background: theme.trialStatusBar.bar.background,
30 width: '100%',
31 height: 25,
32 order: 10,
33 display: 'flex',
34 alignItems: 'center',
35 fontSize: 12,
36 padding: [0, 10],
37 justifyContent: 'flex-end',
38 },
39 ended: {
40 background: theme.styleTypes.warning.accent,
41 color: theme.styleTypes.warning.contrast,
42 },
43 message: {
44 marginLeft: 20,
45 },
46 action: {
47 marginLeft: 20,
48 fontSize: 12,
49 color: theme.colorText,
50 textDecoration: 'underline',
51 display: 'flex',
52
53 '& svg': {
54 margin: [1, 2, 0, 0],
55 },
56 },
57});
58
59@injectSheet(styles) @observer
60class TrialStatusBar extends Component {
61 static propTypes = {
62 planName: PropTypes.string.isRequired,
63 percent: PropTypes.number.isRequired,
64 upgradeAccount: PropTypes.func.isRequired,
65 hideOverlay: PropTypes.func.isRequired,
66 trialEnd: PropTypes.string.isRequired,
67 hasEnded: PropTypes.bool.isRequired,
68 classes: PropTypes.object.isRequired,
69 };
70
71 static contextTypes = {
72 intl: intlShape,
73 };
74
75 render() {
76 const {
77 planName,
78 percent,
79 upgradeAccount,
80 hideOverlay,
81 trialEnd,
82 hasEnded,
83 classes,
84 } = this.props;
85
86 const { intl } = this.context;
87
88 return (
89 <div
90 className={classnames({
91 [classes.root]: true,
92 [classes.ended]: hasEnded,
93 })}
94 >
95 <ProgressBar
96 percent={percent}
97 />
98 {' '}
99 <span className={classes.message}>
100 {!hasEnded ? (
101 intl.formatMessage(messages.restTime, {
102 plan: planName,
103 time: trialEnd,
104 })
105 ) : (
106 intl.formatMessage(messages.expired, {
107 plan: planName,
108 })
109 )}
110 </span>
111 <button
112 className={classes.action}
113 type="button"
114 onClick={() => {
115 upgradeAccount();
116 }}
117 >
118 <Icon icon={mdiArrowRight} />
119 {intl.formatMessage(messages.cta)}
120 </button>
121 <button
122 className={classes.action}
123 type="button"
124 onClick={() => {
125 hideOverlay();
126 }}
127 >
128 <Icon icon={mdiWindowClose} />
129 </button>
130 </div>
131 );
132 }
133}
134
135export default TrialStatusBar;
diff --git a/src/features/trialStatusBar/containers/TrialStatusBarScreen.js b/src/features/trialStatusBar/containers/TrialStatusBarScreen.js
new file mode 100644
index 000000000..e15a1204f
--- /dev/null
+++ b/src/features/trialStatusBar/containers/TrialStatusBarScreen.js
@@ -0,0 +1,109 @@
1import React, { Component } from 'react';
2import { observer, inject } from 'mobx-react';
3import PropTypes from 'prop-types';
4import ms from 'ms';
5import { intlShape } from 'react-intl';
6
7import FeaturesStore from '../../../stores/FeaturesStore';
8import UserStore from '../../../stores/UserStore';
9import TrialStatusBar from '../components/TrialStatusBar';
10import ErrorBoundary from '../../../components/util/ErrorBoundary';
11import { trialStatusBarStore } from '..';
12import { i18nPlanName } from '../../../helpers/plan-helpers';
13
14@inject('stores', 'actions') @observer
15class TrialStatusBarScreen extends Component {
16 static contextTypes = {
17 intl: intlShape,
18 };
19
20 state = {
21 showOverlay: true,
22 percent: 0,
23 restTime: '',
24 hasEnded: false,
25 };
26
27 percentInterval = null;
28
29 componentDidMount() {
30 this.percentInterval = setInterval(() => {
31 this.calculateRestTime();
32 }, ms('1m'));
33
34 this.calculateRestTime();
35 }
36
37 componentWillUnmount() {
38 clearInterval(this.percentInterval);
39 }
40
41 calculateRestTime() {
42 const { trialEndTime } = trialStatusBarStore;
43 const percent = Math.abs(100 - Math.abs(trialEndTime.asMilliseconds()) * 100 / ms('14d')).toFixed(2);
44 const restTime = trialEndTime.humanize();
45 const hasEnded = trialEndTime.asMilliseconds() > 0;
46
47 this.setState({
48 percent,
49 restTime,
50 hasEnded,
51 });
52 }
53
54 hideOverlay() {
55 this.setState({
56 showOverlay: false,
57 });
58 }
59
60
61 render() {
62 const { intl } = this.context;
63
64 const {
65 showOverlay,
66 percent,
67 restTime,
68 hasEnded,
69 } = this.state;
70
71 if (!trialStatusBarStore || !trialStatusBarStore.isFeatureActive || !showOverlay || !trialStatusBarStore.showTrialStatusBarOverlay) {
72 return null;
73 }
74
75 const { user } = this.props.stores;
76 const { upgradeAccount } = this.props.actions.payment;
77
78 const planName = i18nPlanName(user.team.plan, intl);
79
80 return (
81 <ErrorBoundary>
82 <TrialStatusBar
83 planName={planName}
84 percent={parseFloat(percent < 5 ? 5 : percent)}
85 trialEnd={restTime}
86 upgradeAccount={() => upgradeAccount({
87 planId: user.team.plan,
88 })}
89 hideOverlay={() => this.hideOverlay()}
90 hasEnded={hasEnded}
91 />
92 </ErrorBoundary>
93 );
94 }
95}
96
97export default TrialStatusBarScreen;
98
99TrialStatusBarScreen.wrappedComponent.propTypes = {
100 stores: PropTypes.shape({
101 features: PropTypes.instanceOf(FeaturesStore).isRequired,
102 user: PropTypes.instanceOf(UserStore).isRequired,
103 }).isRequired,
104 actions: PropTypes.shape({
105 payment: PropTypes.shape({
106 upgradeAccount: PropTypes.func.isRequired,
107 }),
108 }).isRequired,
109};
diff --git a/src/features/trialStatusBar/index.js b/src/features/trialStatusBar/index.js
new file mode 100644
index 000000000..ec84cdfd7
--- /dev/null
+++ b/src/features/trialStatusBar/index.js
@@ -0,0 +1,30 @@
1import { reaction } from 'mobx';
2import TrialStatusBarStore from './store';
3
4const debug = require('debug')('Franz:feature:trialStatusBar');
5
6export const GA_CATEGORY_TRIAL_STATUS_BAR = 'trialStatusBar';
7
8export const trialStatusBarStore = new TrialStatusBarStore();
9
10export default function initTrialStatusBar(stores, actions) {
11 stores.trialStatusBar = trialStatusBarStore;
12 const { features } = stores;
13
14 // Toggle trialStatusBar feature
15 reaction(
16 () => features.features.isTrialStatusBarEnabled,
17 (isEnabled) => {
18 if (isEnabled) {
19 debug('Initializing `trialStatusBar` feature');
20 trialStatusBarStore.start(stores, actions);
21 } else if (trialStatusBarStore.isFeatureActive) {
22 debug('Disabling `trialStatusBar` feature');
23 trialStatusBarStore.stop();
24 }
25 },
26 {
27 fireImmediately: true,
28 },
29 );
30}
diff --git a/src/features/trialStatusBar/store.js b/src/features/trialStatusBar/store.js
new file mode 100644
index 000000000..89cf32392
--- /dev/null
+++ b/src/features/trialStatusBar/store.js
@@ -0,0 +1,72 @@
1import {
2 action,
3 observable,
4 computed,
5} from 'mobx';
6import moment from 'moment';
7
8import { trialStatusBarActions } from './actions';
9import { FeatureStore } from '../utils/FeatureStore';
10import { createActionBindings } from '../utils/ActionBinding';
11
12const debug = require('debug')('Franz:feature:trialStatusBar:store');
13
14export default class TrialStatusBarStore extends FeatureStore {
15 @observable isFeatureActive = false;
16
17 @observable isFeatureEnabled = false;
18
19 @computed get showTrialStatusBarOverlay() {
20 if (this.isFeatureActive) {
21 const { team } = this.stores.user;
22 if (team && !this.hideOverlay) {
23 return team.state !== 'expired' && team.isTrial;
24 }
25 }
26
27 return false;
28 }
29
30 @computed get trialEndTime() {
31 if (this.isFeatureActive) {
32 const { team } = this.stores.user;
33
34 if (team && !this.hideOverlay) {
35 return moment.duration(moment().diff(team.trialEnd));
36 }
37 }
38
39 return moment.duration();
40 }
41
42 // ========== PUBLIC API ========= //
43
44 @action start(stores, actions, api) {
45 debug('TrialStatusBarStore::start');
46 this.stores = stores;
47 this.actions = actions;
48 this.api = api;
49
50 // ACTIONS
51
52 this._registerActions(createActionBindings([
53 [trialStatusBarActions.hideOverlay, this._hideOverlay],
54 ]));
55
56 this.isFeatureActive = true;
57 }
58
59 @action stop() {
60 super.stop();
61 debug('TrialStatusBarStore::stop');
62 this.isFeatureActive = false;
63 }
64
65 // ========== PRIVATE METHODS ========= //
66
67 // Actions
68
69 @action _hideOverlay = () => {
70 this.hideOverlay = true;
71 }
72}
diff --git a/src/features/workspaces/components/WorkspaceDrawer.js b/src/features/workspaces/components/WorkspaceDrawer.js
index e991b9909..baa94f6b3 100644
--- a/src/features/workspaces/components/WorkspaceDrawer.js
+++ b/src/features/workspaces/components/WorkspaceDrawer.js
@@ -7,7 +7,7 @@ import { H1, Icon, ProBadge } from '@meetfranz/ui';
7import { Button } from '@meetfranz/forms/lib'; 7import { Button } from '@meetfranz/forms/lib';
8import ReactTooltip from 'react-tooltip'; 8import ReactTooltip from 'react-tooltip';
9 9
10import { mdiPlusBox, mdiSettings } from '@mdi/js'; 10import { mdiPlusBox, mdiSettings, mdiStar } from '@mdi/js';
11import WorkspaceDrawerItem from './WorkspaceDrawerItem'; 11import WorkspaceDrawerItem from './WorkspaceDrawerItem';
12import { workspaceActions } from '../actions'; 12import { workspaceActions } from '../actions';
13import { workspaceStore } from '../index'; 13import { workspaceStore } from '../index';
@@ -51,6 +51,8 @@ const styles = theme => ({
51 drawer: { 51 drawer: {
52 background: theme.workspaces.drawer.background, 52 background: theme.workspaces.drawer.background,
53 width: `${theme.workspaces.drawer.width}px`, 53 width: `${theme.workspaces.drawer.width}px`,
54 display: 'flex',
55 flexDirection: 'column',
54 }, 56 },
55 headline: { 57 headline: {
56 fontSize: '24px', 58 fontSize: '24px',
@@ -74,6 +76,7 @@ const styles = theme => ({
74 }, 76 },
75 workspaces: { 77 workspaces: {
76 height: 'auto', 78 height: 'auto',
79 overflowY: 'scroll',
77 }, 80 },
78 premiumAnnouncement: { 81 premiumAnnouncement: {
79 padding: '20px', 82 padding: '20px',
@@ -88,7 +91,7 @@ const styles = theme => ({
88 addNewWorkspaceLabel: { 91 addNewWorkspaceLabel: {
89 height: 'auto', 92 height: 'auto',
90 color: theme.workspaces.drawer.buttons.color, 93 color: theme.workspaces.drawer.buttons.color,
91 marginTop: 40, 94 margin: [40, 0],
92 textAlign: 'center', 95 textAlign: 'center',
93 '& > svg': { 96 '& > svg': {
94 fill: theme.workspaces.drawer.buttons.color, 97 fill: theme.workspaces.drawer.buttons.color,
@@ -172,7 +175,7 @@ class WorkspaceDrawer extends Component {
172 className={classes.premiumCtaButton} 175 className={classes.premiumCtaButton}
173 buttonType="primary" 176 buttonType="primary"
174 label={intl.formatMessage(messages.reactivatePremiumAccount)} 177 label={intl.formatMessage(messages.reactivatePremiumAccount)}
175 icon="mdiStar" 178 icon={mdiStar}
176 onClick={() => { 179 onClick={() => {
177 onUpgradeAccountClick(); 180 onUpgradeAccountClick();
178 }} 181 }}
diff --git a/src/features/workspaces/components/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.js
index 977b23999..b499e02a4 100644
--- a/src/features/workspaces/components/WorkspacesDashboard.js
+++ b/src/features/workspaces/components/WorkspacesDashboard.js
@@ -5,6 +5,7 @@ import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss'; 5import injectSheet from 'react-jss';
6import { Infobox, Badge } from '@meetfranz/ui'; 6import { Infobox, Badge } from '@meetfranz/ui';
7 7
8import { mdiCheckboxMarkedCircleOutline } from '@mdi/js';
8import Loader from '../../../components/ui/Loader'; 9import Loader from '../../../components/ui/Loader';
9import WorkspaceItem from './WorkspaceItem'; 10import WorkspaceItem from './WorkspaceItem';
10import CreateWorkspaceForm from './CreateWorkspaceForm'; 11import CreateWorkspaceForm from './CreateWorkspaceForm';
@@ -128,7 +129,7 @@ class WorkspacesDashboard extends Component {
128 <Appear className={classes.appear}> 129 <Appear className={classes.appear}>
129 <Infobox 130 <Infobox
130 type="success" 131 type="success"
131 icon="mdiCheckboxMarkedCircleOutline" 132 icon={mdiCheckboxMarkedCircleOutline}
132 dismissable 133 dismissable
133 onUnmount={updateWorkspaceRequest.reset} 134 onUnmount={updateWorkspaceRequest.reset}
134 > 135 >
@@ -142,7 +143,7 @@ class WorkspacesDashboard extends Component {
142 <Appear className={classes.appear}> 143 <Appear className={classes.appear}>
143 <Infobox 144 <Infobox
144 type="success" 145 type="success"
145 icon="mdiCheckboxMarkedCircleOutline" 146 icon={mdiCheckboxMarkedCircleOutline}
146 dismissable 147 dismissable
147 onUnmount={deleteWorkspaceRequest.reset} 148 onUnmount={deleteWorkspaceRequest.reset}
148 > 149 >
diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js
index 949f8a792..5c90ff180 100644
--- a/src/features/workspaces/store.js
+++ b/src/features/workspaces/store.js
@@ -47,6 +47,11 @@ export default class WorkspacesStore extends FeatureStore {
47 return getUserWorkspacesRequest.result || []; 47 return getUserWorkspacesRequest.result || [];
48 } 48 }
49 49
50 @computed get isLoadingWorkspaces() {
51 if (!this.isFeatureActive) return false;
52 return getUserWorkspacesRequest.isExecutingFirstTime;
53 }
54
50 @computed get settings() { 55 @computed get settings() {
51 return localStorage.getItem('workspaces') || {}; 56 return localStorage.getItem('workspaces') || {};
52 } 57 }
diff --git a/src/helpers/plan-helpers.js b/src/helpers/plan-helpers.js
index e0f1fd89a..ee22e4471 100644
--- a/src/helpers/plan-helpers.js
+++ b/src/helpers/plan-helpers.js
@@ -4,19 +4,19 @@ import { PLANS_MAPPING, PLANS } from '../config';
4const messages = defineMessages({ 4const messages = defineMessages({
5 [PLANS.PRO]: { 5 [PLANS.PRO]: {
6 id: 'pricing.plan.pro', 6 id: 'pricing.plan.pro',
7 defaultMessage: '!!!Franz Professional', 7 defaultMessage: '!!!Professional',
8 }, 8 },
9 [PLANS.PERSONAL]: { 9 [PLANS.PERSONAL]: {
10 id: 'pricing.plan.personal', 10 id: 'pricing.plan.personal',
11 defaultMessage: '!!!Franz Personal', 11 defaultMessage: '!!!Personal',
12 }, 12 },
13 [PLANS.FREE]: { 13 [PLANS.FREE]: {
14 id: 'pricing.plan.free', 14 id: 'pricing.plan.free',
15 defaultMessage: '!!!Franz Free', 15 defaultMessage: '!!!Free',
16 }, 16 },
17 [PLANS.LEGACY]: { 17 [PLANS.LEGACY]: {
18 id: 'pricing.plan.legacy', 18 id: 'pricing.plan.legacy',
19 defaultMessage: '!!!Franz Premium', 19 defaultMessage: '!!!Premium',
20 }, 20 },
21}); 21});
22 22
diff --git a/src/i18n/locales/defaultMessages.json b/src/i18n/locales/defaultMessages.json
index b056f0d1b..bb65ccdf2 100644
--- a/src/i18n/locales/defaultMessages.json
+++ b/src/i18n/locales/defaultMessages.json
@@ -539,120 +539,172 @@
539 { 539 {
540 "descriptors": [ 540 "descriptors": [
541 { 541 {
542 "defaultMessage": "!!!Franz Professional", 542 "defaultMessage": "!!!Hi {name}, welcome to Franz",
543 "end": { 543 "end": {
544 "column": 3, 544 "column": 3,
545 "line": 18 545 "line": 18
546 }, 546 },
547 "file": "src/components/auth/Pricing.js", 547 "file": "src/components/auth/Pricing.js",
548 "id": "pricing.trial.headline", 548 "id": "pricing.trial.headline.pro",
549 "start": { 549 "start": {
550 "column": 12, 550 "column": 12,
551 "line": 15 551 "line": 15
552 } 552 }
553 }, 553 },
554 { 554 {
555 "defaultMessage": "!!!Your personal welcome offer:", 555 "defaultMessage": "!!!We have a special treat for you.",
556 "end": { 556 "end": {
557 "column": 3, 557 "column": 3,
558 "line": 22 558 "line": 22
559 }, 559 },
560 "file": "src/components/auth/Pricing.js", 560 "file": "src/components/auth/Pricing.js",
561 "id": "pricing.trial.subheadline", 561 "id": "pricing.trial.intro.specialTreat",
562 "start": { 562 "start": {
563 "column": 17, 563 "column": 16,
564 "line": 19 564 "line": 19
565 } 565 }
566 }, 566 },
567 { 567 {
568 "defaultMessage": "!!!No strings attached", 568 "defaultMessage": "!!!Enjoy the full Franz Professional experience completely free for 14 days.",
569 "end": { 569 "end": {
570 "column": 3, 570 "column": 3,
571 "line": 26 571 "line": 26
572 }, 572 },
573 "file": "src/components/auth/Pricing.js", 573 "file": "src/components/auth/Pricing.js",
574 "id": "pricing.trial.intro.tryPro",
575 "start": {
576 "column": 10,
577 "line": 23
578 }
579 },
580 {
581 "defaultMessage": "!!!Happy messaging,",
582 "end": {
583 "column": 3,
584 "line": 30
585 },
586 "file": "src/components/auth/Pricing.js",
587 "id": "pricing.trial.intro.happyMessaging",
588 "start": {
589 "column": 18,
590 "line": 27
591 }
592 },
593 {
594 "defaultMessage": "!!!No strings attached",
595 "end": {
596 "column": 3,
597 "line": 34
598 },
599 "file": "src/components/auth/Pricing.js",
574 "id": "pricing.trial.terms.headline", 600 "id": "pricing.trial.terms.headline",
575 "start": { 601 "start": {
576 "column": 29, 602 "column": 29,
577 "line": 23 603 "line": 31
578 } 604 }
579 }, 605 },
580 { 606 {
581 "defaultMessage": "!!!No credit card required", 607 "defaultMessage": "!!!No credit card required",
582 "end": { 608 "end": {
583 "column": 3, 609 "column": 3,
584 "line": 30 610 "line": 38
585 }, 611 },
586 "file": "src/components/auth/Pricing.js", 612 "file": "src/components/auth/Pricing.js",
587 "id": "pricing.trial.terms.noCreditCard", 613 "id": "pricing.trial.terms.noCreditCard",
588 "start": { 614 "start": {
589 "column": 16, 615 "column": 16,
590 "line": 27 616 "line": 35
591 } 617 }
592 }, 618 },
593 { 619 {
594 "defaultMessage": "!!!Your free trial ends automatically after 14 days", 620 "defaultMessage": "!!!Your free trial ends automatically after 14 days",
595 "end": { 621 "end": {
596 "column": 3, 622 "column": 3,
597 "line": 34 623 "line": 42
598 }, 624 },
599 "file": "src/components/auth/Pricing.js", 625 "file": "src/components/auth/Pricing.js",
600 "id": "pricing.trial.terms.automaticTrialEnd", 626 "id": "pricing.trial.terms.automaticTrialEnd",
601 "start": { 627 "start": {
602 "column": 21, 628 "column": 21,
603 "line": 31 629 "line": 39
630 }
631 },
632 {
633 "defaultMessage": "!!!Free trial (normally {currency}{price} per month)",
634 "end": {
635 "column": 3,
636 "line": 46
637 },
638 "file": "src/components/auth/Pricing.js",
639 "id": "pricing.trial.terms.trialWorth",
640 "start": {
641 "column": 14,
642 "line": 43
604 } 643 }
605 }, 644 },
606 { 645 {
607 "defaultMessage": "!!!Sorry, we could not activate your trial!", 646 "defaultMessage": "!!!Sorry, we could not activate your trial!",
608 "end": { 647 "end": {
609 "column": 3, 648 "column": 3,
610 "line": 38 649 "line": 50
611 }, 650 },
612 "file": "src/components/auth/Pricing.js", 651 "file": "src/components/auth/Pricing.js",
613 "id": "pricing.trial.error", 652 "id": "pricing.trial.error",
614 "start": { 653 "start": {
615 "column": 19, 654 "column": 19,
616 "line": 35 655 "line": 47
617 } 656 }
618 }, 657 },
619 { 658 {
620 "defaultMessage": "!!!Yes, upgrade my account to Franz Professional", 659 "defaultMessage": "!!!Start my 14-day Franz Professional Trial",
621 "end": { 660 "end": {
622 "column": 3, 661 "column": 3,
623 "line": 42 662 "line": 54
624 }, 663 },
625 "file": "src/components/auth/Pricing.js", 664 "file": "src/components/auth/Pricing.js",
626 "id": "pricing.trial.cta.accept", 665 "id": "pricing.trial.cta.accept",
627 "start": { 666 "start": {
628 "column": 13, 667 "column": 13,
629 "line": 39 668 "line": 51
669 }
670 },
671 {
672 "defaultMessage": "!!!Start using Franz",
673 "end": {
674 "column": 3,
675 "line": 58
676 },
677 "file": "src/components/auth/Pricing.js",
678 "id": "pricing.trial.cta.start",
679 "start": {
680 "column": 12,
681 "line": 55
630 } 682 }
631 }, 683 },
632 { 684 {
633 "defaultMessage": "!!!Continue to Ferdi", 685 "defaultMessage": "!!!Continue to Ferdi",
634 "end": { 686 "end": {
635 "column": 3, 687 "column": 3,
636 "line": 46 688 "line": 62
637 }, 689 },
638 "file": "src/components/auth/Pricing.js", 690 "file": "src/components/auth/Pricing.js",
639 "id": "pricing.trial.cta.skip", 691 "id": "pricing.trial.cta.skip",
640 "start": { 692 "start": {
641 "column": 11, 693 "column": 11,
642 "line": 43 694 "line": 59
643 } 695 }
644 }, 696 },
645 { 697 {
646 "defaultMessage": "!!!Franz Professional includes:", 698 "defaultMessage": "!!!Franz Professional includes:",
647 "end": { 699 "end": {
648 "column": 3, 700 "column": 3,
649 "line": 50 701 "line": 66
650 }, 702 },
651 "file": "src/components/auth/Pricing.js", 703 "file": "src/components/auth/Pricing.js",
652 "id": "pricing.trial.features.headline", 704 "id": "pricing.trial.features.headline",
653 "start": { 705 "start": {
654 "column": 20, 706 "column": 20,
655 "line": 47 707 "line": 63
656 } 708 }
657 } 709 }
658 ], 710 ],
@@ -882,52 +934,52 @@
882 "defaultMessage": "!!!Your services have been updated.", 934 "defaultMessage": "!!!Your services have been updated.",
883 "end": { 935 "end": {
884 "column": 3, 936 "column": 3,
885 "line": 31 937 "line": 33
886 }, 938 },
887 "file": "src/components/layout/AppLayout.js", 939 "file": "src/components/layout/AppLayout.js",
888 "id": "infobar.servicesUpdated", 940 "id": "infobar.servicesUpdated",
889 "start": { 941 "start": {
890 "column": 19, 942 "column": 19,
891 "line": 28 943 "line": 30
892 } 944 }
893 }, 945 },
894 { 946 {
895 "defaultMessage": "!!!Reload services", 947 "defaultMessage": "!!!Reload services",
896 "end": { 948 "end": {
897 "column": 3, 949 "column": 3,
898 "line": 35 950 "line": 37
899 }, 951 },
900 "file": "src/components/layout/AppLayout.js", 952 "file": "src/components/layout/AppLayout.js",
901 "id": "infobar.buttonReloadServices", 953 "id": "infobar.buttonReloadServices",
902 "start": { 954 "start": {
903 "column": 24, 955 "column": 24,
904 "line": 32 956 "line": 34
905 } 957 }
906 }, 958 },
907 { 959 {
908 "defaultMessage": "!!!Could not load services and user information", 960 "defaultMessage": "!!!Could not load services and user information",
909 "end": { 961 "end": {
910 "column": 3, 962 "column": 3,
911 "line": 39 963 "line": 41
912 }, 964 },
913 "file": "src/components/layout/AppLayout.js", 965 "file": "src/components/layout/AppLayout.js",
914 "id": "infobar.requiredRequestsFailed", 966 "id": "infobar.requiredRequestsFailed",
915 "start": { 967 "start": {
916 "column": 26, 968 "column": 26,
917 "line": 36 969 "line": 38
918 } 970 }
919 }, 971 },
920 { 972 {
921 "defaultMessage": "!!!There were errors while trying to perform an authenticated request. Please try logging out and back in if this error persists.", 973 "defaultMessage": "!!!There were errors while trying to perform an authenticated request. Please try logging out and back in if this error persists.",
922 "end": { 974 "end": {
923 "column": 3, 975 "column": 3,
924 "line": 43 976 "line": 45
925 }, 977 },
926 "file": "src/components/layout/AppLayout.js", 978 "file": "src/components/layout/AppLayout.js",
927 "id": "infobar.authRequestFailed", 979 "id": "infobar.authRequestFailed",
928 "start": { 980 "start": {
929 "column": 21, 981 "column": 21,
930 "line": 40 982 "line": 42
931 } 983 }
932 } 984 }
933 ], 985 ],
@@ -3495,133 +3547,198 @@
3495 { 3547 {
3496 "descriptors": [ 3548 "descriptors": [
3497 { 3549 {
3550 "defaultMessage": "!!!Choose from more than 70 Services",
3551 "end": {
3552 "column": 3,
3553 "line": 12
3554 },
3555 "file": "src/components/ui/FeatureList.js",
3556 "id": "pricing.features.recipes",
3557 "start": {
3558 "column": 20,
3559 "line": 9
3560 }
3561 },
3562 {
3563 "defaultMessage": "!!!Account Synchronisation",
3564 "end": {
3565 "column": 3,
3566 "line": 16
3567 },
3568 "file": "src/components/ui/FeatureList.js",
3569 "id": "pricing.features.accountSync",
3570 "start": {
3571 "column": 15,
3572 "line": 13
3573 }
3574 },
3575 {
3576 "defaultMessage": "!!!Desktop Notifications",
3577 "end": {
3578 "column": 3,
3579 "line": 20
3580 },
3581 "file": "src/components/ui/FeatureList.js",
3582 "id": "pricing.features.desktopNotifications",
3583 "start": {
3584 "column": 24,
3585 "line": 17
3586 }
3587 },
3588 {
3498 "defaultMessage": "!!!Add unlimited services", 3589 "defaultMessage": "!!!Add unlimited services",
3499 "end": { 3590 "end": {
3500 "column": 3, 3591 "column": 3,
3501 "line": 11 3592 "line": 24
3502 }, 3593 },
3503 "file": "src/components/ui/FeatureList.js", 3594 "file": "src/components/ui/FeatureList.js",
3504 "id": "pricing.features.unlimitedServices", 3595 "id": "pricing.features.unlimitedServices",
3505 "start": { 3596 "start": {
3506 "column": 21, 3597 "column": 21,
3507 "line": 8 3598 "line": 21
3599 }
3600 },
3601 {
3602 "defaultMessage": "!!!Add up to 3 services",
3603 "end": {
3604 "column": 3,
3605 "line": 28
3606 },
3607 "file": "src/components/ui/FeatureList.js",
3608 "id": "pricing.features.upToThreeServices",
3609 "start": {
3610 "column": 21,
3611 "line": 25
3612 }
3613 },
3614 {
3615 "defaultMessage": "!!!Add up to 6 services",
3616 "end": {
3617 "column": 3,
3618 "line": 32
3619 },
3620 "file": "src/components/ui/FeatureList.js",
3621 "id": "pricing.features.upToSixServices",
3622 "start": {
3623 "column": 19,
3624 "line": 29
3508 } 3625 }
3509 }, 3626 },
3510 { 3627 {
3511 "defaultMessage": "!!!Spellchecker support", 3628 "defaultMessage": "!!!Spellchecker support",
3512 "end": { 3629 "end": {
3513 "column": 3, 3630 "column": 3,
3514 "line": 15 3631 "line": 36
3515 }, 3632 },
3516 "file": "src/components/ui/FeatureList.js", 3633 "file": "src/components/ui/FeatureList.js",
3517 "id": "pricing.features.spellchecker", 3634 "id": "pricing.features.spellchecker",
3518 "start": { 3635 "start": {
3519 "column": 16, 3636 "column": 16,
3520 "line": 12 3637 "line": 33
3521 } 3638 }
3522 }, 3639 },
3523 { 3640 {
3524 "defaultMessage": "!!!Workspaces", 3641 "defaultMessage": "!!!Workspaces",
3525 "end": { 3642 "end": {
3526 "column": 3, 3643 "column": 3,
3527 "line": 19 3644 "line": 40
3528 }, 3645 },
3529 "file": "src/components/ui/FeatureList.js", 3646 "file": "src/components/ui/FeatureList.js",
3530 "id": "pricing.features.workspaces", 3647 "id": "pricing.features.workspaces",
3531 "start": { 3648 "start": {
3532 "column": 14, 3649 "column": 14,
3533 "line": 16 3650 "line": 37
3534 } 3651 }
3535 }, 3652 },
3536 { 3653 {
3537 "defaultMessage": "!!!Add Custom Websites", 3654 "defaultMessage": "!!!Add Custom Websites",
3538 "end": { 3655 "end": {
3539 "column": 3, 3656 "column": 3,
3540 "line": 23 3657 "line": 44
3541 }, 3658 },
3542 "file": "src/components/ui/FeatureList.js", 3659 "file": "src/components/ui/FeatureList.js",
3543 "id": "pricing.features.customWebsites", 3660 "id": "pricing.features.customWebsites",
3544 "start": { 3661 "start": {
3545 "column": 18, 3662 "column": 18,
3546 "line": 20 3663 "line": 41
3547 } 3664 }
3548 }, 3665 },
3549 { 3666 {
3550 "defaultMessage": "!!!On-premise & other Hosted Services", 3667 "defaultMessage": "!!!On-premise & other Hosted Services",
3551 "end": { 3668 "end": {
3552 "column": 3, 3669 "column": 3,
3553 "line": 27 3670 "line": 48
3554 }, 3671 },
3555 "file": "src/components/ui/FeatureList.js", 3672 "file": "src/components/ui/FeatureList.js",
3556 "id": "pricing.features.onPremise", 3673 "id": "pricing.features.onPremise",
3557 "start": { 3674 "start": {
3558 "column": 13, 3675 "column": 13,
3559 "line": 24 3676 "line": 45
3560 } 3677 }
3561 }, 3678 },
3562 { 3679 {
3563 "defaultMessage": "!!!Install 3rd party services", 3680 "defaultMessage": "!!!Install 3rd party services",
3564 "end": { 3681 "end": {
3565 "column": 3, 3682 "column": 3,
3566 "line": 31 3683 "line": 52
3567 }, 3684 },
3568 "file": "src/components/ui/FeatureList.js", 3685 "file": "src/components/ui/FeatureList.js",
3569 "id": "pricing.features.thirdPartyServices", 3686 "id": "pricing.features.thirdPartyServices",
3570 "start": { 3687 "start": {
3571 "column": 22, 3688 "column": 22,
3572 "line": 28 3689 "line": 49
3573 } 3690 }
3574 }, 3691 },
3575 { 3692 {
3576 "defaultMessage": "!!!Service Proxies", 3693 "defaultMessage": "!!!Service Proxies",
3577 "end": { 3694 "end": {
3578 "column": 3, 3695 "column": 3,
3579 "line": 35 3696 "line": 56
3580 }, 3697 },
3581 "file": "src/components/ui/FeatureList.js", 3698 "file": "src/components/ui/FeatureList.js",
3582 "id": "pricing.features.serviceProxies", 3699 "id": "pricing.features.serviceProxies",
3583 "start": { 3700 "start": {
3584 "column": 18, 3701 "column": 18,
3585 "line": 32 3702 "line": 53
3586 } 3703 }
3587 }, 3704 },
3588 { 3705 {
3589 "defaultMessage": "!!!Team Management", 3706 "defaultMessage": "!!!Team Management",
3590 "end": { 3707 "end": {
3591 "column": 3, 3708 "column": 3,
3592 "line": 39 3709 "line": 60
3593 }, 3710 },
3594 "file": "src/components/ui/FeatureList.js", 3711 "file": "src/components/ui/FeatureList.js",
3595 "id": "pricing.features.teamManagement", 3712 "id": "pricing.features.teamManagement",
3596 "start": { 3713 "start": {
3597 "column": 18, 3714 "column": 18,
3598 "line": 36 3715 "line": 57
3599 } 3716 }
3600 }, 3717 },
3601 { 3718 {
3602 "defaultMessage": "!!!No Waiting Screens", 3719 "defaultMessage": "!!!No Waiting Screens",
3603 "end": { 3720 "end": {
3604 "column": 3, 3721 "column": 3,
3605 "line": 43 3722 "line": 64
3606 }, 3723 },
3607 "file": "src/components/ui/FeatureList.js", 3724 "file": "src/components/ui/FeatureList.js",
3608 "id": "pricing.features.appDelays", 3725 "id": "pricing.features.appDelays",
3609 "start": { 3726 "start": {
3610 "column": 13, 3727 "column": 13,
3611 "line": 40 3728 "line": 61
3612 } 3729 }
3613 }, 3730 },
3614 { 3731 {
3615 "defaultMessage": "!!!Forever ad-free", 3732 "defaultMessage": "!!!Forever ad-free",
3616 "end": { 3733 "end": {
3617 "column": 3, 3734 "column": 3,
3618 "line": 47 3735 "line": 68
3619 }, 3736 },
3620 "file": "src/components/ui/FeatureList.js", 3737 "file": "src/components/ui/FeatureList.js",
3621 "id": "pricing.features.adFree", 3738 "id": "pricing.features.adFree",
3622 "start": { 3739 "start": {
3623 "column": 10, 3740 "column": 10,
3624 "line": 44 3741 "line": 65
3625 } 3742 }
3626 } 3743 }
3627 ], 3744 ],
@@ -4450,7 +4567,7 @@
4450 } 4567 }
4451 }, 4568 },
4452 { 4569 {
4453 "defaultMessage": "!!!Get a Franz Supporter License", 4570 "defaultMessage": "!!!Upgrade Franz",
4454 "end": { 4571 "end": {
4455 "column": 3, 4572 "column": 3,
4456 "line": 25 4573 "line": 25
@@ -4494,6 +4611,299 @@
4494 { 4611 {
4495 "descriptors": [ 4612 "descriptors": [
4496 { 4613 {
4614 "defaultMessage": "!!!per month",
4615 "end": {
4616 "column": 3,
4617 "line": 18
4618 },
4619 "file": "src/features/planSelection/components/PlanItem.js",
4620 "id": "subscription.interval.perMonth",
4621 "start": {
4622 "column": 12,
4623 "line": 15
4624 }
4625 },
4626 {
4627 "defaultMessage": "!!!per month & user",
4628 "end": {
4629 "column": 3,
4630 "line": 22
4631 },
4632 "file": "src/features/planSelection/components/PlanItem.js",
4633 "id": "subscription.interval.perMonthPerUser",
4634 "start": {
4635 "column": 19,
4636 "line": 19
4637 }
4638 },
4639 {
4640 "defaultMessage": "!!!Best value",
4641 "end": {
4642 "column": 3,
4643 "line": 26
4644 },
4645 "file": "src/features/planSelection/components/PlanItem.js",
4646 "id": "subscription.bestValue",
4647 "start": {
4648 "column": 13,
4649 "line": 23
4650 }
4651 }
4652 ],
4653 "path": "src/features/planSelection/components/PlanItem.json"
4654 },
4655 {
4656 "descriptors": [
4657 {
4658 "defaultMessage": "!!!Are you ready to choose, {name}",
4659 "end": {
4660 "column": 3,
4661 "line": 20
4662 },
4663 "file": "src/features/planSelection/components/PlanSelection.js",
4664 "id": "feature.planSelection.fullscreen.welcome",
4665 "start": {
4666 "column": 11,
4667 "line": 17
4668 }
4669 },
4670 {
4671 "defaultMessage": "!!!It's time to make a choice. Franz works best on our Personal and Professional plans. Please have a look and choose the best one for you.",
4672 "end": {
4673 "column": 3,
4674 "line": 24
4675 },
4676 "file": "src/features/planSelection/components/PlanSelection.js",
4677 "id": "feature.planSelection.fullscreen.subheadline",
4678 "start": {
4679 "column": 15,
4680 "line": 21
4681 }
4682 },
4683 {
4684 "defaultMessage": "!!!Basic functionality",
4685 "end": {
4686 "column": 3,
4687 "line": 28
4688 },
4689 "file": "src/features/planSelection/components/PlanSelection.js",
4690 "id": "feature.planSelection.free.text",
4691 "start": {
4692 "column": 12,
4693 "line": 25
4694 }
4695 },
4696 {
4697 "defaultMessage": "!!!More services, no waiting - ideal for personal use.",
4698 "end": {
4699 "column": 3,
4700 "line": 32
4701 },
4702 "file": "src/features/planSelection/components/PlanSelection.js",
4703 "id": "feature.planSelection.personal.text",
4704 "start": {
4705 "column": 16,
4706 "line": 29
4707 }
4708 },
4709 {
4710 "defaultMessage": "!!!Unlimited services and professional features for you - and your team.",
4711 "end": {
4712 "column": 3,
4713 "line": 36
4714 },
4715 "file": "src/features/planSelection/components/PlanSelection.js",
4716 "id": "feature.planSelection.pro.text",
4717 "start": {
4718 "column": 20,
4719 "line": 33
4720 }
4721 },
4722 {
4723 "defaultMessage": "!!!Stay on Free",
4724 "end": {
4725 "column": 3,
4726 "line": 40
4727 },
4728 "file": "src/features/planSelection/components/PlanSelection.js",
4729 "id": "feature.planSelection.cta.stayOnFree",
4730 "start": {
4731 "column": 17,
4732 "line": 37
4733 }
4734 },
4735 {
4736 "defaultMessage": "!!!Downgrade to Free",
4737 "end": {
4738 "column": 3,
4739 "line": 44
4740 },
4741 "file": "src/features/planSelection/components/PlanSelection.js",
4742 "id": "feature.planSelection.cta.ctaDowngradeFree",
4743 "start": {
4744 "column": 20,
4745 "line": 41
4746 }
4747 },
4748 {
4749 "defaultMessage": "!!!Start my free 14-days Trial",
4750 "end": {
4751 "column": 3,
4752 "line": 48
4753 },
4754 "file": "src/features/planSelection/components/PlanSelection.js",
4755 "id": "feature.planSelection.cta.trial",
4756 "start": {
4757 "column": 15,
4758 "line": 45
4759 }
4760 },
4761 {
4762 "defaultMessage": "!!!Choose Personal",
4763 "end": {
4764 "column": 3,
4765 "line": 52
4766 },
4767 "file": "src/features/planSelection/components/PlanSelection.js",
4768 "id": "feature.planSelection.cta.upgradePersonal",
4769 "start": {
4770 "column": 23,
4771 "line": 49
4772 }
4773 },
4774 {
4775 "defaultMessage": "!!!Choose Professional",
4776 "end": {
4777 "column": 3,
4778 "line": 56
4779 },
4780 "file": "src/features/planSelection/components/PlanSelection.js",
4781 "id": "feature.planSelection.cta.upgradePro",
4782 "start": {
4783 "column": 18,
4784 "line": 53
4785 }
4786 },
4787 {
4788 "defaultMessage": "!!!Complete comparison of all plans",
4789 "end": {
4790 "column": 3,
4791 "line": 60
4792 },
4793 "file": "src/features/planSelection/components/PlanSelection.js",
4794 "id": "feature.planSelection.fullFeatureList",
4795 "start": {
4796 "column": 19,
4797 "line": 57
4798 }
4799 },
4800 {
4801 "defaultMessage": "!!!All prices based on yearly payment",
4802 "end": {
4803 "column": 3,
4804 "line": 64
4805 },
4806 "file": "src/features/planSelection/components/PlanSelection.js",
4807 "id": "feature.planSelection.pricesBasedOnAnnualPayment",
4808 "start": {
4809 "column": 30,
4810 "line": 61
4811 }
4812 }
4813 ],
4814 "path": "src/features/planSelection/components/PlanSelection.json"
4815 },
4816 {
4817 "descriptors": [
4818 {
4819 "defaultMessage": "!!!per {interval}",
4820 "end": {
4821 "column": 3,
4822 "line": 19
4823 },
4824 "file": "src/features/planSelection/components/PlanTeaser.js",
4825 "id": "subscription.interval.per",
4826 "start": {
4827 "column": 7,
4828 "line": 16
4829 }
4830 },
4831 {
4832 "defaultMessage": "!!!Upgrade Account",
4833 "end": {
4834 "column": 3,
4835 "line": 23
4836 },
4837 "file": "src/features/planSelection/components/PlanTeaser.js",
4838 "id": "subscription.planItem.upgradeAccount",
4839 "start": {
4840 "column": 7,
4841 "line": 20
4842 }
4843 }
4844 ],
4845 "path": "src/features/planSelection/components/PlanTeaser.json"
4846 },
4847 {
4848 "descriptors": [
4849 {
4850 "defaultMessage": "!!!Downgrade your Franz Plan",
4851 "end": {
4852 "column": 3,
4853 "line": 19
4854 },
4855 "file": "src/features/planSelection/containers/PlanSelectionScreen.js",
4856 "id": "feature.planSelection.fullscreen.dialog.title",
4857 "start": {
4858 "column": 15,
4859 "line": 16
4860 }
4861 },
4862 {
4863 "defaultMessage": "!!!You're about to downgrade to our Free account. Are you sure? Click here instead to get more services and functionality for just {currency}{price} a month.",
4864 "end": {
4865 "column": 3,
4866 "line": 23
4867 },
4868 "file": "src/features/planSelection/containers/PlanSelectionScreen.js",
4869 "id": "feature.planSelection.fullscreen.dialog.message",
4870 "start": {
4871 "column": 17,
4872 "line": 20
4873 }
4874 },
4875 {
4876 "defaultMessage": "!!!Downgrade to Free",
4877 "end": {
4878 "column": 3,
4879 "line": 27
4880 },
4881 "file": "src/features/planSelection/containers/PlanSelectionScreen.js",
4882 "id": "feature.planSelection.fullscreen.dialog.cta.downgrade",
4883 "start": {
4884 "column": 22,
4885 "line": 24
4886 }
4887 },
4888 {
4889 "defaultMessage": "!!!Choose Personal",
4890 "end": {
4891 "column": 3,
4892 "line": 31
4893 },
4894 "file": "src/features/planSelection/containers/PlanSelectionScreen.js",
4895 "id": "feature.planSelection.fullscreen.dialog.cta.upgrade",
4896 "start": {
4897 "column": 20,
4898 "line": 28
4899 }
4900 }
4901 ],
4902 "path": "src/features/planSelection/containers/PlanSelectionScreen.json"
4903 },
4904 {
4905 "descriptors": [
4906 {
4497 "defaultMessage": "!!!QuickSwitch", 4907 "defaultMessage": "!!!QuickSwitch",
4498 "end": { 4908 "end": {
4499 "column": 3, 4909 "column": 3,
@@ -4727,6 +5137,107 @@
4727 { 5137 {
4728 "descriptors": [ 5138 "descriptors": [
4729 { 5139 {
5140 "defaultMessage": "!!!Your Free Franz {plan} Trial ends in {time}.",
5141 "end": {
5142 "column": 3,
5143 "line": 16
5144 },
5145 "file": "src/features/trialStatusBar/components/TrialStatusBar.js",
5146 "id": "feature.trialStatusBar.restTime",
5147 "start": {
5148 "column": 12,
5149 "line": 13
5150 }
5151 },
5152 {
5153 "defaultMessage": "!!!Your free Franz {plan} Trial has expired, please upgrade your account.",
5154 "end": {
5155 "column": 3,
5156 "line": 20
5157 },
5158 "file": "src/features/trialStatusBar/components/TrialStatusBar.js",
5159 "id": "feature.trialStatusBar.expired",
5160 "start": {
5161 "column": 11,
5162 "line": 17
5163 }
5164 },
5165 {
5166 "defaultMessage": "!!!Upgrade now",
5167 "end": {
5168 "column": 3,
5169 "line": 24
5170 },
5171 "file": "src/features/trialStatusBar/components/TrialStatusBar.js",
5172 "id": "feature.trialStatusBar.cta",
5173 "start": {
5174 "column": 7,
5175 "line": 21
5176 }
5177 }
5178 ],
5179 "path": "src/features/trialStatusBar/components/TrialStatusBar.json"
5180 },
5181 {
5182 "descriptors": [
5183 {
5184 "defaultMessage": "!!!Downgrade your Franz Plan",
5185 "end": {
5186 "column": 3,
5187 "line": 19
5188 },
5189 "file": "src/features/trialStatusBar/containers/TrialStatusBarScreen.js",
5190 "id": "feature.trialStatusBar.fullscreen.dialog.title",
5191 "start": {
5192 "column": 15,
5193 "line": 16
5194 }
5195 },
5196 {
5197 "defaultMessage": "!!!You're about to downgrade to our Free account. Are you sure? Click here instead to get more services and functionality for just {currency}{price} a month.",
5198 "end": {
5199 "column": 3,
5200 "line": 23
5201 },
5202 "file": "src/features/trialStatusBar/containers/TrialStatusBarScreen.js",
5203 "id": "feature.trialStatusBar.fullscreen.dialog.message",
5204 "start": {
5205 "column": 17,
5206 "line": 20
5207 }
5208 },
5209 {
5210 "defaultMessage": "!!!Downgrade to Free",
5211 "end": {
5212 "column": 3,
5213 "line": 27
5214 },
5215 "file": "src/features/trialStatusBar/containers/TrialStatusBarScreen.js",
5216 "id": "feature.trialStatusBar.fullscreen.dialog.cta.downgrade",
5217 "start": {
5218 "column": 22,
5219 "line": 24
5220 }
5221 },
5222 {
5223 "defaultMessage": "!!!Choose Personal",
5224 "end": {
5225 "column": 3,
5226 "line": 31
5227 },
5228 "file": "src/features/trialStatusBar/containers/TrialStatusBarScreen.js",
5229 "id": "feature.trialStatusBar.fullscreen.dialog.cta.upgrade",
5230 "start": {
5231 "column": 20,
5232 "line": 28
5233 }
5234 }
5235 ],
5236 "path": "src/features/trialStatusBar/containers/TrialStatusBarScreen.json"
5237 },
5238 {
5239 "descriptors": [
5240 {
4730 "defaultMessage": "!!!Home", 5241 "defaultMessage": "!!!Home",
4731 "end": { 5242 "end": {
4732 "column": 3, 5243 "column": 3,
@@ -5093,104 +5604,104 @@
5093 "defaultMessage": "!!!Your workspaces", 5604 "defaultMessage": "!!!Your workspaces",
5094 "end": { 5605 "end": {
5095 "column": 3, 5606 "column": 3,
5096 "line": 22 5607 "line": 23
5097 }, 5608 },
5098 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 5609 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
5099 "id": "settings.workspaces.headline", 5610 "id": "settings.workspaces.headline",
5100 "start": { 5611 "start": {
5101 "column": 12, 5612 "column": 12,
5102 "line": 19 5613 "line": 20
5103 } 5614 }
5104 }, 5615 },
5105 { 5616 {
5106 "defaultMessage": "!!!You haven't added any workspaces yet.", 5617 "defaultMessage": "!!!You haven't added any workspaces yet.",
5107 "end": { 5618 "end": {
5108 "column": 3, 5619 "column": 3,
5109 "line": 26 5620 "line": 27
5110 }, 5621 },
5111 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 5622 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
5112 "id": "settings.workspaces.noWorkspacesAdded", 5623 "id": "settings.workspaces.noWorkspacesAdded",
5113 "start": { 5624 "start": {
5114 "column": 19, 5625 "column": 19,
5115 "line": 23 5626 "line": 24
5116 } 5627 }
5117 }, 5628 },
5118 { 5629 {
5119 "defaultMessage": "!!!Could not load your workspaces", 5630 "defaultMessage": "!!!Could not load your workspaces",
5120 "end": { 5631 "end": {
5121 "column": 3, 5632 "column": 3,
5122 "line": 30 5633 "line": 31
5123 }, 5634 },
5124 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 5635 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
5125 "id": "settings.workspaces.workspacesRequestFailed", 5636 "id": "settings.workspaces.workspacesRequestFailed",
5126 "start": { 5637 "start": {
5127 "column": 27, 5638 "column": 27,
5128 "line": 27 5639 "line": 28
5129 } 5640 }
5130 }, 5641 },
5131 { 5642 {
5132 "defaultMessage": "!!!Try again", 5643 "defaultMessage": "!!!Try again",
5133 "end": { 5644 "end": {
5134 "column": 3, 5645 "column": 3,
5135 "line": 34 5646 "line": 35
5136 }, 5647 },
5137 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 5648 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
5138 "id": "settings.workspaces.tryReloadWorkspaces", 5649 "id": "settings.workspaces.tryReloadWorkspaces",
5139 "start": { 5650 "start": {
5140 "column": 23, 5651 "column": 23,
5141 "line": 31 5652 "line": 32
5142 } 5653 }
5143 }, 5654 },
5144 { 5655 {
5145 "defaultMessage": "!!!Your changes have been saved", 5656 "defaultMessage": "!!!Your changes have been saved",
5146 "end": { 5657 "end": {
5147 "column": 3, 5658 "column": 3,
5148 "line": 38 5659 "line": 39
5149 }, 5660 },
5150 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 5661 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
5151 "id": "settings.workspaces.updatedInfo", 5662 "id": "settings.workspaces.updatedInfo",
5152 "start": { 5663 "start": {
5153 "column": 15, 5664 "column": 15,
5154 "line": 35 5665 "line": 36
5155 } 5666 }
5156 }, 5667 },
5157 { 5668 {
5158 "defaultMessage": "!!!Workspace has been deleted", 5669 "defaultMessage": "!!!Workspace has been deleted",
5159 "end": { 5670 "end": {
5160 "column": 3, 5671 "column": 3,
5161 "line": 42 5672 "line": 43
5162 }, 5673 },
5163 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 5674 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
5164 "id": "settings.workspaces.deletedInfo", 5675 "id": "settings.workspaces.deletedInfo",
5165 "start": { 5676 "start": {
5166 "column": 15, 5677 "column": 15,
5167 "line": 39 5678 "line": 40
5168 } 5679 }
5169 }, 5680 },
5170 { 5681 {
5171 "defaultMessage": "!!!Info about workspace feature", 5682 "defaultMessage": "!!!Info about workspace feature",
5172 "end": { 5683 "end": {
5173 "column": 3, 5684 "column": 3,
5174 "line": 46 5685 "line": 47
5175 }, 5686 },
5176 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 5687 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
5177 "id": "settings.workspaces.workspaceFeatureInfo", 5688 "id": "settings.workspaces.workspaceFeatureInfo",
5178 "start": { 5689 "start": {
5179 "column": 24, 5690 "column": 24,
5180 "line": 43 5691 "line": 44
5181 } 5692 }
5182 }, 5693 },
5183 { 5694 {
5184 "defaultMessage": "!!!Less is More: Introducing Ferdi Workspaces", 5695 "defaultMessage": "!!!Less is More: Introducing Ferdi Workspaces",
5185 "end": { 5696 "end": {
5186 "column": 3, 5697 "column": 3,
5187 "line": 50 5698 "line": 51
5188 }, 5699 },
5189 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 5700 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
5190 "id": "settings.workspaces.workspaceFeatureHeadline", 5701 "id": "settings.workspaces.workspaceFeatureHeadline",
5191 "start": { 5702 "start": {
5192 "column": 28, 5703 "column": 28,
5193 "line": 47 5704 "line": 48
5194 } 5705 }
5195 } 5706 }
5196 ], 5707 ],
@@ -5217,7 +5728,7 @@
5217 { 5728 {
5218 "descriptors": [ 5729 "descriptors": [
5219 { 5730 {
5220 "defaultMessage": "!!!Franz Professional", 5731 "defaultMessage": "!!!Professional",
5221 "end": { 5732 "end": {
5222 "column": 3, 5733 "column": 3,
5223 "line": 8 5734 "line": 8
@@ -5230,7 +5741,7 @@
5230 } 5741 }
5231 }, 5742 },
5232 { 5743 {
5233 "defaultMessage": "!!!Franz Personal", 5744 "defaultMessage": "!!!Personal",
5234 "end": { 5745 "end": {
5235 "column": 3, 5746 "column": 3,
5236 "line": 12 5747 "line": 12
@@ -5243,7 +5754,7 @@
5243 } 5754 }
5244 }, 5755 },
5245 { 5756 {
5246 "defaultMessage": "!!!Franz Free", 5757 "defaultMessage": "!!!Free",
5247 "end": { 5758 "end": {
5248 "column": 3, 5759 "column": 3,
5249 "line": 16 5760 "line": 16
@@ -5256,7 +5767,7 @@
5256 } 5767 }
5257 }, 5768 },
5258 { 5769 {
5259 "defaultMessage": "!!!Franz Premium", 5770 "defaultMessage": "!!!Premium",
5260 "end": { 5771 "end": {
5261 "column": 3, 5772 "column": 3,
5262 "line": 20 5773 "line": 20
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 477bdf43c..a34da1848 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -9,6 +9,22 @@
9 "feature.delayApp.trial.headline": "Get the free Ferdi Professional 14 day trial and skip the line", 9 "feature.delayApp.trial.headline": "Get the free Ferdi Professional 14 day trial and skip the line",
10 "feature.delayApp.upgrade.action": "Get a Ferdi Supporter License", 10 "feature.delayApp.upgrade.action": "Get a Ferdi Supporter License",
11 "feature.delayApp.upgrade.actionShort": "Upgrade account", 11 "feature.delayApp.upgrade.actionShort": "Upgrade account",
12 "feature.planSelection.cta.ctaDowngradeFree": "Downgrade to Free",
13 "feature.planSelection.cta.stayOnFree": "Stay on Free",
14 "feature.planSelection.cta.trial": "Start my free 14-days Trial",
15 "feature.planSelection.cta.upgradePersonal": "Choose Personal",
16 "feature.planSelection.cta.upgradePro": "Choose Professional",
17 "feature.planSelection.free.text": "Basic functionality",
18 "feature.planSelection.fullFeatureList": "Complete comparison of all plans",
19 "feature.planSelection.fullscreen.dialog.cta.downgrade": "Downgrade to Free",
20 "feature.planSelection.fullscreen.dialog.cta.upgrade": "Choose Personal",
21 "feature.planSelection.fullscreen.dialog.message": "You're about to downgrade to our Free account. Are you sure? Click here instead to get more services and functionality for just {currency}{price} a month.",
22 "feature.planSelection.fullscreen.dialog.title": "Downgrade your Ferdi Plan",
23 "feature.planSelection.fullscreen.subheadline": "It's time to make a choice. Ferdi works best on our Personal and Professional plans. Please have a look and choose the best one for you.",
24 "feature.planSelection.fullscreen.welcome": "Are you ready to choose, {name}",
25 "feature.planSelection.personal.text": "More services, no waiting - ideal for personal use.",
26 "feature.planSelection.pricesBasedOnAnnualPayment": "All prices based on yearly payment",
27 "feature.planSelection.pro.text": "Unlimited services and professional features for you - and your team.",
12 "feature.quickSwitch.info": "Select a service with TAB, ↑ and ↓. Open a service with ENTER.", 28 "feature.quickSwitch.info": "Select a service with TAB, ↑ and ↓. Open a service with ENTER.",
13 "feature.quickSwitch.search": "Search...", 29 "feature.quickSwitch.search": "Search...",
14 "feature.quickSwitch.title": "QuickSwitch", 30 "feature.quickSwitch.title": "QuickSwitch",
@@ -23,6 +39,13 @@
23 "feature.todos.premium.info": "Ferdi Todos are available to premium users now!", 39 "feature.todos.premium.info": "Ferdi Todos are available to premium users now!",
24 "feature.todos.premium.rollout": "Everyone else will have to wait a little longer.", 40 "feature.todos.premium.rollout": "Everyone else will have to wait a little longer.",
25 "feature.todos.premium.upgrade": "Upgrade Account", 41 "feature.todos.premium.upgrade": "Upgrade Account",
42 "feature.trialStatusBar.cta": "Upgrade now",
43 "feature.trialStatusBar.expired": "Your free Ferdi {plan} Trial has expired, please upgrade your account.",
44 "feature.trialStatusBar.fullscreen.dialog.cta.downgrade": "Downgrade to Free",
45 "feature.trialStatusBar.fullscreen.dialog.cta.upgrade": "Choose Personal",
46 "feature.trialStatusBar.fullscreen.dialog.message": "You're about to downgrade to our Free account. Are you sure? Click here instead to get more services and functionality for just {currency}{price} a month.",
47 "feature.trialStatusBar.fullscreen.dialog.title": "Downgrade your Ferdi Plan",
48 "feature.trialStatusBar.restTime": "Your Free Ferdi {plan} Trial ends in {time}.",
26 "global.api.unhealthy": "Can't connect to Ferdi online services", 49 "global.api.unhealthy": "Can't connect to Ferdi online services",
27 "global.franzProRequired": "Ferdi Professional Required", 50 "global.franzProRequired": "Ferdi Professional Required",
28 "global.notConnectedToTheInternet": "You are not connected to the internet.", 51 "global.notConnectedToTheInternet": "You are not connected to the internet.",
@@ -140,15 +163,20 @@
140 "password.submit.label": "Submit", 163 "password.submit.label": "Submit",
141 "password.successInfo": "Please check your email", 164 "password.successInfo": "Please check your email",
142 "premiumFeature.button.upgradeAccount": "Upgrade account", 165 "premiumFeature.button.upgradeAccount": "Upgrade account",
166 "pricing.features.accountSync": "Account Synchronisation",
143 "pricing.features.adFree": "Forever ad-free", 167 "pricing.features.adFree": "Forever ad-free",
144 "pricing.features.appDelays": "No Waiting Screens", 168 "pricing.features.appDelays": "No Waiting Screens",
145 "pricing.features.customWebsites": "Add Custom Websites", 169 "pricing.features.customWebsites": "Add Custom Websites",
170 "pricing.features.desktopNotifications": "Desktop Notifications",
146 "pricing.features.onPremise": "On-premise & other Hosted Services", 171 "pricing.features.onPremise": "On-premise & other Hosted Services",
172 "pricing.features.recipes": "Choose from more than 70 Services",
147 "pricing.features.serviceProxies": "Service Proxies", 173 "pricing.features.serviceProxies": "Service Proxies",
148 "pricing.features.spellchecker": "Spellchecker support", 174 "pricing.features.spellchecker": "Spellchecker support",
149 "pricing.features.teamManagement": "Team Management", 175 "pricing.features.teamManagement": "Team Management",
150 "pricing.features.thirdPartyServices": "Install 3rd party services", 176 "pricing.features.thirdPartyServices": "Install 3rd party services",
151 "pricing.features.unlimitedServices": "Add unlimited services", 177 "pricing.features.unlimitedServices": "Add unlimited services",
178 "pricing.features.upToSixServices": "Add up to 6 services",
179 "pricing.features.upToThreeServices": "Add up to 3 services",
152 "pricing.features.workspaces": "Workspaces", 180 "pricing.features.workspaces": "Workspaces",
153 "pricing.plan.free": "Ferdi Free", 181 "pricing.plan.free": "Ferdi Free",
154 "pricing.plan.legacy": "Ferdi Premium", 182 "pricing.plan.legacy": "Ferdi Premium",
@@ -160,13 +188,17 @@
160 "pricing.plan.pro-yearly": "Ferdi Professional Yearly", 188 "pricing.plan.pro-yearly": "Ferdi Professional Yearly",
161 "pricing.trial.cta.accept": "Yes, upgrade my account to Ferdi Professional", 189 "pricing.trial.cta.accept": "Yes, upgrade my account to Ferdi Professional",
162 "pricing.trial.cta.skip": "Continue to Ferdi", 190 "pricing.trial.cta.skip": "Continue to Ferdi",
191 "pricing.trial.cta.start": "Start using Ferdi",
163 "pricing.trial.error": "Sorry, we could not activate your trial!", 192 "pricing.trial.error": "Sorry, we could not activate your trial!",
164 "pricing.trial.features.headline": "Ferdi Professional includes:", 193 "pricing.trial.features.headline": "Ferdi Professional includes:",
165 "pricing.trial.headline": "Ferdi Professional", 194 "pricing.trial.headline.pro": "Hi {name}, welcome to Ferdi",
166 "pricing.trial.subheadline": "Your personal welcome offer:", 195 "pricing.trial.intro.happyMessaging": "Happy messaging,",
196 "pricing.trial.intro.specialTreat": "We have a special treat for you.",
197 "pricing.trial.intro.tryPro": "Enjoy the full Ferdi Professional experience completely free for 14 days.",
167 "pricing.trial.terms.automaticTrialEnd": "Your free trial ends automatically after 14 days", 198 "pricing.trial.terms.automaticTrialEnd": "Your free trial ends automatically after 14 days",
168 "pricing.trial.terms.headline": "No strings attached", 199 "pricing.trial.terms.headline": "No strings attached",
169 "pricing.trial.terms.noCreditCard": "No credit card required", 200 "pricing.trial.terms.noCreditCard": "No credit card required",
201 "pricing.trial.terms.trialWorth": "Free trial (normally {currency}{price} per month)",
170 "service.crashHandler.action": "Reload {name}", 202 "service.crashHandler.action": "Reload {name}",
171 "service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds", 203 "service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds",
172 "service.crashHandler.headline": "Oh no!", 204 "service.crashHandler.headline": "Oh no!",
@@ -405,10 +437,15 @@
405 "signup.link.login": "Already have an account, sign in?", 437 "signup.link.login": "Already have an account, sign in?",
406 "signup.password.label": "Password", 438 "signup.password.label": "Password",
407 "signup.submit.label": "Create account", 439 "signup.submit.label": "Create account",
440 "subscription.bestValue": "Best value",
408 "subscription.cta.activateTrial": "Yes, start the free Ferdi Professional trial", 441 "subscription.cta.activateTrial": "Yes, start the free Ferdi Professional trial",
409 "subscription.cta.allOptions": "See all options", 442 "subscription.cta.allOptions": "See all options",
410 "subscription.cta.choosePlan": "Choose your plan", 443 "subscription.cta.choosePlan": "Choose your plan",
411 "subscription.includedProFeatures": "The Ferdi Professional Plan includes:", 444 "subscription.includedProFeatures": "The Ferdi Professional Plan includes:",
445 "subscription.interval.per": "per {interval}",
446 "subscription.interval.perMonth": "per month",
447 "subscription.interval.perMonthPerUser": "per month & user",
448 "subscription.planItem.upgradeAccount": "Upgrade Account",
412 "subscription.teaser.includedFeatures": "Paid Ferdi Plans include:", 449 "subscription.teaser.includedFeatures": "Paid Ferdi Plans include:",
413 "subscription.teaser.intro": "Ferdi 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!", 450 "subscription.teaser.intro": "Ferdi 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!",
414 "subscriptionPopup.buttonCancel": "Cancel", 451 "subscriptionPopup.buttonCancel": "Cancel",
diff --git a/src/i18n/locales/zh-Hant.json b/src/i18n/locales/zh-Hant.json
deleted file mode 100644
index 872bfd9d7..000000000
--- a/src/i18n/locales/zh-Hant.json
+++ /dev/null
@@ -1,485 +0,0 @@
1{
2 "app.errorHandler.action": "Reload",
3 "app.errorHandler.headline": "Something went wrong",
4 "feature.announcements.changelog.headline": "Changes in Ferdi {version}",
5 "feature.delayApp.headline": "Please purchase a Ferdi Supporter License to skip waiting",
6 "feature.delayApp.text": "Ferdi will continue in {seconds} seconds.",
7 "feature.delayApp.trial.action": "Yes, I want the free 14 day trial of Ferdi Professional",
8 "feature.delayApp.trial.actionShort": "Activate the free Ferdi Professional trial",
9 "feature.delayApp.trial.headline": "Get the free Ferdi Professional 14 day trial and skip the line",
10 "feature.delayApp.upgrade.action": "Get a Ferdi Supporter License",
11 "feature.delayApp.upgrade.actionShort": "Upgrade account",
12 "feature.planSelection.cta.ctaDowngradeFree": "Downgrade to Free",
13 "feature.planSelection.cta.stayOnFree": "Stay on Free",
14 "feature.planSelection.cta.trial": "Start my free 14-days Trial",
15 "feature.planSelection.cta.upgradePersonal": "Choose Personal",
16 "feature.planSelection.cta.upgradePro": "Choose Professional",
17 "feature.planSelection.free.text": "Basic functionality",
18 "feature.planSelection.fullFeatureList": "Complete comparison of all plans",
19 "feature.planSelection.fullscreen.dialog.cta.downgrade": "Downgrade to Free",
20 "feature.planSelection.fullscreen.dialog.cta.upgrade": "Choose Personal",
21 "feature.planSelection.fullscreen.dialog.message": "You're about to downgrade to our Free account. Are you sure? Click here instead to get more services and functionality for just {currency}{price} a month.",
22 "feature.planSelection.fullscreen.dialog.title": "Downgrade your Ferdi Plan",
23 "feature.planSelection.fullscreen.subheadline": "It's time to make a choice. Ferdi works best on our Personal and Professional plans. Please have a look and choose the best one for you.",
24 "feature.planSelection.fullscreen.welcome": "Are you ready to choose, {name}",
25 "feature.planSelection.personal.text": "More services, no waiting - ideal for personal use.",
26 "feature.planSelection.pricesBasedOnAnnualPayment": "All prices based on yearly payment",
27 "feature.planSelection.pro.text": "Unlimited services and professional features for you - and your team.",
28 "feature.quickSwitch.info": "Select a service with TAB, ↑ and ↓. Open a service with ENTER.",
29 "feature.quickSwitch.search": "Search...",
30 "feature.quickSwitch.title": "QuickSwitch",
31 "feature.serviceLimit.limitReached": "You have added {amount} out of {limit} services that are included in your plan. Please upgrade your account to add more services.",
32 "feature.shareFranz.action.email": "Send as email",
33 "feature.shareFranz.action.facebook": "Share on Facebook",
34 "feature.shareFranz.action.twitter": "Share on Twitter",
35 "feature.shareFranz.headline": "Ferdi is better together!",
36 "feature.shareFranz.shareText.email": "I've added {count} services to Ferdi! Get the free app for WhatsApp, Messenger, Slack, Skype and co at www.getferdi.com",
37 "feature.shareFranz.shareText.twitter": "I've added {count} services to Ferdi! Get the free app for WhatsApp, Messenger, Slack, Skype and co at www.getferdi.com /cc @FerdiMessenger",
38 "feature.shareFranz.text": "Tell your friends and colleagues how awesome Ferdi is and help us to spread the word.",
39 "feature.todos.premium.info": "Ferdi Todos are available to premium users now!",
40 "feature.todos.premium.rollout": "Everyone else will have to wait a little longer.",
41 "feature.todos.premium.upgrade": "Upgrade Account",
42 "feature.trialStatusBar.cta": "Upgrade now",
43 "feature.trialStatusBar.expired": "Your free Ferdi {plan} Trial has expired, please upgrade your account.",
44 "feature.trialStatusBar.fullscreen.dialog.cta.downgrade": "Downgrade to Free",
45 "feature.trialStatusBar.fullscreen.dialog.cta.upgrade": "Choose Personal",
46 "feature.trialStatusBar.fullscreen.dialog.message": "You're about to downgrade to our Free account. Are you sure? Click here instead to get more services and functionality for just {currency}{price} a month.",
47 "feature.trialStatusBar.fullscreen.dialog.title": "Downgrade your Ferdi Plan",
48 "feature.trialStatusBar.restTime": "Your Free Ferdi {plan} Trial ends in {time}.",
49 "global.api.unhealthy": "無法連接到Ferdi網路服務",
50 "global.franzProRequired": "Ferdi Professional Required",
51 "global.notConnectedToTheInternet": "您未連上網際網路",
52 "global.spellchecker.useDefault": "Use System Default ({default})",
53 "global.spellchecking.autodetect": "Detect language automatically",
54 "global.spellchecking.autodetect.short": "Automatic",
55 "global.spellchecking.language": "Spell checking language",
56 "global.upgradeButton.upgradeToPro": "Upgrade to Ferdi Professional",
57 "import.headline": "匯入您的 Ferdi 4 服務",
58 "import.notSupportedHeadline": "此服務不被 Ferdi 5 支持",
59 "import.skip.label": "我想手動匯入",
60 "import.submit.label": "匯入服務",
61 "infobar.authRequestFailed": "There were errors while trying to perform an authenticated request. Please try logging out and back in if this error persists.",
62 "infobar.buttonChangelog": "What is new?",
63 "infobar.buttonInstallUpdate": "重新啟動並且更新",
64 "infobar.buttonReloadServices": "重新載入",
65 "infobar.requiredRequestsFailed": "無法載入服務與帳戶資訊",
66 "infobar.servicesUpdated": "您的服務已更新",
67 "infobar.trialActivated": "Your trial was successfully activated. Happy messaging!",
68 "infobar.updateAvailable": "有新的更新可安裝",
69 "invite.email.label": "電子郵件信箱",
70 "invite.headline.friends": "邀請三個人",
71 "invite.name.label": "名子",
72 "invite.skip.label": "我想晚點進行",
73 "invite.submit.label": "Send invites",
74 "invite.successInfo": "Invitations sent successfully",
75 "locked.headline": "Locked",
76 "locked.info": "Ferdi is currently locked. Please unlock Ferdi with your password to see your messages.",
77 "locked.invalidCredentials": "Password invalid",
78 "locked.password.label": "Password",
79 "locked.submit.label": "Unlock",
80 "login.changeServer": "Change server",
81 "login.customServerQuestion": "Using a custom Ferdi server?",
82 "login.customServerSuggestion": "Try importing your Franz account",
83 "login.email.label": "電子郵件信箱",
84 "login.headline": "登入",
85 "login.invalidCredentials": "電子郵件帳戶或密碼有誤",
86 "login.link.password": "密碼重設",
87 "login.link.signup": "建立一個免費帳戶",
88 "login.password.label": "Password",
89 "login.serverLogout": "登入狀態過期,請重新登入",
90 "login.submit.label": "登入",
91 "login.tokenExpired": "登入狀態過期,請重新登入",
92 "menu.Todoss.closeTodosDrawer": "Close Todos drawer",
93 "menu.Todoss.openTodosDrawer": "Open Todos drawer",
94 "menu.app.about": "About Ferdi",
95 "menu.app.announcement": "What's new?",
96 "menu.app.autohideMenuBar": "Auto-hide menu bar",
97 "menu.app.checkForUpdates": "Check for updates",
98 "menu.app.hide": "Hide",
99 "menu.app.hideOthers": "Hide Others",
100 "menu.app.quit": "Quit",
101 "menu.app.settings": "Settings",
102 "menu.app.unhide": "Unhide",
103 "menu.edit": "Edit",
104 "menu.edit.copy": "Copy",
105 "menu.edit.cut": "Cut",
106 "menu.edit.delete": "Delete",
107 "menu.edit.emojiSymbols": "Emoji & Symbols",
108 "menu.edit.paste": "Paste",
109 "menu.edit.pasteAndMatchStyle": "Paste And Match Style",
110 "menu.edit.redo": "Redo",
111 "menu.edit.selectAll": "Select All",
112 "menu.edit.speech": "Speech",
113 "menu.edit.startDictation": "Start Dictation",
114 "menu.edit.startSpeaking": "Start Speaking",
115 "menu.edit.stopSpeaking": "Stop Speaking",
116 "menu.edit.undo": "Undo",
117 "menu.file": "File",
118 "menu.help": "Help",
119 "menu.help.changelog": "Changelog",
120 "menu.help.debugInfo": "Copy Debug Information",
121 "menu.help.debugInfoCopiedBody": "Your Debug Information has been copied to your clipboard.",
122 "menu.help.debugInfoCopiedHeadline": "Ferdi Debug Information",
123 "menu.help.learnMore": "Learn More",
124 "menu.help.privacy": "Privacy Statement",
125 "menu.help.support": "Support",
126 "menu.help.tos": "Terms of Service",
127 "menu.services": "Services",
128 "menu.services.activatePreviousService": "Activate previous service",
129 "menu.services.addNewService": "Add New Service...",
130 "menu.services.goHome": "Home",
131 "menu.services.setNextServiceActive": "Activate next service",
132 "menu.todos": "Todos",
133 "menu.todos.enableTodos": "Enable Todos",
134 "menu.view": "View",
135 "menu.view.back": "Back",
136 "menu.view.enterFullScreen": "Enter Full Screen",
137 "menu.view.exitFullScreen": "Exit Full Screen",
138 "menu.view.forward": "Forward",
139 "menu.view.lockFerdi": "Lock Ferdi",
140 "menu.view.openQuickSwitch": "Open Quick Switch",
141 "menu.view.reloadFranz": "Reload Ferdi",
142 "menu.view.reloadService": "Reload Service",
143 "menu.view.resetZoom": "Actual Size",
144 "menu.view.toggleDevTools": "Toggle Developer Tools",
145 "menu.view.toggleFullScreen": "Toggle Full Screen",
146 "menu.view.toggleServiceDevTools": "Toggle Service Developer Tools",
147 "menu.view.toggleTodosDevTools": "Toggle Todos Developer Tools",
148 "menu.view.zoomIn": "Zoom In",
149 "menu.view.zoomOut": "Zoom Out",
150 "menu.window": "Window",
151 "menu.window.close": "Close",
152 "menu.window.minimize": "Minimize",
153 "menu.workspaces": "Workspaces",
154 "menu.workspaces.addNewWorkspace": "Add New Workspace...",
155 "menu.workspaces.closeWorkspaceDrawer": "Close workspace drawer",
156 "menu.workspaces.defaultWorkspace": "All services",
157 "menu.workspaces.openWorkspaceDrawer": "Open workspace drawer",
158 "password.email.label": "電子郵件信箱",
159 "password.headline": "密碼重設",
160 "password.link.login": "登入您的帳戶",
161 "password.link.signup": "建立一個免費帳戶",
162 "password.noUser": "此電子郵件帳戶不存在",
163 "password.submit.label": "送出",
164 "password.successInfo": "請重新確認您的電子郵件信箱",
165 "premiumFeature.button.upgradeAccount": "Upgrade account",
166 "pricing.features.accountSync": "Account Synchronisation",
167 "pricing.features.adFree": "Forever ad-free",
168 "pricing.features.appDelays": "No Waiting Screens",
169 "pricing.features.customWebsites": "Add Custom Websites",
170 "pricing.features.desktopNotifications": "Desktop Notifications",
171 "pricing.features.onPremise": "On-premise & other Hosted Services",
172 "pricing.features.recipes": "Choose from more than 70 Services",
173 "pricing.features.serviceProxies": "Service Proxies",
174 "pricing.features.spellchecker": "Spellchecker support",
175 "pricing.features.teamManagement": "Team Management",
176 "pricing.features.thirdPartyServices": "Install 3rd party services",
177 "pricing.features.unlimitedServices": "Add unlimited services",
178 "pricing.features.upToSixServices": "Add up to 6 services",
179 "pricing.features.upToThreeServices": "Add up to 3 services",
180 "pricing.features.workspaces": "Workspaces",
181 "pricing.plan.free": "Ferdi Free",
182 "pricing.plan.legacy": "Ferdi Premium",
183 "pricing.plan.personal": "Ferdi Personal",
184 "pricing.plan.personal-monthly": "Ferdi Personal Monthly",
185 "pricing.plan.personal-yearly": "Ferdi Personal Yearly",
186 "pricing.plan.pro": "Ferdi Professional",
187 "pricing.plan.pro-monthly": "Ferdi Professional Monthly",
188 "pricing.plan.pro-yearly": "Ferdi Professional Yearly",
189 "pricing.trial.cta.accept": "Yes, upgrade my account to Ferdi Professional",
190 "pricing.trial.cta.skip": "Continue to Ferdi",
191 "pricing.trial.cta.start": "Start using Ferdi",
192 "pricing.trial.error": "Sorry, we could not activate your trial!",
193 "pricing.trial.features.headline": "Ferdi Professional includes:",
194 "pricing.trial.headline.pro": "Hi {name}, welcome to Ferdi",
195 "pricing.trial.intro.happyMessaging": "Happy messaging,",
196 "pricing.trial.intro.specialTreat": "We have a special treat for you.",
197 "pricing.trial.intro.tryPro": "Enjoy the full Ferdi Professional experience completely free for 14 days.",
198 "pricing.trial.terms.automaticTrialEnd": "Your free trial ends automatically after 14 days",
199 "pricing.trial.terms.headline": "No strings attached",
200 "pricing.trial.terms.noCreditCard": "No credit card required",
201 "pricing.trial.terms.trialWorth": "Free trial (normally {currency}{price} per month)",
202 "service.crashHandler.action": "Reload {name}",
203 "service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds",
204 "service.crashHandler.headline": "Oh no!",
205 "service.crashHandler.text": "{name} has caused an error.",
206 "service.disabledHandler.action": "Enable {name}",
207 "service.disabledHandler.headline": "{name} is disabled",
208 "service.errorHandler.action": "Reload {name}",
209 "service.errorHandler.editAction": "Edit {name}",
210 "service.errorHandler.headline": "Oh no!",
211 "service.errorHandler.message": "Error",
212 "service.errorHandler.text": "{name} has failed to load.",
213 "service.restrictedHandler.action": "Upgrade Account",
214 "service.restrictedHandler.customUrl.headline": "Ferdi Professional Plan required",
215 "service.restrictedHandler.customUrl.text": "Please upgrade to the Ferdi Professional plan to use custom urls & self hosted services.",
216 "service.restrictedHandler.serviceLimit.headline": "You have reached your service limit.",
217 "service.restrictedHandler.serviceLimit.text": "Please upgrade your account to use more than {count} services.",
218 "service.webviewLoader.loading": "Loading",
219 "services.getStarted": "開始使用",
220 "services.login": "Please login to use Ferdi.",
221 "services.serverInfo": "Optionally, you can change your Ferdi server by clicking the cog in the bottom left corner.",
222 "services.serverless": "Use Ferdi without an Account",
223 "services.welcome": "歡迎使用 Ferdi",
224 "settings.account.account.editButton": "更改帳戶資訊",
225 "settings.account.accountType.basic": "基本帳戶",
226 "settings.account.accountType.premium": "Premium Supporter Account",
227 "settings.account.buttonSave": "更新帳戶資訊",
228 "settings.account.deleteAccount": "Delete account",
229 "settings.account.deleteEmailSent": "You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!",
230 "settings.account.deleteInfo": "If you don't need your Ferdi account any longer, you can delete your account and all related data here.",
231 "settings.account.headline": "帳戶",
232 "settings.account.headlineAccount": "帳戶資訊",
233 "settings.account.headlineDangerZone": "Danger Zone",
234 "settings.account.headlineInvoices": "Invoices",
235 "settings.account.headlinePassword": "更改密碼",
236 "settings.account.headlineProfile": "更新帳戶資訊",
237 "settings.account.headlineSubscription": "您的訂閱",
238 "settings.account.headlineTrialUpgrade": "Get the free 14 day Ferdi Professional Trial",
239 "settings.account.headlineUpgradeAccount": "Upgrade your account & get the full Ferdi experience",
240 "settings.account.invoiceDownload": "下載",
241 "settings.account.manageSubscription.label": "管理訂閱",
242 "settings.account.successInfo": "您的更改已經儲存",
243 "settings.account.trial": "Free Trial",
244 "settings.account.trialEndsIn": "Your free trial ends in {duration}.",
245 "settings.account.trialUpdateBillingInfo": "Please update your billing info to continue using {license} after your trial period.",
246 "settings.account.tryReloadServices": "Try again",
247 "settings.account.tryReloadUserInfoRequest": "Try again",
248 "settings.account.upgradeToPro.label": "Upgrade to Ferdi Professional",
249 "settings.account.userInfoRequestFailed": "無法載入帳戶資訊",
250 "settings.account.yourLicense": "Your Ferdi License",
251 "settings.app.accentColorInfo": "Write your accent color in a CSS-compatible format. (Default: #7367f0)",
252 "settings.app.buttonClearAllCache": "Clear cache",
253 "settings.app.buttonInstallUpdate": "重新啟動並且更新",
254 "settings.app.buttonSearchForUpdate": "Check for updates",
255 "settings.app.cacheInfo": "Ferdi cache is currently using {size} of disk space.",
256 "settings.app.currentVersion": "當前版本:",
257 "settings.app.form.accentColor": "Accent color",
258 "settings.app.form.autoLaunchInBackground": "背景啟動",
259 "settings.app.form.autoLaunchOnStart": "開機時啟動",
260 "settings.app.form.beta": "包含開發中版本",
261 "settings.app.form.darkMode": "Join the Dark Side",
262 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration",
263 "settings.app.form.enableLock": "Enable Ferdi password lock",
264 "settings.app.form.enableSpellchecking": "Enable spell checking",
265 "settings.app.form.enableSystemTray": "在系統匣上顯示",
266 "settings.app.form.enableTodos": "Enable Ferdi Todos",
267 "settings.app.form.hibernate": "Enable service hibernation",
268 "settings.app.form.hibernationStrategy": "Hibernation strategy",
269 "settings.app.form.keepAllWorkspacesLoaded": "Keep all workspaces loaded",
270 "settings.app.form.language": "語言",
271 "settings.app.form.lockPassword": "Ferdi Lock password",
272 "settings.app.form.minimizeToSystemTray": "最小化至系統匣",
273 "settings.app.form.noUpdates": "Disable updates",
274 "settings.app.form.privateNotifications": "Don't show message content in notifications",
275 "settings.app.form.runInBackground": "關閉時保持在背景運作",
276 "settings.app.form.scheduledDNDEnabled": "Enable scheduled Do-not-Disturb",
277 "settings.app.form.scheduledDNDEnd": "To",
278 "settings.app.form.scheduledDNDStart": "From",
279 "settings.app.form.server": "Server",
280 "settings.app.form.showDisabledServices": "Display disabled services tabs",
281 "settings.app.form.showMessagesBadgesWhenMuted": "Show unread message badge when notifications are disabled",
282 "settings.app.form.showServiceNavigationBar": "Always show service navigation bar",
283 "settings.app.form.todoServer": "Todo Server",
284 "settings.app.form.universalDarkMode": "Enable universal Dark Mode",
285 "settings.app.headline": "Settings",
286 "settings.app.headlineAdvanced": "Advanced",
287 "settings.app.headlineAppearance": "Appearance",
288 "settings.app.headlineGeneral": "一般",
289 "settings.app.headlineLanguage": "語言",
290 "settings.app.headlineUpdates": "更新",
291 "settings.app.hibernateInfo": "By default, Ferdi will keep all your services open and loaded in the background so they are ready when you want to use them. Service Hibernation will unload your services after a specified amount. This is useful to save RAM or keeping services from slowing down your computer.",
292 "settings.app.languageDisclaimer": "Official translations are English & German. All other languages are community based translations.",
293 "settings.app.lockInfo": "Ferdi password lock allows you to keep your messages protected.\nUsing Ferdi password lock, you will be prompted to enter your password everytime you start Ferdi or lock Ferdi yourself using the lock symbol in the bottom left corner or the shortcut CMD/CTRL+Shift+L.",
294 "settings.app.lockedPassword": "Ferdi Lock Password",
295 "settings.app.lockedPasswordInfo": "Please make sure to set a password you'll remember.\nIf you loose this password, you will have to reinstall Ferdi.",
296 "settings.app.restartRequired": "Changes require restart",
297 "settings.app.scheduledDNDInfo": "Scheduled Do-not-Disturb allows you to define a period of time in which you do not want to get Notifications from Ferdi.",
298 "settings.app.scheduledDNDTimeInfo": "Times in 24-Hour-Format. End time can be before start time (e.g. start 17:00, end 09:00) to enable Do-not-Disturb overnight.",
299 "settings.app.serverInfo": "We advice you to logout after changing your server as your settings might not be saved otherwise.",
300 "settings.app.serverMoneyInfo": "You are using the official Franz Server for Ferdi.\nWe know that Ferdi allows you to use all its features for free but you are still using Franz's server resources - which Franz's creator has to pay for.\nPlease still consider [Link 1]paying for a Franz account[/Link] or [Link 2]using a self-hosted ferdi-server[/Link] (if you have the knowledge and resources to do so). \nBy using Ferdi, you still profit greatly from Franz's recipe store, server resources and its development.",
301 "settings.app.subheadlineCache": "Cache",
302 "settings.app.todoServerInfo": "This server will be used for the \"Ferdi Todo\" feature. (default: https://app.franztodos.com)",
303 "settings.app.translationHelp": "Help us to translate Ferdi into your language.",
304 "settings.app.universalDarkModeInfo": "Universal Dark Mode tries to dynamically generate dark mode styles for services that are otherwise not currently supported.",
305 "settings.app.updateStatusAvailable": "有可用更新,下載中...",
306 "settings.app.updateStatusSearching": "檢查更新中...",
307 "settings.app.updateStatusUpToDate": "已經是最新版本了",
308 "settings.invite.headline": "Invite Friends",
309 "settings.navigation.account": "帳戶",
310 "settings.navigation.availableServices": "可用服務",
311 "settings.navigation.logout": "登出",
312 "settings.navigation.settings": "Settings",
313 "settings.navigation.supportFerdi": "Support Ferdi",
314 "settings.navigation.team": "Manage Team",
315 "settings.navigation.yourServices": "您的服務",
316 "settings.navigation.yourWorkspaces": "Your workspaces",
317 "settings.recipes.all": "All services",
318 "settings.recipes.custom": "Custom Services",
319 "settings.recipes.customService.headline.communityRecipes": "Community 3rd Party Recipes",
320 "settings.recipes.customService.headline.customRecipes": "Custom 3rd Party Recipes",
321 "settings.recipes.customService.headline.devRecipes": "Your Development Service Recipes",
322 "settings.recipes.customService.intro": "To add a custom service, copy the service recipe to:",
323 "settings.recipes.customService.openDevDocs": "Developer Documentation",
324 "settings.recipes.customService.openFolder": "Open folder",
325 "settings.recipes.headline": "可用服務",
326 "settings.recipes.missingService": "Missing a service?",
327 "settings.recipes.mostPopular": "熱門",
328 "settings.recipes.nothingFound": "抱歉,找不到您所要的服務",
329 "settings.recipes.servicesSuccessfulAddedInfo": "新增服務成功",
330 "settings.searchService": "Search service",
331 "settings.service.error.goBack": "返回",
332 "settings.service.error.headline": "Error",
333 "settings.service.error.message": "無法載入服務元件",
334 "settings.service.form.addServiceHeadline": "新增 {name}",
335 "settings.service.form.availableServices": "可用服務",
336 "settings.service.form.customUrl": "Custom server",
337 "settings.service.form.customUrlPremiumInfo": "To add self hosted services, you need a Ferdi Premium Supporter Account.",
338 "settings.service.form.customUrlUpgradeAccount": "升級帳戶",
339 "settings.service.form.customUrlValidationError": "Could not validate custom {name} server.",
340 "settings.service.form.deleteButton": "刪除",
341 "settings.service.form.editServiceHeadline": "Edit {name}",
342 "settings.service.form.enableAudio": "Enable audio",
343 "settings.service.form.enableBadge": "Show unread message badges",
344 "settings.service.form.enableDarkMode": "Enable Dark Mode",
345 "settings.service.form.enableNotification": "啟用通知",
346 "settings.service.form.enableService": "啟用服務",
347 "settings.service.form.headlineBadges": "Unread message badges",
348 "settings.service.form.headlineGeneral": "一般",
349 "settings.service.form.headlineNotifications": "Notifications",
350 "settings.service.form.icon": "Custom icon",
351 "settings.service.form.iconDelete": "Delete",
352 "settings.service.form.iconUpload": "Drop your image, or click here",
353 "settings.service.form.indirectMessageInfo": "除了 @username, @channel, @here 之外,當您參與的頻道有訊息時,就會通知",
354 "settings.service.form.indirectMessages": "針對全部訊息顯示通知",
355 "settings.service.form.isMutedInfo": "When disabled, all notification sounds and audio playback are muted",
356 "settings.service.form.name": "名子",
357 "settings.service.form.openDarkmodeCss": "Open darkmode.css",
358 "settings.service.form.proxy.headline": "HTTP/HTTPS Proxy Settings",
359 "settings.service.form.proxy.host": "Proxy Host/IP",
360 "settings.service.form.proxy.info": "Proxy settings will not synced with the Ferdi servers.",
361 "settings.service.form.proxy.isEnabled": "Use Proxy",
362 "settings.service.form.proxy.password": "Password (optional)",
363 "settings.service.form.proxy.port": "Port",
364 "settings.service.form.proxy.restartInfo": "Please restart Ferdi after changing proxy Settings.",
365 "settings.service.form.proxy.user": "User (optional)",
366 "settings.service.form.saveButton": "儲存",
367 "settings.service.form.tabHosted": "Hosted",
368 "settings.service.form.tabOnPremise": "Self hosted ⭐️",
369 "settings.service.form.team": "Team",
370 "settings.service.form.useHostedService": "Use the hosted {name} service.",
371 "settings.service.form.yourServices": "您的服務",
372 "settings.services.deletedInfo": "服務已刪除",
373 "settings.services.discoverServices": "服務列表",
374 "settings.services.headline": "您的服務",
375 "settings.services.noServicesAdded": "您還沒加入任何服務",
376 "settings.services.servicesRequestFailed": "Could not load your services",
377 "settings.services.tooltip.isDisabled": "已停用服務",
378 "settings.services.tooltip.isMuted": "All sounds are muted",
379 "settings.services.tooltip.notificationsDisabled": "已停用通知",
380 "settings.services.updatedInfo": "您的更改已經儲存",
381 "settings.supportFerdi.github": "Star on GitHub",
382 "settings.supportFerdi.headline": "Support Ferdi",
383 "settings.supportFerdi.openCollective": "Support our Open Collective",
384 "settings.supportFerdi.share": "Tell your Friends",
385 "settings.supportFerdi.title": "Do you like Ferdi? Spread the love!",
386 "settings.team.contentHeadline": "Ferdi for Teams",
387 "settings.team.copy": "Ferdi for Teams gives you the option to invite co-workers to your team by sending them email invitations and manage their subscriptions in your account’s preferences. Don’t waste time setting up subscriptions for every team member individually, forget about multiple invoices and different billing cycles - one team to rule them all!",
388 "settings.team.headline": "Team",
389 "settings.team.intro": "You and your team use Ferdi? You can now manage Premium subscriptions for as many colleagues, friends or family members as you want, all from within one account.",
390 "settings.team.manageAction": "Manage your Team on getferdi.com",
391 "settings.team.teamsUnavailable": "Teams are unavailable",
392 "settings.team.teamsUnavailableInfo": "Teams are currently only available when using the Franz Server and after paying for Franz Professional. Please change your server to https://api.franzinfra.com to use teams.",
393 "settings.team.upgradeAction": "Upgrade your Account",
394 "settings.user.form.accountType.company": "公司",
395 "settings.user.form.accountType.individual": "個人",
396 "settings.user.form.accountType.label": "帳戶類型",
397 "settings.user.form.accountType.non-profit": "非營利",
398 "settings.user.form.currentPassword": "舊密碼",
399 "settings.user.form.email": "電子郵件信箱",
400 "settings.user.form.firstname": "名子",
401 "settings.user.form.lastname": "姓氏",
402 "settings.user.form.newPassword": "新密碼",
403 "settings.workspace.add.form.name": "名子",
404 "settings.workspace.add.form.submitButton": "Create workspace",
405 "settings.workspace.form.buttonDelete": "Delete workspace",
406 "settings.workspace.form.buttonSave": "Save workspace",
407 "settings.workspace.form.keepLoaded": "Keep this workspace loaded*",
408 "settings.workspace.form.keepLoadedInfo": "*This option will be overwritten by the global \"Keep all workspaces loaded\" option.",
409 "settings.workspace.form.name": "名子",
410 "settings.workspace.form.servicesInWorkspaceHeadline": "Services in this Workspace",
411 "settings.workspace.form.yourWorkspaces": "Your workspaces",
412 "settings.workspaces.deletedInfo": "Workspace has been deleted",
413 "settings.workspaces.headline": "Your workspaces",
414 "settings.workspaces.noWorkspacesAdded": "You haven't added any workspaces yet.",
415 "settings.workspaces.tryReloadWorkspaces": "Try again",
416 "settings.workspaces.updatedInfo": "您的更改已經儲存",
417 "settings.workspaces.workspaceFeatureHeadline": "Less is More: Introducing Ferdi Workspaces",
418 "settings.workspaces.workspaceFeatureInfo": "Ferdi Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time. You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.",
419 "settings.workspaces.workspacesRequestFailed": "Could not load your workspaces",
420 "sidebar.addNewService": "Add new service",
421 "sidebar.closeTodosDrawer": "Close Ferdi Todos",
422 "sidebar.closeWorkspaceDrawer": "Close workspace drawer",
423 "sidebar.lockFerdi": "Lock Ferdi",
424 "sidebar.muteApp": "Disable notifications & audio",
425 "sidebar.openTodosDrawer": "Open Ferdi Todos",
426 "sidebar.openWorkspaceDrawer": "Open workspace drawer",
427 "sidebar.settings": "Settings",
428 "sidebar.unmuteApp": "Enable notifications & audio",
429 "signup.email.label": "電子郵件信箱",
430 "signup.emailDuplicate": "此電子郵件信箱已被註冊",
431 "signup.firstname.label": "名子",
432 "signup.headline": "註冊",
433 "signup.lastname.label": "姓氏",
434 "signup.legal.info": "在建立帳戶同時,您同意:",
435 "signup.legal.privacy": "Privacy Statement",
436 "signup.legal.terms": "服務條款",
437 "signup.link.login": "您已有一個帳戶,請問是否要登入?",
438 "signup.password.label": "Password",
439 "signup.submit.label": "建立帳戶",
440 "subscription.bestValue": "Best value",
441 "subscription.cta.activateTrial": "Yes, start the free Ferdi Professional trial",
442 "subscription.cta.allOptions": "See all options",
443 "subscription.cta.choosePlan": "Choose your plan",
444 "subscription.includedProFeatures": "The Ferdi Professional Plan includes:",
445 "subscription.interval.per": "per {interval}",
446 "subscription.interval.perMonth": "per month",
447 "subscription.interval.perMonthPerUser": "per month & user",
448 "subscription.planItem.upgradeAccount": "Upgrade Account",
449 "subscription.teaser.includedFeatures": "Paid Ferdi Plans include:",
450 "subscription.teaser.intro": "Ferdi 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!",
451 "subscriptionPopup.buttonCancel": "取消",
452 "subscriptionPopup.buttonDone": "完成",
453 "tabs.item.deleteService": "刪除",
454 "tabs.item.disableAudio": "Disable audio",
455 "tabs.item.disableNotifications": "停用通知",
456 "tabs.item.disableService": "停用服務",
457 "tabs.item.edit": "Edit",
458 "tabs.item.enableAudio": "Enable audio",
459 "tabs.item.enableNotification": "啟用通知",
460 "tabs.item.enableService": "啟用服務",
461 "tabs.item.reload": "Reload",
462 "validation.email": "{field} is not valid",
463 "validation.minLength": "{field} should be at least {length} characters long",
464 "validation.oneRequired": "At least one is required",
465 "validation.required": "{field} is required",
466 "validation.url": "{field} is not a valid URL",
467 "webControls.back": "Back",
468 "webControls.forward": "Forward",
469 "webControls.goHome": "Home",
470 "webControls.openInBrowser": "Open in Browser",
471 "webControls.reload": "Reload",
472 "welcome.loginButton": "登入",
473 "welcome.signupButton": "建立一個免費帳戶",
474 "workspaceDrawer.addNewWorkspaceLabel": "Add new workspace",
475 "workspaceDrawer.allServices": "All services",
476 "workspaceDrawer.headline": "Workspaces",
477 "workspaceDrawer.item.contextMenuEdit": "edit",
478 "workspaceDrawer.item.noServicesAddedYet": "No services added yet",
479 "workspaceDrawer.premiumCtaButtonLabel": "Create your first workspace",
480 "workspaceDrawer.proFeatureBadge": "Premium feature",
481 "workspaceDrawer.reactivatePremiumAccountLabel": "Reactivate premium account",
482 "workspaceDrawer.workspaceFeatureInfo": "<p>Ferdi Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time.</p><p>You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.</p>",
483 "workspaceDrawer.workspacesSettingsTooltip": "Edit workspaces settings",
484 "workspaces.switchingIndicator.switchingTo": "Switching to"
485}
diff --git a/src/i18n/messages/src/components/auth/Pricing.json b/src/i18n/messages/src/components/auth/Pricing.json
index 0885f6a20..c4c94bb32 100644
--- a/src/i18n/messages/src/components/auth/Pricing.json
+++ b/src/i18n/messages/src/components/auth/Pricing.json
@@ -1,7 +1,7 @@
1[ 1[
2 { 2 {
3 "id": "pricing.trial.headline", 3 "id": "pricing.trial.headline.pro",
4 "defaultMessage": "!!!Franz Professional", 4 "defaultMessage": "!!!Hi {name}, welcome to Franz",
5 "file": "src/components/auth/Pricing.js", 5 "file": "src/components/auth/Pricing.js",
6 "start": { 6 "start": {
7 "line": 15, 7 "line": 15,
@@ -13,12 +13,12 @@
13 } 13 }
14 }, 14 },
15 { 15 {
16 "id": "pricing.trial.subheadline", 16 "id": "pricing.trial.intro.specialTreat",
17 "defaultMessage": "!!!Your personal welcome offer:", 17 "defaultMessage": "!!!We have a special treat for you.",
18 "file": "src/components/auth/Pricing.js", 18 "file": "src/components/auth/Pricing.js",
19 "start": { 19 "start": {
20 "line": 19, 20 "line": 19,
21 "column": 17 21 "column": 16
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 22, 24 "line": 22,
@@ -26,15 +26,41 @@
26 } 26 }
27 }, 27 },
28 { 28 {
29 "id": "pricing.trial.intro.tryPro",
30 "defaultMessage": "!!!Enjoy the full Franz Professional experience completely free for 14 days.",
31 "file": "src/components/auth/Pricing.js",
32 "start": {
33 "line": 23,
34 "column": 10
35 },
36 "end": {
37 "line": 26,
38 "column": 3
39 }
40 },
41 {
42 "id": "pricing.trial.intro.happyMessaging",
43 "defaultMessage": "!!!Happy messaging,",
44 "file": "src/components/auth/Pricing.js",
45 "start": {
46 "line": 27,
47 "column": 18
48 },
49 "end": {
50 "line": 30,
51 "column": 3
52 }
53 },
54 {
29 "id": "pricing.trial.terms.headline", 55 "id": "pricing.trial.terms.headline",
30 "defaultMessage": "!!!No strings attached", 56 "defaultMessage": "!!!No strings attached",
31 "file": "src/components/auth/Pricing.js", 57 "file": "src/components/auth/Pricing.js",
32 "start": { 58 "start": {
33 "line": 23, 59 "line": 31,
34 "column": 29 60 "column": 29
35 }, 61 },
36 "end": { 62 "end": {
37 "line": 26, 63 "line": 34,
38 "column": 3 64 "column": 3
39 } 65 }
40 }, 66 },
@@ -43,11 +69,11 @@
43 "defaultMessage": "!!!No credit card required", 69 "defaultMessage": "!!!No credit card required",
44 "file": "src/components/auth/Pricing.js", 70 "file": "src/components/auth/Pricing.js",
45 "start": { 71 "start": {
46 "line": 27, 72 "line": 35,
47 "column": 16 73 "column": 16
48 }, 74 },
49 "end": { 75 "end": {
50 "line": 30, 76 "line": 38,
51 "column": 3 77 "column": 3
52 } 78 }
53 }, 79 },
@@ -56,11 +82,24 @@
56 "defaultMessage": "!!!Your free trial ends automatically after 14 days", 82 "defaultMessage": "!!!Your free trial ends automatically after 14 days",
57 "file": "src/components/auth/Pricing.js", 83 "file": "src/components/auth/Pricing.js",
58 "start": { 84 "start": {
59 "line": 31, 85 "line": 39,
60 "column": 21 86 "column": 21
61 }, 87 },
62 "end": { 88 "end": {
63 "line": 34, 89 "line": 42,
90 "column": 3
91 }
92 },
93 {
94 "id": "pricing.trial.terms.trialWorth",
95 "defaultMessage": "!!!Free trial (normally {currency}{price} per month)",
96 "file": "src/components/auth/Pricing.js",
97 "start": {
98 "line": 43,
99 "column": 14
100 },
101 "end": {
102 "line": 46,
64 "column": 3 103 "column": 3
65 } 104 }
66 }, 105 },
@@ -69,24 +108,37 @@
69 "defaultMessage": "!!!Sorry, we could not activate your trial!", 108 "defaultMessage": "!!!Sorry, we could not activate your trial!",
70 "file": "src/components/auth/Pricing.js", 109 "file": "src/components/auth/Pricing.js",
71 "start": { 110 "start": {
72 "line": 35, 111 "line": 47,
73 "column": 19 112 "column": 19
74 }, 113 },
75 "end": { 114 "end": {
76 "line": 38, 115 "line": 50,
77 "column": 3 116 "column": 3
78 } 117 }
79 }, 118 },
80 { 119 {
81 "id": "pricing.trial.cta.accept", 120 "id": "pricing.trial.cta.accept",
82 "defaultMessage": "!!!Yes, upgrade my account to Franz Professional", 121 "defaultMessage": "!!!Start my 14-day Franz Professional Trial",
83 "file": "src/components/auth/Pricing.js", 122 "file": "src/components/auth/Pricing.js",
84 "start": { 123 "start": {
85 "line": 39, 124 "line": 51,
86 "column": 13 125 "column": 13
87 }, 126 },
88 "end": { 127 "end": {
89 "line": 42, 128 "line": 54,
129 "column": 3
130 }
131 },
132 {
133 "id": "pricing.trial.cta.start",
134 "defaultMessage": "!!!Start using Franz",
135 "file": "src/components/auth/Pricing.js",
136 "start": {
137 "line": 55,
138 "column": 12
139 },
140 "end": {
141 "line": 58,
90 "column": 3 142 "column": 3
91 } 143 }
92 }, 144 },
@@ -95,11 +147,11 @@
95 "defaultMessage": "!!!Continue to Ferdi", 147 "defaultMessage": "!!!Continue to Ferdi",
96 "file": "src/components/auth/Pricing.js", 148 "file": "src/components/auth/Pricing.js",
97 "start": { 149 "start": {
98 "line": 43, 150 "line": 59,
99 "column": 11 151 "column": 11
100 }, 152 },
101 "end": { 153 "end": {
102 "line": 46, 154 "line": 62,
103 "column": 3 155 "column": 3
104 } 156 }
105 }, 157 },
@@ -108,11 +160,11 @@
108 "defaultMessage": "!!!Franz Professional includes:", 160 "defaultMessage": "!!!Franz Professional includes:",
109 "file": "src/components/auth/Pricing.js", 161 "file": "src/components/auth/Pricing.js",
110 "start": { 162 "start": {
111 "line": 47, 163 "line": 63,
112 "column": 20 164 "column": 20
113 }, 165 },
114 "end": { 166 "end": {
115 "line": 50, 167 "line": 66,
116 "column": 3 168 "column": 3
117 } 169 }
118 } 170 }
diff --git a/src/i18n/messages/src/components/layout/AppLayout.json b/src/i18n/messages/src/components/layout/AppLayout.json
index 0625487b4..bca181d0f 100644
--- a/src/i18n/messages/src/components/layout/AppLayout.json
+++ b/src/i18n/messages/src/components/layout/AppLayout.json
@@ -4,11 +4,11 @@
4 "defaultMessage": "!!!Your services have been updated.", 4 "defaultMessage": "!!!Your services have been updated.",
5 "file": "src/components/layout/AppLayout.js", 5 "file": "src/components/layout/AppLayout.js",
6 "start": { 6 "start": {
7 "line": 28, 7 "line": 30,
8 "column": 19 8 "column": 19
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 31, 11 "line": 33,
12 "column": 3 12 "column": 3
13 } 13 }
14 }, 14 },
@@ -17,11 +17,11 @@
17 "defaultMessage": "!!!Reload services", 17 "defaultMessage": "!!!Reload services",
18 "file": "src/components/layout/AppLayout.js", 18 "file": "src/components/layout/AppLayout.js",
19 "start": { 19 "start": {
20 "line": 32, 20 "line": 34,
21 "column": 24 21 "column": 24
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 35, 24 "line": 37,
25 "column": 3 25 "column": 3
26 } 26 }
27 }, 27 },
@@ -30,11 +30,11 @@
30 "defaultMessage": "!!!Could not load services and user information", 30 "defaultMessage": "!!!Could not load services and user information",
31 "file": "src/components/layout/AppLayout.js", 31 "file": "src/components/layout/AppLayout.js",
32 "start": { 32 "start": {
33 "line": 36, 33 "line": 38,
34 "column": 26 34 "column": 26
35 }, 35 },
36 "end": { 36 "end": {
37 "line": 39, 37 "line": 41,
38 "column": 3 38 "column": 3
39 } 39 }
40 }, 40 },
@@ -43,11 +43,11 @@
43 "defaultMessage": "!!!There were errors while trying to perform an authenticated request. Please try logging out and back in if this error persists.", 43 "defaultMessage": "!!!There were errors while trying to perform an authenticated request. Please try logging out and back in if this error persists.",
44 "file": "src/components/layout/AppLayout.js", 44 "file": "src/components/layout/AppLayout.js",
45 "start": { 45 "start": {
46 "line": 40, 46 "line": 42,
47 "column": 21 47 "column": 21
48 }, 48 },
49 "end": { 49 "end": {
50 "line": 43, 50 "line": 45,
51 "column": 3 51 "column": 3
52 } 52 }
53 } 53 }
diff --git a/src/i18n/messages/src/components/ui/FeatureList.json b/src/i18n/messages/src/components/ui/FeatureList.json
index 497e299a4..3201115b3 100644
--- a/src/i18n/messages/src/components/ui/FeatureList.json
+++ b/src/i18n/messages/src/components/ui/FeatureList.json
@@ -1,14 +1,79 @@
1[ 1[
2 { 2 {
3 "id": "pricing.features.recipes",
4 "defaultMessage": "!!!Choose from more than 70 Services",
5 "file": "src/components/ui/FeatureList.js",
6 "start": {
7 "line": 9,
8 "column": 20
9 },
10 "end": {
11 "line": 12,
12 "column": 3
13 }
14 },
15 {
16 "id": "pricing.features.accountSync",
17 "defaultMessage": "!!!Account Synchronisation",
18 "file": "src/components/ui/FeatureList.js",
19 "start": {
20 "line": 13,
21 "column": 15
22 },
23 "end": {
24 "line": 16,
25 "column": 3
26 }
27 },
28 {
29 "id": "pricing.features.desktopNotifications",
30 "defaultMessage": "!!!Desktop Notifications",
31 "file": "src/components/ui/FeatureList.js",
32 "start": {
33 "line": 17,
34 "column": 24
35 },
36 "end": {
37 "line": 20,
38 "column": 3
39 }
40 },
41 {
3 "id": "pricing.features.unlimitedServices", 42 "id": "pricing.features.unlimitedServices",
4 "defaultMessage": "!!!Add unlimited services", 43 "defaultMessage": "!!!Add unlimited services",
5 "file": "src/components/ui/FeatureList.js", 44 "file": "src/components/ui/FeatureList.js",
6 "start": { 45 "start": {
7 "line": 8, 46 "line": 21,
8 "column": 21 47 "column": 21
9 }, 48 },
10 "end": { 49 "end": {
11 "line": 11, 50 "line": 24,
51 "column": 3
52 }
53 },
54 {
55 "id": "pricing.features.upToThreeServices",
56 "defaultMessage": "!!!Add up to 3 services",
57 "file": "src/components/ui/FeatureList.js",
58 "start": {
59 "line": 25,
60 "column": 21
61 },
62 "end": {
63 "line": 28,
64 "column": 3
65 }
66 },
67 {
68 "id": "pricing.features.upToSixServices",
69 "defaultMessage": "!!!Add up to 6 services",
70 "file": "src/components/ui/FeatureList.js",
71 "start": {
72 "line": 29,
73 "column": 19
74 },
75 "end": {
76 "line": 32,
12 "column": 3 77 "column": 3
13 } 78 }
14 }, 79 },
@@ -17,11 +82,11 @@
17 "defaultMessage": "!!!Spellchecker support", 82 "defaultMessage": "!!!Spellchecker support",
18 "file": "src/components/ui/FeatureList.js", 83 "file": "src/components/ui/FeatureList.js",
19 "start": { 84 "start": {
20 "line": 12, 85 "line": 33,
21 "column": 16 86 "column": 16
22 }, 87 },
23 "end": { 88 "end": {
24 "line": 15, 89 "line": 36,
25 "column": 3 90 "column": 3
26 } 91 }
27 }, 92 },
@@ -30,11 +95,11 @@
30 "defaultMessage": "!!!Workspaces", 95 "defaultMessage": "!!!Workspaces",
31 "file": "src/components/ui/FeatureList.js", 96 "file": "src/components/ui/FeatureList.js",
32 "start": { 97 "start": {
33 "line": 16, 98 "line": 37,
34 "column": 14 99 "column": 14
35 }, 100 },
36 "end": { 101 "end": {
37 "line": 19, 102 "line": 40,
38 "column": 3 103 "column": 3
39 } 104 }
40 }, 105 },
@@ -43,11 +108,11 @@
43 "defaultMessage": "!!!Add Custom Websites", 108 "defaultMessage": "!!!Add Custom Websites",
44 "file": "src/components/ui/FeatureList.js", 109 "file": "src/components/ui/FeatureList.js",
45 "start": { 110 "start": {
46 "line": 20, 111 "line": 41,
47 "column": 18 112 "column": 18
48 }, 113 },
49 "end": { 114 "end": {
50 "line": 23, 115 "line": 44,
51 "column": 3 116 "column": 3
52 } 117 }
53 }, 118 },
@@ -56,11 +121,11 @@
56 "defaultMessage": "!!!On-premise & other Hosted Services", 121 "defaultMessage": "!!!On-premise & other Hosted Services",
57 "file": "src/components/ui/FeatureList.js", 122 "file": "src/components/ui/FeatureList.js",
58 "start": { 123 "start": {
59 "line": 24, 124 "line": 45,
60 "column": 13 125 "column": 13
61 }, 126 },
62 "end": { 127 "end": {
63 "line": 27, 128 "line": 48,
64 "column": 3 129 "column": 3
65 } 130 }
66 }, 131 },
@@ -69,11 +134,11 @@
69 "defaultMessage": "!!!Install 3rd party services", 134 "defaultMessage": "!!!Install 3rd party services",
70 "file": "src/components/ui/FeatureList.js", 135 "file": "src/components/ui/FeatureList.js",
71 "start": { 136 "start": {
72 "line": 28, 137 "line": 49,
73 "column": 22 138 "column": 22
74 }, 139 },
75 "end": { 140 "end": {
76 "line": 31, 141 "line": 52,
77 "column": 3 142 "column": 3
78 } 143 }
79 }, 144 },
@@ -82,11 +147,11 @@
82 "defaultMessage": "!!!Service Proxies", 147 "defaultMessage": "!!!Service Proxies",
83 "file": "src/components/ui/FeatureList.js", 148 "file": "src/components/ui/FeatureList.js",
84 "start": { 149 "start": {
85 "line": 32, 150 "line": 53,
86 "column": 18 151 "column": 18
87 }, 152 },
88 "end": { 153 "end": {
89 "line": 35, 154 "line": 56,
90 "column": 3 155 "column": 3
91 } 156 }
92 }, 157 },
@@ -95,11 +160,11 @@
95 "defaultMessage": "!!!Team Management", 160 "defaultMessage": "!!!Team Management",
96 "file": "src/components/ui/FeatureList.js", 161 "file": "src/components/ui/FeatureList.js",
97 "start": { 162 "start": {
98 "line": 36, 163 "line": 57,
99 "column": 18 164 "column": 18
100 }, 165 },
101 "end": { 166 "end": {
102 "line": 39, 167 "line": 60,
103 "column": 3 168 "column": 3
104 } 169 }
105 }, 170 },
@@ -108,11 +173,11 @@
108 "defaultMessage": "!!!No Waiting Screens", 173 "defaultMessage": "!!!No Waiting Screens",
109 "file": "src/components/ui/FeatureList.js", 174 "file": "src/components/ui/FeatureList.js",
110 "start": { 175 "start": {
111 "line": 40, 176 "line": 61,
112 "column": 13 177 "column": 13
113 }, 178 },
114 "end": { 179 "end": {
115 "line": 43, 180 "line": 64,
116 "column": 3 181 "column": 3
117 } 182 }
118 }, 183 },
@@ -121,11 +186,11 @@
121 "defaultMessage": "!!!Forever ad-free", 186 "defaultMessage": "!!!Forever ad-free",
122 "file": "src/components/ui/FeatureList.js", 187 "file": "src/components/ui/FeatureList.js",
123 "start": { 188 "start": {
124 "line": 44, 189 "line": 65,
125 "column": 10 190 "column": 10
126 }, 191 },
127 "end": { 192 "end": {
128 "line": 47, 193 "line": 68,
129 "column": 3 194 "column": 3
130 } 195 }
131 } 196 }
diff --git a/src/i18n/messages/src/features/delayApp/Component.json b/src/i18n/messages/src/features/delayApp/Component.json
index 77fabf236..f1d6886f5 100644
--- a/src/i18n/messages/src/features/delayApp/Component.json
+++ b/src/i18n/messages/src/features/delayApp/Component.json
@@ -27,7 +27,7 @@
27 }, 27 },
28 { 28 {
29 "id": "feature.delayApp.upgrade.action", 29 "id": "feature.delayApp.upgrade.action",
30 "defaultMessage": "!!!Get a Franz Supporter License", 30 "defaultMessage": "!!!Upgrade Franz",
31 "file": "src/features/delayApp/Component.js", 31 "file": "src/features/delayApp/Component.js",
32 "start": { 32 "start": {
33 "line": 22, 33 "line": 22,
diff --git a/src/i18n/messages/src/features/planSelection/components/PlanItem.json b/src/i18n/messages/src/features/planSelection/components/PlanItem.json
new file mode 100644
index 000000000..5a94f32ee
--- /dev/null
+++ b/src/i18n/messages/src/features/planSelection/components/PlanItem.json
@@ -0,0 +1,41 @@
1[
2 {
3 "id": "subscription.interval.perMonth",
4 "defaultMessage": "!!!per month",
5 "file": "src/features/planSelection/components/PlanItem.js",
6 "start": {
7 "line": 15,
8 "column": 12
9 },
10 "end": {
11 "line": 18,
12 "column": 3
13 }
14 },
15 {
16 "id": "subscription.interval.perMonthPerUser",
17 "defaultMessage": "!!!per month & user",
18 "file": "src/features/planSelection/components/PlanItem.js",
19 "start": {
20 "line": 19,
21 "column": 19
22 },
23 "end": {
24 "line": 22,
25 "column": 3
26 }
27 },
28 {
29 "id": "subscription.bestValue",
30 "defaultMessage": "!!!Best value",
31 "file": "src/features/planSelection/components/PlanItem.js",
32 "start": {
33 "line": 23,
34 "column": 13
35 },
36 "end": {
37 "line": 26,
38 "column": 3
39 }
40 }
41] \ No newline at end of file
diff --git a/src/i18n/messages/src/features/planSelection/components/PlanSelection.json b/src/i18n/messages/src/features/planSelection/components/PlanSelection.json
new file mode 100644
index 000000000..ed354146e
--- /dev/null
+++ b/src/i18n/messages/src/features/planSelection/components/PlanSelection.json
@@ -0,0 +1,158 @@
1[
2 {
3 "id": "feature.planSelection.fullscreen.welcome",
4 "defaultMessage": "!!!Are you ready to choose, {name}",
5 "file": "src/features/planSelection/components/PlanSelection.js",
6 "start": {
7 "line": 17,
8 "column": 11
9 },
10 "end": {
11 "line": 20,
12 "column": 3
13 }
14 },
15 {
16 "id": "feature.planSelection.fullscreen.subheadline",
17 "defaultMessage": "!!!It's time to make a choice. Franz works best on our Personal and Professional plans. Please have a look and choose the best one for you.",
18 "file": "src/features/planSelection/components/PlanSelection.js",
19 "start": {
20 "line": 21,
21 "column": 15
22 },
23 "end": {
24 "line": 24,
25 "column": 3
26 }
27 },
28 {
29 "id": "feature.planSelection.free.text",
30 "defaultMessage": "!!!Basic functionality",
31 "file": "src/features/planSelection/components/PlanSelection.js",
32 "start": {
33 "line": 25,
34 "column": 12
35 },
36 "end": {
37 "line": 28,
38 "column": 3
39 }
40 },
41 {
42 "id": "feature.planSelection.personal.text",
43 "defaultMessage": "!!!More services, no waiting - ideal for personal use.",
44 "file": "src/features/planSelection/components/PlanSelection.js",
45 "start": {
46 "line": 29,
47 "column": 16
48 },
49 "end": {
50 "line": 32,
51 "column": 3
52 }
53 },
54 {
55 "id": "feature.planSelection.pro.text",
56 "defaultMessage": "!!!Unlimited services and professional features for you - and your team.",
57 "file": "src/features/planSelection/components/PlanSelection.js",
58 "start": {
59 "line": 33,
60 "column": 20
61 },
62 "end": {
63 "line": 36,
64 "column": 3
65 }
66 },
67 {
68 "id": "feature.planSelection.cta.stayOnFree",
69 "defaultMessage": "!!!Stay on Free",
70 "file": "src/features/planSelection/components/PlanSelection.js",
71 "start": {
72 "line": 37,
73 "column": 17
74 },
75 "end": {
76 "line": 40,
77 "column": 3
78 }
79 },
80 {
81 "id": "feature.planSelection.cta.ctaDowngradeFree",
82 "defaultMessage": "!!!Downgrade to Free",
83 "file": "src/features/planSelection/components/PlanSelection.js",
84 "start": {
85 "line": 41,
86 "column": 20
87 },
88 "end": {
89 "line": 44,
90 "column": 3
91 }
92 },
93 {
94 "id": "feature.planSelection.cta.trial",
95 "defaultMessage": "!!!Start my free 14-days Trial",
96 "file": "src/features/planSelection/components/PlanSelection.js",
97 "start": {
98 "line": 45,
99 "column": 15
100 },
101 "end": {
102 "line": 48,
103 "column": 3
104 }
105 },
106 {
107 "id": "feature.planSelection.cta.upgradePersonal",
108 "defaultMessage": "!!!Choose Personal",
109 "file": "src/features/planSelection/components/PlanSelection.js",
110 "start": {
111 "line": 49,
112 "column": 23
113 },
114 "end": {
115 "line": 52,
116 "column": 3
117 }
118 },
119 {
120 "id": "feature.planSelection.cta.upgradePro",
121 "defaultMessage": "!!!Choose Professional",
122 "file": "src/features/planSelection/components/PlanSelection.js",
123 "start": {
124 "line": 53,
125 "column": 18
126 },
127 "end": {
128 "line": 56,
129 "column": 3
130 }
131 },
132 {
133 "id": "feature.planSelection.fullFeatureList",
134 "defaultMessage": "!!!Complete comparison of all plans",
135 "file": "src/features/planSelection/components/PlanSelection.js",
136 "start": {
137 "line": 57,
138 "column": 19
139 },
140 "end": {
141 "line": 60,
142 "column": 3
143 }
144 },
145 {
146 "id": "feature.planSelection.pricesBasedOnAnnualPayment",
147 "defaultMessage": "!!!All prices based on yearly payment",
148 "file": "src/features/planSelection/components/PlanSelection.js",
149 "start": {
150 "line": 61,
151 "column": 30
152 },
153 "end": {
154 "line": 64,
155 "column": 3
156 }
157 }
158] \ No newline at end of file
diff --git a/src/i18n/messages/src/features/planSelection/components/PlanTeaser.json b/src/i18n/messages/src/features/planSelection/components/PlanTeaser.json
new file mode 100644
index 000000000..015304a2e
--- /dev/null
+++ b/src/i18n/messages/src/features/planSelection/components/PlanTeaser.json
@@ -0,0 +1,28 @@
1[
2 {
3 "id": "subscription.interval.per",
4 "defaultMessage": "!!!per {interval}",
5 "file": "src/features/planSelection/components/PlanTeaser.js",
6 "start": {
7 "line": 16,
8 "column": 7
9 },
10 "end": {
11 "line": 19,
12 "column": 3
13 }
14 },
15 {
16 "id": "subscription.planItem.upgradeAccount",
17 "defaultMessage": "!!!Upgrade Account",
18 "file": "src/features/planSelection/components/PlanTeaser.js",
19 "start": {
20 "line": 20,
21 "column": 7
22 },
23 "end": {
24 "line": 23,
25 "column": 3
26 }
27 }
28] \ No newline at end of file
diff --git a/src/i18n/messages/src/features/planSelection/containers/PlanSelectionScreen.json b/src/i18n/messages/src/features/planSelection/containers/PlanSelectionScreen.json
new file mode 100644
index 000000000..04b2144b4
--- /dev/null
+++ b/src/i18n/messages/src/features/planSelection/containers/PlanSelectionScreen.json
@@ -0,0 +1,54 @@
1[
2 {
3 "id": "feature.planSelection.fullscreen.dialog.title",
4 "defaultMessage": "!!!Downgrade your Franz Plan",
5 "file": "src/features/planSelection/containers/PlanSelectionScreen.js",
6 "start": {
7 "line": 16,
8 "column": 15
9 },
10 "end": {
11 "line": 19,
12 "column": 3
13 }
14 },
15 {
16 "id": "feature.planSelection.fullscreen.dialog.message",
17 "defaultMessage": "!!!You're about to downgrade to our Free account. Are you sure? Click here instead to get more services and functionality for just {currency}{price} a month.",
18 "file": "src/features/planSelection/containers/PlanSelectionScreen.js",
19 "start": {
20 "line": 20,
21 "column": 17
22 },
23 "end": {
24 "line": 23,
25 "column": 3
26 }
27 },
28 {
29 "id": "feature.planSelection.fullscreen.dialog.cta.downgrade",
30 "defaultMessage": "!!!Downgrade to Free",
31 "file": "src/features/planSelection/containers/PlanSelectionScreen.js",
32 "start": {
33 "line": 24,
34 "column": 22
35 },
36 "end": {
37 "line": 27,
38 "column": 3
39 }
40 },
41 {
42 "id": "feature.planSelection.fullscreen.dialog.cta.upgrade",
43 "defaultMessage": "!!!Choose Personal",
44 "file": "src/features/planSelection/containers/PlanSelectionScreen.js",
45 "start": {
46 "line": 28,
47 "column": 20
48 },
49 "end": {
50 "line": 31,
51 "column": 3
52 }
53 }
54] \ No newline at end of file
diff --git a/src/i18n/messages/src/features/trialStatusBar/components/TrialStatusBar.json b/src/i18n/messages/src/features/trialStatusBar/components/TrialStatusBar.json
new file mode 100644
index 000000000..bf211a016
--- /dev/null
+++ b/src/i18n/messages/src/features/trialStatusBar/components/TrialStatusBar.json
@@ -0,0 +1,41 @@
1[
2 {
3 "id": "feature.trialStatusBar.restTime",
4 "defaultMessage": "!!!Your Free Franz {plan} Trial ends in {time}.",
5 "file": "src/features/trialStatusBar/components/TrialStatusBar.js",
6 "start": {
7 "line": 13,
8 "column": 12
9 },
10 "end": {
11 "line": 16,
12 "column": 3
13 }
14 },
15 {
16 "id": "feature.trialStatusBar.expired",
17 "defaultMessage": "!!!Your free Franz {plan} Trial has expired, please upgrade your account.",
18 "file": "src/features/trialStatusBar/components/TrialStatusBar.js",
19 "start": {
20 "line": 17,
21 "column": 11
22 },
23 "end": {
24 "line": 20,
25 "column": 3
26 }
27 },
28 {
29 "id": "feature.trialStatusBar.cta",
30 "defaultMessage": "!!!Upgrade now",
31 "file": "src/features/trialStatusBar/components/TrialStatusBar.js",
32 "start": {
33 "line": 21,
34 "column": 7
35 },
36 "end": {
37 "line": 24,
38 "column": 3
39 }
40 }
41] \ No newline at end of file
diff --git a/src/i18n/messages/src/features/trialStatusBar/containers/TrialStatusBarScreen.json b/src/i18n/messages/src/features/trialStatusBar/containers/TrialStatusBarScreen.json
new file mode 100644
index 000000000..306cd0fee
--- /dev/null
+++ b/src/i18n/messages/src/features/trialStatusBar/containers/TrialStatusBarScreen.json
@@ -0,0 +1,54 @@
1[
2 {
3 "id": "feature.trialStatusBar.fullscreen.dialog.title",
4 "defaultMessage": "!!!Downgrade your Franz Plan",
5 "file": "src/features/trialStatusBar/containers/TrialStatusBarScreen.js",
6 "start": {
7 "line": 16,
8 "column": 15
9 },
10 "end": {
11 "line": 19,
12 "column": 3
13 }
14 },
15 {
16 "id": "feature.trialStatusBar.fullscreen.dialog.message",
17 "defaultMessage": "!!!You're about to downgrade to our Free account. Are you sure? Click here instead to get more services and functionality for just {currency}{price} a month.",
18 "file": "src/features/trialStatusBar/containers/TrialStatusBarScreen.js",
19 "start": {
20 "line": 20,
21 "column": 17
22 },
23 "end": {
24 "line": 23,
25 "column": 3
26 }
27 },
28 {
29 "id": "feature.trialStatusBar.fullscreen.dialog.cta.downgrade",
30 "defaultMessage": "!!!Downgrade to Free",
31 "file": "src/features/trialStatusBar/containers/TrialStatusBarScreen.js",
32 "start": {
33 "line": 24,
34 "column": 22
35 },
36 "end": {
37 "line": 27,
38 "column": 3
39 }
40 },
41 {
42 "id": "feature.trialStatusBar.fullscreen.dialog.cta.upgrade",
43 "defaultMessage": "!!!Choose Personal",
44 "file": "src/features/trialStatusBar/containers/TrialStatusBarScreen.js",
45 "start": {
46 "line": 28,
47 "column": 20
48 },
49 "end": {
50 "line": 31,
51 "column": 3
52 }
53 }
54] \ No newline at end of file
diff --git a/src/i18n/messages/src/features/workspaces/components/WorkspacesDashboard.json b/src/i18n/messages/src/features/workspaces/components/WorkspacesDashboard.json
index 4111ba760..87b8942ce 100644
--- a/src/i18n/messages/src/features/workspaces/components/WorkspacesDashboard.json
+++ b/src/i18n/messages/src/features/workspaces/components/WorkspacesDashboard.json
@@ -4,11 +4,11 @@
4 "defaultMessage": "!!!Your workspaces", 4 "defaultMessage": "!!!Your workspaces",
5 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 5 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
6 "start": { 6 "start": {
7 "line": 19, 7 "line": 20,
8 "column": 12 8 "column": 12
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 22, 11 "line": 23,
12 "column": 3 12 "column": 3
13 } 13 }
14 }, 14 },
@@ -17,11 +17,11 @@
17 "defaultMessage": "!!!You haven't added any workspaces yet.", 17 "defaultMessage": "!!!You haven't added any workspaces yet.",
18 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 18 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
19 "start": { 19 "start": {
20 "line": 23, 20 "line": 24,
21 "column": 19 21 "column": 19
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 26, 24 "line": 27,
25 "column": 3 25 "column": 3
26 } 26 }
27 }, 27 },
@@ -30,11 +30,11 @@
30 "defaultMessage": "!!!Could not load your workspaces", 30 "defaultMessage": "!!!Could not load your workspaces",
31 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 31 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
32 "start": { 32 "start": {
33 "line": 27, 33 "line": 28,
34 "column": 27 34 "column": 27
35 }, 35 },
36 "end": { 36 "end": {
37 "line": 30, 37 "line": 31,
38 "column": 3 38 "column": 3
39 } 39 }
40 }, 40 },
@@ -43,11 +43,11 @@
43 "defaultMessage": "!!!Try again", 43 "defaultMessage": "!!!Try again",
44 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 44 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
45 "start": { 45 "start": {
46 "line": 31, 46 "line": 32,
47 "column": 23 47 "column": 23
48 }, 48 },
49 "end": { 49 "end": {
50 "line": 34, 50 "line": 35,
51 "column": 3 51 "column": 3
52 } 52 }
53 }, 53 },
@@ -56,11 +56,11 @@
56 "defaultMessage": "!!!Your changes have been saved", 56 "defaultMessage": "!!!Your changes have been saved",
57 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 57 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
58 "start": { 58 "start": {
59 "line": 35, 59 "line": 36,
60 "column": 15 60 "column": 15
61 }, 61 },
62 "end": { 62 "end": {
63 "line": 38, 63 "line": 39,
64 "column": 3 64 "column": 3
65 } 65 }
66 }, 66 },
@@ -69,11 +69,11 @@
69 "defaultMessage": "!!!Workspace has been deleted", 69 "defaultMessage": "!!!Workspace has been deleted",
70 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 70 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
71 "start": { 71 "start": {
72 "line": 39, 72 "line": 40,
73 "column": 15 73 "column": 15
74 }, 74 },
75 "end": { 75 "end": {
76 "line": 42, 76 "line": 43,
77 "column": 3 77 "column": 3
78 } 78 }
79 }, 79 },
@@ -82,11 +82,11 @@
82 "defaultMessage": "!!!Info about workspace feature", 82 "defaultMessage": "!!!Info about workspace feature",
83 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 83 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
84 "start": { 84 "start": {
85 "line": 43, 85 "line": 44,
86 "column": 24 86 "column": 24
87 }, 87 },
88 "end": { 88 "end": {
89 "line": 46, 89 "line": 47,
90 "column": 3 90 "column": 3
91 } 91 }
92 }, 92 },
@@ -95,11 +95,11 @@
95 "defaultMessage": "!!!Less is More: Introducing Ferdi Workspaces", 95 "defaultMessage": "!!!Less is More: Introducing Ferdi Workspaces",
96 "file": "src/features/workspaces/components/WorkspacesDashboard.js", 96 "file": "src/features/workspaces/components/WorkspacesDashboard.js",
97 "start": { 97 "start": {
98 "line": 47, 98 "line": 48,
99 "column": 28 99 "column": 28
100 }, 100 },
101 "end": { 101 "end": {
102 "line": 50, 102 "line": 51,
103 "column": 3 103 "column": 3
104 } 104 }
105 } 105 }
diff --git a/src/i18n/messages/src/helpers/plan-helpers.json b/src/i18n/messages/src/helpers/plan-helpers.json
index df8ee19e3..3f3e7e85d 100644
--- a/src/i18n/messages/src/helpers/plan-helpers.json
+++ b/src/i18n/messages/src/helpers/plan-helpers.json
@@ -1,7 +1,7 @@
1[ 1[
2 { 2 {
3 "id": "pricing.plan.pro", 3 "id": "pricing.plan.pro",
4 "defaultMessage": "!!!Franz Professional", 4 "defaultMessage": "!!!Professional",
5 "file": "src/helpers/plan-helpers.js", 5 "file": "src/helpers/plan-helpers.js",
6 "start": { 6 "start": {
7 "line": 5, 7 "line": 5,
@@ -14,7 +14,7 @@
14 }, 14 },
15 { 15 {
16 "id": "pricing.plan.personal", 16 "id": "pricing.plan.personal",
17 "defaultMessage": "!!!Franz Personal", 17 "defaultMessage": "!!!Personal",
18 "file": "src/helpers/plan-helpers.js", 18 "file": "src/helpers/plan-helpers.js",
19 "start": { 19 "start": {
20 "line": 9, 20 "line": 9,
@@ -27,7 +27,7 @@
27 }, 27 },
28 { 28 {
29 "id": "pricing.plan.free", 29 "id": "pricing.plan.free",
30 "defaultMessage": "!!!Franz Free", 30 "defaultMessage": "!!!Free",
31 "file": "src/helpers/plan-helpers.js", 31 "file": "src/helpers/plan-helpers.js",
32 "start": { 32 "start": {
33 "line": 13, 33 "line": 13,
@@ -40,7 +40,7 @@
40 }, 40 },
41 { 41 {
42 "id": "pricing.plan.legacy", 42 "id": "pricing.plan.legacy",
43 "defaultMessage": "!!!Franz Premium", 43 "defaultMessage": "!!!Premium",
44 "file": "src/helpers/plan-helpers.js", 44 "file": "src/helpers/plan-helpers.js",
45 "start": { 45 "start": {
46 "line": 17, 46 "line": 17,
diff --git a/src/index.js b/src/index.js
index a4880a25a..87aa6357b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -68,8 +68,15 @@ if (isWindows) {
68 app.setAppUserModelId(appId); 68 app.setAppUserModelId(appId);
69} 69}
70 70
71// Initialize Settings
72const settings = new Settings('app', DEFAULT_APP_SETTINGS);
73const proxySettings = new Settings('proxy');
74
75// add `liftSingleInstanceLock` to settings.json to override the single instance lock
76const liftSingleInstanceLock = settings.get('liftSingleInstanceLock') || false;
77
71// Force single window 78// Force single window
72const gotTheLock = app.requestSingleInstanceLock(); 79const gotTheLock = liftSingleInstanceLock ? true : app.requestSingleInstanceLock();
73if (!gotTheLock) { 80if (!gotTheLock) {
74 app.quit(); 81 app.quit();
75} else { 82} else {
@@ -116,10 +123,6 @@ if (isLinux && ['Pantheon', 'Unity:Unity7'].indexOf(process.env.XDG_CURRENT_DESK
116 process.env.XDG_CURRENT_DESKTOP = 'Unity'; 123 process.env.XDG_CURRENT_DESKTOP = 'Unity';
117} 124}
118 125
119// Initialize Settings
120const settings = new Settings('app', DEFAULT_APP_SETTINGS);
121const proxySettings = new Settings('proxy');
122
123// Disable GPU acceleration 126// Disable GPU acceleration
124if (!settings.get('enableGPUAcceleration')) { 127if (!settings.get('enableGPUAcceleration')) {
125 debug('Disable GPU Acceleration'); 128 debug('Disable GPU Acceleration');
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index f223283f9..d7398a126 100644
--- a/src/lib/Menu.js
+++ b/src/lib/Menu.js
@@ -730,7 +730,9 @@ export default class FranzMenu {
730 // need to clone object so we don't modify computed (cached) object 730 // need to clone object so we don't modify computed (cached) object
731 const serviceTpl = Object.assign([], this.serviceTpl()); 731 const serviceTpl = Object.assign([], this.serviceTpl());
732 732
733 if (window.ferdi === undefined) { 733 // Don't initialize when window.franz is undefined or when we are on a payment window route
734 if (window.ferdi === undefined || this.stores.router.location.pathname.startsWith('/payment/')) {
735 console.log('skipping menu init');
734 return; 736 return;
735 } 737 }
736 738
diff --git a/src/lib/TouchBar.js b/src/lib/TouchBar.js
index 1de46d2a3..32f546644 100644
--- a/src/lib/TouchBar.js
+++ b/src/lib/TouchBar.js
@@ -24,6 +24,10 @@ export default class FranzTouchBar {
24 _build() { 24 _build() {
25 const currentWindow = remote.getCurrentWindow(); 25 const currentWindow = remote.getCurrentWindow();
26 26
27 if (this.stores.router.location.pathname.startsWith('/payment/')) {
28 return;
29 }
30
27 if (this.stores.user.isLoggedIn) { 31 if (this.stores.user.isLoggedIn) {
28 const { TouchBar } = remote; 32 const { TouchBar } = remote;
29 const { TouchBarButton, TouchBarSpacer } = TouchBar; 33 const { TouchBarButton, TouchBarSpacer } = TouchBar;
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
index 40d98cf42..c6724c20f 100644
--- a/src/stores/AppStore.js
+++ b/src/stores/AppStore.js
@@ -26,7 +26,9 @@ import { sleep } from '../helpers/async-helpers';
26 26
27const debug = require('debug')('Ferdi:AppStore'); 27const debug = require('debug')('Ferdi:AppStore');
28 28
29const { app, systemPreferences, screen } = remote; 29const {
30 app, systemPreferences, screen, powerMonitor,
31} = remote;
30 32
31const mainWindow = remote.getCurrentWindow(); 33const mainWindow = remote.getCurrentWindow();
32 34
@@ -35,6 +37,8 @@ const autoLauncher = new AutoLaunch({
35 name: 'Ferdi', 37 name: 'Ferdi',
36}); 38});
37 39
40const CATALINA_NOTIFICATION_HACK_KEY = '_temp_askedForCatalinaNotificationPermissions';
41
38export default class AppStore extends Store { 42export default class AppStore extends Store {
39 updateStatusTypes = { 43 updateStatusTypes = {
40 CHECKING: 'CHECKING', 44 CHECKING: 'CHECKING',
@@ -56,6 +60,8 @@ export default class AppStore extends Store {
56 60
57 @observable authRequestFailed = false; 61 @observable authRequestFailed = false;
58 62
63 @observable timeSuspensionStart;
64
59 @observable timeOfflineStart; 65 @observable timeOfflineStart;
60 66
61 @observable updateStatus = null; 67 @observable updateStatus = null;
@@ -76,6 +82,8 @@ export default class AppStore extends Store {
76 82
77 dictionaries = []; 83 dictionaries = [];
78 84
85 fetchDataInterval = null;
86
79 constructor(...args) { 87 constructor(...args) {
80 super(...args); 88 super(...args);
81 89
@@ -97,6 +105,7 @@ export default class AppStore extends Store {
97 this._setLocale.bind(this), 105 this._setLocale.bind(this),
98 this._muteAppHandler.bind(this), 106 this._muteAppHandler.bind(this),
99 this._handleFullScreen.bind(this), 107 this._handleFullScreen.bind(this),
108 this._handleLogout.bind(this),
100 ]); 109 ]);
101 } 110 }
102 111
@@ -124,6 +133,12 @@ export default class AppStore extends Store {
124 this._systemDND(); 133 this._systemDND();
125 setInterval(() => this._systemDND(), ms('5s')); 134 setInterval(() => this._systemDND(), ms('5s'));
126 135
136 this.fetchDataInterval = setInterval(() => {
137 this.stores.user.getUserInfoRequest.invalidate({ immediately: true });
138 this.stores.features.featuresRequest.invalidate({ immediately: true });
139 this.stores.news.latestNewsRequest.invalidate({ immediately: true });
140 }, ms('10m'));
141
127 // Check for updates once every 4 hours 142 // Check for updates once every 4 hours
128 setInterval(() => this._checkForUpdates(), CHECK_INTERVAL); 143 setInterval(() => this._checkForUpdates(), CHECK_INTERVAL);
129 // Check for an update in 30s (need a delay to prevent Squirrel Installer lock file issues) 144 // Check for an update in 30s (need a delay to prevent Squirrel Installer lock file issues)
@@ -175,6 +190,36 @@ export default class AppStore extends Store {
175 190
176 debug('Window is visible/focused', isVisible); 191 debug('Window is visible/focused', isVisible);
177 }); 192 });
193
194 powerMonitor.on('suspend', () => {
195 debug('System suspended starting timer');
196
197 this.timeSuspensionStart = moment();
198 });
199
200 powerMonitor.on('resume', () => {
201 debug('System resumed, last suspended on', this.timeSuspensionStart.toString());
202
203 if (this.timeSuspensionStart.add(10, 'm').isBefore(moment())) {
204 debug('Reloading services, user info and features');
205
206 setTimeout(() => {
207 window.location.reload();
208 }, ms('2s'));
209 }
210 });
211
212 // macOS catalina notifications hack
213 // notifications got stuck after upgrade but forcing a notification
214 // via `new Notification` triggered the permission request
215 if (isMac && !localStorage.getItem(CATALINA_NOTIFICATION_HACK_KEY)) {
216 // eslint-disable-next-line no-new
217 new window.Notification('Welcome to Franz 5', {
218 body: 'Have a wonderful day & happy messaging.',
219 });
220
221 localStorage.setItem(CATALINA_NOTIFICATION_HACK_KEY, true);
222 }
178 } 223 }
179 224
180 @computed get cacheSize() { 225 @computed get cacheSize() {
@@ -383,6 +428,12 @@ export default class AppStore extends Store {
383 } 428 }
384 } 429 }
385 430
431 _handleLogout() {
432 if (!this.stores.user.isLoggedIn) {
433 clearInterval(this.fetchDataInterval);
434 }
435 }
436
386 // Helpers 437 // Helpers
387 _appStartsCounter() { 438 _appStartsCounter() {
388 this.actions.settings.update({ 439 this.actions.settings.update({
diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js
index 3d9542245..ab5d762c7 100644
--- a/src/stores/FeaturesStore.js
+++ b/src/stores/FeaturesStore.js
@@ -21,6 +21,8 @@ import serviceLimit from '../features/serviceLimit';
21import communityRecipes from '../features/communityRecipes'; 21import communityRecipes from '../features/communityRecipes';
22import todos from '../features/todos'; 22import todos from '../features/todos';
23import accentColor from '../features/accentColor'; 23import accentColor from '../features/accentColor';
24import planSelection from '../features/planSelection';
25import trialStatusBar from '../features/trialStatusBar';
24 26
25import { DEFAULT_FEATURES_CONFIG } from '../config'; 27import { DEFAULT_FEATURES_CONFIG } from '../config';
26 28
@@ -67,6 +69,7 @@ export default class FeaturesStore extends Store {
67 if (this.stores.user.isLoggedIn) { 69 if (this.stores.user.isLoggedIn) {
68 this.featuresRequest.invalidate({ immediately: true }); 70 this.featuresRequest.invalidate({ immediately: true });
69 } else { 71 } else {
72 this.defaultFeaturesRequest.execute();
70 this.defaultFeaturesRequest.invalidate({ immediately: true }); 73 this.defaultFeaturesRequest.invalidate({ immediately: true });
71 } 74 }
72 } 75 }
@@ -85,5 +88,7 @@ export default class FeaturesStore extends Store {
85 communityRecipes(this.stores, this.actions); 88 communityRecipes(this.stores, this.actions);
86 todos(this.stores, this.actions); 89 todos(this.stores, this.actions);
87 accentColor(this.stores, this.actions); 90 accentColor(this.stores, this.actions);
91 planSelection(this.stores, this.actions);
92 trialStatusBar(this.stores, this.actions);
88 } 93 }
89} 94}
diff --git a/src/stores/PaymentStore.js b/src/stores/PaymentStore.js
index 8579812ad..69e6eb9c3 100644
--- a/src/stores/PaymentStore.js
+++ b/src/stores/PaymentStore.js
@@ -1,9 +1,12 @@
1import { action, observable, computed } from 'mobx'; 1import { action, observable, computed } from 'mobx';
2import { remote } from 'electron';
2 3
3import Store from './lib/Store'; 4import Store from './lib/Store';
4import CachedRequest from './lib/CachedRequest'; 5import CachedRequest from './lib/CachedRequest';
5import Request from './lib/Request'; 6import Request from './lib/Request';
6 7
8const { BrowserWindow } = remote;
9
7export default class PaymentStore extends Store { 10export default class PaymentStore extends Store {
8 @observable plansRequest = new CachedRequest(this.api.payment, 'plans'); 11 @observable plansRequest = new CachedRequest(this.api.payment, 'plans');
9 12
@@ -13,6 +16,7 @@ export default class PaymentStore extends Store {
13 super(...args); 16 super(...args);
14 17
15 this.actions.payment.createHostedPage.listen(this._createHostedPage.bind(this)); 18 this.actions.payment.createHostedPage.listen(this._createHostedPage.bind(this));
19 this.actions.payment.upgradeAccount.listen(this._upgradeAccount.bind(this));
16 } 20 }
17 21
18 @computed get plan() { 22 @computed get plan() {
@@ -27,4 +31,38 @@ export default class PaymentStore extends Store {
27 31
28 return request; 32 return request;
29 } 33 }
34
35 @action _upgradeAccount({ planId, onCloseWindow = () => null }) {
36 let hostedPageURL = this.stores.features.features.subscribeURL;
37
38 const parsedUrl = new URL(hostedPageURL);
39 const params = new URLSearchParams(parsedUrl.search.slice(1));
40
41 params.set('plan', planId);
42
43 hostedPageURL = this.stores.user.getAuthURL(`${parsedUrl.origin}${parsedUrl.pathname}?${params.toString()}`);
44
45 const win = new BrowserWindow({
46 parent: remote.getCurrentWindow(),
47 modal: true,
48 title: '🔒 Upgrade Your Franz Account',
49 width: 800,
50 height: window.innerHeight - 100,
51 maxWidth: 800,
52 minWidth: 600,
53 autoHideMenuBar: true,
54 webPreferences: {
55 nodeIntegration: true,
56 webviewTag: true,
57 },
58 });
59 win.loadURL(`file://${__dirname}/../index.html#/payment/${encodeURIComponent(hostedPageURL)}`);
60
61 win.on('closed', () => {
62 this.stores.user.getUserInfoRequest.invalidate({ immediately: true });
63 this.stores.features.featuresRequest.invalidate({ immediately: true });
64
65 onCloseWindow();
66 });
67 }
30} 68}
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index 185a6f0ae..934a8a6e0 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -1,4 +1,4 @@
1import { shell } from 'electron'; 1import { shell, remote } from 'electron';
2import { 2import {
3 action, 3 action,
4 reaction, 4 reaction,
@@ -23,6 +23,8 @@ import { KEEP_WS_LOADED_USID } from '../config';
23 23
24const debug = require('debug')('Ferdi:ServiceStore'); 24const debug = require('debug')('Ferdi:ServiceStore');
25 25
26const { app } = remote;
27
26export default class ServicesStore extends Store { 28export default class ServicesStore extends Store {
27 @observable allServicesRequest = new CachedRequest(this.api.services, 'all'); 29 @observable allServicesRequest = new CachedRequest(this.api.services, 'all');
28 30
@@ -818,7 +820,9 @@ export default class ServicesStore extends Store {
818 820
819 if (service.webview) { 821 if (service.webview) {
820 debug('Initialize recipe', service.recipe.id, service.name); 822 debug('Initialize recipe', service.recipe.id, service.name);
821 service.webview.send('initialize-recipe', service.shareWithWebview, service.recipe); 823 service.webview.send('initialize-recipe', Object.assign({
824 franzVersion: app.getVersion(),
825 }, service.shareWithWebview), service.recipe);
822 } 826 }
823 } 827 }
824 828
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js
index 03aa79606..d6a2e5fde 100644
--- a/src/stores/UserStore.js
+++ b/src/stores/UserStore.js
@@ -78,6 +78,8 @@ export default class UserStore extends Store {
78 78
79 @observable logoutReason = null; 79 @observable logoutReason = null;
80 80
81 fetchUserInfoInterval = null;
82
81 constructor(...args) { 83 constructor(...args) {
82 super(...args); 84 super(...args);
83 85
@@ -162,7 +164,7 @@ export default class UserStore extends Store {
162 } 164 }
163 165
164 @computed get isPremiumOverride() { 166 @computed get isPremiumOverride() {
165 return ((!this.team || !this.team.plan) && this.isPremium) || (this.team.state === 'expired' && this.isPremium); 167 return ((!this.team || !this.team.plan) && this.isPremium) || (this.team && this.team.state === 'expired' && this.isPremium);
166 } 168 }
167 169
168 @computed get isPersonal() { 170 @computed get isPersonal() {
@@ -201,7 +203,7 @@ export default class UserStore extends Store {
201 } 203 }
202 204
203 @action async _signup({ 205 @action async _signup({
204 firstname, lastname, email, password, accountType, company, 206 firstname, lastname, email, password, accountType, company, plan, currency,
205 }) { 207 }) {
206 const authToken = await this.signupRequest.execute({ 208 const authToken = await this.signupRequest.execute({
207 firstname, 209 firstname,
@@ -211,6 +213,8 @@ export default class UserStore extends Store {
211 accountType, 213 accountType,
212 company, 214 company,
213 locale: this.stores.app.locale, 215 locale: this.stores.app.locale,
216 plan,
217 currency,
214 }); 218 });
215 219
216 this.hasCompletedSignup = true; 220 this.hasCompletedSignup = true;
diff --git a/src/stores/index.js b/src/stores/index.js
index 10dd56665..4eeef7982 100644
--- a/src/stores/index.js
+++ b/src/stores/index.js
@@ -15,6 +15,7 @@ import { announcementsStore } from '../features/announcements';
15import { serviceLimitStore } from '../features/serviceLimit'; 15import { serviceLimitStore } from '../features/serviceLimit';
16import { communityRecipesStore } from '../features/communityRecipes'; 16import { communityRecipesStore } from '../features/communityRecipes';
17import { todosStore } from '../features/todos'; 17import { todosStore } from '../features/todos';
18import { planSelectionStore } from '../features/planSelection';
18 19
19export default (api, actions, router) => { 20export default (api, actions, router) => {
20 const stores = {}; 21 const stores = {};
@@ -37,6 +38,7 @@ export default (api, actions, router) => {
37 serviceLimit: serviceLimitStore, 38 serviceLimit: serviceLimitStore,
38 communityRecipes: communityRecipesStore, 39 communityRecipes: communityRecipesStore,
39 todos: todosStore, 40 todos: todosStore,
41 planSelection: planSelectionStore,
40 }); 42 });
41 // Initialize all stores 43 // Initialize all stores
42 Object.keys(stores).forEach((name) => { 44 Object.keys(stores).forEach((name) => {
diff --git a/src/styles/settings.scss b/src/styles/settings.scss
index 753288b8d..324175d0b 100644
--- a/src/styles/settings.scss
+++ b/src/styles/settings.scss
@@ -373,7 +373,7 @@
373 .account__subscription-button { margin-left: auto; } 373 .account__subscription-button { margin-left: auto; }
374 .franz-form__button { white-space: nowrap; } 374 .franz-form__button { white-space: nowrap; }
375 div { height: auto; } 375 div { height: auto; }
376 [data-type="franz-button"] div { height: 100% } 376 [data-type="franz-button"] div { height: 20px }
377 377
378 .invoices { 378 .invoices {
379 width: 100%; 379 width: 100%;
diff --git a/src/styles/subscription-popup.scss b/src/styles/subscription-popup.scss
index fb4795d6c..14e05e65d 100644
--- a/src/styles/subscription-popup.scss
+++ b/src/styles/subscription-popup.scss
@@ -2,7 +2,10 @@
2 height: 100%; 2 height: 100%;
3 3
4 &__content { height: calc(100% - 60px); } 4 &__content { height: calc(100% - 60px); }
5 &__webview { height: 100%; } 5 &__webview {
6 height: 100%;
7 background: #FFF;
8 }
6 9
7 &__toolbar { 10 &__toolbar {
8 background: $theme-gray-lightest; 11 background: $theme-gray-lightest;
diff --git a/src/webview/lib/RecipeWebview.js b/src/webview/lib/RecipeWebview.js
index 877e45e35..74d05fc2d 100644
--- a/src/webview/lib/RecipeWebview.js
+++ b/src/webview/lib/RecipeWebview.js
@@ -1,7 +1,8 @@
1// @flow
2const { ipcRenderer } = require('electron'); 1const { ipcRenderer } = require('electron');
3const fs = require('fs-extra'); 2const fs = require('fs-extra');
4 3
4const debug = require('debug')('Franz:Plugin:RecipeWebview');
5
5class RecipeWebview { 6class RecipeWebview {
6 constructor() { 7 constructor() {
7 this.countCache = { 8 this.countCache = {
@@ -11,6 +12,8 @@ class RecipeWebview {
11 12
12 ipcRenderer.on('poll', () => { 13 ipcRenderer.on('poll', () => {
13 this.loopFunc(); 14 this.loopFunc();
15
16 debug('Poll event');
14 }); 17 });
15 } 18 }
16 19
@@ -50,8 +53,11 @@ class RecipeWebview {
50 indirect: indirectInt > 0 ? indirectInt : 0, 53 indirect: indirectInt > 0 ? indirectInt : 0,
51 }; 54 };
52 55
56
53 ipcRenderer.sendToHost('messages', count); 57 ipcRenderer.sendToHost('messages', count);
54 Object.assign(this.countCache, count); 58 Object.assign(this.countCache, count);
59
60 debug('Sending badge count to host', count);
55 } 61 }
56 62
57 /** 63 /**
@@ -67,6 +73,8 @@ class RecipeWebview {
67 styles.innerHTML = data.toString(); 73 styles.innerHTML = data.toString();
68 74
69 document.querySelector('head').appendChild(styles); 75 document.querySelector('head').appendChild(styles);
76
77 debug('Append styles', styles);
70 }); 78 });
71 } 79 }
72 80
diff --git a/src/webview/recipe.js b/src/webview/recipe.js
index ddfd0e139..2bf8f757a 100644
--- a/src/webview/recipe.js
+++ b/src/webview/recipe.js
@@ -242,7 +242,9 @@ window.open = (url, frameName, features) => {
242 return ipcRenderer.sendToHost('new-window', url); 242 return ipcRenderer.sendToHost('new-window', url);
243 } 243 }
244 244
245 return originalWindowOpen(url, frameName, features); 245 if (url) {
246 return originalWindowOpen(url, frameName, features);
247 }
246}; 248};
247 249
248if (isDevMode) { 250if (isDevMode) {