Skip to content

alexandrmih89/react-native-jwt-core

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

What's it about?

We are developing a lot of FullStack Apps and came to conclusion we needed a starter that could be maintainable and upgradable. We use JWT, ReactNavigation connected to Redux, redux-saga and redux-form. So we proudly present and share our work to the community. Use it with https://github.com/alvelig/express-jwt-auth-router, which gives a great advantage in bringing JWT, ACL and all the stuff for the backend, and life will become much easier.

Installation

react-native init exampleProject
cd exampleProject
git clone [email protected]:alvelig/react-native-jwt-core.git src/core && src/core/install.sh

Configure react-native-config https://github.com/luggit/react-native-config#extra-step-for-ios-to-support-infoplist

Enjoy ReactNative app

Usage

Disclaimer:

We are encouraging you to use modular structure (ORGANIZE BY FEATURE). See https://jaysoo.ca/2016/02/28/organizing-redux-application/
Also it's welcome to understand how redux-saga works, as we are using it extensively. This lib also uses ReactNavigation connected to Redux store.

First define your routes/navigators.js:

//import ReactNavigation Stacks:
import { StackNavigator, TabNavigator } from 'react-navigation';

//Define your Login Screen (We'll change it later)
const LoginScreen = () => <View><Text>LoginScreen</Text></View>;

//Define your NotAuthenticated stack:
export const NotAuthenticatedNavigator = StackNavigator({
  Login: {
    screen: LoginScreen
  },
});

//Let's define an example Profile (Authenticated view)
const ProfileScreen = () => <View><Text>ProfileScreen</Text></View>;

//Define your Authenticated stack;
export const AuthenticatedNavigator = TabNavigator({
  Login: {
    screen: ProfileScreen
  },
});

Run and it should give you the LoginScreen.

So, let's start a new module. For example, Login.

Let's start with modules/Login/LoginActions.js:

import { createRoutine } from 'redux-saga-api-call-routines';

const ENTITY = 'LOGIN_FORM';

const LoginFormRoutine = createRoutine(ENTITY);

export default LoginFormRoutine;

Ok. Then, let's add modules/Login/LoginSagas.js:

import { put, take } from 'redux-saga/effects';
import { SubmissionError } from 'redux-form';
import LoginFormRoutine from './LoginActions';
import { ApiCall } from 'redux-saga-api-call-routines';

//Our Nav login action
import { login } from 'redux-saga-auth';

//This is to validate your form if you use redux-form
/***
 * Validate your form here
 ***/
const isValid = (form) => {

  return true;
};

//Here we define error strings returned for fields that did not validate. 
/*** Return errors for your form
 * { [field]: string }
 */
const getFormErrors = (form) => {
  return {};
};

export default function* formSubmitWatcher() {
  /***
   * don't take any actions until the flow is complete
   * if another LoginFormRoutine.TRIGGER is dispatched, it will not be taken, and will not affect our flow
   * this is done intentionally to avoid double click and other side effects
   */
  while(true) {
    const { payload } = yield take(LoginFormRoutine.TRIGGER);
    const { values } = payload || {};

    if (!isValid(payload)) {

      const errors = getFormErrors(payload);
      yield put(LoginFormRoutine.failure(new SubmissionError(errors)));

    } else {

      const { username, password } = values;

      const opts = {
        url: 'signin',
        method: 'POST',
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify({ username, password })
      };

      //Magically works making request to the api
      const { response, error } = yield* ApiCall(LoginFormRoutine, opts);

      if(response) {
        /*** resolve the routine promise ***/
        yield put(LoginFormRoutine.success(response.json));
        /*** and tell AuthSaga that you are in ***/
        const { accessToken, refreshToken, user } = response.json;
        yield put(login({
          user,
          accessToken,
          refreshToken
        }));
      } else {

        const _error = error.json && error.json.message ? error.json.message : "Error desconocido";

        yield put(LoginFormRoutine.failure(new SubmissionError({ _error })));
      }
    }

    /*** trigger fulfill action to end routine lifecycle ***/
    yield put(LoginFormRoutine.fulfill());
  }
}

Configure .env, change BASE_URL for your API.

So, let's merge it all in modules/Login/LoginForm.js:

import React from 'react';
import { connect } from 'react-redux';
import {
  View,
  Button,
  TextInput
} from 'react-native';
import { reduxForm, Field } from 'redux-form';

//Our Nav action creator to go to another screen 
import { goTo } from '../../core/modules/Nav/NavActions';
import LoginFormRoutine from './LoginActions';

