-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
254 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"presets": ["es2015", "stage-2", "react"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<!doctype html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>redux-saga-rxjs</title> | ||
</head> | ||
<body> | ||
<div id="app"></div> | ||
<script type="text/javascript" src="/app.bundle.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
{ | ||
"name": "redux-saga-rxjs-example-auth", | ||
"version": "0.1.0", | ||
"scripts": { | ||
"start": "./node_modules/.bin/webpack-dev-server --config webpack.config.js --port 3000 --hot --content-base ./dev" | ||
}, | ||
"devDependencies": { | ||
"babel-cli": "^6.5.1", | ||
"babel-core": "^6.5.2", | ||
"babel-eslint": "^4.1.8", | ||
"babel-loader": "^6.2.2", | ||
"babel-preset-es2015": "^6.5.0", | ||
"babel-preset-react": "^6.5.0", | ||
"babel-preset-stage-2": "^6.5.0", | ||
"webpack": "^1.12.4", | ||
"webpack-dev-server": "^1.12.1" | ||
}, | ||
"dependencies": { | ||
"babel-runtime": "^6.5.0", | ||
"moment": "^2.11.2", | ||
"react": "^0.14.2", | ||
"react-dom": "^0.14.2", | ||
"react-redux": "^4.0.0", | ||
"redux": "^3.0.4", | ||
"redux-saga-rxjs": "^0.2.0", | ||
"rxjs": "^5.0.0-beta.2" | ||
}, | ||
"author": "Tomas Weiss <tomas.weiss2@gmail.com>", | ||
"license": "MIT" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import * as Actions from '../constants/actions'; | ||
|
||
export const logIn = credentials => ({type: Actions.LOG_IN, payload: credentials}); | ||
export const logOut = () => ({type: Actions.LOG_OUT, payload: null}); | ||
export const changeCredentials = credentials => ({type: Actions.CHANGE_CREDENTIALS, payload: credentials}); | ||
export const tokenRefreshed = refreshed => ({type: Actions.TOKEN_REFRESHED, payload: refreshed}); | ||
export const hideToast = () => ({type: Actions.HIDE_TOAST, payload: null}); | ||
export const loggedIn = (credentials, refreshed) => ({type: Actions.LOGGED_IN, payload: { credentials, refreshed }}); | ||
export const logInFailure = () => ({type: Actions.LOG_IN_FAILURE, payload: null}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React from 'react'; | ||
import { connect } from 'react-redux'; | ||
|
||
import * as ActionCreators from '../actions/actionCreators'; | ||
|
||
export default connect(appState => appState)(({ dispatch, credentials, loginError, loggedIn, lastTokenRefresh, apiInProgress }) => { | ||
if (apiInProgress) { | ||
return <div>API call in progress</div>; | ||
} else if (loggedIn) { | ||
return ( | ||
<div> | ||
<div>Token last refreshed {lastTokenRefresh}</div> | ||
<button onClick={() => dispatch(ActionCreators.logOut())}>Log out</button> | ||
</div> | ||
); | ||
} else { | ||
return ( | ||
<div> | ||
<label htmlFor="credentials">Credentials (valid credentials is <b>saga</b>): </label> | ||
<input | ||
id="credentials" | ||
type="text" | ||
value={credentials} | ||
onChange={ev => dispatch(ActionCreators.changeCredentials(ev.target.value))} | ||
/><br /> | ||
<button onClick={() => dispatch(ActionCreators.logIn(credentials))}>Log in</button><br /> | ||
{loginError ? <span style={{color: 'red'}}>Invalid credentials provided</span> : false} | ||
</div> | ||
); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// User actions | ||
export const LOG_IN = 'LOG_IN'; | ||
export const LOG_OUT = 'LOG_OUT'; | ||
export const CHANGE_CREDENTIALS = 'CHANGE_CREDENTIALS'; | ||
|
||
// Saga actions | ||
export const TOKEN_REFRESHED = 'TOKEN_REFRESHED'; | ||
export const HIDE_TOAST = 'HIDE_TOAST'; | ||
|
||
// API callbacks | ||
export const LOGGED_IN = 'LOGGED_IN'; | ||
export const LOG_IN_FAILURE = 'LOG_IN_FAILURE'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import React from 'react'; | ||
import { render } from 'react-dom'; | ||
import { createStore, applyMiddleware } from 'redux'; | ||
import { Provider } from 'react-redux'; | ||
import sagaMiddleware from 'redux-saga-rxjs'; | ||
|
||
import Application from './components/Application'; | ||
import rootReducer from './reducers/rootReducer'; | ||
import authSaga from './sagas/authSaga'; | ||
|
||
const store = createStore(rootReducer, undefined, applyMiddleware(sagaMiddleware(authSaga))); | ||
|
||
render(( | ||
<Provider store={store}> | ||
<Application /> | ||
</Provider> | ||
), document.getElementById('app')); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import * as Actions from '../constants/actions'; | ||
|
||
const initialAppState = { | ||
loggedIn: false, | ||
lastTokenRefresh: '', | ||
apiInProgress: false, | ||
credentials: 'saga', | ||
loginError: false | ||
}; | ||
|
||
export default (appState = initialAppState, { type, payload }) => { | ||
|
||
switch (type) { | ||
case Actions.CHANGE_CREDENTIALS: | ||
return { ...appState, credentials: payload }; | ||
|
||
case Actions.LOG_IN: | ||
return { ...appState, apiInProgress: true }; | ||
|
||
case Actions.LOG_OUT: | ||
return initialAppState; | ||
|
||
case Actions.LOGGED_IN: | ||
return { ...appState, loggedIn: true, apiInProgress: false, lastTokenRefresh: payload.refreshed }; | ||
|
||
case Actions.LOG_IN_FAILURE: | ||
return { ...appState, loggedIn: false, apiInProgress: false, loginError: true }; | ||
|
||
case Actions.TOKEN_REFRESHED: | ||
return { ...appState, lastTokenRefresh: payload }; | ||
|
||
case Actions.HIDE_TOAST: | ||
return { ...appState, loginError: false }; | ||
|
||
default: | ||
return appState; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { Observable } from 'rxjs'; | ||
import moment from 'moment'; | ||
|
||
import * as Actions from '../constants/actions'; | ||
import * as ActionCreators from '../actions/actionCreators'; | ||
|
||
const logInApi = crendetials => new Promise((res, rej) => setTimeout(() => { | ||
if (crendetials === 'saga') { | ||
res(moment().format('HH:mm:ss')); | ||
} else { | ||
rej('Invalid credentials'); | ||
} | ||
}, 500)); | ||
|
||
const createDelay = time => new Promise(res => setTimeout(() => res(), time)); | ||
const actionOrder = (actions, order) => actions.every(({ action }, index) => action.type === order[index]); | ||
const actionPredicate = actions => ({ action }) => actions.some(someAction => someAction === action.type); | ||
|
||
const AUTH_EXPIRATION = 1000; | ||
|
||
const LOG_OUT_ACTIONS_ORDER = [ | ||
Actions.LOG_IN, | ||
Actions.LOGGED_IN, | ||
Actions.LOG_OUT | ||
]; | ||
|
||
// User clicked the log in button, | ||
// call the API and respond either success or failure | ||
const authGetTokenSaga = iterable => iterable | ||
.filter(actionPredicate([Actions.LOG_IN])) | ||
.flatMap(({ action }) => Observable | ||
.fromPromise(logInApi(action.payload)) | ||
.map(refreshed => ActionCreators.loggedIn(action.payload, refreshed)) | ||
.catch(() => Observable.of(ActionCreators.logInFailure()))); | ||
|
||
// After the user is successfuly logged in, | ||
// let's schedule an infinite interval stream | ||
// which can be interrupted either by LOG_OUT | ||
// or failure in refreshing (TOKEN_REFRESHING_FAILED) | ||
const authRefreshTokenSaga = iterable => iterable | ||
.filter(actionPredicate([Actions.LOGGED_IN])) | ||
.flatMap(({ action }) => Observable | ||
.interval(AUTH_EXPIRATION) | ||
.flatMap(() => Observable | ||
.fromPromise(logInApi(action.payload.credentials)) | ||
.map(refreshed => ActionCreators.tokenRefreshed(refreshed)) | ||
) | ||
.takeUntil(iterable.filter(actionPredicate([Actions.LOG_OUT]))) | ||
); | ||
|
||
// Observe all the actions in specific order | ||
// to determine whether user wants to log out | ||
const authHandleLogOutSaga = iterable => iterable | ||
.filter(actionPredicate(LOG_OUT_ACTIONS_ORDER)) | ||
.bufferCount(LOG_OUT_ACTIONS_ORDER.length) | ||
.filter(actions => actionOrder(actions, LOG_OUT_ACTIONS_ORDER)) | ||
.map(() => ActionCreators.logOut()); | ||
|
||
const authShowLogInFailureToast = iterable => iterable | ||
.filter(actionPredicate([Actions.LOG_IN_FAILURE])) | ||
.flatMap(() => | ||
Observable.race( | ||
Observable.fromPromise(createDelay(5000)), | ||
iterable.filter(actionPredicate([Actions.CHANGE_CREDENTIALS])) | ||
) | ||
.map(() => ActionCreators.hideToast())); | ||
|
||
export default iterable => Observable.merge( | ||
authGetTokenSaga(iterable), | ||
authRefreshTokenSaga(iterable), | ||
authHandleLogOutSaga(iterable), | ||
authShowLogInFailureToast(iterable) | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
var path = require('path'); | ||
var webpack = require('webpack'); | ||
|
||
module.exports = { | ||
debug: true, | ||
target: 'web', | ||
devtool: 'sourcemap', | ||
plugins: [ | ||
new webpack.NoErrorsPlugin() | ||
], | ||
entry: [ | ||
'webpack-dev-server/client?http://localhost:3000', | ||
'webpack/hot/only-dev-server', | ||
'./src/main.js' | ||
], | ||
output: { | ||
path: path.join(__dirname, './dev'), | ||
filename: 'app.bundle.js' | ||
}, | ||
module: { | ||
loaders: [{ | ||
test: /\.jsx$|\.js$/, | ||
loaders: ['babel-loader'], | ||
include: path.join(__dirname, './src') | ||
}] | ||
}, | ||
resolve: { | ||
extensions: ['', '.js', '.jsx'] | ||
} | ||
}; |