aboutsummaryrefslogtreecommitdiffstats
path: root/src
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
parentpolishing (diff)
downloadferdium-app-0b08e1e7e6a07acd21af71fd27f4c4acfa34dbba.tar.gz
ferdium-app-0b08e1e7e6a07acd21af71fd27f4c4acfa34dbba.tar.zst
ferdium-app-0b08e1e7e6a07acd21af71fd27f4c4acfa34dbba.zip
Add trialStatusBar & polishing
Diffstat (limited to 'src')
-rw-r--r--src/actions/index.js2
-rw-r--r--src/actions/payment.js4
-rw-r--r--src/components/layout/AppLayout.js2
-rw-r--r--src/components/ui/FeatureList.js3
-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
-rw-r--r--src/i18n/locales/defaultMessages.json125
-rw-r--r--src/i18n/locales/en-US.json7
-rw-r--r--src/i18n/messages/src/components/layout/AppLayout.json12
-rw-r--r--src/i18n/messages/src/features/trialStatusBar/components/TrialStatusBar.json41
-rw-r--r--src/i18n/messages/src/features/trialStatusBar/containers/TrialStatusBarScreen.json54
-rw-r--r--src/stores/AppStore.js14
-rw-r--r--src/stores/FeaturesStore.js2
-rw-r--r--src/stores/PaymentStore.js37
-rw-r--r--src/stores/UserStore.js4
23 files changed, 695 insertions, 68 deletions
diff --git a/src/actions/index.js b/src/actions/index.js
index 1c033fb96..9d3684edc 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -15,6 +15,7 @@ import announcements from '../features/announcements/actions';
15import workspaces from '../features/workspaces/actions'; 15import workspaces from '../features/workspaces/actions';
16import todos from '../features/todos/actions'; 16import todos from '../features/todos/actions';
17import planSelection from '../features/planSelection/actions'; 17import planSelection from '../features/planSelection/actions';
18import trialStatusBar from '../features/trialStatusBar/actions';
18 19
19const actions = Object.assign({}, { 20const actions = Object.assign({}, {
20 service, 21 service,
@@ -35,4 +36,5 @@ export default Object.assign(
35 { workspaces }, 36 { workspaces },
36 { todos }, 37 { todos },
37 { planSelection }, 38 { planSelection },
39 { trialStatusBar },
38); 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/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index fe81b1911..9b110262a 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -20,6 +20,7 @@ import AppUpdateInfoBar from '../AppUpdateInfoBar';
20import TrialActivationInfoBar from '../TrialActivationInfoBar'; 20import TrialActivationInfoBar from '../TrialActivationInfoBar';
21import Todos from '../../features/todos/containers/TodosScreen'; 21import Todos from '../../features/todos/containers/TodosScreen';
22import PlanSelection from '../../features/planSelection/containers/PlanSelectionScreen'; 22import PlanSelection from '../../features/planSelection/containers/PlanSelectionScreen';
23import TrialStatusBar from '../../features/trialStatusBar/containers/TrialStatusBarScreen';
23 24
24function createMarkup(HTMLString) { 25function createMarkup(HTMLString) {
25 return { __html: HTMLString }; 26 return { __html: HTMLString };
@@ -174,6 +175,7 @@ class AppLayout extends Component {
174 <ShareFranz /> 175 <ShareFranz />
175 {services} 176 {services}
176 {children} 177 {children}
178 <TrialStatusBar />
177 </div> 179 </div>
178 <Todos /> 180 <Todos />
179 </div> 181 </div>
diff --git a/src/components/ui/FeatureList.js b/src/components/ui/FeatureList.js
index 732b40e40..7ba8b54d7 100644
--- a/src/components/ui/FeatureList.js
+++ b/src/components/ui/FeatureList.js
@@ -72,12 +72,13 @@ export class FeatureList extends Component {
72 static propTypes = { 72 static propTypes = {
73 className: PropTypes.string, 73 className: PropTypes.string,
74 featureClassName: PropTypes.string, 74 featureClassName: PropTypes.string,
75 plan: PropTypes.oneOf(PLANS).isRequired, 75 plan: PropTypes.oneOf(PLANS),
76 }; 76 };
77 77
78 static defaultProps = { 78 static defaultProps = {
79 className: '', 79 className: '',
80 featureClassName: '', 80 featureClassName: '',
81 plan: false,
81 } 82 }
82 83
83 static contextTypes = { 84 static contextTypes = {
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}
diff --git a/src/i18n/locales/defaultMessages.json b/src/i18n/locales/defaultMessages.json
index eafac1f87..98f37cf8a 100644
--- a/src/i18n/locales/defaultMessages.json
+++ b/src/i18n/locales/defaultMessages.json
@@ -734,39 +734,39 @@
734 "defaultMessage": "!!!Your services have been updated.", 734 "defaultMessage": "!!!Your services have been updated.",
735 "end": { 735 "end": {
736 "column": 3, 736 "column": 3,
737 "line": 32 737 "line": 33
738 }, 738 },
739 "file": "src/components/layout/AppLayout.js", 739 "file": "src/components/layout/AppLayout.js",
740 "id": "infobar.servicesUpdated", 740 "id": "infobar.servicesUpdated",
741 "start": { 741 "start": {
742 "column": 19, 742 "column": 19,
743 "line": 29 743 "line": 30
744 } 744 }
745 }, 745 },
746 { 746 {
747 "defaultMessage": "!!!Reload services", 747 "defaultMessage": "!!!Reload services",
748 "end": { 748 "end": {
749 "column": 3, 749 "column": 3,
750 "line": 36 750 "line": 37
751 }, 751 },
752 "file": "src/components/layout/AppLayout.js", 752 "file": "src/components/layout/AppLayout.js",
753 "id": "infobar.buttonReloadServices", 753 "id": "infobar.buttonReloadServices",
754 "start": { 754 "start": {
755 "column": 24, 755 "column": 24,
756 "line": 33 756 "line": 34
757 } 757 }
758 }, 758 },
759 { 759 {
760 "defaultMessage": "!!!Could not load services and user information", 760 "defaultMessage": "!!!Could not load services and user information",
761 "end": { 761 "end": {
762 "column": 3, 762 "column": 3,
763 "line": 40 763 "line": 41
764 }, 764 },
765 "file": "src/components/layout/AppLayout.js", 765 "file": "src/components/layout/AppLayout.js",
766 "id": "infobar.requiredRequestsFailed", 766 "id": "infobar.requiredRequestsFailed",
767 "start": { 767 "start": {
768 "column": 26, 768 "column": 26,
769 "line": 37 769 "line": 38
770 } 770 }
771 } 771 }
772 ], 772 ],
@@ -3915,39 +3915,39 @@
3915 "defaultMessage": "!!!per month", 3915 "defaultMessage": "!!!per month",
3916 "end": { 3916 "end": {
3917 "column": 3, 3917 "column": 3,
3918 "line": 22 3918 "line": 18
3919 }, 3919 },
3920 "file": "src/features/planSelection/components/PlanItem.js", 3920 "file": "src/features/planSelection/components/PlanItem.js",
3921 "id": "subscription.interval.perMonth", 3921 "id": "subscription.interval.perMonth",
3922 "start": { 3922 "start": {
3923 "column": 12, 3923 "column": 12,
3924 "line": 19 3924 "line": 15
3925 } 3925 }
3926 }, 3926 },
3927 { 3927 {
3928 "defaultMessage": "!!!per month & user", 3928 "defaultMessage": "!!!per month & user",
3929 "end": { 3929 "end": {
3930 "column": 3, 3930 "column": 3,
3931 "line": 26 3931 "line": 22
3932 }, 3932 },
3933 "file": "src/features/planSelection/components/PlanItem.js", 3933 "file": "src/features/planSelection/components/PlanItem.js",
3934 "id": "subscription.interval.perMonthPerUser", 3934 "id": "subscription.interval.perMonthPerUser",
3935 "start": { 3935 "start": {
3936 "column": 19, 3936 "column": 19,
3937 "line": 23 3937 "line": 19
3938 } 3938 }
3939 }, 3939 },
3940 { 3940 {
3941 "defaultMessage": "!!!Best value", 3941 "defaultMessage": "!!!Best value",
3942 "end": { 3942 "end": {
3943 "column": 3, 3943 "column": 3,
3944 "line": 30 3944 "line": 26
3945 }, 3945 },
3946 "file": "src/features/planSelection/components/PlanItem.js", 3946 "file": "src/features/planSelection/components/PlanItem.js",
3947 "id": "subscription.bestValue", 3947 "id": "subscription.bestValue",
3948 "start": { 3948 "start": {
3949 "column": 13, 3949 "column": 13,
3950 "line": 27 3950 "line": 23
3951 } 3951 }
3952 } 3952 }
3953 ], 3953 ],
@@ -4381,6 +4381,107 @@
4381 { 4381 {
4382 "descriptors": [ 4382 "descriptors": [
4383 { 4383 {
4384 "defaultMessage": "!!!Your Free Franz {plan} Trial ends in {time}.",
4385 "end": {
4386 "column": 3,
4387 "line": 16
4388 },
4389 "file": "src/features/trialStatusBar/components/TrialStatusBar.js",
4390 "id": "feature.trialStatusBar.restTime",
4391 "start": {
4392 "column": 12,
4393 "line": 13
4394 }
4395 },
4396 {
4397 "defaultMessage": "!!!Your free Franz {plan} Trial has expired, please upgrade your account.",
4398 "end": {
4399 "column": 3,
4400 "line": 20
4401 },
4402 "file": "src/features/trialStatusBar/components/TrialStatusBar.js",
4403 "id": "feature.trialStatusBar.expired",
4404 "start": {
4405 "column": 11,
4406 "line": 17
4407 }
4408 },
4409 {
4410 "defaultMessage": "!!!Upgrade now",
4411 "end": {
4412 "column": 3,
4413 "line": 24
4414 },
4415 "file": "src/features/trialStatusBar/components/TrialStatusBar.js",
4416 "id": "feature.trialStatusBar.cta",
4417 "start": {
4418 "column": 7,
4419 "line": 21
4420 }
4421 }
4422 ],
4423 "path": "src/features/trialStatusBar/components/TrialStatusBar.json"
4424 },
4425 {
4426 "descriptors": [
4427 {
4428 "defaultMessage": "!!!Downgrade your Franz Plan",
4429 "end": {
4430 "column": 3,
4431 "line": 19
4432 },
4433 "file": "src/features/trialStatusBar/containers/TrialStatusBarScreen.js",
4434 "id": "feature.trialStatusBar.fullscreen.dialog.title",
4435 "start": {
4436 "column": 15,
4437 "line": 16
4438 }
4439 },
4440 {
4441 "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.",
4442 "end": {
4443 "column": 3,
4444 "line": 23
4445 },
4446 "file": "src/features/trialStatusBar/containers/TrialStatusBarScreen.js",
4447 "id": "feature.trialStatusBar.fullscreen.dialog.message",
4448 "start": {
4449 "column": 17,
4450 "line": 20
4451 }
4452 },
4453 {
4454 "defaultMessage": "!!!Downgrade to Free",
4455 "end": {
4456 "column": 3,
4457 "line": 27
4458 },
4459 "file": "src/features/trialStatusBar/containers/TrialStatusBarScreen.js",
4460 "id": "feature.trialStatusBar.fullscreen.dialog.cta.downgrade",
4461 "start": {
4462 "column": 22,
4463 "line": 24
4464 }
4465 },
4466 {
4467 "defaultMessage": "!!!Choose Personal",
4468 "end": {
4469 "column": 3,
4470 "line": 31
4471 },
4472 "file": "src/features/trialStatusBar/containers/TrialStatusBarScreen.js",
4473 "id": "feature.trialStatusBar.fullscreen.dialog.cta.upgrade",
4474 "start": {
4475 "column": 20,
4476 "line": 28
4477 }
4478 }
4479 ],
4480 "path": "src/features/trialStatusBar/containers/TrialStatusBarScreen.json"
4481 },
4482 {
4483 "descriptors": [
4484 {
4384 "defaultMessage": "!!!Home", 4485 "defaultMessage": "!!!Home",
4385 "end": { 4486 "end": {
4386 "column": 3, 4487 "column": 3,
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 6977ec096..1ba91bdfa 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -35,6 +35,13 @@
35 "feature.todos.premium.info": "Franz Todos are available to premium users now!", 35 "feature.todos.premium.info": "Franz Todos are available to premium users now!",
36 "feature.todos.premium.rollout": "Everyone else will have to wait a little longer.", 36 "feature.todos.premium.rollout": "Everyone else will have to wait a little longer.",
37 "feature.todos.premium.upgrade": "Upgrade Account", 37 "feature.todos.premium.upgrade": "Upgrade Account",
38 "feature.trialStatusBar.cta": "Upgrade now",
39 "feature.trialStatusBar.expired": "Your free Franz {plan} Trial has expired, please upgrade your account.",
40 "feature.trialStatusBar.fullscreen.dialog.cta.downgrade": "Downgrade to Free",
41 "feature.trialStatusBar.fullscreen.dialog.cta.upgrade": "Choose Personal",
42 "feature.trialStatusBar.fullscreen.dialog.message": "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.",
43 "feature.trialStatusBar.fullscreen.dialog.title": "Downgrade your Franz Plan",
44 "feature.trialStatusBar.restTime": "Your Free Franz {plan} Trial ends in {time}.",
38 "global.api.unhealthy": "Can't connect to Franz online services", 45 "global.api.unhealthy": "Can't connect to Franz online services",
39 "global.franzProRequired": "Franz Professional Required", 46 "global.franzProRequired": "Franz Professional Required",
40 "global.notConnectedToTheInternet": "You are not connected to the internet.", 47 "global.notConnectedToTheInternet": "You are not connected to the internet.",
diff --git a/src/i18n/messages/src/components/layout/AppLayout.json b/src/i18n/messages/src/components/layout/AppLayout.json
index 22f11cedd..95da24042 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": 29, 7 "line": 30,
8 "column": 19 8 "column": 19
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 32, 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": 33, 20 "line": 34,
21 "column": 24 21 "column": 24
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 36, 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": 37, 33 "line": 38,
34 "column": 26 34 "column": 26
35 }, 35 },
36 "end": { 36 "end": {
37 "line": 40, 37 "line": 41,
38 "column": 3 38 "column": 3
39 } 39 }
40 } 40 }
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/stores/AppStore.js b/src/stores/AppStore.js
index f102fc370..329c43f32 100644
--- a/src/stores/AppStore.js
+++ b/src/stores/AppStore.js
@@ -81,6 +81,8 @@ export default class AppStore extends Store {
81 81
82 dictionaries = []; 82 dictionaries = [];
83 83
84 fetchDataInterval = null;
85
84 constructor(...args) { 86 constructor(...args) {
85 super(...args); 87 super(...args);
86 88
@@ -102,6 +104,7 @@ export default class AppStore extends Store {
102 this._setLocale.bind(this), 104 this._setLocale.bind(this),
103 this._muteAppHandler.bind(this), 105 this._muteAppHandler.bind(this),
104 this._handleFullScreen.bind(this), 106 this._handleFullScreen.bind(this),
107 this._handleLogout.bind(this),
105 ]); 108 ]);
106 } 109 }
107 110
@@ -129,6 +132,11 @@ export default class AppStore extends Store {
129 this._systemDND(); 132 this._systemDND();
130 setInterval(() => this._systemDND(), ms('5s')); 133 setInterval(() => this._systemDND(), ms('5s'));
131 134
135 this.fetchDataInterval = setInterval(() => {
136 this.stores.user.getUserInfoRequest.invalidate({ immediately: true });
137 this.stores.features.featuresRequest.invalidate({ immediately: true });
138 }, ms('10s'));
139
132 // Check for updates once every 4 hours 140 // Check for updates once every 4 hours
133 setInterval(() => this._checkForUpdates(), CHECK_INTERVAL); 141 setInterval(() => this._checkForUpdates(), CHECK_INTERVAL);
134 // Check for an update in 30s (need a delay to prevent Squirrel Installer lock file issues) 142 // Check for an update in 30s (need a delay to prevent Squirrel Installer lock file issues)
@@ -430,6 +438,12 @@ export default class AppStore extends Store {
430 } 438 }
431 } 439 }
432 440
441 _handleLogout() {
442 if (!this.stores.user.isLoggedIn) {
443 clearInterval(this.fetchDataInterval);
444 }
445 }
446
433 // Helpers 447 // Helpers
434 _appStartsCounter() { 448 _appStartsCounter() {
435 this.actions.settings.update({ 449 this.actions.settings.update({
diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js
index bffcb01bc..5d379fd3e 100644
--- a/src/stores/FeaturesStore.js
+++ b/src/stores/FeaturesStore.js
@@ -20,6 +20,7 @@ import serviceLimit from '../features/serviceLimit';
20import communityRecipes from '../features/communityRecipes'; 20import communityRecipes from '../features/communityRecipes';
21import todos from '../features/todos'; 21import todos from '../features/todos';
22import planSelection from '../features/planSelection'; 22import planSelection from '../features/planSelection';
23import trialStatusBar from '../features/trialStatusBar';
23 24
24import { DEFAULT_FEATURES_CONFIG } from '../config'; 25import { DEFAULT_FEATURES_CONFIG } from '../config';
25 26
@@ -83,5 +84,6 @@ export default class FeaturesStore extends Store {
83 communityRecipes(this.stores, this.actions); 84 communityRecipes(this.stores, this.actions);
84 todos(this.stores, this.actions); 85 todos(this.stores, this.actions);
85 planSelection(this.stores, this.actions); 86 planSelection(this.stores, this.actions);
87 trialStatusBar(this.stores, this.actions);
86 } 88 }
87} 89}
diff --git a/src/stores/PaymentStore.js b/src/stores/PaymentStore.js
index d4de476c8..b90e8f006 100644
--- a/src/stores/PaymentStore.js
+++ b/src/stores/PaymentStore.js
@@ -1,10 +1,13 @@
1import { action, observable, computed } from 'mobx'; 1import { action, observable, computed } from 'mobx';
2import { remote } from 'electron';
2 3
3import Store from './lib/Store'; 4import Store from './lib/Store';
4import CachedRequest from './lib/CachedRequest'; 5import CachedRequest from './lib/CachedRequest';
5import Request from './lib/Request'; 6import Request from './lib/Request';
6import { gaEvent } from '../lib/analytics'; 7import { gaEvent } from '../lib/analytics';
7 8
9const { BrowserWindow } = remote;
10
8export default class PaymentStore extends Store { 11export default class PaymentStore extends Store {
9 @observable plansRequest = new CachedRequest(this.api.payment, 'plans'); 12 @observable plansRequest = new CachedRequest(this.api.payment, 'plans');
10 13
@@ -14,6 +17,7 @@ export default class PaymentStore extends Store {
14 super(...args); 17 super(...args);
15 18
16 this.actions.payment.createHostedPage.listen(this._createHostedPage.bind(this)); 19 this.actions.payment.createHostedPage.listen(this._createHostedPage.bind(this));
20 this.actions.payment.upgradeAccount.listen(this._upgradeAccount.bind(this));
17 } 21 }
18 22
19 @computed get plan() { 23 @computed get plan() {
@@ -30,4 +34,37 @@ export default class PaymentStore extends Store {
30 34
31 return request; 35 return request;
32 } 36 }
37
38 @action _upgradeAccount({ planId, onCloseWindow = () => null }) {
39 let hostedPageURL = this.stores.features.features.subscribeURL;
40
41 const parsedUrl = new URL(hostedPageURL);
42 const params = new URLSearchParams(parsedUrl.search.slice(1));
43
44 params.set('plan', planId);
45
46 hostedPageURL = this.stores.user.getAuthURL(`${parsedUrl.origin}${parsedUrl.pathname}?${params.toString()}`);
47
48 const win = new BrowserWindow({
49 parent: remote.getCurrentWindow(),
50 modal: true,
51 title: '🔒 Upgrade Your Franz Account',
52 width: 800,
53 height: window.innerHeight - 100,
54 maxWidth: 800,
55 minWidth: 600,
56 webPreferences: {
57 nodeIntegration: true,
58 webviewTag: true,
59 },
60 });
61 win.loadURL(`file://${__dirname}/../index.html#/payment/${encodeURIComponent(hostedPageURL)}`);
62
63 win.on('closed', () => {
64 this.stores.user.getUserInfoRequest.invalidate({ immediately: true });
65 this.stores.features.featuresRequest.invalidate({ immediately: true });
66
67 onCloseWindow();
68 });
69 }
33} 70}
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js
index b652098f9..735e8f886 100644
--- a/src/stores/UserStore.js
+++ b/src/stores/UserStore.js
@@ -77,6 +77,8 @@ export default class UserStore extends Store {
77 77
78 @observable logoutReason = null; 78 @observable logoutReason = null;
79 79
80 fetchUserInfoInterval = null;
81
80 constructor(...args) { 82 constructor(...args) {
81 super(...args); 83 super(...args);
82 84
@@ -161,7 +163,7 @@ export default class UserStore extends Store {
161 } 163 }
162 164
163 @computed get isPremiumOverride() { 165 @computed get isPremiumOverride() {
164 return ((!this.team || !this.team.plan) && this.isPremium) || (this.team.state === 'expired' && this.isPremium); 166 return ((!this.team || !this.team.plan) && this.isPremium) || (this.team && this.team.state === 'expired' && this.isPremium);
165 } 167 }
166 168
167 @computed get isPersonal() { 169 @computed get isPersonal() {