From 0c18bb192675c10dc3e895d4c641bdf2fd0ab934 Mon Sep 17 00:00:00 2001 From: dzucconi Date: Fri, 3 Aug 2018 17:14:34 -0400 Subject: [PATCH] Implements invitation acceptance --- apps/authentication/Routes.js | 6 +- apps/authentication/index.js | 5 +- assets/auth.js | 10 +-- react/components/RegistrationForm/index.js | 78 ++++++++++++------- .../mutations/acceptInvitation.js | 30 +++++++ .../RegistrationForm/mutations/register.js | 18 ++++- react/components/UI/Box/index.js | 6 +- react/components/UI/CenteringBox/index.js | 1 + .../AcceptInvitationPage/fragments/invitee.js | 9 +++ .../AcceptInvitationPage/index.js | 56 ++++++++++++- .../AcceptInvitationPage/queries/invitee.js | 12 +++ 11 files changed, 183 insertions(+), 48 deletions(-) create mode 100644 react/components/RegistrationForm/mutations/acceptInvitation.js create mode 100644 react/pages/authentication/AcceptInvitationPage/fragments/invitee.js create mode 100644 react/pages/authentication/AcceptInvitationPage/queries/invitee.js diff --git a/apps/authentication/Routes.js b/apps/authentication/Routes.js index 70e15f62e7..dbe7d8552e 100644 --- a/apps/authentication/Routes.js +++ b/apps/authentication/Routes.js @@ -26,11 +26,11 @@ export default () => ( /> ( ))} /> diff --git a/apps/authentication/index.js b/apps/authentication/index.js index 11226d57c6..aecdc64170 100644 --- a/apps/authentication/index.js +++ b/apps/authentication/index.js @@ -13,13 +13,14 @@ app .set('views', `${__dirname}/templates`) .set('view engine', 'jade') - .get(/^\/(sign_up|log_in|forgot|register\/\w+)/, apolloMiddleware, (req, res) => { + .get(/^\/(sign_up|log_in|forgot|register\/\w+)/, apolloMiddleware, (req, res, next) => { if (req.user && req.user.id) return res.redirect('/'); res.locals.sd.REDIRECT_TO = req.query['redirect-to'] || '/'; return req.apollo.render(withStaticRouter(Routes)) - .then(apollo => res.render('index', { apollo })); + .then(apollo => res.render('index', { apollo })) + .catch(next); }) .get('/me/sign_out', logoutMiddleware, redirectToMiddleware) .get('/me/refresh', (req, res, next) => { diff --git a/assets/auth.js b/assets/auth.js index 2adebcc308..70c06f95a7 100644 --- a/assets/auth.js +++ b/assets/auth.js @@ -1,14 +1,8 @@ -import sharify from 'sharify'; - import { mountWithApolloProvider } from 'react/apollo'; import withBrowserRouter from 'react/hocs/WithBrowserRouter'; import Routes from 'apps/authentication/Routes'; -const { data: { APOLLO } } = sharify; - -document.addEventListener('DOMContentLoaded', () => { - const mountPoint = document.getElementById('apolloMount'); - mountWithApolloProvider(withBrowserRouter(Routes), APOLLO, mountPoint); -}); +document.addEventListener('DOMContentLoaded', () => + mountWithApolloProvider(withBrowserRouter(Routes), {}, document.getElementById('apolloMount'))); diff --git a/react/components/RegistrationForm/index.js b/react/components/RegistrationForm/index.js index 74611537bd..82cf8c481d 100644 --- a/react/components/RegistrationForm/index.js +++ b/react/components/RegistrationForm/index.js @@ -1,10 +1,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; -import { graphql } from 'react-apollo'; +import { compose, graphql } from 'react-apollo'; import axios from 'axios'; import mapErrors from 'react/util/mapErrors'; +import compactObject from 'react/util/compactObject'; import Button from 'react/components/UI/GenericButton'; import Box from 'react/components/UI/Box'; @@ -13,25 +14,38 @@ import AuthForm from 'react/components/AuthForm'; import { Checkbox, Label, Input, ErrorMessage } from 'react/components/UI/Inputs'; import registerMutation from 'react/components/RegistrationForm/mutations/register'; +import acceptInvitationMutation from 'react/components/RegistrationForm/mutations/acceptInvitation'; const { REDIRECT_TO } = require('sharify').data; class RegistrationForm extends Component { static propTypes = { register: PropTypes.func.isRequired, + acceptInvitation: PropTypes.func.isRequired, + email: PropTypes.string, + raw_invitation_token: PropTypes.string, } - state = { - mode: 'resting', - email: '', - first_name: '', - last_name: '', - password: '', - password_confirmation: '', - accept_terms: false, - receive_newsletter: false, - attributeErrors: {}, - errorMessage: null, + static defaultProps = { + email: null, + raw_invitation_token: null, + } + + constructor(props) { + super(props); + + this.state = { + mode: 'resting', + email: props.email || '', + first_name: '', + last_name: '', + password: '', + password_confirmation: '', + accept_terms: false, + receive_newsletter: false, + attributeErrors: {}, + errorMessage: null, + }; } handleInput = fieldName => ({ target: { value: fieldValue } }) => @@ -58,7 +72,14 @@ class RegistrationForm extends Component { handleSubmit = (e) => { e.preventDefault(); - const { register } = this.props; + const { + register, + acceptInvitation, + raw_invitation_token, + } = this.props; + + const mutation = raw_invitation_token ? + acceptInvitation : register; const { email, @@ -72,17 +93,18 @@ class RegistrationForm extends Component { this.setState({ mode: 'registering' }); - return register({ - variables: { - email, - first_name, - last_name, - password, - accept_terms, - password_confirmation, - receive_newsletter, - }, - }) + const variables = compactObject({ + email, + first_name, + last_name, + password, + accept_terms, + password_confirmation, + receive_newsletter, + invitation_token: raw_invitation_token, + }); + + return mutation({ variables }) .then(() => { this.setState({ mode: 'logging_in' }); return axios.post('/me/sign_in', { email, password }); @@ -127,6 +149,7 @@ class RegistrationForm extends Component { onChange={this.handleEmail} value={email} errorMessage={attributeErrors.email} + readOnly={!!this.props.email} required /> @@ -224,6 +247,7 @@ class RegistrationForm extends Component { } } -export default graphql(registerMutation, { - name: 'register', -})(RegistrationForm); +export default compose( + graphql(registerMutation, { name: 'register' }), + graphql(acceptInvitationMutation, { name: 'acceptInvitation' }), +)(RegistrationForm); diff --git a/react/components/RegistrationForm/mutations/acceptInvitation.js b/react/components/RegistrationForm/mutations/acceptInvitation.js new file mode 100644 index 0000000000..9118940a48 --- /dev/null +++ b/react/components/RegistrationForm/mutations/acceptInvitation.js @@ -0,0 +1,30 @@ +import gql from 'graphql-tag'; + +export default gql` + mutation acceptInvitationMutation( + $invitation_token: String! + $first_name: String!, + $last_name: String!, + $email: String!, + $password: String!, + $password_confirmation: String!, + $receive_newsletter: Boolean + $receive_tips_emails: Boolean + ) { + accept_invitation(input: { + invitation_token: $invitation_token + first_name: $first_name + last_name: $last_name + email: $email + password: $password + password_confirmation: $password_confirmation + receive_newsletter: $receive_newsletter + receive_tips_emails: $receive_tips_emails + }) { + me { + id + email + } + } + } +`; diff --git a/react/components/RegistrationForm/mutations/register.js b/react/components/RegistrationForm/mutations/register.js index 9d53181189..9b9f9a26ee 100644 --- a/react/components/RegistrationForm/mutations/register.js +++ b/react/components/RegistrationForm/mutations/register.js @@ -1,8 +1,22 @@ import gql from 'graphql-tag'; export default gql` - mutation registerMutation($first_name: String!, $last_name: String!, $email: String!, $password: String!, $password_confirmation: String!, $receive_newsletter: Boolean) { - registration(input: {first_name: $first_name, last_name: $last_name, email: $email, password: $password, password_confirmation: $password_confirmation, receive_newsletter: $receive_newsletter}) { + mutation registerMutation( + $first_name: String!, + $last_name: String!, + $email: String!, + $password: String!, + $password_confirmation: String!, + $receive_newsletter: Boolean + ) { + registration(input: { + first_name: $first_name, + last_name: $last_name, + email: $email, + password: $password, + password_confirmation: $password_confirmation, + receive_newsletter: $receive_newsletter + }) { me { id } diff --git a/react/components/UI/Box/index.js b/react/components/UI/Box/index.js index 92082be9e9..276f217926 100644 --- a/react/components/UI/Box/index.js +++ b/react/components/UI/Box/index.js @@ -1,10 +1,12 @@ import styled from 'styled-components'; -import { display, space, width, alignItems, minHeight } from 'styled-system'; +import { display, space, width, alignItems, minHeight, justifyContent, flexDirection } from 'styled-system'; export default styled.div` ${display} ${width} + ${minHeight} ${space} ${alignItems} - ${minHeight} + ${justifyContent} + ${flexDirection} `; diff --git a/react/components/UI/CenteringBox/index.js b/react/components/UI/CenteringBox/index.js index 7dc43ce6d8..9565526145 100644 --- a/react/components/UI/CenteringBox/index.js +++ b/react/components/UI/CenteringBox/index.js @@ -7,5 +7,6 @@ export default styled(Box).attrs({ width: '100%', minHeight: '100vh', alignItems: 'center', + justifyContent: 'center', })` `; diff --git a/react/pages/authentication/AcceptInvitationPage/fragments/invitee.js b/react/pages/authentication/AcceptInvitationPage/fragments/invitee.js new file mode 100644 index 0000000000..344f51d0fe --- /dev/null +++ b/react/pages/authentication/AcceptInvitationPage/fragments/invitee.js @@ -0,0 +1,9 @@ +import gql from 'graphql-tag'; + +export default gql` + fragment Invitee on Invitee { + __typename + id + email + } +`; diff --git a/react/pages/authentication/AcceptInvitationPage/index.js b/react/pages/authentication/AcceptInvitationPage/index.js index 20a7e64cd1..dc6f64dd9a 100644 --- a/react/pages/authentication/AcceptInvitationPage/index.js +++ b/react/pages/authentication/AcceptInvitationPage/index.js @@ -1,14 +1,62 @@ import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Query } from 'react-apollo'; +import inviteeQuery from 'react/pages/authentication/AcceptInvitationPage/queries/invitee'; + +import Icon from 'react/components/UI/Icon'; import CenteringBox from 'react/components/UI/CenteringBox'; +import Text from 'react/components/UI/Text'; import RegistrationForm from 'react/components/RegistrationForm'; -export default class RegistrationPage extends Component { +export default class AcceptInvitationPage extends Component { + static propTypes = { + // `invitation_token` is used to locate the invite + // it is a digest of `raw_invitation_token` and exists on the user record. + invitation_token: PropTypes.string.isRequired, + // `raw_invitation_token` is used to accept the invite + // it is not in the database directly. + // At the moment this is passed as `invite_token` in the + // query string of the URL in the invitation email + raw_invitation_token: PropTypes.string.isRequired, + } + render() { + const { invitation_token, raw_invitation_token } = this.props; + return ( - - - + + {({ loading, error, data }) => { + if (loading) return
; + + if (error) { + return ( + + + + + We cannot find that invitation code. + + + + If you believe this is in error please contact + {' '} + help@are.na + + + ); + } + + return ( + + + + ); + }} + ); } } diff --git a/react/pages/authentication/AcceptInvitationPage/queries/invitee.js b/react/pages/authentication/AcceptInvitationPage/queries/invitee.js new file mode 100644 index 0000000000..5765dbe244 --- /dev/null +++ b/react/pages/authentication/AcceptInvitationPage/queries/invitee.js @@ -0,0 +1,12 @@ +import gql from 'graphql-tag'; + +import inviteeFragment from 'react/pages/authentication/AcceptInvitationPage/fragments/invitee'; + +export default gql` + query Invitee($invitation_token: String!) { + invitee(invitation_token: $invitation_token) { + ...Invitee + } + } + ${inviteeFragment} +`;