Modal dialog auth with react-router - javascript

I have react/redux/react-router application with public and private parts.
Login and register form are shown as modal dialogs and don't have their own routes.
Desired flow:
user clicks on link -> modal dialog is shown on current page -> in case of successful auth transition to linked page, else leave user on current page.
If there is no current page-show index page and continue with the flow
I'm tried to archive this using onEnter hook, but as far as i can see transition happens before hook is executed. If I try to use history.goBack() it's causes rerendering of page and looks nasty.
Is there any way to solve this problem without unnecessary redirects and extra render calls?

OK - I think I came up with a way to handle this that covers all the corner cases. It does require that you have some way to access application state from almost any component though. I am using Redux for that. This also assumes that login, register, etc do NOT have routes.
What I did was create two 'wrapper' components. The first one wraps any insecure routes and stores the location to a state value so that we always have a reference to the last insecure route...
import { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { setRoute } from './redux/client/ducks/auth';
function mapDispatchToProps(dispatch) {
return {
setRoute: bindActionCreators(setRoute, dispatch)
};
}
#connect(null, mapDispatchToProps)
export default class InsecureWrapper extends Component {
componentDidMount() {
const { location, setRoute } = this.props;
setRoute(location.pathname);
}
render() {
return (
<div>
{this.props.children}
</div>
)
}
}
The other one wraps all secure routes. It displays the login dialog (can also toggle back and forth between login and register or whatever) and prevents the content from being displayed unless logged into the app...
import { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as authActions from './redux/client/ducks/auth';
function mapStateToProps(state) {
return {
auth: state.auth
};
}
function mapDispatchToProps(dispatch) {
return {
authActions: bindActionCreators(authActions, dispatch)
};
}
#connect(
mapStateToProps,
mapDispatchToProps
)
export default class SecureWrapper extends Component {
componentDidMount() {
const { auth, authActions } = this.props;
//see if user and if not prompt for login
if(!auth.loggedIn) {
authActions.openLogin();
}
}
//close any open dialogs in case the user hit browser back or whatever
componentWillUnmount() {
const { authActions } = this.props;
authActions.resetAuthDialogs();
}
render() {
const { auth, children } = this.props;
return (
<div className="container">
{auth.loggedIn &&
{children} ||
<span>YOU MUST BE LOGGED IN TO VIEW THIS AREA!</span>
}
</div>
);
}
}
Then in the routes, just wrap them in the wrappers as needed...
import App from './containers/App';
import Dashboard from './containers/Dashboard';
import Secure from './containers/Secure';
import AuthWrapper from './common/client/components/AuthWrapper';
import InsecureWrapper from './common/client/components/InsecureWrapper';
export default [
{path: '/', component: App, //common header or whatever can go here
childRoutes: [
{component: InsecureWrapper,
childRoutes: [ //<- ***INSECURE ROUTES ARE CHILDREN HERE***
{path: 'dashboard', component: Dashboard}
]
},
{component: SecureWrapper,
//***SECURE ROUTES ARE CHILDREN HERE***
childRoutes: [
{path: 'secure', component:Secure}
]
}
]}
]
Last but not least... in your Dialogs, you need to handle the cancel by pushing (or replacing) the location to the saved state value. And of course on successful login, just close them...
import { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as authActions from './redux/client/ducks/auth';
import LoginDialog from './common/client/components/dialogs/LoginDialog';
import RegisterDialog from './common/client/components/dialogs/RegisterDialog';
// this could also be replacePath if you wanted to overwrite the history
import { pushPath } from 'redux-simple-router';
function mapStateToProps(state) {
return {
auth: state.auth
};
}
function mapDispatchToProps(dispatch) {
return {
authActions: bindActionCreators(authActions, dispatch),
pushPath: bindActionCreators(pushPath, dispatch),
};
}
#connect(
mapStateToProps,
mapDispatchToProps
)
export default class AuthContainer extends Component {
_handleLoginCancel = (e) => {
const { auth, authActions, pushPath } = this.props;
pushPath(auth.prevRoute); // from our saved state value
authActions.closeLogin();
};
_handleLoginSubmit = (e) => {
const { authActions } = this.props;
// do whatever you need to login here
authActions.closeLogin();
};
render() {
const { auth } = this.props;
return (
<div>
<LoginDialog
open={auth.showLogin}
handleCancel={this._handleLoginCancel}
handleSubmit={this._handleLoginSubmit}
submitLabel="Login"
/>
...
</div>
)
}
}
I am obviously using ES6, Babel, and webpack... but the principles should apply without them as should not using Redux (you could store the prev route in local storage or something). Also I left out some of the intermediate components that pass props down for brevity.
Some of these could be functional components, but I left them full to show more detail. There is also some room for improvement by abstracting some of this, but again I left it redundant to show more detail. Hope this helps!

Related

React/Redux Saga: Need to trigger a route change after user clicks button on form

I'm working on a project that was built using Redux Saga. I have a component that features a group of offers each with their own button to select that specific offer. This component lives on page 1 of 4 and I'm trying to figure out how to trigger a redirect to the next page after the user clicks a selection, but I have no idea how to do this within the context of Redux or Redux Saga. I've included both the navigation handler and my component below. I started to edit the navigation component with a proposed action UPDATE_PAGE: but I'm not sure I'm even heading down the right path. Any suggestions/explanations or code samples on how to do this would be hugely appreciated
Offer Component
import React, { Fragment, Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Route, Switch, Redirect } from 'react-router-dom'
import styled from 'styled-components'
import NavigationContainer from '../../containers/NavigationContainer'
import RouteLoader from '../../components/core/RouteLoader'
import { getCustomer } from '../../actions/customer'
import { routerPropTypes, routePropTypes } from '../../propTypes'
class OrdersRoutes extends Component {
static propTypes = {
customer: PropTypes.object,
getCustomer: PropTypes.func.isRequired,
routes: routePropTypes,
...routerPropTypes
}
getAppData = data => {
this.props.getCustomer(data)
}
render() {
const { match, customer } = this.props
const { offers: offersRoute, ...routes } = this.props.routes
return (
<Fragment>
<Navigation>
<Route
path={match.path}
component={NavigationContainer}
/>
</Navigation>
<PageContainer>
<Switch>
<RouteLoader
exact
path={`${match.path}${offersRoute.path}`}
component={offersRoute.component}
shouldLoad={customer !== undefined}
onLoad={this.getAppData}
/>
{Object.values(routes).map(
({
path, id, component, disabled
}) =>
!disabled && (
<Route
exact
key={id}
path={`${match.path}${path}`}
component={component}
/>
)
)}
<Redirect to={`${match.path}${offersRoute.path}`} />
</Switch>
</PageContainer>
</Fragment>
)
}
}
const Navigation = styled.div`
padding: 0 10px;
`
const PageContainer = styled.div`
padding: 0 20px;
margin-top: 10px;
`
const mapStateToProps = state => ({
routes: state.navigation.pages.orders.routes,
customer: state.customer.details
})
const mapDispatchToProps = { getCustomer }
export default connect(
mapStateToProps,
mapDispatchToProps
)(OrdersRoutes)
Navigation Component
import * as pages from '../constants/pages'
export const initialState = {
pages: {
...pages
}
}
function navigationReducer(state = initialState, { type, payload }) {
switch (type) {
UPDATE_PAGE: {
const { id, page } = payload
return {
...state,
pages: {
...state.pages,
[id]: {
...state.pages[id],
...page
}
}
}
}
default: {
return state
}
}
}
export default navigationReducer
The trick is to include react-router's history into the payload of UPDATE_PAGE.
So 1) we wrap a component which triggers this action withRouter. This gives us access to history as a prop; 2) when dispatching UPDATE_PAGE, include history as a payload in addition to id and page 3) in redux-saga perform a redirect on every UPDATE_PAGE:
yield takeEvery(UPDATE_PAGE, function*(action){ action.payload.history.push('/new-route'); })

