From 6415f2746e38ebe5cb328c2af94413a4d4e5da07 Mon Sep 17 00:00:00 2001 From: André Oliveira <37463445+SpecialAro@users.noreply.github.com> Date: Tue, 12 Jul 2022 17:59:43 +0100 Subject: Refactor the 'Welcome' screen and the 'SetupAssistant' for better UX (#472) * Change auth styling and add back button * Add Skip button on 'SetupAssistant' --- src/components/auth/AuthLayout.js | 118 ------------ src/components/auth/AuthLayout.jsx | 124 ++++++++++++ src/components/auth/ChangeServer.js | 141 -------------- src/components/auth/ChangeServer.jsx | 153 +++++++++++++++ src/components/auth/Login.js | 203 -------------------- src/components/auth/Login.jsx | 205 ++++++++++++++++++++ src/components/auth/SetupAssistant.js | 331 -------------------------------- src/components/auth/SetupAssistant.jsx | 336 +++++++++++++++++++++++++++++++++ src/components/auth/Signup.js | 191 ------------------- src/components/auth/Signup.jsx | 206 ++++++++++++++++++++ src/i18n/locales/en-US.json | 4 +- src/styles/auth.scss | 15 +- 12 files changed, 1036 insertions(+), 991 deletions(-) delete mode 100644 src/components/auth/AuthLayout.js create mode 100644 src/components/auth/AuthLayout.jsx delete mode 100644 src/components/auth/ChangeServer.js create mode 100644 src/components/auth/ChangeServer.jsx delete mode 100644 src/components/auth/Login.js create mode 100644 src/components/auth/Login.jsx delete mode 100644 src/components/auth/SetupAssistant.js create mode 100644 src/components/auth/SetupAssistant.jsx delete mode 100644 src/components/auth/Signup.js create mode 100644 src/components/auth/Signup.jsx (limited to 'src') diff --git a/src/components/auth/AuthLayout.js b/src/components/auth/AuthLayout.js deleted file mode 100644 index 41bda2f67..000000000 --- a/src/components/auth/AuthLayout.js +++ /dev/null @@ -1,118 +0,0 @@ -import { cloneElement, Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import { TitleBar } from 'electron-react-titlebar/renderer'; - -import { injectIntl } from 'react-intl'; -import { mdiFlash } from '@mdi/js'; -import Link from '../ui/Link'; -import InfoBar from '../ui/InfoBar'; - -import { Component as PublishDebugInfo } from '../../features/publishDebugInfo'; - -import { - oneOrManyChildElements, - globalError as globalErrorPropType, -} from '../../prop-types'; -import globalMessages from '../../i18n/globalMessages'; - -import { isWindows } from '../../environment'; -import AppUpdateInfoBar from '../AppUpdateInfoBar'; -import { GITHUB_FERDIUM_URL } from '../../config'; -import Icon from '../ui/icon'; - -import { serverName } from '../../api/apiBase'; - -class AuthLayout extends Component { - static propTypes = { - children: oneOrManyChildElements.isRequired, - error: globalErrorPropType.isRequired, - isOnline: PropTypes.bool.isRequired, - isAPIHealthy: PropTypes.bool.isRequired, - retryHealthCheck: PropTypes.func.isRequired, - isHealthCheckLoading: PropTypes.bool.isRequired, - isFullScreen: PropTypes.bool.isRequired, - installAppUpdate: PropTypes.func.isRequired, - appUpdateIsDownloaded: PropTypes.bool.isRequired, - }; - - state = { - shouldShowAppUpdateInfoBar: true, - }; - - render() { - const { - children, - error, - isOnline, - isAPIHealthy, - retryHealthCheck, - isHealthCheckLoading, - isFullScreen, - installAppUpdate, - appUpdateIsDownloaded, - } = this.props; - - const { intl } = this.props; - - let serverNameParse = serverName(); - serverNameParse = - serverNameParse === 'Custom' ? 'your Custom Server' : serverNameParse; - - return ( - <> - {isWindows && !isFullScreen && ( - - )} -
- {!isOnline && ( - - - {intl.formatMessage(globalMessages.notConnectedToTheInternet)} - - )} - {appUpdateIsDownloaded && this.state.shouldShowAppUpdateInfoBar && ( - { - this.setState({ shouldShowAppUpdateInfoBar: false }); - }} - /> - )} - {isOnline && !isAPIHealthy && ( - - - {intl.formatMessage(globalMessages.APIUnhealthy, { serverNameParse })} - - )} -
- {/* Inject globalError into children */} - {cloneElement(children, { - error, - })} -
- {/*
*/} - - - - - - - ); - } -} - -export default injectIntl(observer(AuthLayout)); diff --git a/src/components/auth/AuthLayout.jsx b/src/components/auth/AuthLayout.jsx new file mode 100644 index 000000000..8a88cedb1 --- /dev/null +++ b/src/components/auth/AuthLayout.jsx @@ -0,0 +1,124 @@ +import { cloneElement, Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react'; +import { TitleBar } from 'electron-react-titlebar/renderer'; + +import { injectIntl } from 'react-intl'; +import { mdiFlash } from '@mdi/js'; +import Link from '../ui/Link'; +import InfoBar from '../ui/InfoBar'; + +import { Component as PublishDebugInfo } from '../../features/publishDebugInfo'; + +import { + oneOrManyChildElements, + globalError as globalErrorPropType, +} from '../../prop-types'; +import globalMessages from '../../i18n/globalMessages'; + +import { isWindows } from '../../environment'; +import AppUpdateInfoBar from '../AppUpdateInfoBar'; +import { GITHUB_FERDIUM_URL } from '../../config'; +import Icon from '../ui/icon'; + +import { serverName } from '../../api/apiBase'; + +class AuthLayout extends Component { + static propTypes = { + children: oneOrManyChildElements.isRequired, + error: globalErrorPropType.isRequired, + isOnline: PropTypes.bool.isRequired, + isAPIHealthy: PropTypes.bool.isRequired, + retryHealthCheck: PropTypes.func.isRequired, + isHealthCheckLoading: PropTypes.bool.isRequired, + isFullScreen: PropTypes.bool.isRequired, + installAppUpdate: PropTypes.func.isRequired, + appUpdateIsDownloaded: PropTypes.bool.isRequired, + }; + + constructor() { + super(); + + this.state = { + shouldShowAppUpdateInfoBar: true, + }; + } + + render() { + const { + children, + error, + isOnline, + isAPIHealthy, + retryHealthCheck, + isHealthCheckLoading, + isFullScreen, + installAppUpdate, + appUpdateIsDownloaded, + } = this.props; + + const { intl } = this.props; + + let serverNameParse = serverName(); + serverNameParse = + serverNameParse === 'Custom' ? 'your Custom Server' : serverNameParse; + + return ( + <> + {isWindows && !isFullScreen && ( + + )} +
+ {!isOnline && ( + + + {intl.formatMessage(globalMessages.notConnectedToTheInternet)} + + )} + {appUpdateIsDownloaded && this.state.shouldShowAppUpdateInfoBar && ( + { + this.setState({ shouldShowAppUpdateInfoBar: false }); + }} + /> + )} + {isOnline && !isAPIHealthy && ( + + + {intl.formatMessage(globalMessages.APIUnhealthy, { + serverNameParse, + })} + + )} +
+ {/* Inject globalError into children */} + {cloneElement(children, { + error, + })} +
+ {/*
*/} + + + + + + + ); + } +} + +export default injectIntl(observer(AuthLayout)); diff --git a/src/components/auth/ChangeServer.js b/src/components/auth/ChangeServer.js deleted file mode 100644 index 8d0960843..000000000 --- a/src/components/auth/ChangeServer.js +++ /dev/null @@ -1,141 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import { defineMessages, injectIntl } from 'react-intl'; -import Form from '../../lib/Form'; -import Input from '../ui/Input'; -import Select from '../ui/Select'; -import Button from '../ui/button'; -import Link from '../ui/Link'; -import Infobox from '../ui/Infobox'; -import { url, required } from '../../helpers/validation-helpers'; -import { LIVE_FERDIUM_API, LIVE_FRANZ_API } from '../../config'; -import globalMessages from '../../i18n/globalMessages'; -import { H1 } from '../ui/headline'; - -const messages = defineMessages({ - headline: { - id: 'changeserver.headline', - defaultMessage: 'Change server', - }, - label: { - id: 'changeserver.label', - defaultMessage: 'Server', - }, - warning: { - id: 'changeserver.warning', - defaultMessage: 'Extra settings offered by Ferdium will not be saved', - }, - customServerLabel: { - id: 'changeserver.customServerLabel', - defaultMessage: 'Custom server', - }, - urlError: { - id: 'changeserver.urlError', - defaultMessage: 'Enter a valid URL', - }, -}); - -class ChangeServer extends Component { - static propTypes = { - onSubmit: PropTypes.func.isRequired, - server: PropTypes.string.isRequired, - }; - - ferdiumServer = LIVE_FERDIUM_API; - - franzServer = LIVE_FRANZ_API; - - defaultServers = [ this.ferdiumServer, this.franzServer ]; - - form = (() => { - const { intl } = this.props; - return new Form({ - fields: { - server: { - label: intl.formatMessage(messages.label), - value: this.props.server, - options: [ - { value: this.ferdiumServer, label: 'Ferdium (Default)' }, - { value: this.franzServer, label: 'Franz' }, - { - value: this.defaultServers.includes(this.props.server) - ? '' - : this.props.server, - label: 'Custom', - }, - ], - }, - customServer: { - label: intl.formatMessage(messages.customServerLabel), - value: '', - validators: [url, required], - }, - }, - }, - intl, - ); - })(); - - componentDidMount() { - if (this.defaultServers.includes(this.props.server)) { - this.form.$('server').value = this.props.server; - } else { - this.form.$('server').value = ''; - this.form.$('customServer').value = this.props.server; - } - } - - submit(e) { - e.preventDefault(); - this.form.submit({ - onSuccess: form => { - if (!this.defaultServers.includes(form.values().server)) { - form.$('server').onChange(form.values().customServer); - } - this.props.onSubmit(form.values()); - }, - onError: form => { - if (this.defaultServers.includes(form.values().server)) { - this.props.onSubmit(form.values()); - } - }, - }); - } - - render() { - const { form } = this; - const { intl } = this.props; - return ( -
-
this.submit(e)}> - -

{intl.formatMessage(messages.headline)}

- {(form.$('server').value === this.franzServer) && ( - - {intl.formatMessage(messages.warning)} - - )} - { - this.form.$('customServer').value = this.form.$('customServer').value.replace(/\/$/, ""); - this.submit(e) - }} - field={form.$('customServer')} - /> - )} -
- ); - } -} - -export default injectIntl(observer(ChangeServer)); diff --git a/src/components/auth/ChangeServer.jsx b/src/components/auth/ChangeServer.jsx new file mode 100644 index 000000000..8f4b85fbb --- /dev/null +++ b/src/components/auth/ChangeServer.jsx @@ -0,0 +1,153 @@ +import { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react'; +import { defineMessages, injectIntl } from 'react-intl'; +import { mdiArrowLeftCircle } from '@mdi/js'; +import Form from '../../lib/Form'; +import Input from '../ui/Input'; +import Select from '../ui/Select'; +import Button from '../ui/button'; +import Link from '../ui/Link'; +import Infobox from '../ui/Infobox'; +import { url, required } from '../../helpers/validation-helpers'; +import { LIVE_FERDIUM_API, LIVE_FRANZ_API } from '../../config'; +import globalMessages from '../../i18n/globalMessages'; +import { H1 } from '../ui/headline'; +import Icon from '../ui/icon'; + +const messages = defineMessages({ + headline: { + id: 'changeserver.headline', + defaultMessage: 'Change server', + }, + label: { + id: 'changeserver.label', + defaultMessage: 'Server', + }, + warning: { + id: 'changeserver.warning', + defaultMessage: 'Extra settings offered by Ferdium will not be saved', + }, + customServerLabel: { + id: 'changeserver.customServerLabel', + defaultMessage: 'Custom server', + }, + urlError: { + id: 'changeserver.urlError', + defaultMessage: 'Enter a valid URL', + }, +}); + +class ChangeServer extends Component { + static propTypes = { + onSubmit: PropTypes.func.isRequired, + server: PropTypes.string.isRequired, + }; + + ferdiumServer = LIVE_FERDIUM_API; + + franzServer = LIVE_FRANZ_API; + + defaultServers = [this.ferdiumServer, this.franzServer]; + + form = (() => { + const { intl } = this.props; + return new Form( + { + fields: { + server: { + label: intl.formatMessage(messages.label), + value: this.props.server, + options: [ + { value: this.ferdiumServer, label: 'Ferdium (Default)' }, + { value: this.franzServer, label: 'Franz' }, + { + value: this.defaultServers.includes(this.props.server) + ? '' + : this.props.server, + label: 'Custom', + }, + ], + }, + customServer: { + label: intl.formatMessage(messages.customServerLabel), + value: '', + validators: [url, required], + }, + }, + }, + intl, + ); + })(); + + componentDidMount() { + if (this.defaultServers.includes(this.props.server)) { + this.form.$('server').value = this.props.server; + } else { + this.form.$('server').value = ''; + this.form.$('customServer').value = this.props.server; + } + } + + submit(e) { + e.preventDefault(); + this.form.submit({ + onSuccess: form => { + if (!this.defaultServers.includes(form.values().server)) { + form.$('server').onChange(form.values().customServer); + } + this.props.onSubmit(form.values()); + }, + onError: form => { + if (this.defaultServers.includes(form.values().server)) { + this.props.onSubmit(form.values()); + } + }, + }); + } + + render() { + const { form } = this; + const { intl } = this.props; + return ( +
+
this.submit(e)}> + + + +

