mxstbr / login-flow

:key: A login/register flow built with React&Redux
https://loginflow.mxstbr.com
MIT License
1.6k stars 217 forks source link

Upgrade to latest react-boilerplate #28

Open kopax opened 7 years ago

kopax commented 7 years ago

Hi, I have downloaded both react boiler plate and this application. I can't recognize the boilerplate in login-flow, is this code compatible with the new app folder design / boot ?

Perhaps it is not so long to upgrade this project to demonstrate the integration using react-boilerplate OR add this example directly in the sample project of react boilerplate (before the clean)

gihrig commented 7 years ago

Hey @kopax you might find this helpful.

From the author of this project #25

This project isn't really maintained anymore, and is quite out of date with react-boilerplate.

mxstbr commented 7 years ago

I totally agree, we need to update this to v3. Feel free to submit a PR!

kopax commented 7 years ago

Grüß Gott @mxstbr, I would be happy to PR for you because I need a form login and as far as I know, I will do it on v3.

I have not so much experience with react but I read a lot about it and I know all the other js client library so it might take a while. That's why I choose to go straight to your project because you seems to have good answers to problem and some experience in the React world.

Since I am new to react boilerplate, It would be nice if you could provide me some form example with xhr request in v3, to see how it goes with redux-saga and adapt it.

There is no such case in the sample project in v3.

kopax commented 7 years ago

I have tried to implement it in v3 but I still got a bit confuse how to write the sagas.

I have no PageHome so I just do the following :

in routes.js

{
  path: pages.pageLogin.path,
  name: pages.pageLogin.name,
  getComponent(nextState, cb) {
    const importModules = Promise.all([
      System.import('containers/LoginPage/reducer'),
      System.import('containers/LoginPage/sagas'),
      System.import('containers/LoginPage'),
    ]);

    const renderRoute = loadModule(cb);

    importModules.then(([reducer, sagas, component]) => {
      injectReducer('login', reducer.default);
      injectSagas(sagas.default);

      renderRoute(component);
    });

    importModules.catch(errorLoading);
  },
}

in containers/LoginPage/reducer.js

/*
 * AppReducer
 *
 * The reducer takes care of our data. Using actions, we can change our
 * application state.
 * To add a new action, add it to the switch statement in the reducer function
 *
 * Example:
 * case YOUR_ACTION_CONSTANT:
 *   return state.set('yourStateVariable', true);
 */

import { CHANGE_FORM, SET_AUTH, SENDING_REQUEST } from './constants';
// Object.assign is not yet fully supported in all browsers, so we fallback to
// a polyfill
import { fromJS } from 'immutable';
import auth from 'utils/auth';

// The initial application state
const initialState = fromJS({
  formState: {
    username: '',
    password: ''
  },
  currentlySending: false,
  loggedIn: auth.loggedIn()
});

// Takes care of changing the application state
export function appReducer(state = initialState, action) {
  switch (action.type) {
    case CHANGE_FORM:
      return state.set('formState', action.newState);
      break;
    case SET_AUTH:
      return state.set('loggedIn', action.newState);
      break;
    case SENDING_REQUEST:
      return state.set('currentlySending', action.newState);
      break;
    default:
      return state;
  }
}

in containers/LoginPage/sagas.js

import { take, call, put, select, fork, cancel } from 'redux-saga/effects';
import { LOCATION_CHANGE } from 'react-router-redux';
import { CHANGE_FORM, SENDING_REQUEST, SET_AUTH } from './constants';
import { login, logout } from './actions';

import request from 'utils/request';

export function* postLogin(){
  // Select username from store
  const username = yield select(selectUsername());
  const requestURL = `http://localhost:8080/login`;

  // Call our request helper (see 'utils/request')
  const options = {
    method: 'POST',
    body: new FormData(form),
  };
  const repos = yield call(request, call(requestURL, options));

  if (!repos.err) {
//        yield put(reposLoaded(repos.data, username));
  } else {
//        yield put(repoLoadingError(repos.err));
  }
}

// Bootstrap sagas
export default [
  postLogin,
];

constants.js

/*
 * AppConstants
 * These are the variables that determine what our central data store (reducer.js)
 * changes in our state. When you add a new action, you have to add a new constant here
 *
 * Follow this format:
 * export const YOUR_ACTION_CONSTANT = 'YOUR_ACTION_CONSTANT';
 */
export const CHANGE_FORM = 'CHANGE_FORM';
export const SET_AUTH = 'SET_AUTH';
export const SENDING_REQUEST = 'SENDING_REQUEST';

actions.js

/*
 * Actions change things in your application
 * Since this boilerplate uses a uni-directional data flow, specifically redux,
 * we have these actions which are the only way your application interacts with
 * your appliction state. This guarantees that your state is up to date and nobody
 * messes it up weirdly somewhere.
 *
 * To add a new Action:
 * 1) Import your constant
 * 2) Add a function like this:
 *    export function yourAction(var) {
 *        return { type: YOUR_ACTION_CONSTANT, var: var }
 *    }
 * 3) (optional) Add an async function like this:
 *    export function asyncYourAction(var) {
 *        return function(dispatch) {
 *             // Do async stuff here
 *             return dispatch(yourAction(var));
 *        }
 *    }
 *
 *    If you add an async function, remove the export from the function
 *    created in the second step
 */

