aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2018-02-12 14:16:33 +0100
committerLibravatar GitHub <noreply@github.com>2018-02-12 14:16:33 +0100
commit7a1872e64262a76f17ac231a47fd8f57cd5a13ac (patch)
tree29ed8aa4dfcb9b179f4fe47c30fb33cf78c2e2ce
parentMerge pull request #668 from meetfranz/fix/import-screen-toggles (diff)
parentfeat(App) Feature Invite Friends in Settings (diff)
downloadferdium-app-7a1872e64262a76f17ac231a47fd8f57cd5a13ac.tar.gz
ferdium-app-7a1872e64262a76f17ac231a47fd8f57cd5a13ac.tar.zst
ferdium-app-7a1872e64262a76f17ac231a47fd8f57cd5a13ac.zip
Merge pull request #654 from meetfranz/feature/invite-button
Invite Button in Settings
-rw-r--r--src/app.js2
-rw-r--r--src/components/auth/Invite.js121
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js11
-rw-r--r--src/components/settings/services/ServiceError.js2
-rw-r--r--src/containers/auth/InviteScreen.js2
-rw-r--r--src/containers/settings/InviteScreen.js44
-rw-r--r--src/i18n/locales/en-US.json3
-rw-r--r--src/stores/UserStore.js12
-rw-r--r--src/styles/invite.scss15
-rw-r--r--src/styles/main.scss1
10 files changed, 184 insertions, 29 deletions
diff --git a/src/app.js b/src/app.js
index 97be9f898..814bfacf1 100644
--- a/src/app.js
+++ b/src/app.js
@@ -27,6 +27,7 @@ import EditServiceScreen from './containers/settings/EditServiceScreen';
27import AccountScreen from './containers/settings/AccountScreen'; 27import AccountScreen from './containers/settings/AccountScreen';
28import EditUserScreen from './containers/settings/EditUserScreen'; 28import EditUserScreen from './containers/settings/EditUserScreen';
29import EditSettingsScreen from './containers/settings/EditSettingsScreen'; 29import EditSettingsScreen from './containers/settings/EditSettingsScreen';
30import InviteSettingsScreen from './containers/settings/InviteScreen';
30import WelcomeScreen from './containers/auth/WelcomeScreen'; 31import WelcomeScreen from './containers/auth/WelcomeScreen';
31import LoginScreen from './containers/auth/LoginScreen'; 32import LoginScreen from './containers/auth/LoginScreen';
32import PasswordScreen from './containers/auth/PasswordScreen'; 33import PasswordScreen from './containers/auth/PasswordScreen';
@@ -74,6 +75,7 @@ window.addEventListener('load', () => {
74 <Route path="/settings/user" component={AccountScreen} /> 75 <Route path="/settings/user" component={AccountScreen} />
75 <Route path="/settings/user/edit" component={EditUserScreen} /> 76 <Route path="/settings/user/edit" component={EditUserScreen} />
76 <Route path="/settings/app" component={EditSettingsScreen} /> 77 <Route path="/settings/app" component={EditSettingsScreen} />
78 <Route path="/settings/invite" component={InviteSettingsScreen} />
77 </Route> 79 </Route>
78 </Route> 80 </Route>
79 <Route path="/auth" component={AuthLayoutContainer}> 81 <Route path="/auth" component={AuthLayoutContainer}>
diff --git a/src/components/auth/Invite.js b/src/components/auth/Invite.js
index c6dca3a65..f1c16986b 100644
--- a/src/components/auth/Invite.js
+++ b/src/components/auth/Invite.js
@@ -3,13 +3,20 @@ import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import { Link } from 'react-router'; 5import { Link } from 'react-router';
6import classnames from 'classnames';
6 7
8import Infobox from '../ui/Infobox';
9import Appear from '../ui/effects/Appear';
7import Form from '../../lib/Form'; 10import Form from '../../lib/Form';
8import { email } from '../../helpers/validation-helpers'; 11import { email } from '../../helpers/validation-helpers';
9import Input from '../ui/Input'; 12import Input from '../ui/Input';
10import Button from '../ui/Button'; 13import Button from '../ui/Button';
11 14
12const messages = defineMessages({ 15const messages = defineMessages({
16 settingsHeadline: {
17 id: 'settings.invite.headline',
18 defaultMessage: '!!!Invite Friends',
19 },
13 headline: { 20 headline: {
14 id: 'invite.headline.friends', 21 id: 'invite.headline.friends',
15 defaultMessage: '!!!Invite 3 of your friends or colleagues', 22 defaultMessage: '!!!Invite 3 of your friends or colleagues',
@@ -30,41 +37,77 @@ const messages = defineMessages({
30 id: 'invite.skip.label', 37 id: 'invite.skip.label',
31 defaultMessage: '!!!I want to do this later', 38 defaultMessage: '!!!I want to do this later',
32 }, 39 },
40 inviteSuccessInfo: {
41 id: 'invite.successInfo',
42 defaultMessage: '!!!Invitations sent successfully',
43 },
33}); 44});
34 45
35@observer 46@observer
36export default class Invite extends Component { 47export default class Invite extends Component {
37 static propTypes = { 48 static propTypes = {
38 onSubmit: PropTypes.func.isRequired, 49 onSubmit: PropTypes.func.isRequired,
50 embed: PropTypes.bool,
51 isInviteSuccessful: PropTypes.bool,
52 isLoadingInvite: PropTypes.bool,
53 };
54
55 static defaultProps = {
56 embed: false,
57 isInviteSuccessful: false,
58 isLoadingInvite: false,
39 }; 59 };
40 60
41 static contextTypes = { 61 static contextTypes = {
42 intl: intlShape, 62 intl: intlShape,
43 }; 63 };
44 64
45 form = new Form({ 65 state = { showSuccessInfo: false };
46 fields: { 66
47 invite: [...Array(3).fill({ 67 componentWillMount() {
48 fields: { 68 const handlers = {
49 name: { 69 onChange: () => {
50 label: this.context.intl.formatMessage(messages.nameLabel), 70 this.setState({ showSuccessInfo: false });
51 placeholder: this.context.intl.formatMessage(messages.nameLabel), 71 },
52 }, 72 };
53 email: { 73
54 label: this.context.intl.formatMessage(messages.emailLabel), 74 this.form = new Form({
55 placeholder: this.context.intl.formatMessage(messages.emailLabel), 75 fields: {
56 validators: [email], 76 invite: [...Array(3).fill({
77 fields: {
78 name: {
79 label: this.context.intl.formatMessage(messages.nameLabel),
80 placeholder: this.context.intl.formatMessage(messages.nameLabel),
81 handlers,
82 // related: ['invite.0.email'], // path accepted but does not work
83 },
84 email: {
85 label: this.context.intl.formatMessage(messages.emailLabel),
86 placeholder: this.context.intl.formatMessage(messages.emailLabel),
87 handlers,
88 validators: [email],
89 },
57 }, 90 },
58 }, 91 })],
59 })], 92 },
60 }, 93 }, this.context.intl);
61 }, this.context.intl); 94 }
95
96 componentDidMount() {
97 document.querySelector('input:first-child').focus();
98 }
62 99
63 submit(e) { 100 submit(e) {
64 e.preventDefault(); 101 e.preventDefault();
102
65 this.form.submit({ 103 this.form.submit({
66 onSuccess: (form) => { 104 onSuccess: (form) => {
67 this.props.onSubmit({ invites: form.values().invite }); 105 this.props.onSubmit({ invites: form.values().invite });
106
107 this.form.clear();
108 // this.form.$('invite.0.name').focus(); // path accepted but does not focus ;(
109 document.querySelector('input:first-child').focus();
110 this.setState({ showSuccessInfo: true });
68 }, 111 },
69 onError: () => {}, 112 onError: () => {},
70 }); 113 });
@@ -73,20 +116,38 @@ export default class Invite extends Component {
73 render() { 116 render() {
74 const { form } = this; 117 const { form } = this;
75 const { intl } = this.context; 118 const { intl } = this.context;
119 const { embed, isInviteSuccessful, isLoadingInvite } = this.props;
76 120
77 const atLeastOneEmailAddress = form.$('invite') 121 const atLeastOneEmailAddress = form.$('invite')
78 .map(invite => invite.$('email').value) 122 .map(invite => invite.$('email').value)
79 .some(emailValue => emailValue.trim() !== ''); 123 .some(emailValue => emailValue.trim() !== '');
80 124
81 return ( 125 const sendButtonClassName = classnames({
82 <div className="auth__container auth__container--signup"> 126 auth__button: true,
127 'invite__embed--button': embed,
128 });
129
130 const renderForm = (
131 <div>
132 {this.state.showSuccessInfo && isInviteSuccessful && (
133 <Appear>
134 <Infobox
135 type="success"
136 icon="checkbox-marked-circle-outline"
137 dismissable
138 >
139 {intl.formatMessage(messages.inviteSuccessInfo)}
140 </Infobox>
141 </Appear>
142 )}
143
83 <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> 144 <form className="franz-form auth__form" onSubmit={e => this.submit(e)}>
84 <img 145 {!embed && (<img
85 src="./assets/images/logo.svg" 146 src="./assets/images/logo.svg"
86 className="auth__logo" 147 className="auth__logo"
87 alt="" 148 alt=""
88 /> 149 />)}
89 <h1> 150 <h1 className={embed && 'invite__embed'}>
90 {intl.formatMessage(messages.headline)} 151 {intl.formatMessage(messages.headline)}
91 </h1> 152 </h1>
92 {form.$('invite').map(invite => ( 153 {form.$('invite').map(invite => (
@@ -99,18 +160,30 @@ export default class Invite extends Component {
99 ))} 160 ))}
100 <Button 161 <Button
101 type="submit" 162 type="submit"
102 className="auth__button" 163 className={sendButtonClassName}
103 disabled={!atLeastOneEmailAddress} 164 disabled={!atLeastOneEmailAddress}
104 label={intl.formatMessage(messages.submitButtonLabel)} 165 label={intl.formatMessage(messages.submitButtonLabel)}
166 loaded={!isLoadingInvite}
105 /> 167 />
106 <Link 168 {!embed && (<Link
107 to="/" 169 to="/"
108 className="franz-form__button franz-form__button--secondary auth__button auth__button--skip" 170 className="franz-form__button franz-form__button--secondary auth__button auth__button--skip"
109 > 171 >
110 {intl.formatMessage(messages.skipButtonLabel)} 172 {intl.formatMessage(messages.skipButtonLabel)}
111 </Link> 173 </Link>)}
112 </form> 174 </form>
113 </div> 175 </div>
114 ); 176 );
177
178 return (
179 <div className={!embed ? 'auth__container auth__container--signup' : 'settings__main'}>
180 {embed && (
181 <div className="settings__header">
182 <h1>{this.context.intl.formatMessage(messages.settingsHeadline)}</h1>
183 </div>
184 )}
185 {!embed ? <div>{renderForm}</div> : <div className="settings__body invite__form">{renderForm}</div>}
186 </div>
187 );
115 } 188 }
116} 189}
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js
index fea8d682d..66539f324 100644
--- a/src/components/settings/navigation/SettingsNavigation.js
+++ b/src/components/settings/navigation/SettingsNavigation.js
@@ -21,6 +21,10 @@ const messages = defineMessages({
21 id: 'settings.navigation.settings', 21 id: 'settings.navigation.settings',
22 defaultMessage: '!!!Settings', 22 defaultMessage: '!!!Settings',
23 }, 23 },
24 inviteFriends: {
25 id: 'settings.navigation.inviteFriends',
26 defaultMessage: '!!!Invite Friends',
27 },
24 logout: { 28 logout: {
25 id: 'settings.navigation.logout', 29 id: 'settings.navigation.logout',
26 defaultMessage: '!!!Logout', 30 defaultMessage: '!!!Logout',
@@ -70,6 +74,13 @@ export default class SettingsNavigation extends Component {
70 > 74 >
71 {intl.formatMessage(messages.settings)} 75 {intl.formatMessage(messages.settings)}
72 </Link> 76 </Link>
77 <Link
78 to="/settings/invite"
79 className="settings-navigation__link"
80 activeClassName="is-active"
81 >
82 {intl.formatMessage(messages.inviteFriends)}
83 </Link>
73 <span className="settings-navigation__expander" /> 84 <span className="settings-navigation__expander" />
74 <Link 85 <Link
75 to="/auth/logout" 86 to="/auth/logout"
diff --git a/src/components/settings/services/ServiceError.js b/src/components/settings/services/ServiceError.js
index 923053296..1f1512927 100644
--- a/src/components/settings/services/ServiceError.js
+++ b/src/components/settings/services/ServiceError.js
@@ -26,7 +26,7 @@ const messages = defineMessages({
26}); 26});
27 27
28@observer 28@observer
29export default class EditServiceForm extends Component { 29export default class ServiceError extends Component {
30 static contextTypes = { 30 static contextTypes = {
31 intl: intlShape, 31 intl: intlShape,
32 }; 32 };
diff --git a/src/containers/auth/InviteScreen.js b/src/containers/auth/InviteScreen.js
index 51971f436..059888c99 100644
--- a/src/containers/auth/InviteScreen.js
+++ b/src/containers/auth/InviteScreen.js
@@ -12,9 +12,11 @@ export default class InviteScreen extends Component {
12 12
13 render() { 13 render() {
14 const { actions } = this.props; 14 const { actions } = this.props;
15
15 return ( 16 return (
16 <Invite 17 <Invite
17 onSubmit={actions.user.invite} 18 onSubmit={actions.user.invite}
19 embed={false}
18 /> 20 />
19 ); 21 );
20 } 22 }
diff --git a/src/containers/settings/InviteScreen.js b/src/containers/settings/InviteScreen.js
new file mode 100644
index 000000000..5f341b1b3
--- /dev/null
+++ b/src/containers/settings/InviteScreen.js
@@ -0,0 +1,44 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react';
4
5import Invite from '../../components/auth/Invite';
6import { gaPage } from '../../lib/analytics';
7
8@inject('stores', 'actions') @observer
9export default class InviteScreen extends Component {
10 componentDidMount() {
11 gaPage('Settings/Invite');
12 }
13
14 componentWillUnmount() {
15 this.props.stores.user.inviteRequest.reset();
16 }
17
18 render() {
19 const { actions } = this.props;
20 const { user } = this.props.stores;
21
22 return (
23 <Invite
24 onSubmit={actions.user.invite}
25 isLoadingInvite={user.inviteRequest.isExecuting}
26 isInviteSuccessful={user.inviteRequest.wasExecuted && !user.inviteRequest.isError}
27 embed
28 />
29 );
30 }
31}
32
33InviteScreen.wrappedComponent.propTypes = {
34 actions: PropTypes.shape({
35 user: PropTypes.shape({
36 invite: PropTypes.func.isRequired,
37 }).isRequired,
38 }).isRequired,
39 stores: PropTypes.shape({
40 user: PropTypes.shape({
41 inviteRequest: PropTypes.object,
42 }).isRequired,
43 }).isRequired,
44};
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 63d7ce60b..3c09fca3b 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -45,6 +45,7 @@
45 "invite.name.label": "Name", 45 "invite.name.label": "Name",
46 "invite.email.label": "Email address", 46 "invite.email.label": "Email address",
47 "invite.skip.label": "I want to do this later", 47 "invite.skip.label": "I want to do this later",
48 "invite.successInfo": "Invitations sent successfully",
48 "subscription.submit.label": "I want to support the development of Franz", 49 "subscription.submit.label": "I want to support the development of Franz",
49 "subscription.paymentSessionError": "Could not initialize payment form", 50 "subscription.paymentSessionError": "Could not initialize payment form",
50 "subscription.includedFeatures": "Paid Franz Premium Supporter Account includes", 51 "subscription.includedFeatures": "Paid Franz Premium Supporter Account includes",
@@ -87,10 +88,12 @@
87 "settings.account.deleteAccount": "Delete account", 88 "settings.account.deleteAccount": "Delete account",
88 "settings.account.deleteInfo": "If you don't need your Franz account any longer, you can delete your account and all related data here.", 89 "settings.account.deleteInfo": "If you don't need your Franz account any longer, you can delete your account and all related data here.",
89 "settings.account.deleteEmailSent": "You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!", 90 "settings.account.deleteEmailSent": "You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!",
91 "settings.invite.headline": "Invite Friends",
90 "settings.navigation.availableServices": "Available services", 92 "settings.navigation.availableServices": "Available services",
91 "settings.navigation.yourServices": "Your services", 93 "settings.navigation.yourServices": "Your services",
92 "settings.navigation.account": "Account", 94 "settings.navigation.account": "Account",
93 "settings.navigation.settings": "Settings", 95 "settings.navigation.settings": "Settings",
96 "settings.navigation.inviteFriends": "Invite Friends",
94 "settings.navigation.logout": "Logout", 97 "settings.navigation.logout": "Logout",
95 "settings.recipes.headline": "Available services", 98 "settings.recipes.headline": "Available services",
96 "settings.recipes.mostPopular": "Most popular", 99 "settings.recipes.mostPopular": "Most popular",
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js
index 427b6454b..abec4df5d 100644
--- a/src/stores/UserStore.js
+++ b/src/stores/UserStore.js
@@ -161,13 +161,17 @@ export default class UserStore extends Store {
161 gaEvent('User', 'retrievePassword'); 161 gaEvent('User', 'retrievePassword');
162 } 162 }
163 163
164 @action _invite({ invites }) { 164 @action async _invite({ invites }) {
165 const data = invites.filter(invite => invite.email !== ''); 165 const data = invites.filter(invite => invite.email !== '');
166 166
167 this.inviteRequest.execute(data); 167 const response = await this.inviteRequest.execute(data)._promise;
168 168
169 // we do not wait for a server response before redirecting the user 169 this.actionStatus = response.status || [];
170 this.stores.router.push('/'); 170
171 // we do not wait for a server response before redirecting the user ONLY DURING SIGNUP
172 if (this.stores.router.location.pathname.includes(this.INVITE_ROUTE)) {
173 this.stores.router.push('/');
174 }
171 175
172 gaEvent('User', 'inviteUsers'); 176 gaEvent('User', 'inviteUsers');
173 } 177 }
diff --git a/src/styles/invite.scss b/src/styles/invite.scss
new file mode 100644
index 000000000..bfb1a4b6b
--- /dev/null
+++ b/src/styles/invite.scss
@@ -0,0 +1,15 @@
1.invite__form {
2 /* play with values to see different layouts */
3 // display: flex;
4 align-items: center;
5 align-self: center;
6 justify-content: center;
7}
8
9.invite__embed {
10 text-align: center;
11}
12
13.invite__embed--button {
14 width: 100%;
15} \ No newline at end of file
diff --git a/src/styles/main.scss b/src/styles/main.scss
index 261396f6f..446bdca14 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -27,6 +27,7 @@ $mdi-font-path: '../node_modules/mdi/fonts';
27@import './subscription.scss'; 27@import './subscription.scss';
28@import './subscription-popup.scss'; 28@import './subscription-popup.scss';
29@import './content-tabs.scss'; 29@import './content-tabs.scss';
30@import './invite.scss';
30 31
31// form 32// form
32@import './input.scss'; 33@import './input.scss';