{intl.formatMessage(messages.headline)}

+ {form.$('server').value === this.franzServer && ( + + {intl.formatMessage(messages.warning)} + + )} + { + this.form.$('customServer').value = this.form + .$('customServer') + .value.replace(/\/$/, ''); + this.submit(e); + }} + field={form.$('customServer')} + /> + )} +
+ ); + } +} + +export default injectIntl(observer(ChangeServer)); diff --git a/src/components/auth/Login.js b/src/components/auth/Login.js deleted file mode 100644 index a5893e181..000000000 --- a/src/components/auth/Login.js +++ /dev/null @@ -1,203 +0,0 @@ -/* eslint jsx-a11y/anchor-is-valid: 0 */ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer, inject } from 'mobx-react'; -import { defineMessages, injectIntl } from 'react-intl'; - -import { LIVE_FRANZ_API } from '../../config'; -import { API_VERSION } from '../../environment-remote'; -import { serverBase } from '../../api/apiBase'; // TODO: Remove this line after fixing password recovery in-app -import Form from '../../lib/Form'; -import { required, email } from '../../helpers/validation-helpers'; -import Input from '../ui/Input'; -import Button from '../ui/button'; -import Link from '../ui/Link'; - -import { globalError as globalErrorPropType } from '../../prop-types'; -import { H1 } from '../ui/headline'; - -const messages = defineMessages({ - headline: { - id: 'login.headline', - defaultMessage: 'Sign in', - }, - emailLabel: { - id: 'login.email.label', - defaultMessage: 'Email address', - }, - passwordLabel: { - id: 'login.password.label', - defaultMessage: 'Password', - }, - submitButtonLabel: { - id: 'login.submit.label', - defaultMessage: 'Sign in', - }, - invalidCredentials: { - id: 'login.invalidCredentials', - defaultMessage: 'Email or password not valid', - }, - customServerQuestion: { - id: 'login.customServerQuestion', - defaultMessage: 'Using a custom Ferdium server?', - }, - customServerSuggestion: { - id: 'login.customServerSuggestion', - defaultMessage: 'Try importing your Franz account', - }, - tokenExpired: { - id: 'login.tokenExpired', - defaultMessage: 'Your session expired, please login again.', - }, - serverLogout: { - id: 'login.serverLogout', - defaultMessage: 'Your session expired, please login again.', - }, - signupLink: { - id: 'login.link.signup', - defaultMessage: 'Create a free account', - }, - passwordLink: { - id: 'login.link.password', - defaultMessage: 'Reset password', - }, - backToWelcome: { - id: 'login.backToWelcome', - defaultMessage: 'Click the Ferdium icon to go back to the Welcome screen', - }, -}); - -class Login extends Component { - static propTypes = { - onSubmit: PropTypes.func.isRequired, - isSubmitting: PropTypes.bool.isRequired, - isTokenExpired: PropTypes.bool.isRequired, - isServerLogout: PropTypes.bool.isRequired, - signupRoute: PropTypes.string.isRequired, - // passwordRoute: PropTypes.string.isRequired, // TODO: Uncomment this line after fixing password recovery in-app - error: globalErrorPropType.isRequired, - }; - - form = (() => { - const { intl } = this.props; - return new Form({ - fields: { - email: { - label: intl.formatMessage(messages.emailLabel), - value: '', - validators: [required, email], - }, - password: { - label: intl.formatMessage(messages.passwordLabel), - value: '', - validators: [required], - type: 'password', - }, - }, - }, - intl, - )})(); - - submit(e) { - e.preventDefault(); - this.form.submit({ - onSuccess: form => { - this.props.onSubmit(form.values()); - }, - onError: () => {}, - }); - } - - render() { - const { form } = this; - const { intl } = this.props; - const { - isSubmitting, - isTokenExpired, - isServerLogout, - signupRoute, - // passwordRoute, // TODO: Uncomment this line after fixing password recovery in-app - error, - } = this.props; - - return ( -
-
this.submit(e)}> - -