How push route to another path in react-redux connected component

i wan't to make conditional routing in my component. I'm using react-redux. I have tried different things like but getting undefined error.
this.props.history.push('/pathname')
so what i wan't to do, on component will mount i'm checking some boolean value from my redux state if this value will return false i want to redirect user to another route.
my component is:
import React, {Component} from 'react'
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'
class SomeComponent extends Component {
componentWillMount() {
this.props.someFunc()
if ( this.props.userLogin[0].isUserLogin === false ) {
/// here i want to redirect to another route
}
}
render() {
return(
<div>Some Content</div>
)
}
}
const mapStateToProps = state => ({
userLogin: state.UserLogin,
})
function mapDispatchToProps(dispatch) {
return {
someFunc: bindActionCreators(someAction, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SomeComponent)
Like in example i want to redirect in component will mount user to another path. How i can do that?
You need Redirect
import {Redirect} from 'react-router-dom';
{boolFlag && <Redirect to="/hello" />} // put this in render
Do you mean "history is undefined"?
You can get history from parent component

React: Using a redux export connect from other components

I'm writing a mobile app with React Native. I have two js files as following:
Error.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getTranslate } from 'react-localize-redux';
function mapStateToProps(state) {
return {
t: getTranslate(state.locale)
};
}
export default connect(
mapStateToProps
)(
({ t }) => ({
e001: t('wrong_format'),
e002: t('invalid_email'),
})
);
SignIn.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getTranslate } from 'react-localize-redux';
import { Field, reduxForm } from 'redux-form';
import Error from './Error';
const validate = (values) => {
console.log('error: ', Error);
// Process validate redux-form with messages from Error.js
};
class SignIn extends Component {
// Process login form with redux-form
}
function mapStateToProps(state) {
return {
t: getTranslate(state.locale),
};
}
const SignInForm = {
form: 'SignIn',
validate,
};
export default connect(
mapStateToProps
)(
reduxForm(SignInForm)(
SignIn
)
);
How can I use the data that exported from Error.js in SignIn.js? (e.g. values of 'e001', 'e002')
Example from validate function (in SignIn.js) I wanna show the value of code "e001" from Error.js.
For more detail, my idea is collect all error messages from language file (using react-localize-redux) into Error.js, then from validate functions of redux-form, i can show those messages easier.
Its look like you want Error.js as a helper file.But you are implemented it as react-redux container.
If you implementing it as react-redux container then necessarily it will call every time whenever store will change.
Instead simply export it.
import { getTranslate } from 'react-localize-redux';
const format = ({
e001: getTranslate('wrong_format'),
e002: getTranslate('invalid_email'),
})
export default format;
Called it as once imported
console.log(format.e001);