import bcrypt from 'bcryptjs';
import { SET_AUTH, CHANGE_FORM, SENDING_REQUEST } from './constants';
import auth from 'utils/auth';
import genSalt from 'utils/salt';
import { browserHistory } from 'react-router';

/**
 * Logs an user in
 * @param  {string} username The username of the user to be logged in
 * @param  {string} password The password of the user to be logged in
 */
export function login(username, password) {
  return (dispatch) => {
    // Show the loading indicator, hide the last error
    dispatch(sendingRequest(true));
    removeLastFormError();
    // If no username or password was specified, throw a field-missing error
    if (anyElementsEmpty({ username, password })) {
      requestFailed({
        type: "field-missing"
      });
      dispatch(sendingRequest(false));
      return;
    }
    // Generate salt for password encryption
    const salt = genSalt(username);
    // Encrypt password
    bcrypt.hash(password, salt, (err, hash) => {
      // Something wrong while hashing
      if (err) {
        requestFailed({
          type: 'failed'
        });
        return;
      }
      // Use auth.js to fake a request
      auth.login(username, hash, (success, err) => {
        // When the request is finished, hide the loading indicator
        dispatch(sendingRequest(false));
        dispatch(setAuthState(success));
        if (success === true) {
          // If the login worked, forward the user to the dashboard and clear the form
          forwardTo('/dashboard');
          dispatch(changeForm({
            username: "",
            password: ""
          }));
        } else {
          requestFailed(err);
        }
      });
    });
  }
}

/**
 * Logs the current user out
 */
export function logout() {
  return (dispatch) => {
    dispatch(sendingRequest(true));
    auth.logout((success, err) => {
      if (success === true) {
        dispatch(sendingRequest(false));
        dispatch(setAuthState(false));
        browserHistory.replace(null, '/');
      } else {
        requestFailed(err);
      }
    });
  }
}

/**
 * Registers a user
 * @param  {string} username The username of the new user
 * @param  {string} password The password of the new user
 */
export function register(username, password) {
  return (dispatch) => {
    // Show the loading indicator, hide the last error
    dispatch(sendingRequest(true));
    removeLastFormError();
    // If no username or password was specified, throw a field-missing error
    if (anyElementsEmpty({ username, password })) {
      requestFailed({
        type: "field-missing"
      });
      dispatch(sendingRequest(false));
      return;
    }
    // Generate salt for password encryption
    const salt = genSalt(username);
    // Encrypt password
    bcrypt.hash(password, salt, (err, hash) => {
      // Something wrong while hashing
      if (err) {
        requestFailed({
          type: 'failed'
        });
        return;
      }
      // Use auth.js to fake a request
      auth.register(username, hash, (success, err) => {
        // When the request is finished, hide the loading indicator
        dispatch(sendingRequest(false));
        dispatch(setAuthState(success));
        if (success) {
          // If the register worked, forward the user to the homepage and clear the form
          forwardTo('/dashboard');
          dispatch(changeForm({
            username: "",
            password: ""
          }));
        } else {
          requestFailed(err);
        }
      });
    });
  }
}

/**
 * Sets the authentication state of the application
 * @param {boolean} newState True means a user is logged in, false means no user is logged in
 */
export function setAuthState(newState) {
  return { type: SET_AUTH, newState };
}

/**
 * Sets the form state
 * @param  {object} newState          The new state of the form
 * @param  {string} newState.username The new text of the username input field of the form
 * @param  {string} newState.password The new text of the password input field of the form
 * @return {object}                   Formatted action for the reducer to handle
 */
export function changeForm(newState) {
  return { type: CHANGE_FORM, newState };
}

/**
 * Sets the requestSending state, which displays a loading indicator during requests
 * @param  {boolean} sending The new state the app should have
 * @return {object}          Formatted action for the reducer to handle
 */
export function sendingRequest(sending) {
  return { type: SENDING_REQUEST, sending };
}

/**
 * Forwards the user
 * @param {string} location The route the user should be forwarded to
 */
function forwardTo(location) {
  console.log('forwardTo(' + location + ')');
  browserHistory.push(location);
}

let lastErrType = "";

/**
 * Called when a request failes
 * @param  {object} err An object containing information about the error
 * @param  {string} err.type The js-form__err + err.type class will be set on the form
 */
function requestFailed(err) {
  // Remove the class of the last error so there can only ever be one
  removeLastFormError();
  const form = document.querySelector('.form-page__form-wrapper');
  // And add the respective classes
  form.classList.add('js-form__err');
  form.classList.add('js-form__err-animation');
  form.classList.add('js-form__err--' + err.type);
  lastErrType = err.type;
  // Remove the animation class after the animation is finished, so it
  // can play again on the next error
  setTimeout(() => {
    form.classList.remove('js-form__err-animation');
  }, 150);
}

/**
 * Removes the last error from the form
 */
function removeLastFormError() {
  const form = document.querySelector('.form-page__form-wrapper');
  form.classList.remove('js-form__err--' + lastErrType);
}