{intl.formatMessage(messages.headline)}

- {isTokenExpired && ( -

- {intl.formatMessage(messages.tokenExpired)} -

- )} - {isServerLogout && ( -

- {intl.formatMessage(messages.serverLogout)} -

- )} - - - {error.code === 'invalid-credentials' && ( - <> -

- {intl.formatMessage(messages.invalidCredentials)} -

- {window['ferdium'].stores.settings.all.app.server !== - LIVE_FRANZ_API && ( -

- {intl.formatMessage(messages.customServerQuestion)}{' '} - - {intl.formatMessage(messages.customServerSuggestion)} - -

- )} - - )} - {isSubmitting ? ( -
- ); - } -} - -export default injectIntl(inject('actions')(observer(Login))); diff --git a/src/components/auth/Login.jsx b/src/components/auth/Login.jsx new file mode 100644 index 000000000..33b4d3e0d --- /dev/null +++ b/src/components/auth/Login.jsx @@ -0,0 +1,205 @@ +/* eslint jsx-a11y/anchor-is-valid: 0 */ +import { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer, inject } from 'mobx-react'; +import { defineMessages, injectIntl } from 'react-intl'; + +import { mdiArrowLeftCircle } from '@mdi/js'; +import Icon from '../ui/icon'; +import { LIVE_FRANZ_API } from '../../config'; +import { API_VERSION } from '../../environment-remote'; +import { serverBase } from '../../api/apiBase'; // TODO: Remove this line after fixing password recovery in-app +import Form from '../../lib/Form'; +import { required, email } from '../../helpers/validation-helpers'; +import Input from '../ui/Input'; +import Button from '../ui/button'; +import Link from '../ui/Link'; + +import { globalError as globalErrorPropType } from '../../prop-types'; +import { H1 } from '../ui/headline'; + +const messages = defineMessages({ + headline: { + id: 'login.headline', + defaultMessage: 'Sign in', + }, + emailLabel: { + id: 'login.email.label', + defaultMessage: 'Email address', + }, + passwordLabel: { + id: 'login.password.label', + defaultMessage: 'Password', + }, + submitButtonLabel: { + id: 'login.submit.label', + defaultMessage: 'Sign in', + }, + invalidCredentials: { + id: 'login.invalidCredentials', + defaultMessage: 'Email or password not valid', + }, + customServerQuestion: { + id: 'login.customServerQuestion', + defaultMessage: 'Using a custom Ferdium server?', + }, + customServerSuggestion: { + id: 'login.customServerSuggestion', + defaultMessage: 'Try importing your Franz account', + }, + tokenExpired: { + id: 'login.tokenExpired', + defaultMessage: 'Your session expired, please login again.', + }, + serverLogout: { + id: 'login.serverLogout', + defaultMessage: 'Your session expired, please login again.', + }, + signupLink: { + id: 'login.link.signup', + defaultMessage: 'Create a free account', + }, + passwordLink: { + id: 'login.link.password', + defaultMessage: 'Reset password', + }, +}); + +class Login extends Component { + static propTypes = { + onSubmit: PropTypes.func.isRequired, + isSubmitting: PropTypes.bool.isRequired, + isTokenExpired: PropTypes.bool.isRequired, + isServerLogout: PropTypes.bool.isRequired, + signupRoute: PropTypes.string.isRequired, + // passwordRoute: PropTypes.string.isRequired, // TODO: Uncomment this line after fixing password recovery in-app + error: globalErrorPropType.isRequired, + }; + + form = (() => { + const { intl } = this.props; + return new Form( + { + fields: { + email: { + label: intl.formatMessage(messages.emailLabel), + value: '', + validators: [required, email], + }, + password: { + label: intl.formatMessage(messages.passwordLabel), + value: '', + validators: [required], + type: 'password', + }, + }, + }, + intl, + ); + })(); + + submit(e) { + e.preventDefault(); + this.form.submit({ + onSuccess: form => { + this.props.onSubmit(form.values()); + }, + onError: () => {}, + }); + } + + render() { + const { form } = this; + const { intl } = this.props; + const { + isSubmitting, + isTokenExpired, + isServerLogout, + signupRoute, + // passwordRoute, // TODO: Uncomment this line after fixing password recovery in-app + error, + } = this.props; + + return ( +
+
this.submit(e)}> + + + +

