diff options
author | 2019-10-15 21:40:14 +0200 | |
---|---|---|
committer | 2019-10-15 21:40:14 +0200 | |
commit | 91a0fb20ef02dfa342cf26df3e047b2bd4370b9f (patch) | |
tree | f411b3d7d83a24b015a2a1ed723df2e2a324cc0c /src/features/planSelection | |
parent | Optimize button width (diff) | |
download | ferdium-app-91a0fb20ef02dfa342cf26df3e047b2bd4370b9f.tar.gz ferdium-app-91a0fb20ef02dfa342cf26df3e047b2bd4370b9f.tar.zst ferdium-app-91a0fb20ef02dfa342cf26df3e047b2bd4370b9f.zip |
simplify plan selection
Diffstat (limited to 'src/features/planSelection')
-rw-r--r-- | src/features/planSelection/actions.js | 13 | ||||
-rw-r--r-- | src/features/planSelection/api.js | 26 | ||||
-rw-r--r-- | src/features/planSelection/components/PlanItem.js | 186 | ||||
-rw-r--r-- | src/features/planSelection/components/PlanSelection.js | 233 | ||||
-rw-r--r-- | src/features/planSelection/containers/PlanSelectionScreen.js | 138 | ||||
-rw-r--r-- | src/features/planSelection/index.js | 30 | ||||
-rw-r--r-- | src/features/planSelection/store.js | 113 |
7 files changed, 739 insertions, 0 deletions
diff --git a/src/features/planSelection/actions.js b/src/features/planSelection/actions.js new file mode 100644 index 000000000..21aa38ace --- /dev/null +++ b/src/features/planSelection/actions.js | |||
@@ -0,0 +1,13 @@ | |||
1 | import PropTypes from 'prop-types'; | ||
2 | import { createActionsFromDefinitions } from '../../actions/lib/actions'; | ||
3 | |||
4 | export const planSelectionActions = 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 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..a49cd40d3 --- /dev/null +++ b/src/features/planSelection/components/PlanItem.js | |||
@@ -0,0 +1,186 @@ | |||
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 | // import { FeatureList } from '../ui/FeatureList'; | ||
14 | // import { PLANS, PAYMENT_INTERVAL } from '../../config'; | ||
15 | // import { i18nPlanName, i18nIntervalName } from '../../helpers/plan-helpers'; | ||
16 | // import { PLAN_INTERVAL_CONFIG_TYPE } from './types'; | ||
17 | |||
18 | const messages = defineMessages({ | ||
19 | perMonth: { | ||
20 | id: 'subscription.interval.perMonth', | ||
21 | defaultMessage: '!!!per month', | ||
22 | }, | ||
23 | perMonthPerUser: { | ||
24 | id: 'subscription.interval.perMonthPerUser', | ||
25 | defaultMessage: '!!!per month & user', | ||
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: 20, | ||
42 | fontSize: 30, | ||
43 | color: theme.styleTypes.primary.contrast, | ||
44 | // fontWeight: 'bold', | ||
45 | }, | ||
46 | }, | ||
47 | currency: { | ||
48 | fontSize: 35, | ||
49 | }, | ||
50 | priceWrapper: { | ||
51 | height: 50, | ||
52 | }, | ||
53 | price: { | ||
54 | fontSize: 50, | ||
55 | |||
56 | '& sup': { | ||
57 | fontSize: 20, | ||
58 | verticalAlign: 20, | ||
59 | }, | ||
60 | }, | ||
61 | interval: { | ||
62 | // paddingBottom: 40, | ||
63 | }, | ||
64 | text: { | ||
65 | marginBottom: 'auto', | ||
66 | }, | ||
67 | cta: { | ||
68 | background: theme.styleTypes.primary.accent, | ||
69 | color: theme.styleTypes.primary.contrast, | ||
70 | margin: [40, 'auto', 0, 'auto'], | ||
71 | |||
72 | // '&:active': { | ||
73 | // opacity: 0.7, | ||
74 | // }, | ||
75 | }, | ||
76 | divider: { | ||
77 | width: 40, | ||
78 | border: 0, | ||
79 | borderTop: [1, 'solid', theme.styleTypes.primary.contrast], | ||
80 | margin: [30, 'auto'], | ||
81 | }, | ||
82 | header: { | ||
83 | padding: 20, | ||
84 | background: color(theme.styleTypes.primary.accent).darken(0.25).hex(), | ||
85 | color: theme.styleTypes.primary.contrast, | ||
86 | }, | ||
87 | content: { | ||
88 | padding: 20, | ||
89 | // border: [1, 'solid', 'red'], | ||
90 | background: '#EFEFEF', | ||
91 | }, | ||
92 | simpleCTA: { | ||
93 | background: 'none', | ||
94 | color: theme.styleTypes.primary.accent, | ||
95 | |||
96 | '& svg': { | ||
97 | fill: theme.styleTypes.primary.accent, | ||
98 | }, | ||
99 | }, | ||
100 | }); | ||
101 | |||
102 | |||
103 | export default @observer @injectSheet(styles) class PlanItem extends Component { | ||
104 | static propTypes = { | ||
105 | name: PropTypes.string.isRequired, | ||
106 | text: PropTypes.string.isRequired, | ||
107 | price: PropTypes.number.isRequired, | ||
108 | currency: PropTypes.string.isRequired, | ||
109 | upgrade: PropTypes.func.isRequired, | ||
110 | ctaLabel: PropTypes.string.isRequired, | ||
111 | simpleCTA: PropTypes.bool, | ||
112 | perUser: PropTypes.bool, | ||
113 | classes: PropTypes.object.isRequired, | ||
114 | children: PropTypes.element, | ||
115 | }; | ||
116 | |||
117 | static defaultProps = { | ||
118 | simpleCTA: false, | ||
119 | perUser: false, | ||
120 | children: null, | ||
121 | } | ||
122 | |||
123 | static contextTypes = { | ||
124 | intl: intlShape, | ||
125 | }; | ||
126 | |||
127 | render() { | ||
128 | const { | ||
129 | name, | ||
130 | text, | ||
131 | price, | ||
132 | currency, | ||
133 | classes, | ||
134 | upgrade, | ||
135 | ctaLabel, | ||
136 | simpleCTA, | ||
137 | perUser, | ||
138 | children, | ||
139 | } = this.props; | ||
140 | const { intl } = this.context; | ||
141 | |||
142 | const priceParts = `${price}`.split('.'); | ||
143 | // const intervalName = i18nIntervalName(PAYMENT_INTERVAL.MONTHLY, intl); | ||
144 | |||
145 | return ( | ||
146 | <div className={classes.root}> | ||
147 | <div className={classes.header}> | ||
148 | <H2 className={classes.planName}>{name}</H2> | ||
149 | <p className={classes.text}> | ||
150 | {text} | ||
151 | </p> | ||
152 | <hr className={classes.divider} /> | ||
153 | <p className={classes.priceWrapper}> | ||
154 | <span className={classes.currency}>{currency}</span> | ||
155 | <span className={classes.price}> | ||
156 | {priceParts[0]} | ||
157 | <sup>{priceParts[1]}</sup> | ||
158 | </span> | ||
159 | </p> | ||
160 | <p className={classes.interval}> | ||
161 | {intl.formatMessage(perUser ? messages.perMonthPerUser : messages.perMonth)} | ||
162 | </p> | ||
163 | </div> | ||
164 | |||
165 | <div className={classes.content}> | ||
166 | {children} | ||
167 | |||
168 | <Button | ||
169 | className={classnames({ | ||
170 | [classes.cta]: true, | ||
171 | [classes.simpleCTA]: simpleCTA, | ||
172 | })} | ||
173 | icon={simpleCTA ? mdiArrowRight : null} | ||
174 | label={( | ||
175 | <> | ||
176 | {ctaLabel} | ||
177 | </> | ||
178 | )} | ||
179 | onClick={upgrade} | ||
180 | /> | ||
181 | </div> | ||
182 | |||
183 | </div> | ||
184 | ); | ||
185 | } | ||
186 | } | ||
diff --git a/src/features/planSelection/components/PlanSelection.js b/src/features/planSelection/components/PlanSelection.js new file mode 100644 index 000000000..84d2d9e89 --- /dev/null +++ b/src/features/planSelection/components/PlanSelection.js | |||
@@ -0,0 +1,233 @@ | |||
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 } 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 | |||
15 | const messages = defineMessages({ | ||
16 | welcome: { | ||
17 | id: 'feature.planSelection.fullscreen.welcome', | ||
18 | defaultMessage: '!!!Welcome back, {name}', | ||
19 | }, | ||
20 | subheadline: { | ||
21 | id: 'feature.planSelection.fullscreen.subheadline', | ||
22 | 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.', | ||
23 | }, | ||
24 | textFree: { | ||
25 | id: 'feature.planSelection.free.text', | ||
26 | defaultMessage: '!!!Basic functionality', | ||
27 | }, | ||
28 | textPersonal: { | ||
29 | id: 'feature.planSelection.personal.text', | ||
30 | defaultMessage: '!!!More services, no waiting - ideal for personal use.', | ||
31 | }, | ||
32 | textProfessional: { | ||
33 | id: 'feature.planSelection.pro.text', | ||
34 | defaultMessage: '!!!Unlimited services and professional features for you - and your team.', | ||
35 | }, | ||
36 | ctaStayOnFree: { | ||
37 | id: 'feature.planSelection.cta.stayOnFree', | ||
38 | defaultMessage: '!!!Stay on Free', | ||
39 | }, | ||
40 | ctaDowngradeFree: { | ||
41 | id: 'feature.planSelection.cta.ctaDowngradeFree', | ||
42 | defaultMessage: '!!!Downgrade to Free', | ||
43 | }, | ||
44 | actionTrial: { | ||
45 | id: 'feature.planSelection.cta.trial', | ||
46 | defaultMessage: '!!!Start my free 14-days Trial', | ||
47 | }, | ||
48 | shortActionPersonal: { | ||
49 | id: 'feature.planSelection.cta.upgradePersonal', | ||
50 | defaultMessage: '!!!Choose Personal', | ||
51 | }, | ||
52 | shortActionPro: { | ||
53 | id: 'feature.planSelection.cta.upgradePro', | ||
54 | defaultMessage: '!!!Choose Professional', | ||
55 | }, | ||
56 | fullFeatureList: { | ||
57 | id: 'feature.planSelection.fullFeatureList', | ||
58 | defaultMessage: '!!!Complete comparison of all plans', | ||
59 | }, | ||
60 | }); | ||
61 | |||
62 | const styles = theme => ({ | ||
63 | root: { | ||
64 | background: theme.colorModalOverlayBackground, | ||
65 | width: '100%', | ||
66 | height: '100%', | ||
67 | position: 'absolute', | ||
68 | top: 0, | ||
69 | left: 0, | ||
70 | display: 'flex', | ||
71 | justifyContent: 'center', | ||
72 | alignItems: 'center', | ||
73 | zIndex: 999999, | ||
74 | }, | ||
75 | container: { | ||
76 | width: '80%', | ||
77 | height: 'auto', | ||
78 | background: theme.styleTypes.primary.accent, | ||
79 | padding: 40, | ||
80 | borderRadius: theme.borderRadius, | ||
81 | maxWidth: 1000, | ||
82 | |||
83 | '& h1, & h2': { | ||
84 | textAlign: 'center', | ||
85 | }, | ||
86 | }, | ||
87 | plans: { | ||
88 | display: 'flex', | ||
89 | margin: [40, 0, 0], | ||
90 | height: 'auto', | ||
91 | |||
92 | '& > div': { | ||
93 | margin: [0, 15], | ||
94 | flex: 1, | ||
95 | height: 'auto', | ||
96 | background: theme.styleTypes.primary.contrast, | ||
97 | boxShadow: [0, 2, 30, color('#000').alpha(0.1).rgb().string()], | ||
98 | }, | ||
99 | }, | ||
100 | bigIcon: { | ||
101 | background: theme.styleTypes.danger.accent, | ||
102 | width: 120, | ||
103 | height: 120, | ||
104 | display: 'flex', | ||
105 | alignItems: 'center', | ||
106 | borderRadius: '100%', | ||
107 | justifyContent: 'center', | ||
108 | margin: [-100, 'auto', 20], | ||
109 | |||
110 | '& svg': { | ||
111 | width: '80px !important', | ||
112 | }, | ||
113 | }, | ||
114 | headline: { | ||
115 | fontSize: 40, | ||
116 | }, | ||
117 | subheadline: { | ||
118 | maxWidth: 660, | ||
119 | fontSize: 22, | ||
120 | lineHeight: 1.1, | ||
121 | margin: [0, 'auto'], | ||
122 | }, | ||
123 | featureList: { | ||
124 | '& li': { | ||
125 | borderBottom: [1, 'solid', '#CECECE'], | ||
126 | }, | ||
127 | }, | ||
128 | fullFeatureList: { | ||
129 | marginTop: 40, | ||
130 | textAlign: 'center', | ||
131 | display: 'block', | ||
132 | color: `${theme.styleTypes.primary.contrast} !important`, | ||
133 | }, | ||
134 | }); | ||
135 | |||
136 | @injectSheet(styles) @observer | ||
137 | class PlanSelection extends Component { | ||
138 | static propTypes = { | ||
139 | classes: PropTypes.object.isRequired, | ||
140 | firstname: PropTypes.string.isRequired, | ||
141 | plans: PropTypes.object.isRequired, | ||
142 | currency: PropTypes.string.isRequired, | ||
143 | subscriptionExpired: PropTypes.bool.isRequired, | ||
144 | upgradeAccount: PropTypes.func.isRequired, | ||
145 | stayOnFree: PropTypes.func.isRequired, | ||
146 | hadSubscription: PropTypes.bool.isRequired, | ||
147 | }; | ||
148 | |||
149 | static contextTypes = { | ||
150 | intl: intlShape, | ||
151 | }; | ||
152 | |||
153 | render() { | ||
154 | const { | ||
155 | classes, | ||
156 | firstname, | ||
157 | plans, | ||
158 | currency, | ||
159 | subscriptionExpired, | ||
160 | upgradeAccount, | ||
161 | stayOnFree, | ||
162 | hadSubscription, | ||
163 | } = this.props; | ||
164 | |||
165 | const { intl } = this.context; | ||
166 | |||
167 | return ( | ||
168 | <div | ||
169 | className={classes.root} | ||
170 | > | ||
171 | <div className={classes.container}> | ||
172 | <div className={classes.bigIcon}> | ||
173 | <Icon icon={mdiRocket} /> | ||
174 | </div> | ||
175 | <H1 className={classes.headline}>{intl.formatMessage(messages.welcome, { name: firstname })}</H1> | ||
176 | <H2 className={classes.subheadline}>{intl.formatMessage(messages.subheadline)}</H2> | ||
177 | <div className={classes.plans}> | ||
178 | <PlanItem | ||
179 | name={i18nPlanName(PLANS.FREE, intl)} | ||
180 | text={intl.formatMessage(messages.textFree)} | ||
181 | price={0} | ||
182 | currency={currency} | ||
183 | ctaLabel={intl.formatMessage(subscriptionExpired ? messages.ctaDowngradeFree : messages.ctaStayOnFree)} | ||
184 | upgrade={() => stayOnFree()} | ||
185 | simpleCTA | ||
186 | > | ||
187 | <FeatureList | ||
188 | plan={PLANS.FREE} | ||
189 | className={classes.featureList} | ||
190 | /> | ||
191 | </PlanItem> | ||
192 | <PlanItem | ||
193 | name={i18nPlanName(plans.personal.yearly.id, intl)} | ||
194 | text={intl.formatMessage(messages.textPersonal)} | ||
195 | price={plans.personal.yearly.price} | ||
196 | currency={currency} | ||
197 | ctaLabel={intl.formatMessage(hadSubscription ? messages.shortActionPersonal : messages.actionTrial)} | ||
198 | upgrade={() => upgradeAccount(plans.personal.yearly.id)} | ||
199 | > | ||
200 | <FeatureList | ||
201 | plan={PLANS.PERSONAL} | ||
202 | className={classes.featureList} | ||
203 | /> | ||
204 | </PlanItem> | ||
205 | <PlanItem | ||
206 | name={i18nPlanName(plans.pro.yearly.id, intl)} | ||
207 | text={intl.formatMessage(messages.textProfessional)} | ||
208 | price={plans.pro.yearly.price} | ||
209 | currency={currency} | ||
210 | ctaLabel={intl.formatMessage(hadSubscription ? messages.shortActionPro : messages.actionTrial)} | ||
211 | upgrade={() => upgradeAccount(plans.personal.yearly.id)} | ||
212 | perUser | ||
213 | > | ||
214 | <FeatureList | ||
215 | plan={PLANS.PRO} | ||
216 | className={classes.featureList} | ||
217 | /> | ||
218 | </PlanItem> | ||
219 | </div> | ||
220 | <a | ||
221 | href="https://meetfranz.com/pricing" | ||
222 | target="_blank" | ||
223 | className={classes.fullFeatureList} | ||
224 | > | ||
225 | {intl.formatMessage(messages.fullFeatureList)} | ||
226 | </a> | ||
227 | </div> | ||
228 | </div> | ||
229 | ); | ||
230 | } | ||
231 | } | ||
232 | |||
233 | 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..b0d9b5ab5 --- /dev/null +++ b/src/features/planSelection/containers/PlanSelectionScreen.js | |||
@@ -0,0 +1,138 @@ | |||
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 } from '..'; | ||
12 | |||
13 | const { dialog, app } = remote; | ||
14 | |||
15 | const messages = defineMessages({ | ||
16 | dialogTitle: { | ||
17 | id: 'feature.planSelection.fullscreen.dialog.title', | ||
18 | defaultMessage: '!!!Downgrade your Franz Plan', | ||
19 | }, | ||
20 | dialogMessage: { | ||
21 | id: 'feature.planSelection.fullscreen.dialog.message', | ||
22 | defaultMessage: '!!!You\'re about to downgrade to our Free account. Are you sure? Click here instead to get more services and functionality for just {currency}{price} a month.', | ||
23 | }, | ||
24 | dialogCTADowngrade: { | ||
25 | id: 'feature.planSelection.fullscreen.dialog.cta.downgrade', | ||
26 | defaultMessage: '!!!Downgrade to Free', | ||
27 | }, | ||
28 | dialogCTAUpgrade: { | ||
29 | id: 'feature.planSelection.fullscreen.dialog.cta.upgrade', | ||
30 | defaultMessage: '!!!Choose Personal', | ||
31 | }, | ||
32 | }); | ||
33 | |||
34 | @inject('stores', 'actions') @observer | ||
35 | class PlanSelectionScreen extends Component { | ||
36 | static contextTypes = { | ||
37 | intl: intlShape, | ||
38 | }; | ||
39 | |||
40 | upgradeAccount(planId) { | ||
41 | const { user, features } = this.props.stores; | ||
42 | const { upgradeAccount, hideOverlay } = this.props.actions.planSelection; | ||
43 | |||
44 | upgradeAccount({ | ||
45 | planId, | ||
46 | onCloseWindow: () => { | ||
47 | hideOverlay(); | ||
48 | user.getUserInfoRequest.invalidate({ immediately: true }); | ||
49 | features.featuresRequest.invalidate({ immediately: true }); | ||
50 | }, | ||
51 | }); | ||
52 | } | ||
53 | |||
54 | render() { | ||
55 | if (!planSelectionStore || !planSelectionStore.isFeatureActive || !planSelectionStore.showPlanSelectionOverlay) { | ||
56 | return null; | ||
57 | } | ||
58 | |||
59 | const { intl } = this.context; | ||
60 | |||
61 | const { user, features } = this.props.stores; | ||
62 | const { plans, currency } = features.features.pricingConfig; | ||
63 | const { activateTrial } = this.props.actions.user; | ||
64 | const { upgradeAccount, downgradeAccount, hideOverlay } = this.props.actions.planSelection; | ||
65 | |||
66 | // const planConfig = [{ | ||
67 | // id: 'free', | ||
68 | // price: 0, | ||
69 | // }, { | ||
70 | // id: plans.personal.yearly.id, | ||
71 | // price: plans.personal.yearly.price, | ||
72 | // }, { | ||
73 | // id: plans.pro.yearly.id, | ||
74 | // price: plans.pro.yearly.price, | ||
75 | // }]; | ||
76 | |||
77 | return ( | ||
78 | <ErrorBoundary> | ||
79 | <PlanSelection | ||
80 | firstname={user.data.firstname} | ||
81 | plans={plans} | ||
82 | currency={currency} | ||
83 | upgradeAccount={(planId) => { | ||
84 | if (user.data.hadSubscription) { | ||
85 | this.upgradeAccount(planId); | ||
86 | } else { | ||
87 | activateTrial({ | ||
88 | planId, | ||
89 | }); | ||
90 | } | ||
91 | }} | ||
92 | stayOnFree={() => { | ||
93 | const selection = dialog.showMessageBoxSync(app.mainWindow, { | ||
94 | type: 'question', | ||
95 | message: intl.formatMessage(messages.dialogTitle), | ||
96 | detail: intl.formatMessage(messages.dialogMessage, { | ||
97 | currency, | ||
98 | price: plans.personal.yearly.price, | ||
99 | }), | ||
100 | buttons: [ | ||
101 | intl.formatMessage(messages.dialogCTADowngrade), | ||
102 | intl.formatMessage(messages.dialogCTAUpgrade), | ||
103 | ], | ||
104 | }); | ||
105 | |||
106 | if (selection === 0) { | ||
107 | downgradeAccount(); | ||
108 | hideOverlay(); | ||
109 | } else { | ||
110 | upgradeAccount(plans.personal.yearly.id); | ||
111 | } | ||
112 | }} | ||
113 | subscriptionExpired={user.team && user.team.state === 'expired' && !user.team.userHasDowngraded} | ||
114 | hadSubscription={user.data.hadSubscription} | ||
115 | /> | ||
116 | </ErrorBoundary> | ||
117 | ); | ||
118 | } | ||
119 | } | ||
120 | |||
121 | export default PlanSelectionScreen; | ||
122 | |||
123 | PlanSelectionScreen.wrappedComponent.propTypes = { | ||
124 | stores: PropTypes.shape({ | ||
125 | features: PropTypes.instanceOf(FeaturesStore).isRequired, | ||
126 | user: PropTypes.instanceOf(UserStore).isRequired, | ||
127 | }).isRequired, | ||
128 | actions: PropTypes.shape({ | ||
129 | planSelection: PropTypes.shape({ | ||
130 | upgradeAccount: PropTypes.func.isRequired, | ||
131 | downgradeAccount: PropTypes.func.isRequired, | ||
132 | hideOverlay: PropTypes.func.isRequired, | ||
133 | }), | ||
134 | user: PropTypes.shape({ | ||
135 | activateTrial: PropTypes.func.isRequired, | ||
136 | }), | ||
137 | }).isRequired, | ||
138 | }; | ||
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..50e46dfb3 --- /dev/null +++ b/src/features/planSelection/store.js | |||
@@ -0,0 +1,113 @@ | |||
1 | import { | ||
2 | action, | ||
3 | observable, | ||
4 | computed, | ||
5 | } from 'mobx'; | ||
6 | import { remote } from 'electron'; | ||
7 | |||
8 | import { planSelectionActions } from './actions'; | ||
9 | import { FeatureStore } from '../utils/FeatureStore'; | ||
10 | // import { createReactions } from '../../stores/lib/Reaction'; | ||
11 | import { createActionBindings } from '../utils/ActionBinding'; | ||
12 | import { downgradeUserRequest } from './api'; | ||
13 | |||
14 | const debug = require('debug')('Franz:feature:planSelection:store'); | ||
15 | |||
16 | const { BrowserWindow } = remote; | ||
17 | |||
18 | export default class PlanSelectionStore extends FeatureStore { | ||
19 | @observable isFeatureEnabled = false; | ||
20 | |||
21 | @observable isFeatureActive = false; | ||
22 | |||
23 | @observable hideOverlay = false; | ||
24 | |||
25 | @computed get showPlanSelectionOverlay() { | ||
26 | const { team } = this.stores.user; | ||
27 | if (team && !this.hideOverlay) { | ||
28 | return team.state === 'expired' && !team.userHasDowngraded; | ||
29 | } | ||
30 | |||
31 | return false; | ||
32 | } | ||
33 | |||
34 | // ========== PUBLIC API ========= // | ||
35 | |||
36 | @action start(stores, actions, api) { | ||
37 | debug('PlanSelectionStore::start'); | ||
38 | this.stores = stores; | ||
39 | this.actions = actions; | ||
40 | this.api = api; | ||
41 | |||
42 | // ACTIONS | ||
43 | |||
44 | this._registerActions(createActionBindings([ | ||
45 | [planSelectionActions.upgradeAccount, this._upgradeAccount], | ||
46 | [planSelectionActions.downgradeAccount, this._downgradeAccount], | ||
47 | [planSelectionActions.hideOverlay, this._hideOverlay], | ||
48 | ])); | ||
49 | |||
50 | // REACTIONS | ||
51 | |||
52 | // this._allReactions = createReactions([ | ||
53 | // this._setFeatureEnabledReaction, | ||
54 | // this._updateTodosConfig, | ||
55 | // this._firstLaunchReaction, | ||
56 | // this._routeCheckReaction, | ||
57 | // ]); | ||
58 | |||
59 | // this._registerReactions(this._allReactions); | ||
60 | |||
61 | this.isFeatureActive = true; | ||
62 | } | ||
63 | |||
64 | @action stop() { | ||
65 | super.stop(); | ||
66 | debug('PlanSelectionStore::stop'); | ||
67 | this.reset(); | ||
68 | this.isFeatureActive = false; | ||
69 | } | ||
70 | |||
71 | // ========== PRIVATE METHODS ========= // | ||
72 | |||
73 | // Actions | ||
74 | |||
75 | @action _upgradeAccount = ({ planId, onCloseWindow = () => null }) => { | ||
76 | let hostedPageURL = this.stores.features.features.subscribeURL; | ||
77 | |||
78 | const parsedUrl = new URL(hostedPageURL); | ||
79 | const params = new URLSearchParams(parsedUrl.search.slice(1)); | ||
80 | |||
81 | params.set('plan', planId); | ||
82 | |||
83 | hostedPageURL = this.stores.user.getAuthURL(`${parsedUrl.origin}${parsedUrl.pathname}?${params.toString()}`); | ||
84 | |||
85 | const win = new BrowserWindow({ | ||
86 | parent: remote.getCurrentWindow(), | ||
87 | modal: true, | ||
88 | title: '🔒 Upgrade Your Franz Account', | ||
89 | width: 800, | ||
90 | height: window.innerHeight - 100, | ||
91 | maxWidth: 800, | ||
92 | minWidth: 600, | ||
93 | webPreferences: { | ||
94 | nodeIntegration: true, | ||
95 | webviewTag: true, | ||
96 | }, | ||
97 | }); | ||
98 | win.loadURL(`file://${__dirname}/../../index.html#/payment/${encodeURIComponent(hostedPageURL)}`); | ||
99 | |||
100 | win.on('closed', () => { | ||
101 | onCloseWindow(); | ||
102 | }); | ||
103 | }; | ||
104 | |||
105 | @action _downgradeAccount = () => { | ||
106 | console.log('downgrade to free', downgradeUserRequest); | ||
107 | downgradeUserRequest.execute(); | ||
108 | } | ||
109 | |||
110 | @action _hideOverlay = () => { | ||
111 | this.hideOverlay = true; | ||
112 | } | ||
113 | } | ||