const mapStateToProps = () => ({});
const mapDispatchToProps = {
  goTo
};

const renderTextField = ({ input, label, meta: { touched, error }, ...props }) => (
  <TextInput
    {...input}
    {...props}
  />
);

class LoginForm extends React.Component {

  render() {

    const { handleSubmit, goTo, error, submitting } = this.props;

    return (
      <View>

        <Field
          name="username"
          placeholder="Email"
          keyboardType="email-address"
          component={renderTextField} />

        <Field
          name="password"
          secureTextEntry
          placeholder="Contraseña"
          component={renderTextField}
        />
        
        <Button onPress={handleSubmit(LoginFormRoutine)} title="Login" />

      </View>
    );
  }
}

LoginForm = connect(mapStateToProps, mapDispatchToProps)(LoginForm);

LoginForm = reduxForm({
  form: 'LoginForm'
})(LoginForm);

export default LoginForm;

Now let's change a little our LoginScreen in routes/navigators.js:

import LoginForm from '../modules/Login/LoginForm';

const LoginScreen = () => ( 
  <View>
    <Text>LoginScreen</Text>
    <LoginForm />
  </View>
);

The only thing left would be to edit modules/sagas.js:

import LoginFormSaga from '../modules/Login/LoginSagas.js'
const appSagas = [
  LoginFormSaga
];
export default appSagas;

Now imagine we need to have a button to create an account. Let's edit our routes/navigators.js:

const RegisterScreen = () => <View><Text>Register</Text></View>;

//Define your NotAuthenticated stack:
export const NotAuthenticatedNavigator = StackNavigator({
  Login: {
    screen: LoginScreen
  },
  Register: {
    screen: RegisterScreen
  },
});

Then we will add a Register button to our LoginForm.js (this can be done in any connected or with access to dispatch component):

//add this within render method  
<Button onPress={this.props.goTo("Register")} title="Sign up" />

goTo accepts String or object with typical Navigation params

If we want to return from RegisterScreen to LoginScreen we need to import { goBack } from '../../core/modules/Nav/NavActions'; and then just dispatch(goBack()) in a Button.

ReactNavigation Reset action is also available: import { resetTo } from '../../core/modules/Nav/NavActions';

Ok, LoginSaga seems a little bit complicated. Here's an example of simple saga (Kind of redux-thunk simplest case):

import { put, takeEvery } from 'redux-saga/effects';
import { SubmissionError } from 'redux-form';
import MainMapRoutine from './MainMapActions';
import { ApiCall } from 'redux-saga-api-call-routines';


function* getStoresOnMainMap({ payload }) {

  // auth: true means we will use JWT for this request
  const opts = {
    auth: true,
    url: 'stores'
  };

  const { response, error } = yield* ApiCall(MainMapRoutine, opts);

  if(response) {
    yield put(MainMapRoutine.success(response.json));
  } else {

    const _error = error.json && error.json.message ? error.json.message : "Error desconocido";

    yield put(MainMapRoutine.failure(new SubmissionError({_error})));
  }

  yield put(MainMapRoutine.fulfill());
}

export default function* registerWatcherSaga() {
  yield takeEvery(MainMapRoutine.TRIGGER, getStoresOnMainMap);
}

Action creators

core/modules/Nav/NavActions exposes the following actionCreators (See NavReducers.js):

Action creator Description Arguments
goTo pushes a new screen in a Stack Screen name (String) or Navigation params
goBack pops current screen from the Stack o restores last navigator state -
resetTo resets current stack to the provided screen Screen name (String) or Navigation params
setParams sets params for current screen Navigation params object
--- --- ---
login Login action creator - sets Navigator state to authenticated (Changes the stack from NotAuthenticated to Authenticated). { user, accessToken, refreshToken }
register Register action creator - sets Navigator state to authenticated (Changes the stack from NotAuthenticated to Authenticated). { user, accessToken, refreshToken }
logout Logout action creator - sets Navigator state to not authenticated (Changes the stack from Authenticated to NotAuthenticated). -

ApiCall

import { ApiCall } from 'redux-saga-api-call-routines';

ApiCall translates your Routine to a request. Options (pretty the same as for fetch):

Option
auth If use or not JWT authentication
url Api url
body Request Body
headers Request Headers

About

A JWT compliant ReactNative starter (boilerplate) for usage with https://github.com/alvelig/express-jwt-auth-router

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published