{intl.formatMessage(messages.headline)}

+ {isTokenExpired && ( +

+ {intl.formatMessage(messages.tokenExpired)} +

+ )} + {isServerLogout && ( +

+ {intl.formatMessage(messages.serverLogout)} +

+ )} + + + {error.code === 'invalid-credentials' && ( + <> +

+ {intl.formatMessage(messages.invalidCredentials)} +

+ {window['ferdium'].stores.settings.all.app.server !== + LIVE_FRANZ_API && ( +

+ {intl.formatMessage(messages.customServerQuestion)}{' '} + + {intl.formatMessage(messages.customServerSuggestion)} + +

+ )} + + )} + {isSubmitting ? ( +
+ ); + } +} + +export default injectIntl(inject('actions')(observer(Login))); diff --git a/src/components/auth/SetupAssistant.js b/src/components/auth/SetupAssistant.js deleted file mode 100644 index e3d2d88d1..000000000 --- a/src/components/auth/SetupAssistant.js +++ /dev/null @@ -1,331 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import { defineMessages, injectIntl } from 'react-intl'; -import injectSheet from 'react-jss'; -import classnames from 'classnames'; - -import Input from '../ui/input/index'; -import Button from '../ui/button'; -import Badge from '../ui/badge'; -import Modal from '../ui/Modal'; -import Infobox from '../ui/Infobox'; -import Appear from '../ui/effects/Appear'; -import globalMessages from '../../i18n/globalMessages'; - -import { CDN_URL } from '../../config'; -import { H1, H2 } from '../ui/headline'; - -const SLACK_ID = 'slack'; - -const messages = defineMessages({ - headline: { - id: 'setupAssistant.headline', - defaultMessage: "Let's get started", - }, - subHeadline: { - id: 'setupAssistant.subheadline', - defaultMessage: - 'Choose from our most used services and get back on top of your messaging now.', - }, - submitButtonLabel: { - id: 'setupAssistant.submit.label', - defaultMessage: "Let's go", - }, - inviteSuccessInfo: { - id: 'invite.successInfo', - defaultMessage: 'Invitations sent successfully', - }, -}); - -let transition = 'none'; - -if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) { - transition = 'all 0.25s'; -} - -const styles = theme => ({ - root: { - width: '500px !important', - textAlign: 'center', - padding: 20, - - '& h1': {}, - }, - servicesGrid: { - display: 'flex', - flexWrap: 'wrap', - justifyContent: 'space-between', - }, - serviceContainer: { - background: theme.colorBackground, - position: 'relative', - width: '32%', - display: 'flex', - alignItems: 'center', - flexDirection: 'column', - justifyContent: 'center', - padding: 20, - borderRadius: theme.borderRadius, - marginBottom: 10, - opacity: 0.5, - transition, - border: [3, 'solid', 'transparent'], - - '& h2': { - margin: [10, 0, 0], - color: theme.colorText, - }, - - '&:hover': { - border: [3, 'solid', theme.brandPrimary], - '& $serviceIcon': {}, - }, - }, - selected: { - border: [3, 'solid', theme.brandPrimary], - background: `${theme.brandPrimary}47`, - opacity: 1, - }, - serviceIcon: { - width: 50, - transition, - }, - - slackModalContent: { - textAlign: 'center', - - '& img': { - width: 50, - marginBottom: 20, - }, - }, - modalActionContainer: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - }, - ctaCancel: { - background: 'none !important', - }, - slackBadge: { - position: 'absolute', - bottom: 4, - height: 'auto', - padding: '0px 4px', - borderRadius: theme.borderRadiusSmall, - margin: 0, - display: 'flex', - overflow: 'hidden', - }, - clearSlackWorkspace: { - background: theme.inputPrefixColor, - marginLeft: 5, - height: '100%', - color: theme.colorText, - display: 'inline-flex', - justifyContent: 'center', - alignItems: 'center', - marginRight: -4, - padding: [0, 5], - }, -}); - -class SetupAssistant extends Component { - static propTypes = { - classes: PropTypes.object.isRequired, - onSubmit: PropTypes.func.isRequired, - isInviteSuccessful: PropTypes.bool, - services: PropTypes.object.isRequired, - isSettingUpServices: PropTypes.bool.isRequired, - }; - - static defaultProps = { - isInviteSuccessful: false, - }; - - state = { - services: [ - { - id: 'whatsapp', - }, - { - id: 'messenger', - }, - { - id: 'gmail', - }, - ], - isSlackModalOpen: false, - slackWorkspace: '', - }; - - slackWorkspaceHandler() { - const { slackWorkspace = '', services } = this.state; - - const sanitizedWorkspace = slackWorkspace - .trim() - .replace(/^https?:\/\//, ''); - - if (sanitizedWorkspace) { - const index = services.findIndex(s => s.id === SLACK_ID); - - if (index === -1) { - const newServices = services; - newServices.push({ id: SLACK_ID, team: sanitizedWorkspace }); - this.setState({ services: newServices }); - } - } - - this.setState({ - isSlackModalOpen: false, - slackWorkspace: sanitizedWorkspace, - }); - } - - render() { - const { intl } = this.props; - const { - classes, - isInviteSuccessful, - onSubmit, - services, - isSettingUpServices, - } = this.props; - const { - isSlackModalOpen, - slackWorkspace, - services: addedServices, - } = this.state; - - return ( -
- {this.state.showSuccessInfo && isInviteSuccessful && ( - - - {intl.formatMessage(messages.inviteSuccessInfo)} - - - )} - - -

{intl.formatMessage(messages.headline)}

-

{intl.formatMessage(messages.subHeadline)}

-
- {Object.keys(services).map(id => { - const service = services[id]; - return ( - - - )} - - ); - })} -
- this.setState({ isSlackModalOpen: false })} - > -
- -

