Skip to content

Commit 0c18bb1

Browse files
committed
Implements invitation acceptance
1 parent 6d8a13b commit 0c18bb1

11 files changed

Lines changed: 183 additions & 48 deletions

File tree

apps/authentication/Routes.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ export default () => (
2626
/>
2727

2828
<Route
29-
path="/register/:token"
29+
path="/register/:invitation_token"
3030
render={parseRoute(({ params, query }) => (
3131
<AcceptInvitationPage
32-
{...params}
33-
{...query}
32+
invitation_token={params.invitation_token}
33+
raw_invitation_token={query.invite_token}
3434
/>
3535
))}
3636
/>

apps/authentication/index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ app
1313
.set('views', `${__dirname}/templates`)
1414
.set('view engine', 'jade')
1515

16-
.get(/^\/(sign_up|log_in|forgot|register\/\w+)/, apolloMiddleware, (req, res) => {
16+
.get(/^\/(sign_up|log_in|forgot|register\/\w+)/, apolloMiddleware, (req, res, next) => {
1717
if (req.user && req.user.id) return res.redirect('/');
1818

1919
res.locals.sd.REDIRECT_TO = req.query['redirect-to'] || '/';
2020

2121
return req.apollo.render(withStaticRouter(Routes))
22-
.then(apollo => res.render('index', { apollo }));
22+
.then(apollo => res.render('index', { apollo }))
23+
.catch(next);
2324
})
2425
.get('/me/sign_out', logoutMiddleware, redirectToMiddleware)
2526
.get('/me/refresh', (req, res, next) => {

assets/auth.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
import sharify from 'sharify';
2-
31
import { mountWithApolloProvider } from 'react/apollo';
42

53
import withBrowserRouter from 'react/hocs/WithBrowserRouter';
64

75
import Routes from 'apps/authentication/Routes';
86

9-
const { data: { APOLLO } } = sharify;
10-
11-
document.addEventListener('DOMContentLoaded', () => {
12-
const mountPoint = document.getElementById('apolloMount');
13-
mountWithApolloProvider(withBrowserRouter(Routes), APOLLO, mountPoint);
14-
});
7+
document.addEventListener('DOMContentLoaded', () =>
8+
mountWithApolloProvider(withBrowserRouter(Routes), {}, document.getElementById('apolloMount')));

react/components/RegistrationForm/index.js

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
33
import { Link } from 'react-router-dom';
4-
import { graphql } from 'react-apollo';
4+
import { compose, graphql } from 'react-apollo';
55
import axios from 'axios';
66

77
import mapErrors from 'react/util/mapErrors';
8+
import compactObject from 'react/util/compactObject';
89

910
import Button from 'react/components/UI/GenericButton';
1011
import Box from 'react/components/UI/Box';
@@ -13,25 +14,38 @@ import AuthForm from 'react/components/AuthForm';
1314
import { Checkbox, Label, Input, ErrorMessage } from 'react/components/UI/Inputs';
1415

1516
import registerMutation from 'react/components/RegistrationForm/mutations/register';
17+
import acceptInvitationMutation from 'react/components/RegistrationForm/mutations/acceptInvitation';
1618

1719
const { REDIRECT_TO } = require('sharify').data;
1820

1921
class RegistrationForm extends Component {
2022
static propTypes = {
2123
register: PropTypes.func.isRequired,
24+
acceptInvitation: PropTypes.func.isRequired,
25+
email: PropTypes.string,
26+
raw_invitation_token: PropTypes.string,
2227
}
2328

24-
state = {
25-
mode: 'resting',
26-
email: '',
27-
first_name: '',
28-
last_name: '',
29-
password: '',
30-
password_confirmation: '',
31-
accept_terms: false,
32-
receive_newsletter: false,
33-
attributeErrors: {},
34-
errorMessage: null,
29+
static defaultProps = {
30+
email: null,
31+
raw_invitation_token: null,
32+
}
33+
34+
constructor(props) {
35+
super(props);
36+
37+
this.state = {
38+
mode: 'resting',
39+
email: props.email || '',
40+
first_name: '',
41+
last_name: '',
42+
password: '',
43+
password_confirmation: '',
44+
accept_terms: false,
45+
receive_newsletter: false,
46+
attributeErrors: {},
47+
errorMessage: null,
48+
};
3549
}
3650

3751
handleInput = fieldName => ({ target: { value: fieldValue } }) =>
@@ -58,7 +72,14 @@ class RegistrationForm extends Component {
5872
handleSubmit = (e) => {
5973
e.preventDefault();
6074

61-
const { register } = this.props;
75+
const {
76+
register,
77+
acceptInvitation,
78+
raw_invitation_token,
79+
} = this.props;
80+
81+
const mutation = raw_invitation_token ?
82+
acceptInvitation : register;
6283

6384
const {
6485
email,
@@ -72,17 +93,18 @@ class RegistrationForm extends Component {
7293

7394
this.setState({ mode: 'registering' });
7495

75-
return register({
76-
variables: {
77-
email,
78-
first_name,
79-
last_name,
80-
password,
81-
accept_terms,
82-
password_confirmation,
83-
receive_newsletter,
84-
},
85-
})
96+
const variables = compactObject({
97+
email,
98+
first_name,
99+
last_name,
100+
password,
101+
accept_terms,
102+
password_confirmation,
103+
receive_newsletter,
104+
invitation_token: raw_invitation_token,
105+
});
106+
107+
return mutation({ variables })
86108
.then(() => {
87109
this.setState({ mode: 'logging_in' });
88110
return axios.post('/me/sign_in', { email, password });
@@ -127,6 +149,7 @@ class RegistrationForm extends Component {
127149
onChange={this.handleEmail}
128150
value={email}
129151
errorMessage={attributeErrors.email}
152+
readOnly={!!this.props.email}
130153
required
131154
/>
132155

@@ -224,6 +247,7 @@ class RegistrationForm extends Component {
224247
}
225248
}
226249

227-
export default graphql(registerMutation, {
228-
name: 'register',
229-
})(RegistrationForm);
250+
export default compose(
251+
graphql(registerMutation, { name: 'register' }),
252+
graphql(acceptInvitationMutation, { name: 'acceptInvitation' }),
253+
)(RegistrationForm);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import gql from 'graphql-tag';
2+
3+
export default gql`
4+
mutation acceptInvitationMutation(
5+
$invitation_token: String!
6+
$first_name: String!,
7+
$last_name: String!,
8+
$email: String!,
9+
$password: String!,
10+
$password_confirmation: String!,
11+
$receive_newsletter: Boolean
12+
$receive_tips_emails: Boolean
13+
) {
14+
accept_invitation(input: {
15+
invitation_token: $invitation_token
16+
first_name: $first_name
17+
last_name: $last_name
18+
email: $email
19+
password: $password
20+
password_confirmation: $password_confirmation
21+
receive_newsletter: $receive_newsletter
22+
receive_tips_emails: $receive_tips_emails
23+
}) {
24+
me {
25+
id
26+
email
27+
}
28+
}
29+
}
30+
`;

react/components/RegistrationForm/mutations/register.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
11
import gql from 'graphql-tag';
22

33
export default gql`
4-
mutation registerMutation($first_name: String!, $last_name: String!, $email: String!, $password: String!, $password_confirmation: String!, $receive_newsletter: Boolean) {
5-
registration(input: {first_name: $first_name, last_name: $last_name, email: $email, password: $password, password_confirmation: $password_confirmation, receive_newsletter: $receive_newsletter}) {
4+
mutation registerMutation(
5+
$first_name: String!,
6+
$last_name: String!,
7+
$email: String!,
8+
$password: String!,
9+
$password_confirmation: String!,
10+
$receive_newsletter: Boolean
11+
) {
12+
registration(input: {
13+
first_name: $first_name,
14+
last_name: $last_name,
15+
email: $email,
16+
password: $password,
17+
password_confirmation: $password_confirmation,
18+
receive_newsletter: $receive_newsletter
19+
}) {
620
me {
721
id
822
}

react/components/UI/Box/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import styled from 'styled-components';
2-
import { display, space, width, alignItems, minHeight } from 'styled-system';
2+
import { display, space, width, alignItems, minHeight, justifyContent, flexDirection } from 'styled-system';
33

44
export default styled.div`
55
${display}
66
${width}
7+
${minHeight}
78
${space}
89
${alignItems}
9-
${minHeight}
10+
${justifyContent}
11+
${flexDirection}
1012
`;

react/components/UI/CenteringBox/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ export default styled(Box).attrs({
77
width: '100%',
88
minHeight: '100vh',
99
alignItems: 'center',
10+
justifyContent: 'center',
1011
})`
1112
`;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import gql from 'graphql-tag';
2+
3+
export default gql`
4+
fragment Invitee on Invitee {
5+
__typename
6+
id
7+
email
8+
}
9+
`;
Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,62 @@
11
import React, { Component } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { Query } from 'react-apollo';
24

5+
import inviteeQuery from 'react/pages/authentication/AcceptInvitationPage/queries/invitee';
6+
7+
import Icon from 'react/components/UI/Icon';
38
import CenteringBox from 'react/components/UI/CenteringBox';
9+
import Text from 'react/components/UI/Text';
410
import RegistrationForm from 'react/components/RegistrationForm';
511

6-
export default class RegistrationPage extends Component {
12+
export default class AcceptInvitationPage extends Component {
13+
static propTypes = {
14+
// `invitation_token` is used to locate the invite
15+
// it is a digest of `raw_invitation_token` and exists on the user record.
16+
invitation_token: PropTypes.string.isRequired,
17+
// `raw_invitation_token` is used to accept the invite
18+
// it is not in the database directly.
19+
// At the moment this is passed as `invite_token` in the
20+
// query string of the URL in the invitation email
21+
raw_invitation_token: PropTypes.string.isRequired,
22+
}
23+
724
render() {
25+
const { invitation_token, raw_invitation_token } = this.props;
26+
827
return (
9-
<CenteringBox p={7}>
10-
<RegistrationForm />
11-
</CenteringBox>
28+
<Query query={inviteeQuery} variables={{ invitation_token }}>
29+
{({ loading, error, data }) => {
30+
if (loading) return <div />;
31+
32+
if (error) {
33+
return (
34+
<CenteringBox p={7} flexDirection="column">
35+
<Icon name="ArenaMark" size={7} mb={9} />
36+
37+
<Text f={5} mb={6}>
38+
We cannot find that invitation code.
39+
</Text>
40+
41+
<Text f={2} underlineLinks>
42+
If you believe this is in error please contact
43+
{' '}
44+
<a href="mailto:help@are.na">help@are.na</a>
45+
</Text>
46+
</CenteringBox>
47+
);
48+
}
49+
50+
return (
51+
<CenteringBox p={7}>
52+
<RegistrationForm
53+
email={data.invitee.email}
54+
raw_invitation_token={raw_invitation_token}
55+
/>
56+
</CenteringBox>
57+
);
58+
}}
59+
</Query>
1260
);
1361
}
1462
}

0 commit comments

Comments
 (0)