aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar vantezzen <hello@vantezzen.io>2019-10-24 15:15:42 +0200
committerLibravatar vantezzen <hello@vantezzen.io>2019-10-24 15:15:42 +0200
commit54f8b191a94bd78a85b046bbf21dd2245d3a6f3e (patch)
treeada5876f0e8a697ba4693bba07f5e0f31fea1fc9 /src
parentUpdate submodules (diff)
parentbump version to 5.4.0 (diff)
downloadferdium-app-54f8b191a94bd78a85b046bbf21dd2245d3a6f3e.tar.gz
ferdium-app-54f8b191a94bd78a85b046bbf21dd2245d3a6f3e.tar.zst
ferdium-app-54f8b191a94bd78a85b046bbf21dd2245d3a6f3e.zip
Merge https://github.com/meetfranz/franz into franz-5.4.0-release
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/Pricing.js122
-rw-r--r--src/components/layout/AppLayout.js4
-rw-r--r--src/components/settings/account/AccountDashboard.js18
-rw-r--r--src/components/subscription/SubscriptionForm.js4
-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.js2
-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/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.js281
-rw-r--r--src/features/planSelection/containers/PlanSelectionScreen.js132
-rw-r--r--src/features/planSelection/index.js30
-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/messages/src/components/auth/Pricing.json92
-rw-r--r--src/i18n/messages/src/components/layout/AppLayout.json12
-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.js57
-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
65 files changed, 2185 insertions, 165 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/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/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/subscription/SubscriptionForm.js b/src/components/subscription/SubscriptionForm.js
index effd04f7a..ec486e5d0 100644
--- a/src/components/subscription/SubscriptionForm.js
+++ b/src/components/subscription/SubscriptionForm.js
@@ -30,7 +30,8 @@ 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
@@ -62,7 +63,6 @@ export default @injectSheet(styles) @observer 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..611c50468 100644
--- a/src/components/ui/PremiumFeatureContainer/index.js
+++ b/src/components/ui/PremiumFeatureContainer/index.js
@@ -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/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..b6bb9d32d
--- /dev/null
+++ b/src/features/planSelection/components/PlanSelection.js
@@ -0,0 +1,281 @@
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';
15import { gaPage } from '../../../lib/analytics';
16
17const messages = defineMessages({
18 welcome: {
19 id: 'feature.planSelection.fullscreen.welcome',
20 defaultMessage: '!!!Are you ready to choose, {name}',
21 },
22 subheadline: {
23 id: 'feature.planSelection.fullscreen.subheadline',
24 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.',
25 },
26 textFree: {
27 id: 'feature.planSelection.free.text',
28 defaultMessage: '!!!Basic functionality',
29 },
30 textPersonal: {
31 id: 'feature.planSelection.personal.text',
32 defaultMessage: '!!!More services, no waiting - ideal for personal use.',
33 },
34 textProfessional: {
35 id: 'feature.planSelection.pro.text',
36 defaultMessage: '!!!Unlimited services and professional features for you - and your team.',
37 },
38 ctaStayOnFree: {
39 id: 'feature.planSelection.cta.stayOnFree',
40 defaultMessage: '!!!Stay on Free',
41 },
42 ctaDowngradeFree: {
43 id: 'feature.planSelection.cta.ctaDowngradeFree',
44 defaultMessage: '!!!Downgrade to Free',
45 },
46 actionTrial: {
47 id: 'feature.planSelection.cta.trial',
48 defaultMessage: '!!!Start my free 14-days Trial',
49 },
50 shortActionPersonal: {
51 id: 'feature.planSelection.cta.upgradePersonal',
52 defaultMessage: '!!!Choose Personal',
53 },
54 shortActionPro: {
55 id: 'feature.planSelection.cta.upgradePro',
56 defaultMessage: '!!!Choose Professional',
57 },
58 fullFeatureList: {
59 id: 'feature.planSelection.fullFeatureList',
60 defaultMessage: '!!!Complete comparison of all plans',
61 },
62 pricesBasedOnAnnualPayment: {
63 id: 'feature.planSelection.pricesBasedOnAnnualPayment',
64 defaultMessage: '!!!All prices based on yearly payment',
65 },
66});
67
68const styles = theme => ({
69 root: {
70 background: theme.colorModalOverlayBackground,
71 width: '100%',
72 height: '100%',
73 position: 'absolute',
74 top: 0,
75 left: 0,
76 display: 'flex',
77 justifyContent: 'center',
78 alignItems: 'center',
79 zIndex: 999999,
80 overflowY: 'scroll',
81 },
82 container: {
83 width: '80%',
84 height: 'auto',
85 background: theme.styleTypes.primary.accent,
86 padding: 40,
87 borderRadius: theme.borderRadius,
88 maxWidth: 1000,
89
90 '& h1, & h2': {
91 textAlign: 'center',
92 color: theme.styleTypes.primary.contrast,
93 },
94 },
95 plans: {
96 display: 'flex',
97 margin: [40, 0, 0],
98 height: 'auto',
99
100 '& > div': {
101 margin: [0, 15],
102 flex: 1,
103 height: 'auto',
104 background: theme.styleTypes.primary.contrast,
105 boxShadow: [0, 2, 30, color('#000').alpha(0.1).rgb().string()],
106 },
107 },
108 bigIcon: {
109 background: theme.styleTypes.danger.accent,
110 width: 120,
111 height: 120,
112 display: 'flex',
113 alignItems: 'center',
114 borderRadius: '100%',
115 justifyContent: 'center',
116 margin: [-100, 'auto', 20],
117
118 '& svg': {
119 width: '80px !important',
120 height: '80px !important',
121 filter: 'drop-shadow( 0px 2px 3px rgba(0, 0, 0, 0.3))',
122 fill: theme.styleTypes.danger.contrast,
123 },
124 },
125 headline: {
126 fontSize: 40,
127 },
128 subheadline: {
129 maxWidth: 660,
130 fontSize: 22,
131 lineHeight: 1.1,
132 margin: [0, 'auto'],
133 },
134 featureList: {
135 '& li': {
136 borderBottom: [1, 'solid', '#CECECE'],
137 },
138 },
139 footer: {
140 display: 'flex',
141 color: theme.styleTypes.primary.contrast,
142 marginTop: 20,
143 padding: [0, 15],
144 },
145 fullFeatureList: {
146 marginRight: 'auto',
147 textAlign: 'center',
148 display: 'flex',
149 justifyContent: 'center',
150 alignItems: 'center',
151 color: `${theme.styleTypes.primary.contrast} !important`,
152
153 '& svg': {
154 marginRight: 5,
155 },
156 },
157 scrollContainer: {
158 border: '1px solid red',
159 overflow: 'scroll-x',
160 },
161 featuredPlan: {
162 transform: 'scale(1.05)',
163 },
164 disclaimer: {
165 textAlign: 'right',
166 margin: [10, 15, 0, 0],
167 },
168});
169
170@injectSheet(styles) @observer
171class PlanSelection extends Component {
172 static propTypes = {
173 classes: PropTypes.object.isRequired,
174 firstname: PropTypes.string.isRequired,
175 plans: PropTypes.object.isRequired,
176 currency: PropTypes.string.isRequired,
177 subscriptionExpired: PropTypes.bool.isRequired,
178 upgradeAccount: PropTypes.func.isRequired,
179 stayOnFree: PropTypes.func.isRequired,
180 hadSubscription: PropTypes.bool.isRequired,
181 };
182
183 static contextTypes = {
184 intl: intlShape,
185 };
186
187 componentDidMount() {
188 gaPage('/select-plan');
189 }
190
191 render() {
192 const {
193 classes,
194 firstname,
195 plans,
196 currency,
197 subscriptionExpired,
198 upgradeAccount,
199 stayOnFree,
200 hadSubscription,
201 } = this.props;
202
203 const { intl } = this.context;
204
205 return (
206 <Appear>
207 <div
208 className={classes.root}
209 >
210 <div className={classes.container}>
211 <div className={classes.bigIcon}>
212 <Icon icon={mdiRocket} />
213 </div>
214 <H1 className={classes.headline}>{intl.formatMessage(messages.welcome, { name: firstname })}</H1>
215 <H2 className={classes.subheadline}>{intl.formatMessage(messages.subheadline)}</H2>
216 <div className={classes.plans}>
217 <PlanItem
218 name={i18nPlanName(PLANS.FREE, intl)}
219 text={intl.formatMessage(messages.textFree)}
220 price={0}
221 currency={currency}
222 ctaLabel={intl.formatMessage(subscriptionExpired ? messages.ctaDowngradeFree : messages.ctaStayOnFree)}
223 upgrade={() => stayOnFree()}
224 simpleCTA
225 >
226 <FeatureList
227 plan={PLANS.FREE}
228 className={classes.featureList}
229 />
230 </PlanItem>
231 <PlanItem
232 name={i18nPlanName(plans.pro.yearly.id, intl)}
233 text={intl.formatMessage(messages.textProfessional)}
234 price={plans.pro.yearly.price}
235 currency={currency}
236 ctaLabel={intl.formatMessage(hadSubscription ? messages.shortActionPro : messages.actionTrial)}
237 upgrade={() => upgradeAccount(plans.pro.yearly.id)}
238 className={classes.featuredPlan}
239 perUser
240 bestValue
241 >
242 <FeatureList
243 plan={PLANS.PRO}
244 className={classes.featureList}
245 />
246 </PlanItem>
247 <PlanItem
248 name={i18nPlanName(plans.personal.yearly.id, intl)}
249 text={intl.formatMessage(messages.textPersonal)}
250 price={plans.personal.yearly.price}
251 currency={currency}
252 ctaLabel={intl.formatMessage(hadSubscription ? messages.shortActionPersonal : messages.actionTrial)}
253 upgrade={() => upgradeAccount(plans.personal.yearly.id)}
254 >
255 <FeatureList
256 plan={PLANS.PERSONAL}
257 className={classes.featureList}
258 />
259 </PlanItem>
260 </div>
261 <div className={classes.footer}>
262 <a
263 href="https://meetfranz.com/pricing"
264 target="_blank"
265 className={classes.fullFeatureList}
266 >
267 <Icon icon={mdiArrowRight} />
268 {intl.formatMessage(messages.fullFeatureList)}
269 </a>
270 {/* <p className={classes.disclaimer}> */}
271 {intl.formatMessage(messages.pricesBasedOnAnnualPayment)}
272 {/* </p> */}
273 </div>
274 </div>
275 </div>
276 </Appear>
277 );
278 }
279}
280
281export default PlanSelection;
diff --git a/src/features/planSelection/containers/PlanSelectionScreen.js b/src/features/planSelection/containers/PlanSelectionScreen.js
new file mode 100644
index 000000000..cb62f45d3
--- /dev/null
+++ b/src/features/planSelection/containers/PlanSelectionScreen.js
@@ -0,0 +1,132 @@
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, GA_CATEGORY_PLAN_SELECTION } from '..';
12import { gaEvent, gaPage } from '../../../lib/analytics';
13
14const { dialog, app } = remote;
15
16const messages = defineMessages({
17 dialogTitle: {
18 id: 'feature.planSelection.fullscreen.dialog.title',
19 defaultMessage: '!!!Downgrade your Franz Plan',
20 },
21 dialogMessage: {
22 id: 'feature.planSelection.fullscreen.dialog.message',
23 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.',
24 },
25 dialogCTADowngrade: {
26 id: 'feature.planSelection.fullscreen.dialog.cta.downgrade',
27 defaultMessage: '!!!Downgrade to Free',
28 },
29 dialogCTAUpgrade: {
30 id: 'feature.planSelection.fullscreen.dialog.cta.upgrade',
31 defaultMessage: '!!!Choose Personal',
32 },
33});
34
35@inject('stores', 'actions') @observer
36class PlanSelectionScreen extends Component {
37 static contextTypes = {
38 intl: intlShape,
39 };
40
41 upgradeAccount(planId) {
42 const { upgradeAccount } = this.props.actions.payment;
43
44 upgradeAccount({
45 planId,
46 });
47 }
48
49 render() {
50 if (!planSelectionStore || !planSelectionStore.isFeatureActive || !planSelectionStore.showPlanSelectionOverlay) {
51 return null;
52 }
53
54 const { intl } = this.context;
55
56 const { user, features } = this.props.stores;
57 const { plans, currency } = features.features.pricingConfig;
58 const { activateTrial } = this.props.actions.user;
59 const { downgradeAccount, hideOverlay } = this.props.actions.planSelection;
60
61 return (
62 <ErrorBoundary>
63 <PlanSelection
64 firstname={user.data.firstname}
65 plans={plans}
66 currency={currency}
67 upgradeAccount={(planId) => {
68 if (user.data.hadSubscription) {
69 this.upgradeAccount(planId);
70
71 gaEvent(GA_CATEGORY_PLAN_SELECTION, 'SelectPlan', planId);
72 } else {
73 activateTrial({
74 planId,
75 });
76 }
77 }}
78 stayOnFree={() => {
79 gaPage('/select-plan/downgrade');
80
81 const selection = dialog.showMessageBoxSync(app.mainWindow, {
82 type: 'question',
83 message: intl.formatMessage(messages.dialogTitle),
84 detail: intl.formatMessage(messages.dialogMessage, {
85 currency,
86 price: plans.personal.yearly.price,
87 }),
88 buttons: [
89 intl.formatMessage(messages.dialogCTADowngrade),
90 intl.formatMessage(messages.dialogCTAUpgrade),
91 ],
92 });
93
94 gaEvent(GA_CATEGORY_PLAN_SELECTION, 'SelectPlan', 'Stay on Free');
95
96 if (selection === 0) {
97 downgradeAccount();
98 hideOverlay();
99 } else {
100 this.upgradeAccount(plans.personal.yearly.id);
101
102 gaEvent(GA_CATEGORY_PLAN_SELECTION, 'SelectPlan', 'Downgrade');
103 }
104 }}
105 subscriptionExpired={user.team && user.team.state === 'expired' && !user.team.userHasDowngraded}
106 hadSubscription={user.data.hadSubscription}
107 />
108 </ErrorBoundary>
109 );
110 }
111}
112
113export default PlanSelectionScreen;
114
115PlanSelectionScreen.wrappedComponent.propTypes = {
116 stores: PropTypes.shape({
117 features: PropTypes.instanceOf(FeaturesStore).isRequired,
118 user: PropTypes.instanceOf(UserStore).isRequired,
119 }).isRequired,
120 actions: PropTypes.shape({
121 payment: PropTypes.shape({
122 upgradeAccount: PropTypes.func.isRequired,
123 }),
124 planSelection: PropTypes.shape({
125 downgradeAccount: PropTypes.func.isRequired,
126 hideOverlay: PropTypes.func.isRequired,
127 }),
128 user: PropTypes.shape({
129 activateTrial: PropTypes.func.isRequired,
130 }),
131 }).isRequired,
132};
diff --git a/src/features/planSelection/index.js b/src/features/planSelection/index.js
new file mode 100644
index 000000000..81189207a
--- /dev/null
+++ b/src/features/planSelection/index.js
@@ -0,0 +1,30 @@
1import { reaction } from 'mobx';
2import PlanSelectionStore from './store';
3
4const debug = require('debug')('Franz:feature:planSelection');
5
6export const GA_CATEGORY_PLAN_SELECTION = 'planSelection';
7
8export const planSelectionStore = new PlanSelectionStore();
9
10export default function initPlanSelection(stores, actions) {
11 stores.planSelection = planSelectionStore;
12 const { features } = stores;
13
14 // Toggle planSelection feature
15 reaction(
16 () => features.features.isPlanSelectionEnabled,
17 (isEnabled) => {
18 if (isEnabled) {
19 debug('Initializing `planSelection` feature');
20 planSelectionStore.start(stores, actions);
21 } else if (planSelectionStore.isFeatureActive) {
22 debug('Disabling `planSelection` feature');
23 planSelectionStore.stop();
24 }
25 },
26 {
27 fireImmediately: true,
28 },
29 );
30}
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/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..8b7fcf1b2 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 },
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..7f1de6cfd
--- /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": 18,
8 "column": 11
9 },
10 "end": {
11 "line": 21,
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": 22,
21 "column": 15
22 },
23 "end": {
24 "line": 25,
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": 26,
34 "column": 12
35 },
36 "end": {
37 "line": 29,
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": 30,
47 "column": 16
48 },
49 "end": {
50 "line": 33,
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": 34,
60 "column": 20
61 },
62 "end": {
63 "line": 37,
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": 38,
73 "column": 17
74 },
75 "end": {
76 "line": 41,
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": 42,
86 "column": 20
87 },
88 "end": {
89 "line": 45,
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": 46,
99 "column": 15
100 },
101 "end": {
102 "line": 49,
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": 50,
112 "column": 23
113 },
114 "end": {
115 "line": 53,
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": 54,
125 "column": 18
126 },
127 "end": {
128 "line": 57,
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": 58,
138 "column": 19
139 },
140 "end": {
141 "line": 61,
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": 62,
151 "column": 30
152 },
153 "end": {
154 "line": 65,
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..905c6e09a
--- /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": 17,
8 "column": 15
9 },
10 "end": {
11 "line": 20,
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": 21,
21 "column": 17
22 },
23 "end": {
24 "line": 24,
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": 25,
34 "column": 22
35 },
36 "end": {
37 "line": 28,
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": 29,
47 "column": 20
48 },
49 "end": {
50 "line": 32,
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..6ce79f2e2 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',
@@ -55,6 +59,8 @@ export default class AppStore extends Store {
55 @observable isOnline = navigator.onLine; 59 @observable isOnline = navigator.onLine;
56 60
57 @observable authRequestFailed = false; 61 @observable authRequestFailed = false;
62
63 @observable timeSuspensionStart;
58 64
59 @observable timeOfflineStart; 65 @observable timeOfflineStart;
60 66
@@ -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,40 @@ 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 statsEvent('resumed-app');
211 }
212 });
213
214 // macOS catalina notifications hack
215 // notifications got stuck after upgrade but forcing a notification
216 // via `new Notification` triggered the permission request
217 if (isMac && !localStorage.getItem(CATALINA_NOTIFICATION_HACK_KEY)) {
218 // eslint-disable-next-line no-new
219 new window.Notification('Welcome to Franz 5', {
220 body: 'Have a wonderful day & happy messaging.',
221 });
222
223 localStorage.setItem(CATALINA_NOTIFICATION_HACK_KEY, true);
224 }
225
226 statsEvent('app-start');
178 } 227 }
179 228
180 @computed get cacheSize() { 229 @computed get cacheSize() {
@@ -383,6 +432,12 @@ export default class AppStore extends Store {
383 } 432 }
384 } 433 }
385 434
435 _handleLogout() {
436 if (!this.stores.user.isLoggedIn) {
437 clearInterval(this.fetchDataInterval);
438 }
439 }
440
386 // Helpers 441 // Helpers
387 _appStartsCounter() { 442 _appStartsCounter() {
388 this.actions.settings.update({ 443 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) {