Create your first Slack workspace

-
{ - e.preventDefault(); - this.slackWorkspaceHandler(); - }} - > - - this.setState({ slackWorkspace: e.target.value }) - } - value={slackWorkspace} - /> -
-
-
-
-
-
- ); - } -} - -export default injectIntl( - injectSheet(styles, { injectTheme: true })(observer(SetupAssistant)), -); diff --git a/src/components/auth/SetupAssistant.jsx b/src/components/auth/SetupAssistant.jsx new file mode 100644 index 000000000..8d15e36d1 --- /dev/null +++ b/src/components/auth/SetupAssistant.jsx @@ -0,0 +1,336 @@ +import { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react'; +import { defineMessages, injectIntl } from 'react-intl'; +import injectSheet from 'react-jss'; +import classnames from 'classnames'; + +import Input from '../ui/input/index'; +import Button from '../ui/button'; +import Badge from '../ui/badge'; +import Modal from '../ui/Modal'; +import Infobox from '../ui/Infobox'; +import Appear from '../ui/effects/Appear'; +import globalMessages from '../../i18n/globalMessages'; + +import { CDN_URL } from '../../config'; +import { H1, H2 } from '../ui/headline'; + +const SLACK_ID = 'slack'; + +const messages = defineMessages({ + headline: { + id: 'setupAssistant.headline', + defaultMessage: "Let's get started", + }, + subHeadline: { + id: 'setupAssistant.subheadline', + defaultMessage: + 'Choose from our most used services and get back on top of your messaging now.', + }, + submitButtonLabel: { + id: 'setupAssistant.submit.label', + defaultMessage: "Let's go", + }, + skipButtonLabel: { + id: 'setupAssistant.skip.label', + defaultMessage: 'Skip', + }, + inviteSuccessInfo: { + id: 'invite.successInfo', + defaultMessage: 'Invitations sent successfully', + }, +}); + +let transition = 'none'; + +if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) { + transition = 'all 0.25s'; +} + +const styles = theme => ({ + root: { + width: '500px !important', + textAlign: 'center', + padding: 20, + + '& h1': {}, + }, + servicesGrid: { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-between', + }, + serviceContainer: { + background: theme.colorBackground, + position: 'relative', + width: '32%', + display: 'flex', + alignItems: 'center', + flexDirection: 'column', + justifyContent: 'center', + padding: 20, + borderRadius: theme.borderRadius, + marginBottom: 10, + opacity: 0.5, + transition, + border: [3, 'solid', 'transparent'], + + '& h2': { + margin: [10, 0, 0], + color: theme.colorText, + }, + + '&:hover': { + border: [3, 'solid', theme.brandPrimary], + '& $serviceIcon': {}, + }, + }, + selected: { + border: [3, 'solid', theme.brandPrimary], + background: `${theme.brandPrimary}47`, + opacity: 1, + }, + serviceIcon: { + width: 50, + transition, + }, + + slackModalContent: { + textAlign: 'center', + + '& img': { + width: 50, + marginBottom: 20, + }, + }, + modalActionContainer: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }, + ctaCancel: { + background: 'none !important', + }, + slackBadge: { + position: 'absolute', + bottom: 4, + height: 'auto', + padding: '0px 4px', + borderRadius: theme.borderRadiusSmall, + margin: 0, + display: 'flex', + overflow: 'hidden', + }, + clearSlackWorkspace: { + background: theme.inputPrefixColor, + marginLeft: 5, + height: '100%', + color: theme.colorText, + display: 'inline-flex', + justifyContent: 'center', + alignItems: 'center', + marginRight: -4, + padding: [0, 5], + }, +}); + +class SetupAssistant extends Component { + static propTypes = { + classes: PropTypes.object.isRequired, + onSubmit: PropTypes.func.isRequired, + isInviteSuccessful: PropTypes.bool, + services: PropTypes.object.isRequired, + isSettingUpServices: PropTypes.bool.isRequired, + }; + + static defaultProps = { + isInviteSuccessful: false, + }; + + constructor() { + super(); + + this.state = { + services: [], + isSlackModalOpen: false, + slackWorkspace: '', + }; + } + + slackWorkspaceHandler() { + const { slackWorkspace = '', services } = this.state; + + const sanitizedWorkspace = slackWorkspace + .trim() + .replace(/^https?:\/\//, ''); + + if (sanitizedWorkspace) { + const index = services.findIndex(s => s.id === SLACK_ID); + + if (index === -1) { + const newServices = services; + newServices.push({ id: SLACK_ID, team: sanitizedWorkspace }); + this.setState({ services: newServices }); + } + } + + this.setState({ + isSlackModalOpen: false, + slackWorkspace: sanitizedWorkspace, + }); + } + + render() { + const { intl } = this.props; + const { + classes, + isInviteSuccessful, + onSubmit, + services, + isSettingUpServices, + } = this.props; + const { + isSlackModalOpen, + slackWorkspace, + services: addedServices, + } = this.state; + + return ( +
+ {this.state.showSuccessInfo && isInviteSuccessful && ( + + + {intl.formatMessage(messages.inviteSuccessInfo)} + + + )} + + +

{intl.formatMessage(messages.headline)}

+

{intl.formatMessage(messages.subHeadline)}

+
+ {Object.keys(services).map(id => { + const service = services[id]; + return ( + + + )} + + ); + })} +
+ this.setState({ isSlackModalOpen: false })} + > +
+ +

