aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2019-10-16 15:16:26 +0200
committerLibravatar Stefan Malzner <stefan@adlk.io>2019-10-16 15:16:26 +0200
commit0b08e1e7e6a07acd21af71fd27f4c4acfa34dbba (patch)
tree9ee5b287f3c0451012153cf281659159597d63e7 /src/features
parentpolishing (diff)
downloadferdium-app-0b08e1e7e6a07acd21af71fd27f4c4acfa34dbba.tar.gz
ferdium-app-0b08e1e7e6a07acd21af71fd27f4c4acfa34dbba.tar.zst
ferdium-app-0b08e1e7e6a07acd21af71fd27f4c4acfa34dbba.zip
Add trialStatusBar & polishing
Diffstat (limited to 'src/features')
-rw-r--r--src/features/planSelection/actions.js4
-rw-r--r--src/features/planSelection/components/PlanSelection.js2
-rw-r--r--src/features/planSelection/containers/PlanSelectionScreen.js13
-rw-r--r--src/features/planSelection/store.js40
-rw-r--r--src/features/trialStatusBar/actions.js13
-rw-r--r--src/features/trialStatusBar/components/ProgressBar.js46
-rw-r--r--src/features/trialStatusBar/components/TrialStatusBar.js135
-rw-r--r--src/features/trialStatusBar/containers/TrialStatusBarScreen.js101
-rw-r--r--src/features/trialStatusBar/index.js30
-rw-r--r--src/features/trialStatusBar/store.js72
10 files changed, 408 insertions, 48 deletions
diff --git a/src/features/planSelection/actions.js b/src/features/planSelection/actions.js
index 21aa38ace..83f58bfd7 100644
--- a/src/features/planSelection/actions.js
+++ b/src/features/planSelection/actions.js
@@ -2,10 +2,6 @@ import PropTypes from 'prop-types';
2import { createActionsFromDefinitions } from '../../actions/lib/actions'; 2import { createActionsFromDefinitions } from '../../actions/lib/actions';
3 3
4export const planSelectionActions = createActionsFromDefinitions({ 4export const planSelectionActions = createActionsFromDefinitions({
5 upgradeAccount: {
6 planId: PropTypes.string.isRequired,
7 onCloseWindow: PropTypes.func.isRequired,
8 },
9 downgradeAccount: {}, 5 downgradeAccount: {},
10 hideOverlay: {}, 6 hideOverlay: {},
11}, PropTypes.checkPropTypes); 7}, PropTypes.checkPropTypes);
diff --git a/src/features/planSelection/components/PlanSelection.js b/src/features/planSelection/components/PlanSelection.js
index 1a45cf035..cf4474114 100644
--- a/src/features/planSelection/components/PlanSelection.js
+++ b/src/features/planSelection/components/PlanSelection.js
@@ -205,7 +205,7 @@ class PlanSelection extends Component {
205 price={plans.pro.yearly.price} 205 price={plans.pro.yearly.price}
206 currency={currency} 206 currency={currency}
207 ctaLabel={intl.formatMessage(hadSubscription ? messages.shortActionPro : messages.actionTrial)} 207 ctaLabel={intl.formatMessage(hadSubscription ? messages.shortActionPro : messages.actionTrial)}
208 upgrade={() => upgradeAccount(plans.personal.yearly.id)} 208 upgrade={() => upgradeAccount(plans.pro.yearly.id)}
209 className={classes.featuredPlan} 209 className={classes.featuredPlan}
210 perUser 210 perUser
211 bestValue 211 bestValue
diff --git a/src/features/planSelection/containers/PlanSelectionScreen.js b/src/features/planSelection/containers/PlanSelectionScreen.js
index dff9051d8..6e8cdbf47 100644
--- a/src/features/planSelection/containers/PlanSelectionScreen.js
+++ b/src/features/planSelection/containers/PlanSelectionScreen.js
@@ -43,13 +43,10 @@ class PlanSelectionScreen extends Component {
43 } 43 }
44 44
45 upgradeAccount(planId) { 45 upgradeAccount(planId) {
46 const { upgradeAccount, hideOverlay } = this.props.actions.planSelection; 46 const { upgradeAccount } = this.props.actions.payment;
47 47
48 upgradeAccount({ 48 upgradeAccount({
49 planId, 49 planId,
50 onCloseWindow: () => {
51 hideOverlay();
52 },
53 }); 50 });
54 } 51 }
55 52
@@ -63,7 +60,7 @@ class PlanSelectionScreen extends Component {
63 const { user, features } = this.props.stores; 60 const { user, features } = this.props.stores;
64 const { plans, currency } = features.features.pricingConfig; 61 const { plans, currency } = features.features.pricingConfig;
65 const { activateTrial } = this.props.actions.user; 62 const { activateTrial } = this.props.actions.user;
66 const { upgradeAccount, downgradeAccount, hideOverlay } = this.props.actions.planSelection; 63 const { downgradeAccount, hideOverlay } = this.props.actions.planSelection;
67 64
68 return ( 65 return (
69 <ErrorBoundary> 66 <ErrorBoundary>
@@ -102,7 +99,7 @@ class PlanSelectionScreen extends Component {
102 downgradeAccount(); 99 downgradeAccount();
103 hideOverlay(); 100 hideOverlay();
104 } else { 101 } else {
105 upgradeAccount(plans.personal.yearly.id); 102 this.upgradeAccount(plans.personal.yearly.id);
106 103
107 gaEvent(GA_CATEGORY_PLAN_SELECTION, 'SelectPlan', 'Revoke'); 104 gaEvent(GA_CATEGORY_PLAN_SELECTION, 'SelectPlan', 'Revoke');
108 } 105 }
@@ -123,8 +120,10 @@ PlanSelectionScreen.wrappedComponent.propTypes = {
123 user: PropTypes.instanceOf(UserStore).isRequired, 120 user: PropTypes.instanceOf(UserStore).isRequired,
124 }).isRequired, 121 }).isRequired,
125 actions: PropTypes.shape({ 122 actions: PropTypes.shape({
126 planSelection: PropTypes.shape({ 123 payment: PropTypes.shape({
127 upgradeAccount: PropTypes.func.isRequired, 124 upgradeAccount: PropTypes.func.isRequired,
125 }),
126 planSelection: PropTypes.shape({
128 downgradeAccount: PropTypes.func.isRequired, 127 downgradeAccount: PropTypes.func.isRequired,
129 hideOverlay: PropTypes.func.isRequired, 128 hideOverlay: PropTypes.func.isRequired,
130 }), 129 }),
diff --git a/src/features/planSelection/store.js b/src/features/planSelection/store.js
index e229c37e5..0d4672722 100644
--- a/src/features/planSelection/store.js
+++ b/src/features/planSelection/store.js
@@ -42,7 +42,6 @@ export default class PlanSelectionStore extends FeatureStore {
42 // ACTIONS 42 // ACTIONS
43 43
44 this._registerActions(createActionBindings([ 44 this._registerActions(createActionBindings([
45 [planSelectionActions.upgradeAccount, this._upgradeAccount],
46 [planSelectionActions.downgradeAccount, this._downgradeAccount], 45 [planSelectionActions.downgradeAccount, this._downgradeAccount],
47 [planSelectionActions.hideOverlay, this._hideOverlay], 46 [planSelectionActions.hideOverlay, this._hideOverlay],
48 ])); 47 ]));
@@ -64,47 +63,12 @@ export default class PlanSelectionStore extends FeatureStore {
64 @action stop() { 63 @action stop() {
65 super.stop(); 64 super.stop();
66 debug('PlanSelectionStore::stop'); 65 debug('PlanSelectionStore::stop');
67 this.reset();
68 this.isFeatureActive = false; 66 this.isFeatureActive = false;
69 } 67 }
70 68
71 // ========== PRIVATE METHODS ========= // 69 // ========== PRIVATE METHODS ========= //
72 70
73 // Actions 71 // 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 this.stores.user.getUserInfoRequest.invalidate({ immediately: true });
102 this.stores.features.featuresRequest.invalidate({ immediately: true });
103
104 onCloseWindow();
105 });
106 };
107
108 @action _downgradeAccount = () => { 72 @action _downgradeAccount = () => {
109 downgradeUserRequest.execute(); 73 downgradeUserRequest.execute();
110 } 74 }
@@ -112,4 +76,8 @@ export default class PlanSelectionStore extends FeatureStore {
112 @action _hideOverlay = () => { 76 @action _hideOverlay = () => {
113 this.hideOverlay = true; 77 this.hideOverlay = true;
114 } 78 }
79
80 @action _showOverlay = () => {
81 this.hideOverlay = false;
82 }
115} 83}
diff --git a/src/features/trialStatusBar/actions.js b/src/features/trialStatusBar/actions.js
new file mode 100644
index 000000000..38df76458
--- /dev/null
+++ b/src/features/trialStatusBar/actions.js
@@ -0,0 +1,13 @@
1import PropTypes from 'prop-types';
2import { createActionsFromDefinitions } from '../../actions/lib/actions';
3
4export const trialStatusBarActions = createActionsFromDefinitions({
5 upgradeAccount: {
6 planId: PropTypes.string.isRequired,
7 onCloseWindow: PropTypes.func.isRequired,
8 },
9 downgradeAccount: {},
10 hideOverlay: {},
11}, PropTypes.checkPropTypes);
12
13export default trialStatusBarActions;
diff --git a/src/features/trialStatusBar/components/ProgressBar.js b/src/features/trialStatusBar/components/ProgressBar.js
new file mode 100644
index 000000000..80d478d8c
--- /dev/null
+++ b/src/features/trialStatusBar/components/ProgressBar.js
@@ -0,0 +1,46 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5
6const styles = theme => ({
7 root: {
8 background: theme.trialStatusBar.progressBar.background,
9 width: '25%',
10 maxWidth: 200,
11 height: 8,
12 display: 'flex',
13 alignItems: 'center',
14 borderRadius: theme.borderRadius,
15 overflow: 'hidden',
16 },
17 progress: {
18 background: theme.trialStatusBar.progressBar.progressIndicator,
19 width: ({ percent }) => `${percent}%`,
20 height: '100%',
21 },
22});
23
24@injectSheet(styles) @observer
25class ProgressBar extends Component {
26 static propTypes = {
27 classes: PropTypes.object.isRequired,
28 percent: PropTypes.number.isRequired,
29 };
30
31 render() {
32 const {
33 classes,
34 } = this.props;
35
36 return (
37 <div
38 className={classes.root}
39 >
40 <div className={classes.progress} />
41 </div>
42 );
43 }
44}
45
46export default ProgressBar;
diff --git a/src/features/trialStatusBar/components/TrialStatusBar.js b/src/features/trialStatusBar/components/TrialStatusBar.js
new file mode 100644
index 000000000..b8fe4acc9
--- /dev/null
+++ b/src/features/trialStatusBar/components/TrialStatusBar.js
@@ -0,0 +1,135 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import { defineMessages, intlShape } from 'react-intl';
6import { Icon } from '@meetfranz/ui';
7import { mdiArrowRight, mdiWindowClose } from '@mdi/js';
8import classnames from 'classnames';
9
10import ProgressBar from './ProgressBar';
11
12const messages = defineMessages({
13 restTime: {
14 id: 'feature.trialStatusBar.restTime',
15 defaultMessage: '!!!Your Free Franz {plan} Trial ends in {time}.',
16 },
17 expired: {
18 id: 'feature.trialStatusBar.expired',
19 defaultMessage: '!!!Your free Franz {plan} Trial has expired, please upgrade your account.',
20 },
21 cta: {
22 id: 'feature.trialStatusBar.cta',
23 defaultMessage: '!!!Upgrade now',
24 },
25});
26
27const styles = theme => ({
28 root: {
29 background: theme.trialStatusBar.bar.background,
30 width: '100%',
31 height: 25,
32 order: 10,
33 display: 'flex',
34 alignItems: 'center',
35 fontSize: 12,
36 padding: [0, 10],
37 justifyContent: 'flex-end',
38 },
39 ended: {
40 background: theme.styleTypes.warning.accent,
41 color: theme.styleTypes.warning.contrast,
42 },
43 message: {
44 marginLeft: 20,
45 },
46 action: {
47 marginLeft: 20,
48 fontSize: 12,
49 color: theme.colorText,
50 textDecoration: 'underline',
51 display: 'flex',
52
53 '& svg': {
54 margin: [1, 2, 0, 0],
55 },
56 },
57});
58
59@injectSheet(styles) @observer
60class TrialStatusBar extends Component {
61 static propTypes = {
62 planName: PropTypes.string.isRequired,
63 percent: PropTypes.number.isRequired,
64 upgradeAccount: PropTypes.func.isRequired,
65 hideOverlay: PropTypes.func.isRequired,
66 trialEnd: PropTypes.string.isRequired,
67 hasEnded: PropTypes.bool.isRequired,
68 classes: PropTypes.object.isRequired,
69 };
70
71 static contextTypes = {
72 intl: intlShape,
73 };
74
75 render() {
76 const {
77 planName,
78 percent,
79 upgradeAccount,
80 hideOverlay,
81 trialEnd,
82 hasEnded,
83 classes,
84 } = this.props;
85
86 const { intl } = this.context;
87
88 return (
89 <div
90 className={classnames({
91 [classes.root]: true,
92 [classes.ended]: hasEnded,
93 })}
94 >
95 <ProgressBar
96 percent={percent}
97 />
98 {' '}
99 <span className={classes.message}>
100 {!hasEnded ? (
101 intl.formatMessage(messages.restTime, {
102 plan: planName,
103 time: trialEnd,
104 })
105 ) : (
106 intl.formatMessage(messages.expired, {
107 plan: planName,
108 })
109 )}
110 </span>
111 <button
112 className={classes.action}
113 type="button"
114 onClick={() => {
115 upgradeAccount();
116 }}
117 >
118 <Icon icon={mdiArrowRight} />
119 {intl.formatMessage(messages.cta)}
120 </button>
121 <button
122 className={classes.action}
123 type="button"
124 onClick={() => {
125 hideOverlay();
126 }}
127 >
128 <Icon icon={mdiWindowClose} />
129 </button>
130 </div>
131 );
132 }
133}
134
135export default TrialStatusBar;
diff --git a/src/features/trialStatusBar/containers/TrialStatusBarScreen.js b/src/features/trialStatusBar/containers/TrialStatusBarScreen.js
new file mode 100644
index 000000000..eb0aafaea
--- /dev/null
+++ b/src/features/trialStatusBar/containers/TrialStatusBarScreen.js
@@ -0,0 +1,101 @@
1import React, { Component } from 'react';
2import { observer, inject } from 'mobx-react';
3import PropTypes from 'prop-types';
4import ms from 'ms';
5
6import FeaturesStore from '../../../stores/FeaturesStore';
7import UserStore from '../../../stores/UserStore';
8import TrialStatusBar from '../components/TrialStatusBar';
9import ErrorBoundary from '../../../components/util/ErrorBoundary';
10import { trialStatusBarStore } from '..';
11
12@inject('stores', 'actions') @observer
13class TrialStatusBarScreen extends Component {
14 state = {
15 showOverlay: true,
16 percent: 0,
17 restTime: '',
18 hasEnded: false,
19 };
20
21 percentInterval = null;
22
23 componentDidMount() {
24 this.percentInterval = setInterval(() => {
25 this.calculateRestTime();
26 }, ms('1m'));
27
28 this.calculateRestTime();
29 }
30
31 componentWillUnmount() {
32 clearInterval(this.percentInterval);
33 }
34
35 calculateRestTime() {
36 const { trialEndTime } = trialStatusBarStore;
37 const percent = Math.abs(100 - Math.abs(trialEndTime.asMilliseconds()) * 100 / ms('14d')).toFixed(2);
38 const restTime = trialEndTime.humanize();
39 const hasEnded = trialEndTime.asMilliseconds() > 0;
40
41 this.setState({
42 percent,
43 restTime,
44 hasEnded,
45 });
46 }
47
48 hideOverlay() {
49 this.setState({
50 showOverlay: false,
51 });
52 }
53
54
55 render() {
56 const {
57 showOverlay,
58 percent,
59 restTime,
60 hasEnded,
61 } = this.state;
62
63 if (!trialStatusBarStore || !trialStatusBarStore.isFeatureActive || !showOverlay || !trialStatusBarStore.showTrialStatusBarOverlay) {
64 return null;
65 }
66
67 const { user } = this.props.stores;
68 const { upgradeAccount } = this.props.actions.payment;
69
70 console.log('hasEnded', hasEnded);
71
72 return (
73 <ErrorBoundary>
74 <TrialStatusBar
75 planName="Professional"
76 percent={percent < 5 ? 5 : percent}
77 trialEnd={restTime}
78 upgradeAccount={() => upgradeAccount({
79 planId: user.team.plan,
80 })}
81 hideOverlay={() => this.hideOverlay()}
82 hasEnded={hasEnded}
83 />
84 </ErrorBoundary>
85 );
86 }
87}
88
89export default TrialStatusBarScreen;
90
91TrialStatusBarScreen.wrappedComponent.propTypes = {
92 stores: PropTypes.shape({
93 features: PropTypes.instanceOf(FeaturesStore).isRequired,
94 user: PropTypes.instanceOf(UserStore).isRequired,
95 }).isRequired,
96 actions: PropTypes.shape({
97 payment: PropTypes.shape({
98 upgradeAccount: PropTypes.func.isRequired,
99 }),
100 }).isRequired,
101};
diff --git a/src/features/trialStatusBar/index.js b/src/features/trialStatusBar/index.js
new file mode 100644
index 000000000..ec84cdfd7
--- /dev/null
+++ b/src/features/trialStatusBar/index.js
@@ -0,0 +1,30 @@
1import { reaction } from 'mobx';
2import TrialStatusBarStore from './store';
3
4const debug = require('debug')('Franz:feature:trialStatusBar');
5
6export const GA_CATEGORY_TRIAL_STATUS_BAR = 'trialStatusBar';
7
8export const trialStatusBarStore = new TrialStatusBarStore();
9
10export default function initTrialStatusBar(stores, actions) {
11 stores.trialStatusBar = trialStatusBarStore;
12 const { features } = stores;
13
14 // Toggle trialStatusBar feature
15 reaction(
16 () => features.features.isTrialStatusBarEnabled,
17 (isEnabled) => {
18 if (isEnabled) {
19 debug('Initializing `trialStatusBar` feature');
20 trialStatusBarStore.start(stores, actions);
21 } else if (trialStatusBarStore.isFeatureActive) {
22 debug('Disabling `trialStatusBar` feature');
23 trialStatusBarStore.stop();
24 }
25 },
26 {
27 fireImmediately: true,
28 },
29 );
30}
diff --git a/src/features/trialStatusBar/store.js b/src/features/trialStatusBar/store.js
new file mode 100644
index 000000000..89cf32392
--- /dev/null
+++ b/src/features/trialStatusBar/store.js
@@ -0,0 +1,72 @@
1import {
2 action,
3 observable,
4 computed,
5} from 'mobx';
6import moment from 'moment';
7
8import { trialStatusBarActions } from './actions';
9import { FeatureStore } from '../utils/FeatureStore';
10import { createActionBindings } from '../utils/ActionBinding';
11
12const debug = require('debug')('Franz:feature:trialStatusBar:store');
13
14export default class TrialStatusBarStore extends FeatureStore {
15 @observable isFeatureActive = false;
16
17 @observable isFeatureEnabled = false;
18
19 @computed get showTrialStatusBarOverlay() {
20 if (this.isFeatureActive) {
21 const { team } = this.stores.user;
22 if (team && !this.hideOverlay) {
23 return team.state !== 'expired' && team.isTrial;
24 }
25 }
26
27 return false;
28 }
29
30 @computed get trialEndTime() {
31 if (this.isFeatureActive) {
32 const { team } = this.stores.user;
33
34 if (team && !this.hideOverlay) {
35 return moment.duration(moment().diff(team.trialEnd));
36 }
37 }
38
39 return moment.duration();
40 }
41
42 // ========== PUBLIC API ========= //
43
44 @action start(stores, actions, api) {
45 debug('TrialStatusBarStore::start');
46 this.stores = stores;
47 this.actions = actions;
48 this.api = api;
49
50 // ACTIONS
51
52 this._registerActions(createActionBindings([
53 [trialStatusBarActions.hideOverlay, this._hideOverlay],
54 ]));
55
56 this.isFeatureActive = true;
57 }
58
59 @action stop() {
60 super.stop();
61 debug('TrialStatusBarStore::stop');
62 this.isFeatureActive = false;
63 }
64
65 // ========== PRIVATE METHODS ========= //
66
67 // Actions
68
69 @action _hideOverlay = () => {
70 this.hideOverlay = true;
71 }
72}