Improve login/inbox/cookie caching flow

This commit is contained in:
Nicholas Zuber 2018-10-27 01:48:28 -04:00
parent be1f0643f7
commit 5d0eb96836
14 changed files with 296 additions and 57 deletions

33
package-lock.json generated
View File

@ -2548,6 +2548,11 @@
"supports-color": "^5.3.0"
}
},
"change-emitter": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz",
"integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU="
},
"chardet": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
@ -5737,6 +5742,11 @@
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
"integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA=="
},
"hoist-non-react-statics": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
"integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw=="
},
"home-or-tmp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
@ -8027,6 +8037,11 @@
"minimist": "0.0.8"
}
},
"moment": {
"version": "2.22.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
"integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y="
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -11259,6 +11274,19 @@
"util.promisify": "^1.0.0"
}
},
"recompose": {
"version": "0.30.0",
"resolved": "https://registry.npmjs.org/recompose/-/recompose-0.30.0.tgz",
"integrity": "sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w==",
"requires": {
"@babel/runtime": "^7.0.0",
"change-emitter": "^0.1.2",
"fbjs": "^0.8.1",
"hoist-non-react-statics": "^2.3.1",
"react-lifecycles-compat": "^3.0.2",
"symbol-observable": "^1.0.4"
}
},
"recursive-readdir": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz",
@ -12923,6 +12951,11 @@
"util.promisify": "~1.0.0"
}
},
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
},
"symbol-tree": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz",

View File