/**
 * Checks if any elements of a JSON object are empty
 * @param  {object} elements The object that should be checked
 * @return {boolean}         True if there are empty elements, false if there aren't
 */
function anyElementsEmpty(elements) {
  for (let element in elements) {
    if (!elements[element]) {
      return true;
    }
  }
  return false;
}

I encounter the following problems :

What is not very clear for me now is how you get the field from the form the html form.

 import { selectUsername } from 'containers/HomePage/selectors';
...
 const username = yield select(selectUsername());

in containers/HomePage/selectors

/**
 * Homepage selectors
 */

import { createSelector } from 'reselect';

const selectHome = () => (state) => state.get('home');

const selectUsername = () => createSelector(
  selectHome(),
  (homeState) => homeState.get('username')
);

export {
  selectHome,
  selectUsername,
};

I know the implementation is incomplet , and my browser log me this :

warning.js:36Warning: Failed prop type: Required prop `loggedIn` was not specified in `Nav`.
in Nav (created by App)
in App (created by Connect(App))
in Connect(App) (created by RouterContext)
in RouterContext (created by Router)
in ScrollBehaviorContext (created by Router)
in Router
in IntlProvider (created by LanguageProvider)
in LanguageProvider (created by Connect(LanguageProvider))
in Connect(LanguageProvider)

it's a pain to debug as I can't use my usual browser debugger to see where does this error come from in my code.

So here is a basic question, the form is just an example action, should I keep the form attributes in the root state ? Any help would be appreciated.

kopax commented 7 years ago

Hi again @mxstbr, @gihrig So I have figured out where my problem come from. I had to reimplement again and start writing the store first instead of the component. It was a bit painful to get an error message I can understand, I still wasn't able to configure sourcemaps in development.

I keep encountering trouble converting the followings files :

redux-saga was not in the login-flow project, so I have to make my own sagas.

I have a good working sagas example here for fetching repos from github :

And of course, I did the redux-saga tutorial.

However, I can't reproduce a working sagas.js for the LoginPage. It seems that the action.js has a different usage.

Should the sagas fonction be a plain object or a function ? I am 90% to submit a PR, would be nice if a react boilerplate user could help!

Edit: In case someone accept to have a quick look here is the code merged with the sagas borrowed from https://github.com/sotojuan/saga-login-flow :

# clone
$ git clone git@github.com:kopax/react-boilerplate.git login-wip && cd login-wip
# install deps
$ npm install 
# run
$ npm start
# It will listen on port : 3301
mxstbr commented 7 years ago

I am 90% to submit a PR, would be nice if a react boilerplate user could help!

Feel free to submit a WIP PR, it's a bit hard to judge just by your description!

kopax commented 7 years ago

@mxstbr, I found this project: https://github.com/sotojuan/saga-login-flow/issues/25 That helped me to finish this. I had to use his sources of sagas.js and actions.js instead of yours. I can do it again so everyone can get it to work. How should I send the PR? the whole project is different.

mxstbr commented 7 years ago

That helped me to finish this. I had to use his sources of sagas.js and actions.js instead of yours. I can do it again so everyone can get it to work. How should I send the PR? the whole project is different.

Wait sorry, why is it entirely different?

kopax commented 7 years ago

https://github.com/mxstbr/login-flow use a different boilerplate than https://github.com/mxstbr/react-boilerplate.

Also, @sotojuan adjusted the following files :

https://github.com/sotojuan/saga-login-flow/blob/master/app/auth/fakeRequest.js != https://github.com/mxstbr/login-flow/blob/master/js/utils/fakeRequest.js https://github.com/sotojuan/saga-login-flow/blob/master/app/auth/fakeServer.js != https://github.com/mxstbr/login-flow/blob/master/js/utils/fakeServer.js https://github.com/sotojuan/saga-login-flow/blob/master/app/actions/constants.js != https://github.com/mxstbr/login-flow/blob/master/js/constants/AppConstants.js https://github.com/sotojuan/saga-login-flow/blob/master/app/actions/index.js != https://github.com/mxstbr/login-flow/blob/master/js/actions/AppActions.js https://github.com/sotojuan/saga-login-flow/blob/master/app/reducers/index.js != https://github.com/mxstbr/login-flow/blob/master/js/reducers/reducers.js

and created that one :

https://github.com/sotojuan/saga-login-flow/blob/master/app/sagas/index.js

Should I just make my PR using a fork from mxstbr/react-boilerplate?

michaelcuneo commented 7 years ago

Hey, was just posting about the issues I'm having and read this... I need to get this working as well, I'm implementing the React-Boilerplate with Login-Flow, but using the work of sotojuan as well, to implement the saga things... although it's not quite there yet. Having a few issues.

tinavanschelt commented 6 years ago

For anybody else who lands here - I upgraded login-flow to v3.4 (with the help of saga-login-flow) and added json-server integration. It's a WIP and I'm evolving it as I go, but you can find the code on github: https://github.com/tinavanschelt/recycled-login-flow and the app here: http://recycled-login-flow.herokuapp.com/

Cheers.