aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/planSelection
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/planSelection')
-rw-r--r--src/features/planSelection/actions.js9
-rw-r--r--src/features/planSelection/api.js26
-rw-r--r--src/features/planSelection/components/PlanItem.js207
-rw-r--r--src/features/planSelection/components/PlanSelection.js281
-rw-r--r--src/features/planSelection/containers/PlanSelectionScreen.js132
-rw-r--r--src/features/planSelection/index.js30
-rw-r--r--src/features/planSelection/store.js68
7 files changed, 753 insertions, 0 deletions
diff --git a/src/features/planSelection/actions.js b/src/features/planSelection/actions.js
new file mode 100644
index 000000000..83f58bfd7
--- /dev/null
+++ b/src/features/planSelection/actions.js
@@ -0,0 +1,9 @@
1import PropTypes from 'prop-types';
2import { createActionsFromDefinitions } from '../../actions/lib/actions';
3
4export const planSelectionActions = createActionsFromDefinitions({
5 downgradeAccount: {},
6 hideOverlay: {},
7}, PropTypes.checkPropTypes);
8
9export default planSelectionActions;
diff --git a/src/features/planSelection/api.js b/src/features/planSelection/api.js
new file mode 100644
index 000000000..734643f10
--- /dev/null
+++ b/src/features/planSelection/api.js
@@ -0,0 +1,26 @@
1import { sendAuthRequest } from '../../api/utils/auth';
2import { API, API_VERSION } from '../../environment';
3import Request from '../../stores/lib/Request';
4
5const debug = require('debug')('Franz:feature:planSelection:api');
6
7export const planSelectionApi = {
8 downgrade: async () => {
9 const url = `${API}/${API_VERSION}/payment/downgrade`;
10 const options = {
11 method: 'PUT',
12 };
13 debug('downgrade UPDATE', url, options);
14 const result = await sendAuthRequest(url, options);
15 debug('downgrade RESULT', result);
16 if (!result.ok) throw result;
17
18 return result.ok;
19 },
20};
21
22export const downgradeUserRequest = new Request(planSelectionApi, 'downgrade');
23
24export const resetApiRequests = () => {
25 downgradeUserRequest.reset();
26};
diff --git a/src/features/planSelection/components/PlanItem.js b/src/features/planSelection/components/PlanItem.js
new file mode 100644
index 000000000..ec061377b
--- /dev/null
+++ b/src/features/planSelection/components/PlanItem.js
@@ -0,0 +1,207 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss';
6import classnames from 'classnames';
7import color from 'color';
8
9import { H2 } from '@meetfranz/ui';
10
11import { Button } from '@meetfranz/forms';
12import { mdiArrowRight } from '@mdi/js';
13
14const messages = defineMessages({
15 perMonth: {
16 id: 'subscription.interval.perMonth',
17 defaultMessage: '!!!per month',
18 },
19 perMonthPerUser: {
20 id: 'subscription.interval.perMonthPerUser',
21 defaultMessage: '!!!per month & user',
22 },
23 bestValue: {
24 id: 'subscription.bestValue',
25 defaultMessage: '!!!Best value',
26 },
27});
28
29const styles = theme => ({
30 root: {
31 display: 'flex',
32 flexDirection: 'column',
33 borderRadius: theme.borderRadius,
34 flex: 1,
35 color: theme.styleTypes.primary.accent,
36 overflow: 'hidden',
37 textAlign: 'center',
38
39 '& h2': {
40 textAlign: 'center',
41 marginBottom: 10,
42 fontSize: 30,
43 color: theme.styleTypes.primary.contrast,
44 },
45 },
46 currency: {
47 fontSize: 35,
48 },
49 priceWrapper: {
50 height: 50,
51 marginBottom: 0,
52 },
53 price: {
54 fontSize: 50,
55
56 '& sup': {
57 fontSize: 20,
58 verticalAlign: 20,
59 },
60 },
61 text: {
62 marginBottom: 'auto',
63 },
64 cta: {
65 background: theme.styleTypes.primary.accent,
66 color: theme.styleTypes.primary.contrast,
67 margin: [40, 'auto', 0, 'auto'],
68 },
69 divider: {
70 width: 40,
71 border: 0,
72 borderTop: [1, 'solid', theme.styleTypes.primary.contrast],
73 margin: [15, 'auto', 20],
74 },
75 header: {
76 padding: 20,
77 background: color(theme.styleTypes.primary.accent).darken(0.25).hex(),
78 color: theme.styleTypes.primary.contrast,
79 position: 'relative',
80 },
81 content: {
82 padding: [10, 20, 20],
83 background: '#EFEFEF',
84 },
85 simpleCTA: {
86 background: 'none',
87 color: theme.styleTypes.primary.accent,
88
89 '& svg': {
90 fill: theme.styleTypes.primary.accent,
91 },
92 },
93 bestValue: {
94 background: theme.styleTypes.success.accent,
95 color: theme.styleTypes.success.contrast,
96 right: -66,
97 top: -40,
98 height: 'auto',
99 position: 'absolute',
100 transform: 'rotateZ(45deg)',
101 textAlign: 'center',
102 padding: [5, 50],
103 transformOrigin: 'left bottom',
104 fontSize: 12,
105 boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
106 },
107});
108
109
110export default @observer @injectSheet(styles) class PlanItem extends Component {
111 static propTypes = {
112 name: PropTypes.string.isRequired,
113 text: PropTypes.string.isRequired,
114 price: PropTypes.number.isRequired,
115 currency: PropTypes.string.isRequired,
116 upgrade: PropTypes.func.isRequired,
117 ctaLabel: PropTypes.string.isRequired,
118 simpleCTA: PropTypes.bool,
119 perUser: PropTypes.bool,
120 classes: PropTypes.object.isRequired,
121 bestValue: PropTypes.bool,
122 className: PropTypes.string,
123 children: PropTypes.element,
124 };
125
126 static defaultProps = {
127 simpleCTA: false,
128 perUser: false,
129 children: null,
130 bestValue: false,
131 className: '',
132 }
133
134 static contextTypes = {
135 intl: intlShape,
136 };
137
138 render() {
139 const {
140 name,
141 text,
142 price,
143 currency,
144 classes,
145 upgrade,
146 ctaLabel,
147 simpleCTA,
148 perUser,
149 bestValue,
150 className,
151 children,
152 } = this.props;
153 const { intl } = this.context;
154
155 const priceParts = `${price}`.split('.');
156
157 return (
158 <div className={classnames({
159 [classes.root]: true,
160 [className]: className,
161 })}
162 >
163 <div className={classes.header}>
164 {bestValue && (
165 <div className={classes.bestValue}>
166 {intl.formatMessage(messages.bestValue)}
167 </div>
168 )}
169 <H2 className={classes.planName}>{name}</H2>
170 <p className={classes.text}>
171 {text}
172 </p>
173 <hr className={classes.divider} />
174 <p className={classes.priceWrapper}>
175 <span className={classes.currency}>{currency}</span>
176 <span className={classes.price}>
177 {priceParts[0]}
178 <sup>{priceParts[1]}</sup>
179 </span>
180 </p>
181 <p className={classes.interval}>
182 {intl.formatMessage(perUser ? messages.perMonthPerUser : messages.perMonth)}
183 </p>
184 </div>
185
186 <div className={classes.content}>
187 {children}
188
189 <Button
190 className={classnames({
191 [classes.cta]: true,
192 [classes.simpleCTA]: simpleCTA,
193 })}
194 icon={simpleCTA ? mdiArrowRight : null}
195 label={(
196 <>
197 {ctaLabel}
198 </>
199 )}
200 onClick={upgrade}
201 />
202 </div>
203
204 </div>
205 );
206 }
207}
diff --git a/src/features/planSelection/components/PlanSelection.js b/src/features/planSelection/components/PlanSelection.js
new file mode 100644
index 000000000..b6bb9d32d
--- /dev/null
+++ b/src/features/planSelection/components/PlanSelection.js
@@ -0,0 +1,281 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import { defineMessages, intlShape } from 'react-intl';
6import { H1, H2, Icon } from '@meetfranz/ui';
7import color from 'color';
8
9import { mdiRocket, mdiArrowRight } from '@mdi/js';
10import PlanItem from './PlanItem';
11import { i18nPlanName } from '../../../helpers/plan-helpers';
12import { PLANS } from '../../../config';
13import { FeatureList } from '../../../components/ui/FeatureList';
14import Appear from '../../../components/ui/effects/Appear';
15import { gaPage } from '../../../lib/analytics';
16
17const messages = defineMessages({
18 welcome: {
19 id: 'feature.planSelection.fullscreen.welcome',
20 defaultMessage: '!!!Are you ready to choose, {name}',
21 },
22 subheadline: {
23 id: 'feature.planSelection.fullscreen.subheadline',
24 defaultMessage: '!!!It\'s time to make a choice. Franz works best on our Personal and Professional plans. Please have a look and choose the best one for you.',
25 },
26 textFree: {
27 id: 'feature.planSelection.free.text',
28 defaultMessage: '!!!Basic functionality',
29 },
30 textPersonal: {
31 id: 'feature.planSelection.personal.text',
32 defaultMessage: '!!!More services, no waiting - ideal for personal use.',
33 },
34 textProfessional: {
35 id: 'feature.planSelection.pro.text',
36 defaultMessage: '!!!Unlimited services and professional features for you - and your team.',
37 },
38 ctaStayOnFree: {
39 id: 'feature.planSelection.cta.stayOnFree',
40 defaultMessage: '!!!Stay on Free',
41 },
42 ctaDowngradeFree: {
43 id: 'feature.planSelection.cta.ctaDowngradeFree',
44 defaultMessage: '!!!Downgrade to Free',
45 },
46 actionTrial: {
47 id: 'feature.planSelection.cta.trial',
48 defaultMessage: '!!!Start my free 14-days Trial',
49 },
50 shortActionPersonal: {
51 id: 'feature.planSelection.cta.upgradePersonal',
52 defaultMessage: '!!!Choose Personal',
53 },
54 shortActionPro: {
55 id: 'feature.planSelection.cta.upgradePro',
56 defaultMessage: '!!!Choose Professional',
57 },
58 fullFeatureList: {
59 id: 'feature.planSelection.fullFeatureList',
60 defaultMessage: '!!!Complete comparison of all plans',
61 },
62 pricesBasedOnAnnualPayment: {
63 id: 'feature.planSelection.pricesBasedOnAnnualPayment',
64 defaultMessage: '!!!All prices based on yearly payment',
65 },
66});
67
68const styles = theme => ({
69 root: {
70 background: theme.colorModalOverlayBackground,
71 width: '100%',
72 height: '100%',
73 position: 'absolute',
74 top: 0,
75 left: 0,
76 display: 'flex',
77 justifyContent: 'center',
78 alignItems: 'center',
79 zIndex: 999999,
80 overflowY: 'scroll',
81 },
82 container: {
83 width: '80%',
84 height: 'auto',
85 background: theme.styleTypes.primary.accent,
86 padding: 40,
87 borderRadius: theme.borderRadius,
88 maxWidth: 1000,
89
90 '& h1, & h2': {
91 textAlign: 'center',
92 color: theme.styleTypes.primary.contrast,
93 },
94 },
95 plans: {
96 display: 'flex',
97 margin: [40, 0, 0],
98 height: 'auto',
99
100 '& > div': {
101 margin: [0, 15],
102 flex: 1,
103 height: 'auto',
104 background: theme.styleTypes.primary.contrast,
105 boxShadow: [0, 2, 30, color('#000').alpha(0.1).rgb().string()],
106 },
107 },
108 bigIcon: {
109 background: theme.styleTypes.danger.accent,
110 width: 120,
111 height: 120,
112 display: 'flex',
113 alignItems: 'center',
114 borderRadius: '100%',
115 justifyContent: 'center',
116 margin: [-100, 'auto', 20],
117
118 '& svg': {
119 width: '80px !important',
120 height: '80px !important',
121 filter: 'drop-shadow( 0px 2px 3px rgba(0, 0, 0, 0.3))',
122 fill: theme.styleTypes.danger.contrast,
123 },
124 },
125 headline: {
126 fontSize: 40,
127 },
128 subheadline: {
129 maxWidth: 660,
130 fontSize: 22,
131 lineHeight: 1.1,
132 margin: [0, 'auto'],
133 },
134 featureList: {
135 '& li': {
136 borderBottom: [1, 'solid', '#CECECE'],
137 },
138 },
139 footer: {
140 display: 'flex',
141 color: theme.styleTypes.primary.contrast,
142 marginTop: 20,
143 padding: [0, 15],
144 },
145 fullFeatureList: {
146 marginRight: 'auto',
147 textAlign: 'center',
148 display: 'flex',
149 justifyContent: 'center',
150 alignItems: 'center',
151 color: `${theme.styleTypes.primary.contrast} !important`,
152
153 '& svg': {
154 marginRight: 5,
155 },
156 },
157 scrollContainer: {
158 border: '1px solid red',
159 overflow: 'scroll-x',
160 },
161 featuredPlan: {
162 transform: 'scale(1.05)',
163 },
164 disclaimer: {
165 textAlign: 'right',
166 margin: [10, 15, 0, 0],
167 },
168});
169
170@injectSheet(styles) @observer
171class PlanSelection extends Component {
172 static propTypes = {
173 classes: PropTypes.object.isRequired,
174 firstname: PropTypes.string.isRequired,
175 plans: PropTypes.object.isRequired,
176 currency: PropTypes.string.isRequired,
177 subscriptionExpired: PropTypes.bool.isRequired,
178 upgradeAccount: PropTypes.func.isRequired,
179 stayOnFree: PropTypes.func.isRequired,
180 hadSubscription: PropTypes.bool.isRequired,
181 };
182
183 static contextTypes = {
184 intl: intlShape,
185 };
186
187 componentDidMount() {
188 gaPage('/select-plan');
189 }
190
191 render() {
192 const {
193 classes,
194 firstname,
195 plans,
196 currency,
197 subscriptionExpired,
198 upgradeAccount,
199 stayOnFree,
200 hadSubscription,
201 } = this.props;
202
203 const { intl } = this.context;
204
205 return (
206 <Appear>
207 <div
208 className={classes.root}
209 >
210 <div className={classes.container}>
211 <div className={classes.bigIcon}>
212 <Icon icon={mdiRocket} />
213 </div>
214 <H1 className={classes.headline}>{intl.formatMessage(messages.welcome, { name: firstname })}</H1>
215 <H2 className={classes.subheadline}>{intl.formatMessage(messages.subheadline)}</H2>
216 <div className={classes.plans}>
217 <PlanItem
218 name={i18nPlanName(PLANS.FREE, intl)}
219 text={intl.formatMessage(messages.textFree)}
220 price={0}
221 currency={currency}
222 ctaLabel={intl.formatMessage(subscriptionExpired ? messages.ctaDowngradeFree : messages.ctaStayOnFree)}
223 upgrade={() => stayOnFree()}
224 simpleCTA
225 >
226 <FeatureList
227 plan={PLANS.FREE}
228 className={classes.featureList}
229 />
230 </PlanItem>
231 <PlanItem
232 name={i18nPlanName(plans.pro.yearly.id, intl)}
233 text={intl.formatMessage(messages.textProfessional)}
234 price={plans.pro.yearly.price}
235 currency={currency}
236 ctaLabel={intl.formatMessage(hadSubscription ? messages.shortActionPro : messages.actionTrial)}
237 upgrade={() => upgradeAccount(plans.pro.yearly.id)}
238 className={classes.featuredPlan}
239 perUser
240 bestValue
241 >
242 <FeatureList
243 plan={PLANS.PRO}
244 className={classes.featureList}
245 />
246 </PlanItem>
247 <PlanItem
248 name={i18nPlanName(plans.personal.yearly.id, intl)}
249 text={intl.formatMessage(messages.textPersonal)}
250 price={plans.personal.yearly.price}
251 currency={currency}
252 ctaLabel={intl.formatMessage(hadSubscription ? messages.shortActionPersonal : messages.actionTrial)}
253 upgrade={() => upgradeAccount(plans.personal.yearly.id)}
254 >
255 <FeatureList
256 plan={PLANS.PERSONAL}
257 className={classes.featureList}
258 />
259 </PlanItem>
260 </div>
261 <div className={classes.footer}>
262 <a
263 href="https://meetfranz.com/pricing"
264 target="_blank"
265 className={classes.fullFeatureList}
266 >
267 <Icon icon={mdiArrowRight} />
268 {intl.formatMessage(messages.fullFeatureList)}
269 </a>
270 {/* <p className={classes.disclaimer}> */}
271 {intl.formatMessage(messages.pricesBasedOnAnnualPayment)}
272 {/* </p> */}
273 </div>
274 </div>
275 </div>
276 </Appear>
277 );
278 }
279}
280
281export default PlanSelection;
diff --git a/src/features/planSelection/containers/PlanSelectionScreen.js b/src/features/planSelection/containers/PlanSelectionScreen.js
new file mode 100644
index 000000000..cb62f45d3
--- /dev/null
+++ b/src/features/planSelection/containers/PlanSelectionScreen.js
@@ -0,0 +1,132 @@
1import React, { Component } from 'react';
2import { observer, inject } from 'mobx-react';
3import PropTypes from 'prop-types';
4import { remote } from 'electron';
5import { defineMessages, intlShape } from 'react-intl';
6
7import FeaturesStore from '../../../stores/FeaturesStore';
8import UserStore from '../../../stores/UserStore';
9import PlanSelection from '../components/PlanSelection';
10import ErrorBoundary from '../../../components/util/ErrorBoundary';
11import { planSelectionStore, GA_CATEGORY_PLAN_SELECTION } from '..';
12import { gaEvent, gaPage } from '../../../lib/analytics';
13
14const { dialog, app } = remote;
15
16const messages = defineMessages({
17 dialogTitle: {
18 id: 'feature.planSelection.fullscreen.dialog.title',
19 defaultMessage: '!!!Downgrade your Franz Plan',
20 },
21 dialogMessage: {
22 id: 'feature.planSelection.fullscreen.dialog.message',
23 defaultMessage: '!!!You\'re about to downgrade to our Free account. Are you sure? Click here instead to get more services and functionality for just {currency}{price} a month.',
24 },
25 dialogCTADowngrade: {
26 id: 'feature.planSelection.fullscreen.dialog.cta.downgrade',
27 defaultMessage: '!!!Downgrade to Free',
28 },
29 dialogCTAUpgrade: {
30 id: 'feature.planSelection.fullscreen.dialog.cta.upgrade',
31 defaultMessage: '!!!Choose Personal',
32 },
33});
34
35@inject('stores', 'actions') @observer
36class PlanSelectionScreen extends Component {
37 static contextTypes = {
38 intl: intlShape,
39 };
40
41 upgradeAccount(planId) {
42 const { upgradeAccount } = this.props.actions.payment;
43
44 upgradeAccount({
45 planId,
46 });
47 }
48
49 render() {
50 if (!planSelectionStore || !planSelectionStore.isFeatureActive || !planSelectionStore.showPlanSelectionOverlay) {
51 return null;
52 }
53
54 const { intl } = this.context;
55
56 const { user, features } = this.props.stores;
57 const { plans, currency } = features.features.pricingConfig;
58 const { activateTrial } = this.props.actions.user;
59 const { downgradeAccount, hideOverlay } = this.props.actions.planSelection;
60
61 return (
62 <ErrorBoundary>
63 <PlanSelection
64 firstname={user.data.firstname}
65 plans={plans}
66 currency={currency}
67 upgradeAccount={(planId) => {
68 if (user.data.hadSubscription) {
69 this.upgradeAccount(planId);
70
71 gaEvent(GA_CATEGORY_PLAN_SELECTION, 'SelectPlan', planId);
72 } else {
73 activateTrial({
74 planId,
75 });
76 }
77 }}
78 stayOnFree={() => {
79 gaPage('/select-plan/downgrade');
80
81 const selection = dialog.showMessageBoxSync(app.mainWindow, {
82 type: 'question',
83 message: intl.formatMessage(messages.dialogTitle),
84 detail: intl.formatMessage(messages.dialogMessage, {
85 currency,
86 price: plans.personal.yearly.price,
87 }),
88 buttons: [
89 intl.formatMessage(messages.dialogCTADowngrade),
90 intl.formatMessage(messages.dialogCTAUpgrade),
91 ],
92 });
93
94 gaEvent(GA_CATEGORY_PLAN_SELECTION, 'SelectPlan', 'Stay on Free');
95
96 if (selection === 0) {
97 downgradeAccount();
98 hideOverlay();
99 } else {
100 this.upgradeAccount(plans.personal.yearly.id);
101
102 gaEvent(GA_CATEGORY_PLAN_SELECTION, 'SelectPlan', 'Downgrade');
103 }
104 }}
105 subscriptionExpired={user.team && user.team.state === 'expired' && !user.team.userHasDowngraded}
106 hadSubscription={user.data.hadSubscription}
107 />
108 </ErrorBoundary>
109 );
110 }
111}
112
113export default PlanSelectionScreen;
114
115PlanSelectionScreen.wrappedComponent.propTypes = {
116 stores: PropTypes.shape({
117 features: PropTypes.instanceOf(FeaturesStore).isRequired,
118 user: PropTypes.instanceOf(UserStore).isRequired,
119 }).isRequired,
120 actions: PropTypes.shape({
121 payment: PropTypes.shape({
122 upgradeAccount: PropTypes.func.isRequired,
123 }),
124 planSelection: PropTypes.shape({
125 downgradeAccount: PropTypes.func.isRequired,
126 hideOverlay: PropTypes.func.isRequired,
127 }),
128 user: PropTypes.shape({
129 activateTrial: PropTypes.func.isRequired,
130 }),
131 }).isRequired,
132};
diff --git a/src/features/planSelection/index.js b/src/features/planSelection/index.js
new file mode 100644
index 000000000..81189207a
--- /dev/null
+++ b/src/features/planSelection/index.js
@@ -0,0 +1,30 @@
1import { reaction } from 'mobx';
2import PlanSelectionStore from './store';
3
4const debug = require('debug')('Franz:feature:planSelection');
5
6export const GA_CATEGORY_PLAN_SELECTION = 'planSelection';
7
8export const planSelectionStore = new PlanSelectionStore();
9
10export default function initPlanSelection(stores, actions) {
11 stores.planSelection = planSelectionStore;
12 const { features } = stores;
13
14 // Toggle planSelection feature
15 reaction(
16 () => features.features.isPlanSelectionEnabled,
17 (isEnabled) => {
18 if (isEnabled) {
19 debug('Initializing `planSelection` feature');
20 planSelectionStore.start(stores, actions);
21 } else if (planSelectionStore.isFeatureActive) {
22 debug('Disabling `planSelection` feature');
23 planSelectionStore.stop();
24 }
25 },
26 {
27 fireImmediately: true,
28 },
29 );
30}
diff --git a/src/features/planSelection/store.js b/src/features/planSelection/store.js
new file mode 100644
index 000000000..448e323ff
--- /dev/null
+++ b/src/features/planSelection/store.js
@@ -0,0 +1,68 @@
1import {
2 action,
3 observable,
4 computed,
5} from 'mobx';
6
7import { planSelectionActions } from './actions';
8import { FeatureStore } from '../utils/FeatureStore';
9import { createActionBindings } from '../utils/ActionBinding';
10import { downgradeUserRequest } from './api';
11
12const debug = require('debug')('Franz:feature:planSelection:store');
13
14export default class PlanSelectionStore extends FeatureStore {
15 @observable isFeatureEnabled = false;
16
17 @observable isFeatureActive = false;
18
19 @observable hideOverlay = false;
20
21 @computed get showPlanSelectionOverlay() {
22 const { team, isPremium } = this.stores.user;
23 if (team && !this.hideOverlay && !isPremium) {
24 return team.state === 'expired' && !team.userHasDowngraded;
25 }
26
27 return false;
28 }
29
30 // ========== PUBLIC API ========= //
31
32 @action start(stores, actions, api) {
33 debug('PlanSelectionStore::start');
34 this.stores = stores;
35 this.actions = actions;
36 this.api = api;
37
38 // ACTIONS
39
40 this._registerActions(createActionBindings([
41 [planSelectionActions.downgradeAccount, this._downgradeAccount],
42 [planSelectionActions.hideOverlay, this._hideOverlay],
43 ]));
44
45 this.isFeatureActive = true;
46 }
47
48 @action stop() {
49 super.stop();
50 debug('PlanSelectionStore::stop');
51 this.isFeatureActive = false;
52 }
53
54 // ========== PRIVATE METHODS ========= //
55
56 // Actions
57 @action _downgradeAccount = () => {
58 downgradeUserRequest.execute();
59 }
60
61 @action _hideOverlay = () => {
62 this.hideOverlay = true;
63 }
64
65 @action _showOverlay = () => {
66 this.hideOverlay = false;
67 }
68}