@ -34,6 +34,7 @@
"jest-pnp-resolver": "1.0.1",
"jest-resolve": "23.6.0",
"mini-css-extract-plugin": "0.4.3",
"moment": "^2.22.2",
"optimize-css-assets-webpack-plugin": "5.0.1",
"pnp-webpack-plugin": "1.1.0",
"postcss-flexbugs-fixes": "4.1.0",
@ -46,6 +47,7 @@
"react-dev-utils": "^6.0.5",
"react-dom": "^16.6.0",
"react-emotion": "^9.2.12",
"recompose": "^0.30.0",
"resolve": "1.8.1",
"sass-loader": "7.1.0",
"style-loader": "0.23.0",

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { Router } from "@reach/router";
import { Routes } from './constants';
import { routes } from './constants';
import { AuthProvider } from './providers/Auth';
import {
Home,
@ -13,9 +13,9 @@ class App extends Component {
return (
<AuthProvider>
<Router>
<Home path={Routes.HOME} />
<Login path={Routes.LOGIN} />
<Inbox path={Routes.INBOX} />
<Home path={routes.HOME} />
<Login path={routes.LOGIN} />
<Inbox path={routes.INBOX} />
</Router>
</AuthProvider>
);

1
src/constants/cookies.js Normal file
View File

@ -0,0 +1 @@
export const OAUTH_TOKEN_COOKIE = 'meteorite-oauth-token';

View File

@ -1 +1 @@
export {default as Routes} from './routes';
export {default as routes} from './routes';

View File

@ -1,11 +1,41 @@
import React from 'react';
import { Link } from "@reach/router";
import { Routes } from '../constants';
import { compose } from 'recompose';
import { withAuthProvider } from '../providers/Auth';
import { withCookiesProvider } from '../providers/Cookies';
import { routes } from '../constants';
import { OAUTH_TOKEN_COOKIE } from '../constants/cookies';
export default props => (
<div>
Home!
<Link to={Routes.LOGIN}>login</Link>
<Link to={Routes.INBOX}>inbox</Link>
</div>
class HomePage extends React.Component {
render () {
return (
<div>
Home!
{this.props.authApi.token ? (
<React.Fragment>
<p><Link to={routes.INBOX}>inbox</Link></p>
<p><a
href="javascript:void(0);"
onClick={() => {
// Remove cookie and invalidate token on client.
this.props.cookiesApi.removeCookie(OAUTH_TOKEN_COOKIE);
this.props.authApi.invalidateToken();
}}
>
soft logout
</a></p>
</React.Fragment>
) : (
<p><Link to={routes.LOGIN}>login</Link></p>
)}
</div>
);
}
};
const enhance = compose(
withAuthProvider,
withCookiesProvider
);
export default enhance(HomePage);

View File

@ -1,10 +1,31 @@
import React from 'react';
import { Link } from "@reach/router";
import { Routes } from '../constants';
import { Link, Redirect } from "@reach/router";
import { compose } from 'recompose';
import { withNotificationsProvider } from '../providers/Notifications';
import { withAuthProvider } from '../providers/Auth';
import { routes } from '../constants';
export default props => (
<div>
Inbox
<Link to={Routes.HOME}>home</Link>
</div>
class InboxPage extends React.Component {
render () {
if (!this.props.authApi.token) {
return <Redirect noThrow to={routes.LOGIN} />
}
return (
<div>
Inbox
<Link to={routes.HOME}>home</Link>
<button onClick={() => {
this.props.notificationsApi.getNotifications()
}}>click</button>
</div>
);
}
};
const enhance = compose(
withAuthProvider,
withNotificationsProvider
);
export default enhance(InboxPage);

View File

@ -1,11 +1,11 @@
import React from 'react';
import { Link } from "@reach/router";
import styled from 'react-emotion';
import { Routes } from '../../constants';
import { routes } from '../../constants';
import { AuthenticationButton } from '../../components/buttons';
const Container = styled.div({
background: 'red',
const Container = styled('div')({
background: '#f4f4f4',
width: '100%',
height: 100
});
@ -13,8 +13,8 @@ const Container = styled.div({
export default function Scene ({ loading, error, loggedOut, ...props }) {
return (
<Container>
<Link to={Routes.HOME}>home</Link>
<p>
<Link to={routes.HOME}>home</Link>
<div>
{error ? (
<div>
error, try again?
@ -25,9 +25,9 @@ export default function Scene ({ loading, error, loggedOut, ...props }) {
) : loggedOut ? (
<AuthenticationButton />
) : (
<span>logged in!!</span>
<span>logged in!</span>
)}
</p>
</div>
</Container>
);
}

View File

@ -1,8 +1,5 @@
import React from 'react';
import qs from 'query-string';
import { Routes } from '../../constants';
import { AuthenticationButton } from '../../components/buttons';
import { AuthConsumer } from '../../providers/Auth';
export default class TokenHandler extends React.Component {
componentDidMount() {

View File

@ -1,38 +1,53 @@
import React from 'react';
import { Link } from "@reach/router";
import { Routes } from '../../constants';
import { AuthenticationButton } from '../../components/buttons';
import { AuthConsumer } from '../../providers/Auth';
import { Redirect } from '@reach/router';
import { compose } from 'recompose';
import { withAuthProvider } from '../../providers/Auth';
import { withNotificationsProvider } from '../../providers/Notifications';
import { withCookiesProvider } from '../../providers/Cookies';
import TokenHandler from './TokenHandler';
import Scene from './Scene';
import { routes } from '../../constants';
export default class LoginPage extends React.Component {
class LoginPage extends React.Component {
state = {
loading: false,
error: null
}
onSetLoading = loading => this.setState({ loading });
onSetError = error => this.setState({ error });
onSetLoading = loading => {
this.setState({ loading });
}
onSetError = error => {
this.setState({ error });
}
render () {
if (this.props.authApi.token) {
return <Redirect noThrow to={routes.INBOX} />
}
return (
<AuthConsumer>
{({ token, setToken }) => (
<React.Fragment>
<TokenHandler
setToken={setToken}
onSetLoading={this.onSetLoading}
onSetError={this.onSetError}
/>
<Scene
loading={this.state.loading}
error={this.state.error}
loggedOut={!token}
/>
</React.Fragment>
)}
</AuthConsumer>
<React.Fragment>
<TokenHandler
setToken={this.props.authApi.setToken}
onSetLoading={this.onSetLoading}
onSetError={this.onSetError}
/>
<Scene
loading={this.state.loading}
error={this.state.error}
loggedOut={!this.props.authApi.token}
/>
</React.Fragment>
);
}
}
const enhance = compose(
withAuthProvider,
withNotificationsProvider,
withCookiesProvider
);
export default enhance(LoginPage);

View File

@ -1,21 +1,29 @@
import React from 'react';
import { withCookiesProvider } from './Cookies';
import { OAUTH_TOKEN_COOKIE } from '../constants/cookies';
const {Provider, Consumer} = React.createContext('foo');
class AuthProvider extends React.Component {
state = {
token: null
token: this.props.cookiesApi.getCookie(OAUTH_TOKEN_COOKIE)
}
setToken = token => {
this.props.cookiesApi.setCookie(OAUTH_TOKEN_COOKIE, token);
this.setState({ token });
}
invalidateToken = () => {
this.setState({ token: null });
}
render () {
return (
<Provider value={{
token: this.state.token,
setToken: this.setToken
setToken: this.setToken,
invalidateToken: this.invalidateToken
}}>
{this.props.children}
</Provider>
@ -23,7 +31,16 @@ class AuthProvider extends React.Component {
}
}
const withAuthProvider = WrappedComponent => props => (
<Consumer>
{authApi => <WrappedComponent {...props} authApi={authApi} />}
</Consumer>
);
const AuthProviderWithCookies = withCookiesProvider(AuthProvider);
export {
AuthProvider,
Consumer as AuthConsumer
AuthProviderWithCookies as AuthProvider,
Consumer as AuthConsumer,
withAuthProvider
};

61
src/providers/Cookies.js Normal file
View File

@ -0,0 +1,61 @@
import React from 'react';
import moment from 'moment';
class CookiesProvider extends React.Component {
state = {
cookies: {}
}
componentWillMount () {
this.hydrateCookies();
}
mapifyCookies = () => {
const cookiesPairs = document.cookie.split(';').map(cookie => cookie.trim());
const cookies = cookiesPairs.reduce((map, cookiePair) => {
const [key, value] = cookiePair.split('=');
map[key] = value;
return map;
}, {});
return cookies;
}
hydrateCookies = () => {
const cookies = this.mapifyCookies();
this.setState({ cookies });
}
setCookie = (name, value) => {
document.cookie = `${name}=${value}`;
this.hydrateCookies()
}
getCookie = name => {
return this.state.cookies[name];
}
removeCookie = name => {
document.cookie = `${name}=''; expires=${moment().subtract(1, 'day').toString()}`;
this.hydrateCookies();
}
render () {
return this.props.children({
...this.state,
setCookie: this.setCookie,
getCookie: this.getCookie,
removeCookie: this.removeCookie
});
}
}
const withCookiesProvider = WrappedComponent => props => (
<CookiesProvider>
{cookiesApi => <WrappedComponent {...props} cookiesApi={cookiesApi} />}
</CookiesProvider>
);
export {
CookiesProvider,
withCookiesProvider
};

View File

@ -0,0 +1,59 @@
import React from 'react';
import { AuthConsumer } from './Auth';
const BASE_GITHUB_API_URL = 'https://api.github.com';
class NotificationsProvider extends React.Component {
state = {
loading: false,
error: null
}
getNotifications = () => {
if (!this.props.token) {
console.error('unauthenitcated!')
return false;
}
console.warn(this.props.token)
this.setState({ loading: true });
fetch(`${BASE_GITHUB_API_URL}/notifications`, {
method: 'GET',
headers: {
'Authorization': `token ${this.props.token}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
}).then(response => response.json())
.then(data => {
console.warn(data);
})
.catch(error => this.setState({ error }))
.finally(() => this.setState({ loading: false }));
}
render () {
return this.props.children({
...this.state,
getNotifications: this.getNotifications
});
}
}
const withNotificationsProvider = WrappedComponent => props => (
<AuthConsumer>
{({ token }) => (
<NotificationsProvider token={token}>
{(notificationsApi) => (
<WrappedComponent {...props} notificationsApi={notificationsApi} />
)}
</NotificationsProvider>
)}
</AuthConsumer>
);
export {
NotificationsProvider,
withNotificationsProvider
};

View File

@ -1,7 +1,10 @@
@import url('https://rsms.me/inter/inter-ui.css');
body {
background: #f4f4f4;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
font-family: 'Inter UI', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;