Create your first Slack workspace

+
{ + e.preventDefault(); + this.slackWorkspaceHandler(); + }} + > + + this.setState({ slackWorkspace: e.target.value }) + } + value={slackWorkspace} + /> +
+
+
+
+
+
+ ); + } +} + +export default injectIntl( + injectSheet(styles, { injectTheme: true })(observer(SetupAssistant)), +); diff --git a/src/components/auth/Signup.js b/src/components/auth/Signup.js deleted file mode 100644 index eaa68f918..000000000 --- a/src/components/auth/Signup.js +++ /dev/null @@ -1,191 +0,0 @@ -/* eslint jsx-a11y/anchor-is-valid: 0 */ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer, inject } from 'mobx-react'; -import { defineMessages, injectIntl } from 'react-intl'; - -import Form from '../../lib/Form'; -import { required, email, minLength } from '../../helpers/validation-helpers'; -import Input from '../ui/Input'; -import Button from '../ui/button'; -import Link from '../ui/Link'; - -import { globalError as globalErrorPropType } from '../../prop-types'; -import { serverBase } from '../../api/apiBase'; -import { H1 } from '../ui/headline'; - -const messages = defineMessages({ - headline: { - id: 'signup.headline', - defaultMessage: 'Sign up', - }, - firstnameLabel: { - id: 'signup.firstname.label', - defaultMessage: 'First Name', - }, - lastnameLabel: { - id: 'signup.lastname.label', - defaultMessage: 'Last Name', - }, - emailLabel: { - id: 'signup.email.label', - defaultMessage: 'Email address', - }, - // companyLabel: { - // id: 'signup.company.label', - // defaultMessage: 'Company', - // }, - passwordLabel: { - id: 'signup.password.label', - defaultMessage: 'Password', - }, - legalInfo: { - id: 'signup.legal.info', - defaultMessage: 'By creating a Ferdium account you accept the', - }, - terms: { - id: 'signup.legal.terms', - defaultMessage: 'Terms of service', - }, - privacy: { - id: 'signup.legal.privacy', - defaultMessage: 'Privacy Statement', - }, - submitButtonLabel: { - id: 'signup.submit.label', - defaultMessage: 'Create account', - }, - loginLink: { - id: 'signup.link.login', - defaultMessage: 'Already have an account, sign in?', - }, - emailDuplicate: { - id: 'signup.emailDuplicate', - defaultMessage: 'A user with that email address already exists', - }, -}); - -class Signup extends Component { - static propTypes = { - onSubmit: PropTypes.func.isRequired, - isSubmitting: PropTypes.bool.isRequired, - loginRoute: PropTypes.string.isRequired, - error: globalErrorPropType.isRequired, - }; - - form = (() => { - const { intl } = this.props; - return new Form({ - fields: { - firstname: { - label: intl.formatMessage(messages.firstnameLabel), - value: '', - validators: [required], - }, - lastname: { - label: intl.formatMessage(messages.lastnameLabel), - value: '', - validators: [required], - }, - email: { - label: intl.formatMessage(messages.emailLabel), - value: '', - validators: [required, email], - }, - password: { - label: intl.formatMessage(messages.passwordLabel), - value: '', - validators: [required, minLength(6)], - type: 'password', - }, - }, - }, - intl, - )})(); - - submit(e) { - e.preventDefault(); - this.form.submit({ - onSuccess: form => { - this.props.onSubmit(form.values()); - }, - onError: () => {}, - }); - } - - render() { - const { form } = this; - const { intl } = this.props; - const { isSubmitting, loginRoute, error } = this.props; - - return ( -
-
-
this.submit(e)} - > - -

{intl.formatMessage(messages.headline)}

-
- - -
- - - {error.code === 'email-duplicate' && ( -

- {intl.formatMessage(messages.emailDuplicate)} -

- )} - {isSubmitting ? ( -
-
- ); - } -} - -export default injectIntl(inject('actions')(observer(Signup))); diff --git a/src/components/auth/Signup.jsx b/src/components/auth/Signup.jsx new file mode 100644 index 000000000..e0337656c --- /dev/null +++ b/src/components/auth/Signup.jsx @@ -0,0 +1,206 @@ +/* eslint jsx-a11y/anchor-is-valid: 0 */ +import { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer, inject } from 'mobx-react'; +import { defineMessages, injectIntl } from 'react-intl'; + +import { mdiArrowLeftCircle } from '@mdi/js'; +import Form from '../../lib/Form'; +import { required, email, minLength } from '../../helpers/validation-helpers'; +import Input from '../ui/Input'; +import Button from '../ui/button'; +import Link from '../ui/Link'; +import Icon from '../ui/icon'; + +import { globalError as globalErrorPropType } from '../../prop-types'; +import { serverBase } from '../../api/apiBase'; +import { H1 } from '../ui/headline'; + +const messages = defineMessages({ + headline: { + id: 'signup.headline', + defaultMessage: 'Sign up', + }, + firstnameLabel: { + id: 'signup.firstname.label', + defaultMessage: 'First Name', + }, + lastnameLabel: { + id: 'signup.lastname.label', + defaultMessage: 'Last Name', + }, + emailLabel: { + id: 'signup.email.label', + defaultMessage: 'Email address', + }, + // companyLabel: { + // id: 'signup.company.label', + // defaultMessage: 'Company', + // }, + passwordLabel: { + id: 'signup.password.label', + defaultMessage: 'Password', + }, + legalInfo: { + id: 'signup.legal.info', + defaultMessage: 'By creating a Ferdium account you accept the', + }, + terms: { + id: 'signup.legal.terms', + defaultMessage: 'Terms of service', + }, + privacy: { + id: 'signup.legal.privacy', + defaultMessage: 'Privacy Statement', + }, + submitButtonLabel: { + id: 'signup.submit.label', + defaultMessage: 'Create account', + }, + loginLink: { + id: 'signup.link.login', + defaultMessage: 'Already have an account, sign in?', + }, + emailDuplicate: { + id: 'signup.emailDuplicate', + defaultMessage: 'A user with that email address already exists', + }, +}); + +class Signup extends Component { + static propTypes = { + onSubmit: PropTypes.func.isRequired, + isSubmitting: PropTypes.bool.isRequired, + loginRoute: PropTypes.string.isRequired, + error: globalErrorPropType.isRequired, + }; + + form = (() => { + const { intl } = this.props; + return new Form( + { + fields: { + firstname: { + label: intl.formatMessage(messages.firstnameLabel), + value: '', + validators: [required], + }, + lastname: { + label: intl.formatMessage(messages.lastnameLabel), + value: '', + validators: [required], + }, + email: { + label: intl.formatMessage(messages.emailLabel), + value: '', + validators: [required, email], + }, + password: { + label: intl.formatMessage(messages.passwordLabel), + value: '', + validators: [required, minLength(6)], + type: 'password', + }, + }, + }, + intl, + ); + })(); + + submit(e) { + e.preventDefault(); + this.form.submit({ + onSuccess: form => { + this.props.onSubmit(form.values()); + }, + onError: () => {}, + }); + } + + render() { + const { form } = this; + const { intl } = this.props; + const { isSubmitting, loginRoute, error } = this.props; + + return ( +
+
+
this.submit(e)} + > + + + +

{intl.formatMessage(messages.headline)}

+
+ + +
+ + + {error.code === 'email-duplicate' && ( +

+ {intl.formatMessage(messages.emailDuplicate)} +

+ )} + {isSubmitting ? ( +
+
+ ); + } +} + +export default injectIntl(inject('actions')(observer(Signup))); diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index f25d59098..d66a52b0b 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -64,7 +64,6 @@ "locked.touchId": "Unlock with Touch ID", "locked.touchIdPrompt": "unlock via Touch ID", "locked.unlockWithPassword": "Unlock with Password", - "login.backToWelcome": "Click the Ferdium icon to go back to the Welcome screen", "login.changeServer": "Change here!", "login.changeServerMessage": "You are using {serverNameParse} Server, do you want to switch?", "login.customServerQuestion": "Using a custom Ferdium server?", @@ -427,6 +426,7 @@ "setupAssistant.headline": "Let's get started", "setupAssistant.subheadline": "Choose from our most used services and get back on top of your messaging now.", "setupAssistant.submit.label": "Let's go", + "setupAssistant.skip.label": "Skip", "sidebar.addNewService": "Add new service", "sidebar.closeTodosDrawer": "Close Ferdium Todos", "sidebar.closeWorkspaceDrawer": "Close workspace drawer", @@ -480,4 +480,4 @@ "workspaceDrawer.workspaceFeatureInfo": "

Ferdium Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time.

You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.

", "workspaceDrawer.workspacesSettingsTooltip": "Edit workspaces settings", "workspaces.switchingIndicator.switchingTo": "Switching to" -} \ No newline at end of file +} diff --git a/src/styles/auth.scss b/src/styles/auth.scss index 33e3a4a78..e60c4971c 100644 --- a/src/styles/auth.scss +++ b/src/styles/auth.scss @@ -49,6 +49,7 @@ display: flex; align-items: center; justify-content: center; + overflow: auto; } .auth__container { @@ -85,7 +86,10 @@ .auth__button { width: 100%; - &.auth__button--skip { margin: 10px auto 0; } + &.auth__button--skip { + margin: 10px auto 0; + width: 20%; + } } .touchid__button { @@ -127,7 +131,10 @@ } .auth__help { - padding: 5% 2%; + display: flex; + padding-top: 2%; + padding-bottom: 2%; + justify-content: center; } .auth__adlk { @@ -143,10 +150,8 @@ .info-bar { position: absolute; } &__scroll-container { - max-height: 100vh; - padding: 80px 0; - overflow: auto; width: 100%; + height: fit-content; } .available-services { margin-bottom: 15px; } -- cgit v1.2.3-54-g00ecf