diff options
68 files changed, 2211 insertions, 168 deletions
diff --git a/package.json b/package.json index 526a16947..f7b633091 100644 --- a/package.json +++ b/package.json | |||
@@ -2,7 +2,7 @@ | |||
2 | "name": "ferdi", | 2 | "name": "ferdi", |
3 | "productName": "Ferdi", | 3 | "productName": "Ferdi", |
4 | "appId": "com.kytwb.ferdi", | 4 | "appId": "com.kytwb.ferdi", |
5 | "version": "5.4.0-beta.2", | 5 | "version": "5.4.0", |
6 | "description": "Messaging app for WhatsApp, Slack, Telegram, HipChat, Hangouts and many many more.", | 6 | "description": "Messaging app for WhatsApp, Slack, Telegram, HipChat, Hangouts and many many more.", |
7 | "copyright": "kytwb", | 7 | "copyright": "kytwb", |
8 | "main": "index.js", | 8 | "main": "index.js", |
diff --git a/packages/theme/src/themes/dark/index.ts b/packages/theme/src/themes/dark/index.ts index 67d0cfb71..30cc19d99 100644 --- a/packages/theme/src/themes/dark/index.ts +++ b/packages/theme/src/themes/dark/index.ts | |||
@@ -66,7 +66,7 @@ export const selectSearchColor = inputBackground; | |||
66 | 66 | ||
67 | // Modal | 67 | // Modal |
68 | export const colorModalOverlayBackground = color(legacyStyles.darkThemeBlack).alpha(0.8).rgb().string(); | 68 | export const colorModalOverlayBackground = color(legacyStyles.darkThemeBlack).alpha(0.8).rgb().string(); |
69 | export const colorModalBackground = colorContentBackground; | 69 | export const colorModalBackground = legacyStyles.darkThemeGrayDark; |
70 | 70 | ||
71 | // Services | 71 | // Services |
72 | export const services = merge({}, defaultStyles.services, { | 72 | export const services = merge({}, defaultStyles.services, { |
@@ -146,3 +146,14 @@ export const todos = merge({}, defaultStyles.todos, { | |||
146 | background: legacyStyles.themeGrayLight, | 146 | background: legacyStyles.themeGrayLight, |
147 | }, | 147 | }, |
148 | }); | 148 | }); |
149 | |||
150 | // TrialStatusBar | ||
151 | export const trialStatusBar = merge({}, defaultStyles.trialStatusBar, { | ||
152 | bar: { | ||
153 | background: legacyStyles.darkThemeGray, | ||
154 | }, | ||
155 | progressBar: { | ||
156 | background: legacyStyles.darkThemeGrayLighter, | ||
157 | progressIndicator: legacyStyles.darkThemeGrayLightest, | ||
158 | }, | ||
159 | }); | ||
diff --git a/packages/theme/src/themes/default/index.ts b/packages/theme/src/themes/default/index.ts index 1862fef6f..edf56f21e 100644 --- a/packages/theme/src/themes/default/index.ts +++ b/packages/theme/src/themes/default/index.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import color from 'color'; | 1 | import color from 'color'; |
2 | import { cloneDeep } from 'lodash'; | 2 | import { cloneDeep } from 'lodash'; |
3 | 3 | ||
4 | import { darkgreen } from 'color-name'; | ||
4 | import * as legacyStyles from '../legacy'; | 5 | import * as legacyStyles from '../legacy'; |
5 | 6 | ||
6 | export interface IStyleTypes { | 7 | export interface IStyleTypes { |
@@ -224,7 +225,7 @@ export const signup = { | |||
224 | // Todos | 225 | // Todos |
225 | export const todos = { | 226 | export const todos = { |
226 | todosLayer: { | 227 | todosLayer: { |
227 | borderLeftColor: legacyStyles.themeGrayLighter, | 228 | borderLeftColor: color(legacyStyles.themeGrayLighter).darken(0.1).hex(), |
228 | }, | 229 | }, |
229 | toggleButton: { | 230 | toggleButton: { |
230 | background: styleTypes.primary.accent, | 231 | background: styleTypes.primary.accent, |
@@ -238,3 +239,14 @@ export const todos = { | |||
238 | backgroundHover: styleTypes.primary.accent, | 239 | backgroundHover: styleTypes.primary.accent, |
239 | }, | 240 | }, |
240 | }; | 241 | }; |
242 | |||
243 | // TrialStatusBar | ||
244 | export const trialStatusBar = { | ||
245 | bar: { | ||
246 | background: legacyStyles.themeGrayLightest, | ||
247 | }, | ||
248 | progressBar: { | ||
249 | background: color(legacyStyles.themeGrayLighter).darken(0.1).hex(), | ||
250 | progressIndicator: legacyStyles.themeGrayLight, | ||
251 | }, | ||
252 | }; | ||
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'; | |||
14 | import announcements from '../features/announcements/actions'; | 14 | import announcements from '../features/announcements/actions'; |
15 | import workspaces from '../features/workspaces/actions'; | 15 | import workspaces from '../features/workspaces/actions'; |
16 | import todos from '../features/todos/actions'; | 16 | import todos from '../features/todos/actions'; |
17 | import planSelection from '../features/planSelection/actions'; | ||
18 | import trialStatusBar from '../features/trialStatusBar/actions'; | ||
17 | 19 | ||
18 | const actions = Object.assign({}, { | 20 | const 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 | ||
14 | const messages = defineMessages({ | 14 | const 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 | ||
98 | export default @injectSheet(styles) @observer class Signup extends Component { | 143 | export 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'; | |||
19 | import AppUpdateInfoBar from '../AppUpdateInfoBar'; | 19 | import AppUpdateInfoBar from '../AppUpdateInfoBar'; |
20 | import TrialActivationInfoBar from '../TrialActivationInfoBar'; | 20 | import TrialActivationInfoBar from '../TrialActivationInfoBar'; |
21 | import Todos from '../../features/todos/containers/TodosScreen'; | 21 | import Todos from '../../features/todos/containers/TodosScreen'; |
22 | import PlanSelection from '../../features/planSelection/containers/PlanSelectionScreen'; | ||
23 | import TrialStatusBar from '../../features/trialStatusBar/containers/TrialStatusBarScreen'; | ||
22 | 24 | ||
23 | function createMarkup(HTMLString) { | 25 | function 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 | ||
31 | const styles = () => ({ | 31 | const 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 | ||
44 | const styles = theme => ({ | 44 | const 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'; | |||
3 | import { defineMessages, intlShape } from 'react-intl'; | 3 | import { defineMessages, intlShape } from 'react-intl'; |
4 | 4 | ||
5 | import { FeatureItem } from './FeatureItem'; | 5 | import { FeatureItem } from './FeatureItem'; |
6 | import { PLANS } from '../../config'; | ||
6 | 7 | ||
7 | const messages = defineMessages({ | 8 | const 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 | ||
127 | export const PLANS = { | 127 | export 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 | ||
134 | export const PLANS_MAPPING = { | 134 | export 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 | ||
5 | import Signup from '../../components/auth/Signup'; | 5 | import Signup from '../../components/auth/Signup'; |
6 | import UserStore from '../../stores/UserStore'; | 6 | import UserStore from '../../stores/UserStore'; |
7 | import FeaturesStore from '../../stores/FeaturesStore'; | ||
7 | 8 | ||
8 | import { globalError as globalErrorPropType } from '../../prop-types'; | 9 | import { 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'; | |||
24 | import { workspaceActions } from '../../features/workspaces/actions'; | 24 | import { workspaceActions } from '../../features/workspaces/actions'; |
25 | import WorkspaceDrawer from '../../features/workspaces/components/WorkspaceDrawer'; | 25 | import WorkspaceDrawer from '../../features/workspaces/components/WorkspaceDrawer'; |
26 | import { workspaceStore } from '../../features/workspaces'; | 26 | import { workspaceStore } from '../../features/workspaces'; |
27 | import WorkspacesStore from '../../features/workspaces/store'; | ||
27 | 28 | ||
28 | export default @inject('stores', 'actions') @observer class AppLayoutContainer extends Component { | 29 | export 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'; | |||
5 | import PaymentStore from '../../stores/PaymentStore'; | 5 | import PaymentStore from '../../stores/PaymentStore'; |
6 | import UserStore from '../../stores/UserStore'; | 6 | import UserStore from '../../stores/UserStore'; |
7 | import AppStore from '../../stores/AppStore'; | 7 | import AppStore from '../../stores/AppStore'; |
8 | import FeaturesStore from '../../stores/FeaturesStore'; | ||
8 | 9 | ||
9 | import AccountDashboard from '../../components/settings/account/AccountDashboard'; | 10 | import AccountDashboard from '../../components/settings/account/AccountDashboard'; |
10 | import ErrorBoundary from '../../components/util/ErrorBoundary'; | 11 | import ErrorBoundary from '../../components/util/ErrorBoundary'; |
@@ -12,8 +13,9 @@ import { WEBSITE } from '../../environment'; | |||
12 | 13 | ||
13 | export default @inject('stores', 'actions') @observer class AccountScreen extends Component { | 14 | export 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 | |||
72 | AccountScreen.wrappedComponent.propTypes = { | 79 | AccountScreen.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 @@ | |||
1 | import React, { Component } from 'react'; | 1 | import React, { Component } from 'react'; |
2 | import { remote } from 'electron'; | ||
2 | import PropTypes from 'prop-types'; | 3 | import PropTypes from 'prop-types'; |
3 | import { inject, observer } from 'mobx-react'; | 4 | import { inject, observer } from 'mobx-react'; |
4 | 5 | ||
@@ -7,11 +8,21 @@ import PaymentStore from '../../stores/PaymentStore'; | |||
7 | import SubscriptionForm from '../../components/subscription/SubscriptionForm'; | 8 | import SubscriptionForm from '../../components/subscription/SubscriptionForm'; |
8 | import TrialForm from '../../components/subscription/TrialForm'; | 9 | import TrialForm from '../../components/subscription/TrialForm'; |
9 | 10 | ||
11 | const { BrowserWindow } = remote; | ||
12 | |||
10 | export default @inject('stores', 'actions') @observer class SubscriptionFormScreen extends Component { | 13 | export 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'; | |||
3 | import DelayAppComponent from './Component'; | 3 | import DelayAppComponent from './Component'; |
4 | 4 | ||
5 | import { DEFAULT_FEATURES_CONFIG } from '../../config'; | 5 | import { DEFAULT_FEATURES_CONFIG } from '../../config'; |
6 | import { getUserWorkspacesRequest } from '../workspaces/api'; | ||
6 | 7 | ||
7 | const debug = require('debug')('Ferdi:feature:delayApp'); | 8 | const 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 @@ | |||
1 | import PropTypes from 'prop-types'; | ||
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions'; | ||
3 | |||
4 | export const planSelectionActions = createActionsFromDefinitions({ | ||
5 | downgradeAccount: {}, | ||
6 | hideOverlay: {}, | ||
7 | }, PropTypes.checkPropTypes); | ||
8 | |||
9 | export 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 @@ | |||
1 | import { sendAuthRequest } from '../../api/utils/auth'; | ||
2 | import { API, API_VERSION } from '../../environment'; | ||
3 | import Request from '../../stores/lib/Request'; | ||
4 | |||
5 | const debug = require('debug')('Franz:feature:planSelection:api'); | ||
6 | |||
7 | export 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 | |||
22 | export const downgradeUserRequest = new Request(planSelectionApi, 'downgrade'); | ||
23 | |||
24 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | import injectSheet from 'react-jss'; | ||
6 | import classnames from 'classnames'; | ||
7 | import color from 'color'; | ||
8 | |||
9 | import { H2 } from '@meetfranz/ui'; | ||
10 | |||
11 | import { Button } from '@meetfranz/forms'; | ||
12 | import { mdiArrowRight } from '@mdi/js'; | ||
13 | |||
14 | const 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 | |||
29 | const 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 | |||
110 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import injectSheet from 'react-jss'; | ||
5 | import { defineMessages, intlShape } from 'react-intl'; | ||
6 | import { H1, H2, Icon } from '@meetfranz/ui'; | ||
7 | import color from 'color'; | ||
8 | |||
9 | import { mdiRocket, mdiArrowRight } from '@mdi/js'; | ||
10 | import PlanItem from './PlanItem'; | ||
11 | import { i18nPlanName } from '../../../helpers/plan-helpers'; | ||
12 | import { PLANS } from '../../../config'; | ||
13 | import { FeatureList } from '../../../components/ui/FeatureList'; | ||
14 | import Appear from '../../../components/ui/effects/Appear'; | ||
15 | import { gaPage } from '../../../lib/analytics'; | ||
16 | |||
17 | const 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 | |||
68 | const 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 | ||
171 | class 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 | |||
281 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import { observer, inject } from 'mobx-react'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | import { remote } from 'electron'; | ||
5 | import { defineMessages, intlShape } from 'react-intl'; | ||
6 | |||
7 | import FeaturesStore from '../../../stores/FeaturesStore'; | ||
8 | import UserStore from '../../../stores/UserStore'; | ||
9 | import PlanSelection from '../components/PlanSelection'; | ||
10 | import ErrorBoundary from '../../../components/util/ErrorBoundary'; | ||
11 | import { planSelectionStore, GA_CATEGORY_PLAN_SELECTION } from '..'; | ||
12 | import { gaEvent, gaPage } from '../../../lib/analytics'; | ||
13 | |||
14 | const { dialog, app } = remote; | ||
15 | |||
16 | const 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 | ||
36 | class 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 | |||
113 | export default PlanSelectionScreen; | ||
114 | |||
115 | PlanSelectionScreen.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 @@ | |||
1 | import { reaction } from 'mobx'; | ||
2 | import PlanSelectionStore from './store'; | ||
3 | |||
4 | const debug = require('debug')('Franz:feature:planSelection'); | ||
5 | |||
6 | export const GA_CATEGORY_PLAN_SELECTION = 'planSelection'; | ||
7 | |||
8 | export const planSelectionStore = new PlanSelectionStore(); | ||
9 | |||
10 | export 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 @@ | |||
1 | import { | ||
2 | action, | ||
3 | observable, | ||
4 | computed, | ||
5 | } from 'mobx'; | ||
6 | |||
7 | import { planSelectionActions } from './actions'; | ||
8 | import { FeatureStore } from '../utils/FeatureStore'; | ||
9 | import { createActionBindings } from '../utils/ActionBinding'; | ||
10 | import { downgradeUserRequest } from './api'; | ||
11 | |||
12 | const debug = require('debug')('Franz:feature:planSelection:store'); | ||
13 | |||
14 | export 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'; | |||
2 | import ms from 'ms'; | 2 | import ms from 'ms'; |
3 | 3 | ||
4 | import { state as delayAppState } from '../delayApp'; | 4 | import { state as delayAppState } from '../delayApp'; |
5 | import { planSelectionStore } from '../planSelection'; | ||
5 | 6 | ||
6 | export { default as Component } from './Component'; | 7 | export { 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 @@ | |||
1 | import PropTypes from 'prop-types'; | ||
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions'; | ||
3 | |||
4 | export const trialStatusBarActions = createActionsFromDefinitions({ | ||
5 | upgradeAccount: { | ||
6 | planId: PropTypes.string.isRequired, | ||
7 | onCloseWindow: PropTypes.func.isRequired, | ||
8 | }, | ||
9 | downgradeAccount: {}, | ||
10 | hideOverlay: {}, | ||
11 | }, PropTypes.checkPropTypes); | ||
12 | |||
13 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import injectSheet from 'react-jss'; | ||
5 | |||
6 | const 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 | ||
25 | class 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 | |||
45 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import injectSheet from 'react-jss'; | ||
5 | import { defineMessages, intlShape } from 'react-intl'; | ||
6 | import { Icon } from '@meetfranz/ui'; | ||
7 | import { mdiArrowRight, mdiWindowClose } from '@mdi/js'; | ||
8 | import classnames from 'classnames'; | ||
9 | |||
10 | import ProgressBar from './ProgressBar'; | ||
11 | |||
12 | const 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 | |||
27 | const 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 | ||
60 | class 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 | |||
135 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import { observer, inject } from 'mobx-react'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | import ms from 'ms'; | ||
5 | import { intlShape } from 'react-intl'; | ||
6 | |||
7 | import FeaturesStore from '../../../stores/FeaturesStore'; | ||
8 | import UserStore from '../../../stores/UserStore'; | ||
9 | import TrialStatusBar from '../components/TrialStatusBar'; | ||
10 | import ErrorBoundary from '../../../components/util/ErrorBoundary'; | ||
11 | import { trialStatusBarStore } from '..'; | ||
12 | import { i18nPlanName } from '../../../helpers/plan-helpers'; | ||
13 | |||
14 | @inject('stores', 'actions') @observer | ||
15 | class 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 | |||
97 | export default TrialStatusBarScreen; | ||
98 | |||
99 | TrialStatusBarScreen.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 @@ | |||
1 | import { reaction } from 'mobx'; | ||
2 | import TrialStatusBarStore from './store'; | ||
3 | |||
4 | const debug = require('debug')('Franz:feature:trialStatusBar'); | ||
5 | |||
6 | export const GA_CATEGORY_TRIAL_STATUS_BAR = 'trialStatusBar'; | ||
7 | |||
8 | export const trialStatusBarStore = new TrialStatusBarStore(); | ||
9 | |||
10 | export 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 @@ | |||
1 | import { | ||
2 | action, | ||
3 | observable, | ||
4 | computed, | ||
5 | } from 'mobx'; | ||
6 | import moment from 'moment'; | ||
7 | |||
8 | import { trialStatusBarActions } from './actions'; | ||
9 | import { FeatureStore } from '../utils/FeatureStore'; | ||
10 | import { createActionBindings } from '../utils/ActionBinding'; | ||
11 | |||
12 | const debug = require('debug')('Franz:feature:trialStatusBar:store'); | ||
13 | |||
14 | export 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'; | |||
7 | import { Button } from '@meetfranz/forms/lib'; | 7 | import { Button } from '@meetfranz/forms/lib'; |
8 | import ReactTooltip from 'react-tooltip'; | 8 | import ReactTooltip from 'react-tooltip'; |
9 | 9 | ||
10 | import { mdiPlusBox, mdiSettings } from '@mdi/js'; | 10 | import { mdiPlusBox, mdiSettings, mdiStar } from '@mdi/js'; |
11 | import WorkspaceDrawerItem from './WorkspaceDrawerItem'; | 11 | import WorkspaceDrawerItem from './WorkspaceDrawerItem'; |
12 | import { workspaceActions } from '../actions'; | 12 | import { workspaceActions } from '../actions'; |
13 | import { workspaceStore } from '../index'; | 13 | import { 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'; | |||
5 | import injectSheet from 'react-jss'; | 5 | import injectSheet from 'react-jss'; |
6 | import { Infobox, Badge } from '@meetfranz/ui'; | 6 | import { Infobox, Badge } from '@meetfranz/ui'; |
7 | 7 | ||
8 | import { mdiCheckboxMarkedCircleOutline } from '@mdi/js'; | ||
8 | import Loader from '../../../components/ui/Loader'; | 9 | import Loader from '../../../components/ui/Loader'; |
9 | import WorkspaceItem from './WorkspaceItem'; | 10 | import WorkspaceItem from './WorkspaceItem'; |
10 | import CreateWorkspaceForm from './CreateWorkspaceForm'; | 11 | import 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'; | |||
4 | const messages = defineMessages({ | 4 | const 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 | ||
72 | const settings = new Settings('app', DEFAULT_APP_SETTINGS); | ||
73 | const proxySettings = new Settings('proxy'); | ||
74 | |||
75 | // add `liftSingleInstanceLock` to settings.json to override the single instance lock | ||
76 | const liftSingleInstanceLock = settings.get('liftSingleInstanceLock') || false; | ||
77 | |||
71 | // Force single window | 78 | // Force single window |
72 | const gotTheLock = app.requestSingleInstanceLock(); | 79 | const gotTheLock = liftSingleInstanceLock ? true : app.requestSingleInstanceLock(); |
73 | if (!gotTheLock) { | 80 | if (!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 | ||
120 | const settings = new Settings('app', DEFAULT_APP_SETTINGS); | ||
121 | const proxySettings = new Settings('proxy'); | ||
122 | |||
123 | // Disable GPU acceleration | 126 | // Disable GPU acceleration |
124 | if (!settings.get('enableGPUAcceleration')) { | 127 | if (!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 | ||
27 | const debug = require('debug')('Ferdi:AppStore'); | 27 | const debug = require('debug')('Ferdi:AppStore'); |
28 | 28 | ||
29 | const { app, systemPreferences, screen } = remote; | 29 | const { |
30 | app, systemPreferences, screen, powerMonitor, | ||
31 | } = remote; | ||
30 | 32 | ||
31 | const mainWindow = remote.getCurrentWindow(); | 33 | const mainWindow = remote.getCurrentWindow(); |
32 | 34 | ||
@@ -35,6 +37,8 @@ const autoLauncher = new AutoLaunch({ | |||
35 | name: 'Ferdi', | 37 | name: 'Ferdi', |
36 | }); | 38 | }); |
37 | 39 | ||
40 | const CATALINA_NOTIFICATION_HACK_KEY = '_temp_askedForCatalinaNotificationPermissions'; | ||
41 | |||
38 | export default class AppStore extends Store { | 42 | export 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'; | |||
21 | import communityRecipes from '../features/communityRecipes'; | 21 | import communityRecipes from '../features/communityRecipes'; |
22 | import todos from '../features/todos'; | 22 | import todos from '../features/todos'; |
23 | import accentColor from '../features/accentColor'; | 23 | import accentColor from '../features/accentColor'; |
24 | import planSelection from '../features/planSelection'; | ||
25 | import trialStatusBar from '../features/trialStatusBar'; | ||
24 | 26 | ||
25 | import { DEFAULT_FEATURES_CONFIG } from '../config'; | 27 | import { 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 @@ | |||
1 | import { action, observable, computed } from 'mobx'; | 1 | import { action, observable, computed } from 'mobx'; |
2 | import { remote } from 'electron'; | ||
2 | 3 | ||
3 | import Store from './lib/Store'; | 4 | import Store from './lib/Store'; |
4 | import CachedRequest from './lib/CachedRequest'; | 5 | import CachedRequest from './lib/CachedRequest'; |
5 | import Request from './lib/Request'; | 6 | import Request from './lib/Request'; |
6 | 7 | ||
8 | const { BrowserWindow } = remote; | ||
9 | |||
7 | export default class PaymentStore extends Store { | 10 | export 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 @@ | |||
1 | import { shell } from 'electron'; | 1 | import { shell, remote } from 'electron'; |
2 | import { | 2 | import { |
3 | action, | 3 | action, |
4 | reaction, | 4 | reaction, |
@@ -23,6 +23,8 @@ import { KEEP_WS_LOADED_USID } from '../config'; | |||
23 | 23 | ||
24 | const debug = require('debug')('Ferdi:ServiceStore'); | 24 | const debug = require('debug')('Ferdi:ServiceStore'); |
25 | 25 | ||
26 | const { app } = remote; | ||
27 | |||
26 | export default class ServicesStore extends Store { | 28 | export 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'; | |||
15 | import { serviceLimitStore } from '../features/serviceLimit'; | 15 | import { serviceLimitStore } from '../features/serviceLimit'; |
16 | import { communityRecipesStore } from '../features/communityRecipes'; | 16 | import { communityRecipesStore } from '../features/communityRecipes'; |
17 | import { todosStore } from '../features/todos'; | 17 | import { todosStore } from '../features/todos'; |
18 | import { planSelectionStore } from '../features/planSelection'; | ||
18 | 19 | ||
19 | export default (api, actions, router) => { | 20 | export 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 | ||
2 | const { ipcRenderer } = require('electron'); | 1 | const { ipcRenderer } = require('electron'); |
3 | const fs = require('fs-extra'); | 2 | const fs = require('fs-extra'); |
4 | 3 | ||
4 | const debug = require('debug')('Franz:Plugin:RecipeWebview'); | ||
5 | |||
5 | class RecipeWebview { | 6 | class 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 | ||
248 | if (isDevMode) { | 250 | if (isDevMode) { |