mirror of
https://github.com/nickzuber/meteorite.git
synced 2024-08-16 06:50:29 +03:00
Improve login/inbox/cookie caching flow
This commit is contained in:
parent
be1f0643f7
commit
5d0eb96836
33
package-lock.json
generated
33
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
1
src/constants/cookies.js
Normal file
@ -0,0 +1 @@
|
||||
export const OAUTH_TOKEN_COOKIE = 'meteorite-oauth-token';
|
@ -1 +1 @@
|
||||
export {default as Routes} from './routes';
|
||||
export {default as routes} from './routes';
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
@ -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
61
src/providers/Cookies.js
Normal 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
|
||||
};
|
59
src/providers/Notifications.js
Normal file
59
src/providers/Notifications.js
Normal 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
|
||||
};
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user