aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2019-07-04 15:54:27 +0200
committerLibravatar Stefan Malzner <stefan@adlk.io>2019-07-04 15:54:27 +0200
commit268db27162e8d2cd0252b1be9bf69006cf6323ca (patch)
tree6ca47ee4159ba79f88d8848ac2ba9a8a8a7700da /src
parentMerge branch 'release/5.2.0-beta.4' into feature/new-pricing (diff)
downloadferdium-app-268db27162e8d2cd0252b1be9bf69006cf6323ca.tar.gz
ferdium-app-268db27162e8d2cd0252b1be9bf69006cf6323ca.tar.zst
ferdium-app-268db27162e8d2cd0252b1be9bf69006cf6323ca.zip
Add trial onboarding during signup
Diffstat (limited to 'src')
-rw-r--r--src/actions/user.js3
-rw-r--r--src/api/UserApi.js4
-rw-r--r--src/api/server/ServerApi.js18
-rw-r--r--src/components/auth/Pricing.js246
-rw-r--r--src/components/services/content/Services.js80
-rw-r--r--src/components/ui/FeatureItem.js36
-rw-r--r--src/components/ui/FeatureList.js89
-rw-r--r--src/containers/auth/PricingScreen.js40
-rw-r--r--src/containers/layout/AppLayoutContainer.js4
-rw-r--r--src/i18n/messages/src/components/auth/Pricing.json101
-rw-r--r--src/i18n/messages/src/components/services/content/Services.json8
-rw-r--r--src/i18n/messages/src/components/ui/FeatureList.json132
-rw-r--r--src/stores/UserStore.js19
-rw-r--r--src/styles/auth.scss2
-rw-r--r--src/styles/reset.scss1
15 files changed, 636 insertions, 147 deletions
diff --git a/src/actions/user.js b/src/actions/user.js
index ccf1fa56a..5d7d9a899 100644
--- a/src/actions/user.js
+++ b/src/actions/user.js
@@ -17,6 +17,9 @@ export default {
17 retrievePassword: { 17 retrievePassword: {
18 email: PropTypes.string.isRequired, 18 email: PropTypes.string.isRequired,
19 }, 19 },
20 activateTrial: {
21 planId: PropTypes.string.isRequired,
22 },
20 invite: { 23 invite: {
21 invites: PropTypes.array.isRequired, 24 invites: PropTypes.array.isRequired,
22 }, 25 },
diff --git a/src/api/UserApi.js b/src/api/UserApi.js
index edfb88988..8ba8cd1e9 100644
--- a/src/api/UserApi.js
+++ b/src/api/UserApi.js
@@ -25,6 +25,10 @@ export default class UserApi {
25 return this.server.retrievePassword(email); 25 return this.server.retrievePassword(email);
26 } 26 }
27 27
28 activateTrial(data) {
29 return this.server.activateTrial(data);
30 }
31
28 invite(data) { 32 invite(data) {
29 return this.server.inviteUser(data); 33 return this.server.inviteUser(data);
30 } 34 }
diff --git a/src/api/server/ServerApi.js b/src/api/server/ServerApi.js
index a9ce202ff..f2568d597 100644
--- a/src/api/server/ServerApi.js
+++ b/src/api/server/ServerApi.js
@@ -77,6 +77,22 @@ export default class ServerApi {
77 return u.token; 77 return u.token;
78 } 78 }
79 79
80 async activateTrial(data) {
81 const request = await sendAuthRequest(`${API_URL}/payment/trial`, {
82 method: 'POST',
83 body: JSON.stringify(data),
84 });
85 if (!request.ok) {
86 throw request;
87 }
88 const trial = await request.json();
89
90 console.log(trial);
91
92 debug('ServerApi::signup resolves', trial);
93 return true;
94 }
95
80 async inviteUser(data) { 96 async inviteUser(data) {
81 const request = await sendAuthRequest(`${API_URL}/invite`, { 97 const request = await sendAuthRequest(`${API_URL}/invite`, {
82 method: 'POST', 98 method: 'POST',
@@ -469,7 +485,7 @@ export default class ServerApi {
469 return services; 485 return services;
470 } 486 }
471 } catch (err) { 487 } catch (err) {
472 throw (new Error('ServerApi::getLegacyServices no config found')); 488 console.error('ServerApi::getLegacyServices no config found');
473 } 489 }
474 490
475 return []; 491 return [];
diff --git a/src/components/auth/Pricing.js b/src/components/auth/Pricing.js
index 7ab14f429..d20779025 100644
--- a/src/components/auth/Pricing.js
+++ b/src/components/auth/Pricing.js
@@ -1,40 +1,107 @@
1import React, { Component } from 'react'; 1import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5// import { Link } from 'react-router'; 5import injectSheet from 'react-jss';
6import { H2, Loader } from '@meetfranz/ui';
7import classnames from 'classnames';
8
9import { Button } from '@meetfranz/forms';
10import { FeatureItem } from '../ui/FeatureItem';
11import FeatureList from '../ui/FeatureList';
6 12
7// import Button from '../ui/Button';
8import Loader from '../ui/Loader';
9import Appear from '../ui/effects/Appear';
10import SubscriptionForm from '../../containers/subscription/SubscriptionFormScreen';
11 13
12const messages = defineMessages({ 14const messages = defineMessages({
13 headline: { 15 headline: {
14 id: 'pricing.headline', 16 id: 'pricing.trial.headline',
15 defaultMessage: '!!!Support Franz', 17 defaultMessage: '!!!Franz Professional',
18 },
19 personalOffer: {
20 id: 'pricing.trial.subheadline',
21 defaultMessage: '!!!Your personal welcome offer:',
22 },
23 noStringsAttachedHeadline: {
24 id: 'pricing.trial.terms.headline',
25 defaultMessage: '!!!No strings attached',
26 },
27 noCreditCard: {
28 id: 'pricing.trial.terms.noCreditCard',
29 defaultMessage: '!!!No credit card required',
16 }, 30 },
17 monthlySupportLabel: { 31 automaticTrialEnd: {
18 id: 'pricing.support.label', 32 id: 'pricing.trial.terms.automaticTrialEnd',
19 defaultMessage: '!!!Select your support plan', 33 defaultMessage: '!!!Your free trial ends automatically after 14 days',
20 }, 34 },
21 submitButtonLabel: { 35 activationError: {
22 id: 'pricing.submit.label', 36 id: 'pricing.trial.error',
23 defaultMessage: '!!!Support the development of Franz', 37 defaultMessage: '!!!Sorry, we could not activate your trial!',
24 }, 38 },
25 skipPayment: { 39 ctaAccept: {
26 id: 'pricing.link.skipPayment', 40 id: 'pricing.trial.cta.accept',
27 defaultMessage: '!!!I don\'t want to support the development of Franz.', 41 defaultMessage: '!!!Yes, upgrade my account to Franz Professional',
42 },
43 ctaSkip: {
44 id: 'pricing.trial.cta.skip',
45 defaultMessage: '!!!Continue to Franz',
46 },
47 featuresHeadline: {
48 id: 'pricing.trial.features.headline',
49 defaultMessage: '!!!Franz Professional includes:',
28 }, 50 },
29}); 51});
30 52
31export default @observer class Signup extends Component { 53const styles = theme => ({
54 container: {
55 position: 'relative',
56 marginLeft: -150,
57 },
58 welcomeOffer: {
59 textAlign: 'center',
60 fontWeight: 'bold',
61 },
62 keyTerms: {
63 textAlign: 'center',
64 },
65 content: {
66 position: 'relative',
67 zIndex: 20,
68 },
69 featureContainer: {
70 width: 300,
71 position: 'absolute',
72 left: 'calc(100% / 2 + 225px)',
73 top: 155,
74 background: theme.signup.pricing.feature.background,
75 height: 'auto',
76 padding: 20,
77 borderTopRightRadius: theme.borderRadius,
78 borderBottomRightRadius: theme.borderRadius,
79 zIndex: 10,
80 },
81 featureItem: {
82 borderBottom: [1, 'solid', theme.signup.pricing.feature.border],
83 },
84 cta: {
85 marginTop: 40,
86 width: '100%',
87 },
88 skipLink: {
89 textAlign: 'center',
90 marginTop: 10,
91 },
92 error: {
93 margin: [20, 0, 0],
94 color: theme.styleTypes.danger.accent,
95 },
96});
97
98export default @observer @injectSheet(styles) class Signup extends Component {
32 static propTypes = { 99 static propTypes = {
33 donor: MobxPropTypes.objectOrObservableObject.isRequired, 100 onSubmit: PropTypes.func.isRequired,
34 isLoading: PropTypes.bool.isRequired, 101 isLoadingRequiredData: PropTypes.bool.isRequired,
35 isLoadingUser: PropTypes.bool.isRequired, 102 isActivatingTrial: PropTypes.bool.isRequired,
36 onCloseSubscriptionWindow: PropTypes.func.isRequired, 103 trialActivationError: PropTypes.bool.isRequired,
37 skipAction: PropTypes.func.isRequired, 104 classes: PropTypes.object.isRequired,
38 }; 105 };
39 106
40 static contextTypes = { 107 static contextTypes = {
@@ -43,70 +110,37 @@ export default @observer class Signup extends Component {
43 110
44 render() { 111 render() {
45 const { 112 const {
46 donor, 113 onSubmit,
47 isLoading, 114 isLoadingRequiredData,
48 isLoadingUser, 115 isActivatingTrial,
49 onCloseSubscriptionWindow, 116 trialActivationError,
50 skipAction, 117 classes,
51 } = this.props; 118 } = this.props;
52 const { intl } = this.context; 119 const { intl } = this.context;
53 120
54 return ( 121 return (
55 <div className="auth__scroll-container"> 122 <div className={classnames('auth__scroll-container', classes.container)}>
56 <div className="auth__container auth__container--signup"> 123 <div className={classnames('auth__container', 'auth__container--signup', classes.content)}>
57 <form className="franz-form auth__form"> 124 <form className="franz-form auth__form">
58 <img 125 {isLoadingRequiredData ? <Loader /> : (
59 src="./assets/images/sm.png" 126 <img
60 className="auth__logo auth__logo--sm" 127 src="./assets/images/sm.png"
61 alt="" 128 className="auth__logo auth__logo--sm"
62 /> 129 alt=""
130 />
131 )}
132 <p className={classes.welcomeOffer}>{intl.formatMessage(messages.personalOffer)}</p>
63 <h1>{intl.formatMessage(messages.headline)}</h1> 133 <h1>{intl.formatMessage(messages.headline)}</h1>
64 <div className="auth__letter"> 134 <div className="auth__letter">
65 {isLoadingUser && ( 135 <p>
66 <p>Loading</p> 136 We built Franz with a lot of effort, manpower and love,
67 )} 137 to boost up your messaging experience.
68 {!isLoadingUser && ( 138 <br />
69 donor.amount ? ( 139 </p>
70 <span> 140 <p>
71 <p> 141 Get the free 14 day Franz Professional trial and see your communication evolving.
72 Thank you so much for your previous donation of 142 <br />
73 {' '} 143 </p>
74 <strong>
75 $
76 {donor.amount}
77 </strong>
78 .
79 <br />
80 Your support allowed us to get where we are today.
81 <br />
82 </p>
83 <p>
84 As an early supporter, you get
85 {' '}
86 <strong>a lifetime premium supporter license</strong>
87 {' '}
88 without any
89 additional charges.
90 </p>
91 <p>
92 However, If you want to keep supporting us, you are more than welcome to subscribe to a plan.
93 <br />
94 <br />
95 </p>
96 </span>
97 ) : (
98 <span>
99 <p>
100 We built Franz with a lot of effort, manpower and love,
101 to bring you the best messaging experience.
102 <br />
103 </p>
104 <p>
105 Getting a Franz Premium Supporter License will allow us to keep improving Franz for you.
106 </p>
107 </span>
108 )
109 )}
110 <p> 144 <p>
111 Thanks for being a hero. 145 Thanks for being a hero.
112 </p> 146 </p>
@@ -114,20 +148,48 @@ export default @observer class Signup extends Component {
114 <strong>Stefan Malzner</strong> 148 <strong>Stefan Malzner</strong>
115 </p> 149 </p>
116 </div> 150 </div>
117 <Loader loaded={!isLoading}> 151 <div className={classes.keyTerms}>
118 <Appear transitionName="slideDown"> 152 <H2>
119 <span className="label">{intl.formatMessage(messages.monthlySupportLabel)}</span> 153 {intl.formatMessage(messages.noStringsAttachedHeadline)}
120 <SubscriptionForm 154 </H2>
121 onCloseWindow={onCloseSubscriptionWindow} 155 <ul className={classes.keyTermsList}>
122 showSkipOption 156 <FeatureItem icon="👉" name={intl.formatMessage(messages.noCreditCard)} />
123 skipAction={skipAction} 157 <FeatureItem icon="👉" name={intl.formatMessage(messages.automaticTrialEnd)} />
124 hideInfo={Boolean(donor.amount)} 158 </ul>
125 skipButtonLabel={intl.formatMessage(messages.skipPayment)} 159 </div>
126 /> 160 {trialActivationError && (
127 </Appear> 161 <p className={classes.error}>{intl.formatMessage(messages.activationError)}</p>
128 </Loader> 162 )}
163 <Button
164 label={intl.formatMessage(messages.ctaAccept)}
165 className={classes.cta}
166 onClick={onSubmit}
167 busy={isActivatingTrial}
168 disabled={isLoadingRequiredData || isActivatingTrial}
169 />
170 <p className={classes.skipLink}>
171 <a href="#/">{intl.formatMessage(messages.ctaSkip)}</a>
172 </p>
129 </form> 173 </form>
130 </div> 174 </div>
175 <div className={classes.featureContainer}>
176 <H2>
177 {intl.formatMessage(messages.featuresHeadline)}
178 </H2>
179 {/* <ul className={classes.features}>
180 <FeatureItem name="Add unlimited services" className={classes.featureItem} />
181 <FeatureItem name="Spellchecker support" className={classes.featureItem} />
182 <FeatureItem name="Workspaces" className={classes.featureItem} />
183 <FeatureItem name="Add Custom Websites" className={classes.featureItem} />
184 <FeatureItem name="On-premise & other Hosted Services" className={classes.featureItem} />
185 <FeatureItem name="Install 3rd party services" className={classes.featureItem} />
186 <FeatureItem name="Service Proxies" className={classes.featureItem} />
187 <FeatureItem name="Team Management" className={classes.featureItem} />
188 <FeatureItem name="No Waiting Screens" className={classes.featureItem} />
189 <FeatureItem name="Forever ad-free" className={classes.featureItem} />
190 </ul> */}
191 <FeatureList />
192 </div>
131 </div> 193 </div>
132 ); 194 );
133 } 195 }
diff --git a/src/components/services/content/Services.js b/src/components/services/content/Services.js
index 48de0ebaa..7f1624003 100644
--- a/src/components/services/content/Services.js
+++ b/src/components/services/content/Services.js
@@ -3,6 +3,9 @@ import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { Link } from 'react-router'; 4import { Link } from 'react-router';
5import { defineMessages, intlShape } from 'react-intl'; 5import { defineMessages, intlShape } from 'react-intl';
6import Confetti from 'react-confetti';
7import ms from 'ms';
8import injectSheet from 'react-jss';
6 9
7import ServiceView from './ServiceView'; 10import ServiceView from './ServiceView';
8import Appear from '../../ui/effects/Appear'; 11import Appear from '../../ui/effects/Appear';
@@ -18,7 +21,16 @@ const messages = defineMessages({
18 }, 21 },
19}); 22});
20 23
21export default @observer class Services extends Component { 24
25const styles = {
26 confettiContainer: {
27 position: 'absolute',
28 width: '100%',
29 zIndex: 0,
30 },
31};
32
33export default @observer @injectSheet(styles) class Services extends Component {
22 static propTypes = { 34 static propTypes = {
23 services: MobxPropTypes.arrayOrObservableArray, 35 services: MobxPropTypes.arrayOrObservableArray,
24 setWebviewReference: PropTypes.func.isRequired, 36 setWebviewReference: PropTypes.func.isRequired,
@@ -28,6 +40,8 @@ export default @observer class Services extends Component {
28 reload: PropTypes.func.isRequired, 40 reload: PropTypes.func.isRequired,
29 openSettings: PropTypes.func.isRequired, 41 openSettings: PropTypes.func.isRequired,
30 update: PropTypes.func.isRequired, 42 update: PropTypes.func.isRequired,
43 userHasCompletedSignup: PropTypes.bool.isRequired,
44 classes: PropTypes.object.isRequired,
31 }; 45 };
32 46
33 static defaultProps = { 47 static defaultProps = {
@@ -38,6 +52,18 @@ export default @observer class Services extends Component {
38 intl: intlShape, 52 intl: intlShape,
39 }; 53 };
40 54
55 state = {
56 showConfetti: true,
57 }
58
59 componentDidMount() {
60 window.setTimeout(() => {
61 this.setState({
62 showConfetti: false,
63 });
64 }, ms('8s'));
65 }
66
41 render() { 67 render() {
42 const { 68 const {
43 services, 69 services,
@@ -48,29 +74,47 @@ export default @observer class Services extends Component {
48 reload, 74 reload,
49 openSettings, 75 openSettings,
50 update, 76 update,
77 userHasCompletedSignup,
78 classes,
51 } = this.props; 79 } = this.props;
80
81 const {
82 showConfetti,
83 } = this.state;
84
52 const { intl } = this.context; 85 const { intl } = this.context;
53 86
54 return ( 87 return (
55 <div className="services"> 88 <div className="services">
56 {services.length === 0 && ( 89 {services.length === 0 && (
57 <Appear 90 <>
58 timeout={1500} 91 {userHasCompletedSignup && (
59 transitionName="slideUp" 92 <div className={classes.confettiContainer}>
60 > 93 <Confetti
61 <div className="services__no-service"> 94 width={window.width}
62 <img src="./assets/images/logo.svg" alt="" /> 95 height={window.height}
63 <h1>{intl.formatMessage(messages.welcome)}</h1> 96 numberOfPieces={showConfetti ? 200 : 0}
64 <Appear 97 />
65 timeout={300} 98 </div>
66 transitionName="slideUp" 99 )}
67 > 100 <Appear
68 <Link to="/settings/recipes" className="button"> 101 timeout={1500}
69 {intl.formatMessage(messages.getStarted)} 102 transitionName="slideUp"
70 </Link> 103 >
71 </Appear> 104 <div className="services__no-service">
72 </div> 105 <img src="./assets/images/logo.svg" alt="" />
73 </Appear> 106 <h1>{intl.formatMessage(messages.welcome)}</h1>
107 <Appear
108 timeout={300}
109 transitionName="slideUp"
110 >
111 <Link to="/settings/recipes" className="button">
112 {intl.formatMessage(messages.getStarted)}
113 </Link>
114 </Appear>
115 </div>
116 </Appear>
117 </>
74 )} 118 )}
75 {services.map(service => ( 119 {services.map(service => (
76 <ServiceView 120 <ServiceView
diff --git a/src/components/ui/FeatureItem.js b/src/components/ui/FeatureItem.js
new file mode 100644
index 000000000..a63f5f7b5
--- /dev/null
+++ b/src/components/ui/FeatureItem.js
@@ -0,0 +1,36 @@
1import React from 'react';
2import injectSheet from 'react-jss';
3import { Icon } from '@meetfranz/ui';
4import classnames from 'classnames';
5
6const styles = theme => ({
7 featureItem: {
8 borderBottom: [1, 'solid', theme.legacyStyles.themeGrayDark],
9 padding: [8, 0],
10 display: 'flex',
11 alignItems: 'center',
12 },
13 featureIcon: {
14 fill: theme.brandSuccess,
15 marginRight: 10,
16 },
17});
18
19export const FeatureItem = injectSheet(styles)(({
20 classes, className, name, icon,
21}) => (
22 <li className={classnames({
23 [classes.featureItem]: true,
24 [className]: className,
25 })}
26 >
27 {icon ? (
28 <span className={classes.featureIcon}>{icon}</span>
29 ) : (
30 <Icon icon="mdiCheckCircle" className={classes.featureIcon} size={1.5} />
31 )}
32 {name}
33 </li>
34));
35
36export default FeatureItem;
diff --git a/src/components/ui/FeatureList.js b/src/components/ui/FeatureList.js
new file mode 100644
index 000000000..62944ad75
--- /dev/null
+++ b/src/components/ui/FeatureList.js
@@ -0,0 +1,89 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { defineMessages, intlShape } from 'react-intl';
4
5import { FeatureItem } from './FeatureItem';
6
7const messages = defineMessages({
8 unlimitedServices: {
9 id: 'pricing.features.unlimitedServices',
10 defaultMessage: '!!!Add unlimited services',
11 },
12 spellchecker: {
13 id: 'pricing.features.spellchecker',
14 defaultMessage: '!!!Spellchecker support',
15 },
16 workspaces: {
17 id: 'pricing.features.workspaces',
18 defaultMessage: '!!!Workspaces',
19 },
20 customWebsites: {
21 id: 'pricing.features.customWebsites',
22 defaultMessage: '!!!Add Custom Websites',
23 },
24 onPremise: {
25 id: 'pricing.features.onPremise',
26 defaultMessage: '!!!On-premise & other Hosted Services',
27 },
28 thirdPartyServices: {
29 id: 'pricing.features.thirdPartyServices',
30 defaultMessage: '!!!Install 3rd party services',
31 },
32 serviceProxies: {
33 id: 'pricing.features.serviceProxies',
34 defaultMessage: '!!!Service Proxies',
35 },
36 teamManagement: {
37 id: 'pricing.features.teamManagement',
38 defaultMessage: '!!!Team Management',
39 },
40 appDelays: {
41 id: 'pricing.features.appDelays',
42 defaultMessage: '!!!No Waiting Screens',
43 },
44 adFree: {
45 id: 'pricing.features.adFree',
46 defaultMessage: '!!!Forever ad-free',
47 },
48});
49
50export class FeatureList extends Component {
51 static propTypes = {
52 className: PropTypes.string,
53 featureClassName: PropTypes.string,
54 };
55
56 static defaultProps = {
57 className: '',
58 featureClassName: '',
59 }
60
61 static contextTypes = {
62 intl: intlShape,
63 };
64
65 render() {
66 const {
67 className,
68 featureClassName,
69 } = this.props;
70 const { intl } = this.context;
71
72 return (
73 <ul className={className}>
74 <FeatureItem name={intl.formatMessage(messages.unlimitedServices)} className={featureClassName} />
75 <FeatureItem name={intl.formatMessage(messages.spellchecker)} className={featureClassName} />
76 <FeatureItem name={intl.formatMessage(messages.workspaces)} className={featureClassName} />
77 <FeatureItem name={intl.formatMessage(messages.customWebsites)} className={featureClassName} />
78 <FeatureItem name={intl.formatMessage(messages.onPremise)} className={featureClassName} />
79 <FeatureItem name={intl.formatMessage(messages.thirdPartyServices)} className={featureClassName} />
80 <FeatureItem name={intl.formatMessage(messages.serviceProxies)} className={featureClassName} />
81 <FeatureItem name={intl.formatMessage(messages.teamManagement)} className={featureClassName} />
82 <FeatureItem name={intl.formatMessage(messages.appDelays)} className={featureClassName} />
83 <FeatureItem name={intl.formatMessage(messages.adFree)} className={featureClassName} />
84 </ul>
85 );
86 }
87}
88
89export default FeatureList;
diff --git a/src/containers/auth/PricingScreen.js b/src/containers/auth/PricingScreen.js
index 8d179a170..af1651931 100644
--- a/src/containers/auth/PricingScreen.js
+++ b/src/containers/auth/PricingScreen.js
@@ -5,7 +5,6 @@ import { RouterStore } from 'mobx-react-router';
5 5
6import Pricing from '../../components/auth/Pricing'; 6import Pricing from '../../components/auth/Pricing';
7import UserStore from '../../stores/UserStore'; 7import UserStore from '../../stores/UserStore';
8import PaymentStore from '../../stores/PaymentStore';
9 8
10import { globalError as globalErrorPropType } from '../../prop-types'; 9import { globalError as globalErrorPropType } from '../../prop-types';
11 10
@@ -14,20 +13,40 @@ export default @inject('stores', 'actions') @observer class PricingScreen extend
14 error: globalErrorPropType.isRequired, 13 error: globalErrorPropType.isRequired,
15 }; 14 };
16 15
16 async submit() {
17 const {
18 actions,
19 stores,
20 } = this.props;
21
22 const { activateTrialRequest } = stores.user;
23 const { defaultTrialPlan } = stores.features.features;
24
25 actions.user.activateTrial({ planId: defaultTrialPlan });
26 await activateTrialRequest._promise;
27
28 if (!activateTrialRequest.isError) {
29 stores.router.push('/');
30 stores.user.hasCompletedSignup = true;
31 }
32 }
33
17 render() { 34 render() {
18 const { actions, stores, error } = this.props; 35 const {
36 error,
37 stores,
38 } = this.props;
19 39
20 const nextStepRoute = stores.user.legacyServices.length ? stores.user.importRoute : stores.user.inviteRoute; 40 const { getUserInfoRequest, activateTrialRequest } = stores.user;
41 const { featuresRequest } = stores.features;
21 42
22 return ( 43 return (
23 <Pricing 44 <Pricing
24 donor={stores.user.data.donor || {}} 45 onSubmit={this.submit.bind(this)}
25 onSubmit={actions.user.signup} 46 isLoadingRequiredData={(getUserInfoRequest.isExecuting || !getUserInfoRequest.wasExecuted) || (featuresRequest.isExecuting || !featuresRequest.wasExecuted)}
26 onCloseSubscriptionWindow={() => this.props.stores.router.push(nextStepRoute)} 47 isActivatingTrial={activateTrialRequest.isExecuting}
27 isLoading={stores.payment.plansRequest.isExecuting} 48 trialActivationError={activateTrialRequest.isError}
28 isLoadingUser={stores.user.getUserInfoRequest.isExecuting}
29 error={error} 49 error={error}
30 skipAction={() => this.props.stores.router.push(nextStepRoute)}
31 /> 50 />
32 ); 51 );
33 } 52 }
@@ -36,12 +55,11 @@ export default @inject('stores', 'actions') @observer class PricingScreen extend
36PricingScreen.wrappedComponent.propTypes = { 55PricingScreen.wrappedComponent.propTypes = {
37 actions: PropTypes.shape({ 56 actions: PropTypes.shape({
38 user: PropTypes.shape({ 57 user: PropTypes.shape({
39 signup: PropTypes.func.isRequired, 58 activateTrial: PropTypes.func.isRequired,
40 }).isRequired, 59 }).isRequired,
41 }).isRequired, 60 }).isRequired,
42 stores: PropTypes.shape({ 61 stores: PropTypes.shape({
43 user: PropTypes.instanceOf(UserStore).isRequired, 62 user: PropTypes.instanceOf(UserStore).isRequired,
44 payment: PropTypes.instanceOf(PaymentStore).isRequired,
45 router: PropTypes.instanceOf(RouterStore).isRequired, 63 router: PropTypes.instanceOf(RouterStore).isRequired,
46 }).isRequired, 64 }).isRequired,
47}; 65};
diff --git a/src/containers/layout/AppLayoutContainer.js b/src/containers/layout/AppLayoutContainer.js
index cf3da71e8..b4e32cffa 100644
--- a/src/containers/layout/AppLayoutContainer.js
+++ b/src/containers/layout/AppLayoutContainer.js
@@ -10,6 +10,7 @@ import FeaturesStore from '../../stores/FeaturesStore';
10import UIStore from '../../stores/UIStore'; 10import UIStore from '../../stores/UIStore';
11import NewsStore from '../../stores/NewsStore'; 11import NewsStore from '../../stores/NewsStore';
12import SettingsStore from '../../stores/SettingsStore'; 12import SettingsStore from '../../stores/SettingsStore';
13import UserStore from '../../stores/UserStore';
13import RequestStore from '../../stores/RequestStore'; 14import RequestStore from '../../stores/RequestStore';
14import GlobalErrorStore from '../../stores/GlobalErrorStore'; 15import GlobalErrorStore from '../../stores/GlobalErrorStore';
15 16
@@ -39,6 +40,7 @@ export default @inject('stores', 'actions') @observer class AppLayoutContainer e
39 settings, 40 settings,
40 globalError, 41 globalError,
41 requests, 42 requests,
43 user,
42 } = this.props.stores; 44 } = this.props.stores;
43 45
44 const { 46 const {
@@ -125,6 +127,7 @@ export default @inject('stores', 'actions') @observer class AppLayoutContainer e
125 reload={reload} 127 reload={reload}
126 openSettings={openSettings} 128 openSettings={openSettings}
127 update={updateService} 129 update={updateService}
130 userHasCompletedSignup={user.hasCompletedSignup}
128 /> 131 />
129 ); 132 );
130 133
@@ -166,6 +169,7 @@ AppLayoutContainer.wrappedComponent.propTypes = {
166 ui: PropTypes.instanceOf(UIStore).isRequired, 169 ui: PropTypes.instanceOf(UIStore).isRequired,
167 news: PropTypes.instanceOf(NewsStore).isRequired, 170 news: PropTypes.instanceOf(NewsStore).isRequired,
168 settings: PropTypes.instanceOf(SettingsStore).isRequired, 171 settings: PropTypes.instanceOf(SettingsStore).isRequired,
172 user: PropTypes.instanceOf(UserStore).isRequired,
169 requests: PropTypes.instanceOf(RequestStore).isRequired, 173 requests: PropTypes.instanceOf(RequestStore).isRequired,
170 globalError: PropTypes.instanceOf(GlobalErrorStore).isRequired, 174 globalError: PropTypes.instanceOf(GlobalErrorStore).isRequired,
171 }).isRequired, 175 }).isRequired,
diff --git a/src/i18n/messages/src/components/auth/Pricing.json b/src/i18n/messages/src/components/auth/Pricing.json
index f711a55b4..f15617ca5 100644
--- a/src/i18n/messages/src/components/auth/Pricing.json
+++ b/src/i18n/messages/src/components/auth/Pricing.json
@@ -1,53 +1,118 @@
1[ 1[
2 { 2 {
3 "id": "pricing.headline", 3 "id": "pricing.trial.headline",
4 "defaultMessage": "!!!Support Franz", 4 "defaultMessage": "!!!Franz Professional",
5 "file": "src/components/auth/Pricing.js", 5 "file": "src/components/auth/Pricing.js",
6 "start": { 6 "start": {
7 "line": 13, 7 "line": 15,
8 "column": 12 8 "column": 12
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 16, 11 "line": 18,
12 "column": 3 12 "column": 3
13 } 13 }
14 }, 14 },
15 { 15 {
16 "id": "pricing.support.label", 16 "id": "pricing.trial.subheadline",
17 "defaultMessage": "!!!Select your support plan", 17 "defaultMessage": "!!!Your personal welcome offer:",
18 "file": "src/components/auth/Pricing.js", 18 "file": "src/components/auth/Pricing.js",
19 "start": { 19 "start": {
20 "line": 17, 20 "line": 19,
21 "column": 23 21 "column": 17
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 20, 24 "line": 22,
25 "column": 3 25 "column": 3
26 } 26 }
27 }, 27 },
28 { 28 {
29 "id": "pricing.submit.label", 29 "id": "pricing.trial.terms.headline",
30 "defaultMessage": "!!!Support the development of Franz", 30 "defaultMessage": "!!!No strings attached",
31 "file": "src/components/auth/Pricing.js", 31 "file": "src/components/auth/Pricing.js",
32 "start": { 32 "start": {
33 "line": 21, 33 "line": 23,
34 "column": 29
35 },
36 "end": {
37 "line": 26,
38 "column": 3
39 }
40 },
41 {
42 "id": "pricing.trial.terms.noCreditCard",
43 "defaultMessage": "!!!No credit card required",
44 "file": "src/components/auth/Pricing.js",
45 "start": {
46 "line": 27,
47 "column": 16
48 },
49 "end": {
50 "line": 30,
51 "column": 3
52 }
53 },
54 {
55 "id": "pricing.trial.terms.automaticTrialEnd",
56 "defaultMessage": "!!!Your free trial ends automatically after 14 days",
57 "file": "src/components/auth/Pricing.js",
58 "start": {
59 "line": 31,
34 "column": 21 60 "column": 21
35 }, 61 },
36 "end": { 62 "end": {
37 "line": 24, 63 "line": 34,
64 "column": 3
65 }
66 },
67 {
68 "id": "pricing.trial.error",
69 "defaultMessage": "!!!Sorry, we could not activate your trial!",
70 "file": "src/components/auth/Pricing.js",
71 "start": {
72 "line": 35,
73 "column": 19
74 },
75 "end": {
76 "line": 38,
77 "column": 3
78 }
79 },
80 {
81 "id": "pricing.trial.cta.accept",
82 "defaultMessage": "!!!Yes, upgrade my account to Franz Professional",
83 "file": "src/components/auth/Pricing.js",
84 "start": {
85 "line": 39,
86 "column": 13
87 },
88 "end": {
89 "line": 42,
90 "column": 3
91 }
92 },
93 {
94 "id": "pricing.trial.cta.skip",
95 "defaultMessage": "!!!Continue to Franz",
96 "file": "src/components/auth/Pricing.js",
97 "start": {
98 "line": 43,
99 "column": 11
100 },
101 "end": {
102 "line": 46,
38 "column": 3 103 "column": 3
39 } 104 }
40 }, 105 },
41 { 106 {
42 "id": "pricing.link.skipPayment", 107 "id": "pricing.trial.features.headline",
43 "defaultMessage": "!!!I don't want to support the development of Franz.", 108 "defaultMessage": "!!!Franz Professional includes:",
44 "file": "src/components/auth/Pricing.js", 109 "file": "src/components/auth/Pricing.js",
45 "start": { 110 "start": {
46 "line": 25, 111 "line": 47,
47 "column": 15 112 "column": 20
48 }, 113 },
49 "end": { 114 "end": {
50 "line": 28, 115 "line": 50,
51 "column": 3 116 "column": 3
52 } 117 }
53 } 118 }
diff --git a/src/i18n/messages/src/components/services/content/Services.json b/src/i18n/messages/src/components/services/content/Services.json
index 884ab0c90..eb466c0ac 100644
--- a/src/i18n/messages/src/components/services/content/Services.json
+++ b/src/i18n/messages/src/components/services/content/Services.json
@@ -4,11 +4,11 @@
4 "defaultMessage": "!!!Welcome to Franz", 4 "defaultMessage": "!!!Welcome to Franz",
5 "file": "src/components/services/content/Services.js", 5 "file": "src/components/services/content/Services.js",
6 "start": { 6 "start": {
7 "line": 11, 7 "line": 14,
8 "column": 11 8 "column": 11
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 14, 11 "line": 17,
12 "column": 3 12 "column": 3
13 } 13 }
14 }, 14 },
@@ -17,11 +17,11 @@
17 "defaultMessage": "!!!Get started", 17 "defaultMessage": "!!!Get started",
18 "file": "src/components/services/content/Services.js", 18 "file": "src/components/services/content/Services.js",
19 "start": { 19 "start": {
20 "line": 15, 20 "line": 18,
21 "column": 14 21 "column": 14
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 18, 24 "line": 21,
25 "column": 3 25 "column": 3
26 } 26 }
27 } 27 }
diff --git a/src/i18n/messages/src/components/ui/FeatureList.json b/src/i18n/messages/src/components/ui/FeatureList.json
new file mode 100644
index 000000000..497e299a4
--- /dev/null
+++ b/src/i18n/messages/src/components/ui/FeatureList.json
@@ -0,0 +1,132 @@
1[
2 {
3 "id": "pricing.features.unlimitedServices",
4 "defaultMessage": "!!!Add unlimited services",
5 "file": "src/components/ui/FeatureList.js",
6 "start": {
7 "line": 8,
8 "column": 21
9 },
10 "end": {
11 "line": 11,
12 "column": 3
13 }
14 },
15 {
16 "id": "pricing.features.spellchecker",
17 "defaultMessage": "!!!Spellchecker support",
18 "file": "src/components/ui/FeatureList.js",
19 "start": {
20 "line": 12,
21 "column": 16
22 },
23 "end": {
24 "line": 15,
25 "column": 3
26 }
27 },
28 {
29 "id": "pricing.features.workspaces",
30 "defaultMessage": "!!!Workspaces",
31 "file": "src/components/ui/FeatureList.js",
32 "start": {
33 "line": 16,
34 "column": 14
35 },
36 "end": {
37 "line": 19,
38 "column": 3
39 }
40 },
41 {
42 "id": "pricing.features.customWebsites",
43 "defaultMessage": "!!!Add Custom Websites",
44 "file": "src/components/ui/FeatureList.js",
45 "start": {
46 "line": 20,
47 "column": 18
48 },
49 "end": {
50 "line": 23,
51 "column": 3
52 }
53 },
54 {
55 "id": "pricing.features.onPremise",
56 "defaultMessage": "!!!On-premise & other Hosted Services",
57 "file": "src/components/ui/FeatureList.js",
58 "start": {
59 "line": 24,
60 "column": 13
61 },
62 "end": {
63 "line": 27,
64 "column": 3
65 }
66 },
67 {
68 "id": "pricing.features.thirdPartyServices",
69 "defaultMessage": "!!!Install 3rd party services",
70 "file": "src/components/ui/FeatureList.js",
71 "start": {
72 "line": 28,
73 "column": 22
74 },
75 "end": {
76 "line": 31,
77 "column": 3
78 }
79 },
80 {
81 "id": "pricing.features.serviceProxies",
82 "defaultMessage": "!!!Service Proxies",
83 "file": "src/components/ui/FeatureList.js",
84 "start": {
85 "line": 32,
86 "column": 18
87 },
88 "end": {
89 "line": 35,
90 "column": 3
91 }
92 },
93 {
94 "id": "pricing.features.teamManagement",
95 "defaultMessage": "!!!Team Management",
96 "file": "src/components/ui/FeatureList.js",
97 "start": {
98 "line": 36,
99 "column": 18
100 },
101 "end": {
102 "line": 39,
103 "column": 3
104 }
105 },
106 {
107 "id": "pricing.features.appDelays",
108 "defaultMessage": "!!!No Waiting Screens",
109 "file": "src/components/ui/FeatureList.js",
110 "start": {
111 "line": 40,
112 "column": 13
113 },
114 "end": {
115 "line": 43,
116 "column": 3
117 }
118 },
119 {
120 "id": "pricing.features.adFree",
121 "defaultMessage": "!!!Forever ad-free",
122 "file": "src/components/ui/FeatureList.js",
123 "start": {
124 "line": 44,
125 "column": 10
126 },
127 "end": {
128 "line": 47,
129 "column": 3
130 }
131 }
132] \ No newline at end of file
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js
index b5423af3b..6d746254e 100644
--- a/src/stores/UserStore.js
+++ b/src/stores/UserStore.js
@@ -37,6 +37,8 @@ export default class UserStore extends Store {
37 37
38 @observable passwordRequest = new Request(this.api.user, 'password'); 38 @observable passwordRequest = new Request(this.api.user, 'password');
39 39
40 @observable activateTrialRequest = new Request(this.api.user, 'activateTrial');
41
40 @observable inviteRequest = new Request(this.api.user, 'invite'); 42 @observable inviteRequest = new Request(this.api.user, 'invite');
41 43
42 @observable getUserInfoRequest = new CachedRequest(this.api.user, 'getInfo'); 44 @observable getUserInfoRequest = new CachedRequest(this.api.user, 'getInfo');
@@ -57,7 +59,7 @@ export default class UserStore extends Store {
57 59
58 @observable accountType; 60 @observable accountType;
59 61
60 @observable hasCompletedSignup = null; 62 @observable hasCompletedSignup = false;
61 63
62 @observable userData = {}; 64 @observable userData = {};
63 65
@@ -77,6 +79,7 @@ export default class UserStore extends Store {
77 this.actions.user.retrievePassword.listen(this._retrievePassword.bind(this)); 79 this.actions.user.retrievePassword.listen(this._retrievePassword.bind(this));
78 this.actions.user.logout.listen(this._logout.bind(this)); 80 this.actions.user.logout.listen(this._logout.bind(this));
79 this.actions.user.signup.listen(this._signup.bind(this)); 81 this.actions.user.signup.listen(this._signup.bind(this));
82 this.actions.user.activateTrial.listen(this._activateTrial.bind(this));
80 this.actions.user.invite.listen(this._invite.bind(this)); 83 this.actions.user.invite.listen(this._invite.bind(this));
81 this.actions.user.update.listen(this._update.bind(this)); 84 this.actions.user.update.listen(this._update.bind(this));
82 this.actions.user.resetStatus.listen(this._resetStatus.bind(this)); 85 this.actions.user.resetStatus.listen(this._resetStatus.bind(this));
@@ -199,6 +202,20 @@ export default class UserStore extends Store {
199 gaEvent('User', 'retrievePassword'); 202 gaEvent('User', 'retrievePassword');
200 } 203 }
201 204
205 @action async _activateTrial({ planId }) {
206 debug('activate trial', planId);
207
208 this.activateTrialRequest.execute({
209 plan: planId,
210 });
211
212 await this.activateTrialRequest._promise;
213
214 this.stores.features.featuresRequest.invalidate({ immediately: true });
215
216 gaEvent('User', 'activateTrial');
217 }
218
202 @action async _invite({ invites }) { 219 @action async _invite({ invites }) {
203 const data = invites.filter(invite => invite.email !== ''); 220 const data = invites.filter(invite => invite.email !== '');
204 221
diff --git a/src/styles/auth.scss b/src/styles/auth.scss
index 0a075036a..154a71a36 100644
--- a/src/styles/auth.scss
+++ b/src/styles/auth.scss
@@ -9,7 +9,7 @@
9 } 9 }
10 10
11 .auth__logo.auth__logo--sm { 11 .auth__logo.auth__logo--sm {
12 border: 4px solid $dark-theme-black; 12 border: none;
13 box-shadow: 0 0 6px rgba($dark-theme-black, .5); 13 box-shadow: 0 0 6px rgba($dark-theme-black, .5);
14 } 14 }
15 15
diff --git a/src/styles/reset.scss b/src/styles/reset.scss
index f46ede4a2..d87ce652a 100644
--- a/src/styles/reset.scss
+++ b/src/styles/reset.scss
@@ -51,7 +51,6 @@ button {
51 padding: 0; 51 padding: 0;
52 52
53 &:focus { outline: 0; } 53 &:focus { outline: 0; }
54 .theme__dark & { color: $dark-theme-gray-smoke; }
55} 54}
56 55
57html { 56html {