React-Auth0? Working! React-Redux-Auth0? I am losing my mind :/

This is a really long post, but I really need some help :/
I will be eternally grateful if someone would be able to help.
I have managed to get Auth0 working for an application i am working on with just react. It is an Overwatch SR tracker, and is essentially just a spreadsheet so I wasn't too concerned with protecting backend routes when I make them. There isn't any private information there.
My application state/props network became too complicated to manage, and through the process of implementing redux I simply cannot get it to work. I've been at it for three days, and I'm running out of ideas. Do I need Thunk with my current Auth setup to do this? I would imagine it is async since it needs to go get something that isnt there.
Granted I am a junior Dev, and dont have much experience with authentication. Can someone take a look at my working react application and guide me in the direction of what i may need to do to set it up with redux? I do have an understanding of redux flow, so if the proper method to do this was explained to me i feel i might get it.
here is some code:
my Auth.js file :
/*eslint no-restricted-globals: 0 */
import auth0 from "auth0-js";
import jwtDecode from 'jwt-decode';
const LOGIN_SUCCESS_PAGE = '/menu';
const LOGIN_FAILURE_PAGE = '/';
export default class Auth {
auth0 = new auth0.WebAuth({
domain: "redacted.auth0.com",
clientID: "redacted",
redirectUri: "http://localhost:3000/callback",
audience: "https://redacted.auth0.com/userinfo",
responseType: "token id_token",
scope: "openid profile"
});
constructor() {
this.login = this.login.bind(this);
}
login() {
this.auth0.authorize();
}
handleAuthentication() {
this.auth0.parseHash((err, authResults) => {
if (authResults && authResuslts.accessToken && authResults.idToken) {
let expiresAt = JSON.stringify((authResults.expiresIn) * 1000 + new Date().getTime());
localStorage.setItem("access_token", authResults.accessToken);
localStorage.setItem("id_token", authResults.idToken);
localStorage.setItem("expires_at", expiresAt);
location.hash = "";
location.pathname = LOGIN_SUCCESS_PAGE;
} else if (err) {
location.pathname = LOGIN_FAILURE_PAGE;
console.log(err);
}
});
}
isAuthenticated() {
let expiresAt = JSON.parse(localStorage.getItem('expires_at'));
return new Date().getTime() < expiresAt;
}
logout() {
localStorage.removeItem("access_token");
localStorage.removeItem("id_token");
localStorage.removeItem('expires_at');
location.pathname = LOGIN_FAILURE_PAGE;
}
getProfile() {
if (localStorage.getItem("id_token")) {
console.log(jwtDecode(localStorage.getItem("id_token")))
console.log(localStorage.getItem("id_token"));
return jwtDecode(localStorage.getItem("id_token"));
} else {
return {
name: 'Anon',
nickname: 'Anon',
picture: 'placeholder',
uid: null,
}
}
}
}
my index.js file:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import Auth from './Auth';
import { BrowserRouter } from 'react-router-dom';
const auth = new Auth();
let state = {};
window.setState = (changes) => {
state = Object.assign({}, state, changes)
ReactDOM.render(
<BrowserRouter>
<App {...state} />
</BrowserRouter>,
document.getElementById('root'));
}
/* eslint no-restricted-globals: 0*/
let getUserProfile = auth.getProfile();
let initialState = {
owSrTrackInfo: {
infoSaved: false,
accounts: [],
},
user: getUserProfile,
location: location.pathname.replace(/^\/?|\/$/g, ""),
auth,
}
window.setState(initialState);
registerServiceWorker();
my App.js file:
import React, { Component } from "react";
import "./App.css";
import Main from "./Components/Main/Main";
import Menu from "./Pages/Menu/Menu";
import NotFound from "./Components/NotFound/NotFound";
import Callback from './Components/Callback/Callback';
import Header from './Components/Header/Header';
class App extends Component {
render() {
let mainComponent = "";
switch (this.props.location) {
case "":
mainComponent = <Main {...this.props} />;
break;
case "callback":
mainComponent = <Callback />
break;
case "menu":
mainComponent = this.props.auth.isAuthenticated() ? < Menu {...this.props} /> : <NotFound />;
break;
default:
mainComponent = <NotFound />;
}
return (
<div className="app">
<Header {...this.props} />
{mainComponent}
</div>
);
}
}
export default App;
my Callback.js component:
import React, {Component} from 'react';
import Auth from '../../Auth'
export default class Callback extends Component {
componentDidMount() {
const auth = new Auth();
auth.handleAuthentication();
}
render() {
return(
<p className="loading">Loading.....</p>
)
}
}
My current MAIN.js component:
import React, { Component } from "react";
export default class Main extends Component {
render() {
console.log(this.props.auth.getProfile())
return (
<div className="container">
<div className='container--logged-out'>
<h1 className="heading u-margin-bottom-small">welcome to redacteds' overwatch sr tracker</h1>
<p>Hello there {this.props.user.nickname}! Sign in single click or email via Auth0 so we can save your results, and make the app usable by more than one person. I intend for more than one person to use this, so just to launch it and so the app knows your spreadsheet from someone elses I'll tie each user to their own UID. Feel free to come back, log in, and get your spreadsheet for the season back anytime.</p>
</div>
Go to the app menu!
<button onClick={() =>this.props.auth.getProfile()}>asdgkljsdngk</button>
</div>
);
}
}
my current HEADER.js component:
import React, { Component } from 'react';
export default class Header extends Component {
render() {
return (
<header className="header">
<h1 className='header__text'>SR TRACKER</h1>
{this.props.auth.isAuthenticated() ?
<button className='btn btn--logout' onClick={() => this.props.auth.logout()}>Logout</button>
:
<button className='btn btn--login' onClick={() => this.props.auth.login()}>Login or Sign Up</button>}
</header>
)
}
}
I simply want to map this authentication to a redux store instead to be consitent with the rest of my app (when redux is implemented) I have blown it away and started over multiple times, but a rough idea of what my redux flow might look like is like this template i use and have successfully implemented several times:
redux store:
import { createStore, compose, applyMiddleware } from 'redux';
import { createLogger } from 'redux-logger';
import thunk from 'redux-thunk';
import rootReducer from './reducers/rootReducer';
export default function configureStore(initialState) {
const middleware = [
createLogger({
collapsed: false,
duration: true,
diff: true,
}),
thunk,
];
const store = createStore(
rootReducer,
initialState,
compose(
applyMiddleware(...middleware),
window.devToolsExtension ? window.devToolsExtension() : format => format, // add support for Redux dev tools),
),
);
return store;
}
actionTypes.js in actions folder:
const actions = {
GET_FRIENDS: 'GET_FRIENDS',
REMOVE_FRIEND: 'REMOVE_FRIEND',
GET_MOVIES: 'GET_MOVIES',
GET_MOVIES_SUCCESS: 'GET_MOVIES_SUCCESS',
GET_MOVIES_FAILURE: 'GET_MOVIES_FAILURE',
DEVIN_FUN: 'DEVIN_FUN',
};
export default actions;
Sample actions page:
import axios from 'axios';
import actionTypes from './actionTypes';
export const getMoviesSuccess = data => {
return {
type: actionTypes.GET_MOVIES_SUCCESS,
data,
};
};
export const getMoviesFailure = () => {
return {
type: actionTypes.GET_MOVIES_FAILURE,
};
};
export const devinIsHavingFun = () => {
return {
type: actionTypes.DEVIN_FUN,
};
};
export const retrieveMovies = () => {
return function(dispatch) {
const API_KEY = 'trilogy';
dispatch(devinIsHavingFun());
axios
.get(`http://www.omdbapi.com?apikey=${API_KEY}&s=frozen`)
.then(data => {
dispatch(getMoviesSuccess(data.data.Search));
})
.catch(error => {
console.log(error);
dispatch(getMoviesFailure());
});
};
};
in the reducers folder wed have some files like initialState.js and root reducer that look like this respectively:
initialState.js:
export default {
friends: [],
movies: [],
};
rootReducer.js:
import { combineReducers } from 'redux';
import friends from './friendReducer';
import movies from './movieReducer';
const rootReducer = combineReducers({
friends,
movies,
});
export default rootReducer;
and a sample reducer:
import actionTypes from '../actions/actionTypes';
import initialState from './initialState';
export default function movieReducer(state = initialState.movies, action) {
switch (action.type) {
case actionTypes.GET_MOVIES_SUCCESS: {
return action.data;
}
default: {
return state;
}
}
}
I just dont know what to do. Do i need to use thunk? am I overthinking this? I'm pulling my hair out.
I also do connect my components in this fashion when redux is implemented :
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as friendActionCreators from './actions/friendActions';
import * as movieActionCreators from './actions/movieActions';
....................
function mapStateToProps(state) {
return {
myFriends: state.friends,
movies: state.movies,
};
}
function mapDispatchToProps(dispatch) {
return {
friendActions: bindActionCreators(friendActionCreators, dispatch),
movieActions: bindActionCreators(movieActionCreators, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
Please let me know if anyone can point me in the right direction. thank you so much in advance.

MobX + React Native : way to inject stores

I'm trying to work with MobX for a new project.
I started it on May 2017, and everything was working well. I had to stop, and now I go on developing it. I had to make an npm install to manage making it working, but now, I have some problem with stores...
I rely on this tutorial for the structure : https://blog.callstack.io/write-react-native-apps-in-2017-style-with-mobx-e2dffc209fcb
This is my structure :
Main index.js
import { Provider } from 'mobx-react';
import Stack from './router';
import stores from './stores';
export default class App extends Component {
render() {
return (
<Provider {...stores}>
<Stack />
</Provider>
);
}
}
Index.js of my stores in ./stores/index.js
import ChatStore from './ChatStore';
import UserStore from './UserStore';
export default {
UserStore: new UserStore(),
ChatStore: new ChatStore(),
};
./stores/UserStore.js (important parts)
import { observer, inject } from 'mobx-react';
import {autobind} from 'core-decorators';
...
#inject(['ChatStore'])
#observer
#autobind
export default class UserStore {
#observable isAuthenticated = false;
#observable isConnecting = false;
#observable user = null;
#observable messages = [];
#observable hasMoreMessages = false;
#observable skip = 0;
...
login() {
const payload = {
strategy: 'local',
material_id: DeviceInfo.getManufacturer(),
password: DeviceInfo.getManufacturer()
};
return this.authenticate(payload);
}
...
Now, for components part :
Router.js
import { StackNavigator } from 'react-navigation';
import Home from './containers/Home';
const stackNavigatorConfig = {
initialRouteName: 'Home',
};
export default StackNavigator({
Home: {
screen: Home,
},
}, stackNavigatorConfig);
./containers/Home.js
import React, { Component } from 'react';
import { AsyncStorage } from 'react-native';
import { observable } from 'mobx';
import { observer, inject } from 'mobx-react';
#inject('UserStore')
#observer
export default class Home extends Component {
props: Props;
...
render() {
this.props.UserStore.login().catch(error => {
console.log('LOGIN', 'ERROR', JSON.stringify(error), error.message);
});
return {
...
}
}
And then, I get an error :
So, I sum up :
I use <Provider> from MobX, to give all my stores to my app
Then, I get the Store I want in my component with #inject
I use it as a props, using this.props.UserStore...
But it does not work. I rely on this tutorial for the structure : https://blog.callstack.io/write-react-native-apps-in-2017-style-with-mobx-e2dffc209fcb
Maybe there was an update between May 2017 and today, that makes things different... It was working well on May 2017.
I think this is a dummy error, but I can't find which one...
Everything looks good except the decorators on your UserStore class: #inject(['ChatStore']) #observer #autobind. #inject(['ChatStore']) #observer is used on React components, #autobind might still work as intended.
It should work if you remove those.
maybe worth using #action from mobx

Categories