Skip to content

Instantly share code, notes, and snippets.

@simform-solutions
Created June 20, 2018 11:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save simform-solutions/33efbe2843f21a576ff87d911190acd5 to your computer and use it in GitHub Desktop.
Save simform-solutions/33efbe2843f21a576ff87d911190acd5 to your computer and use it in GitHub Desktop.
Auth module with Redux and Saga Integration
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
import { store, persistor } from 'rdx'
import App from 'containers/App/App.component'
import './index.css'
import 'assets/fonts.css'
import registerServiceWorker from './registerServiceWorker'
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={'Loading...'} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
document.getElementById('root')
)
registerServiceWorker()
import React, { Fragment } from 'react'
import cx from 'classnames'
import { connect } from 'react-redux'
import { Icon } from 'antd'
import { Helmet } from 'react-helmet'
import { FormattedMessage } from 'react-intl'
import { withRouter } from 'react-router-dom'
import { truncatePhoneNumber } from 'config/utils'
import { required, phoneConstraints } from 'config/validators'
import { actions } from 'rdx/auth'
import { actionMethods } from 'rdx/ui'
import authService from 'services/auth'
import { routes } from 'config/routes'
import Button from 'components/Button/Button.component'
import Input from 'components/TextBox/TextBox.component'
import TelInput from 'components/TelInput/TelInput.component'
import styles from './Login.scss'
import BackArrowComponent from 'components/BackArrow.component'
import Loader from 'components/Loader.component'
class Login extends React.Component {
state = {
showPasswordField: false,
password: '',
phoneNumber: '+82',
countryCode: ''
}
componentDidMount() {
this.props.changeText('')
this.props.enableBack(false)
}
onPhoneNumberChange = (status, value, countryData, number, id) => {
const val = truncatePhoneNumber(value, countryData.dialCode.toString())
const regex = /[a-zA-z]+(\s+)?/
if (regex.test(value) || val.length > 10) {
return
}
this.setState({
phoneNumber: value,
countryCode: countryData.dialCode.toString()
})
}
onSubmit = async e => {
e.preventDefault()
const { phoneNumber, showPasswordField, countryCode, password } = this.state
const numberWithoutCode = truncatePhoneNumber(phoneNumber, countryCode)
if (!showPasswordField) {
const phoneError = phoneConstraints(phoneNumber)
if (phoneError) {
return this.props.showSnackbar({ message: 'validation.phone' })
}
const response = await authService.accountExists(numberWithoutCode)
if (response) {
const state = { ...this.state }
state.showPasswordField = true
this.setState(state)
} else {
this.props.history.push({
pathname: routes.otp.path,
state: {
phoneNumber: numberWithoutCode,
countryCode
}
})
}
} else {
if (required(password)) {
return this.props.showSnackbar({ message: 'login.passwordRequired' })
}
this.props.login({
phoneNumber: numberWithoutCode,
countryCode,
password
})
}
}
forgotPasswordPage = () => this.props.history.push(routes.forgotPassword.path)
render() {
const { loading } = this.props
const { showPasswordField, password } = this.state
return (
<div className={cx(styles.app, 'w-100')}>
<Helmet>
<title>{routes.login.name}</title>
</Helmet>
<div
className={cx('w-100', 'p-l-r-20', 'p-t-120', 'main-locar-container')}
>
<div className="w-100 desktop-version-container">
<h3 className={cx(styles.titlelogin, 'border-bottom-desktop')}>
<BackArrowComponent path={routes.dashboard.path} />
<FormattedMessage
id={
!showPasswordField
? 'login.enterNumber'
: 'login.enterPassword'
}
/>
</h3>
<div className="w-100 cutom-desktop-content-box">
<form onSubmit={this.onSubmit} method="post">
<TelInput
onPhoneNumberBlur={() => {}}
value={this.state.phoneNumber}
onPhoneNumberChange={this.onPhoneNumberChange}
/>
{showPasswordField && (
<Fragment>
<Input
placeholder="input.password"
type="password"
value={password}
onChange={e =>
this.setState({ password: e.target.value })
}
className={['m-t-10']}
icon={
<Icon
type="icon-ico-password"
style={{
color: password.length ? '#1d1e28' : '#b3bac0',
fontSize: '16px',
paddingLeft: '3px'
}}
/>
}
/>
<p
onClick={this.forgotPasswordPage}
style={{ cursor: 'pointer' }}
className={styles.forgotText}
>
<FormattedMessage id="login.forgotPassword" />
</p>
</Fragment>
)}
<Button type="submit" className={['m-t-20', 'm-b-20']}>
{loading ? (
<Loader size={20} />
) : (
<FormattedMessage id="button.continue" />
)}
</Button>
</form>
</div>
</div>
</div>
</div>
)
}
}
const mapStateToProps = state => ({ loading: state.auth.loading })
const mapDispatchToProps = {
login: ({ phoneNumber, password, countryCode }) => ({
type: actions.LOGIN,
phoneNumber,
countryCode,
password
}),
showSnackbar: actionMethods.showSnackBar,
changeText: actionMethods.changeNavText,
enableBack: actionMethods.toggleBackButton
}
export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps
)(Login)
)
import { createSelector } from 'reselect'
import { defaultLang } from 'config/utils'
export const actions = {
SIGNUP: '[auth] saga signup',
SIGNUP_REQUEST: '[auth] signup requested',
SIGNUP_SUCCESS: '[auth] signup successful',
SIGNUP_ERROR: '[auth] signup error',
UPDATE_PHONE: '[auth] saga update phone number',
UPDATE_PHONE_REQUEST: '[auth] update phone number request',
UPDATE_PHONE_SUCCESS: '[auth] update phone number success',
UPDATE_PHONE_ERROR: '[auth] update phone number error',
UPDATE_EMAIL: '[auth] saga update email',
UPDATE_EMAIL_REQUEST: '[auth] update email request',
UPDATE_EMAIL_SUCCESS: '[auth] update email success',
UPDATE_EMAIL_ERROR: '[auth] update email error',
UPDATE_COMM_LANG: '[auth] saga update communication language',
UPDATE_COMM_LANG_REQUEST: '[auth] update communication language request',
UPDATE_COMM_LANG_SUCCESS: '[auth] update communication language success',
UPDATE_COMM_LANG_ERROR: '[auth] update communication language error',
UPDATE_USER_DETAILS: '[auth] saga update user details',
UPDATE_USER_DETAILS_REQUEST: '[auth] update user details request',
UPDATE_USER_DETAILS_SUCCESS: '[auth] update user details success',
UPDATE_USER_DETAILS_ERROR: '[auth] update user details error',
VERIFY_OTP: '[auth] saga verify otp',
VERIFY_OTP_REQUEST: '[auth] verify otp request',
VERIFY_OTP_SUCCESS: '[auth] verify otp success',
VERIFY_OTP_ERROR: '[auth] verify otp error',
VERIFY_EMAIL: '[auth] saga verify email',
VERIFY_EMAIL_REQUEST: '[auth] verify email request',
VERIFY_EMAIL_SUCCESS: '[auth] verify email success',
VERIFY_EMAIL_ERROR: '[auth] verify email error',
RESEND_OTP: '[auth] saga resend otp',
RESEND_OTP_REQUEST: '[auth] resend otp request',
RESEND_OTP_SUCCESS: '[auth] resend otp success',
RESEND_OTP_ERROR: '[auth] resend otp error',
LOGIN_REQUEST: '[auth] login user requested',
LOGIN: '[auth] saga login',
LOGIN_SUCCESS: '[auth] login success',
LOGIN_ERROR: '[auth] login error',
LOGOUT: '[auth] client unset token',
LANGUAGE_CHANGE: '[user] change language'
}
const userSelector = state => state.auth.user
export const selectors = {
isAuthenticated: createSelector(
userSelector,
user => user.isPhoneVerified && user.isEmailVerified
),
phoneNumber: createSelector(userSelector, user => user.phoneNumber),
name: createSelector(userSelector, user => user.name),
avatar: createSelector(userSelector, user => user.avatar),
countryCode: createSelector(userSelector, user => user.countryCode),
email: createSelector(userSelector, user => user.email),
emergencyContact: createSelector(
userSelector,
user => user.emeregencyContact
),
isphoneVerified: createSelector(userSelector, user => user.isPhoneVerified),
isEmailVerified: createSelector(userSelector, user => user.isEmailVerified),
token: createSelector(userSelector, user => user.token),
appLanguage: createSelector(userSelector, user => user.appLanguage),
communicationLanguage: createSelector(
userSelector,
user => user.communicationLanguage
)
}
const initialState = Object.freeze({
user: {
appLanguage: defaultLang
},
loading: false,
error: null
})
export default (state = initialState, action) => {
switch (action.type) {
case actions.SIGNUP_REQUEST:
case actions.LOGIN_REQUEST:
case actions.VERIFY_OTP_REQUEST:
case actions.VERIFY_EMAIL_REQUEST:
case actions.UPDATE_USER_DETAILS_REQUEST:
case actions.UPDATE_EMAIL_REQUEST:
case actions.UPDATE_COMM_LANG_REQUEST:
case actions.UPDATE_PHONE_REQUEST:
case actions.RESEND_OTP_REQUEST:
return {
...state,
loading: true,
error: null
}
case actions.LOGIN_SUCCESS:
case actions.SIGNUP_SUCCESS:
case actions.VERIFY_OTP_SUCCESS:
case actions.UPDATE_USER_DETAILS_SUCCESS:
case actions.UPDATE_PHONE_SUCCESS:
case actions.UPDATE_EMAIL_SUCCESS:
case actions.UPDATE_COMM_LANG_SUCCESS:
case actions.VERIFY_EMAIL_SUCCESS:
case actions.RESEND_OTP_SUCCESS:
return {
...state,
error: null,
loading: false,
user: { ...state.user, ...action.userData }
}
case actions.SIGNUP_ERROR:
case actions.LOGIN_ERROR:
case actions.UPDATE_PHONE_ERROR:
case actions.UPDATE_USER_DETAILS_ERROR:
case actions.UPDATE_EMAIL_ERROR:
case actions.UPDATE_COMM_LANG_ERROR:
case actions.VERIFY_OTP_ERROR:
case actions.RESEND_OTP_ERROR:
case actions.VERIFY_EMAIL_ERROR:
const { error } = action
return {
...state,
loading: false,
error
}
case actions.LANGUAGE_CHANGE:
return {
...state,
user: { ...state.user, appLanguage: action.language }
}
case actions.LOGOUT:
const { appLanguage } = state.user
return {
...initialState,
user: {
appLanguage
}
}
default:
return state
}
}
import {
call,
put,
fork,
take,
cancel,
cancelled,
takeLatest
} from 'redux-saga/effects'
import { get } from 'lodash'
import { push } from 'react-router-redux'
import { truncatePhoneNumber, createFormData } from 'config/utils'
import { routes } from 'config/routes'
import { actions } from 'rdx/auth'
import authService from 'services/auth'
import { actionMethods } from 'rdx/ui'
function* signUp({ data }) {
try {
yield put({ type: actions.SIGNUP_REQUEST })
data.phoneNumber = truncatePhoneNumber(data.phoneNumber, data.countryCode)
data.countryCode = parseInt(data.countryCode, 10)
const response = yield call(authService.signUp, {
...data,
loginType: 'local',
userType: 'passenger'
})
yield put({ type: actions.SIGNUP_SUCCESS, userData: response.data })
yield put(push(routes.verifyEmail.path))
} catch (e) {
const message = get(e, 'response.data.message', 'Signup Error!')
yield put(actionMethods.showSnackBar({ defaultMessage: message }))
yield put({ type: actions.SIGNUP_ERROR, error: message })
}
}
function* login({ phoneNumber, password, countryCode }) {
let response
try {
response = yield call(authService.login, { phoneNumber, password })
const userData = response.data
yield put({ type: actions.LOGIN_SUCCESS, userData })
if (!userData.isPhoneVerified) {
yield put(
push({
pathname: routes.otp.path,
state: { phoneNumber, countryCode }
})
)
} else if (!userData.isEmailVerified) {
yield put(
push({
pathname: routes.verifyEmail.path,
state: { verifyEmail: true }
})
)
} else {
yield put(push(routes.dashboard.path))
}
} catch (e) {
const message = get(e, 'response.data.message', 'Login Error!')
yield put(actionMethods.showSnackBar({ defaultMessage: message }))
yield put({ type: actions.LOGIN_ERROR, error: message })
} finally {
if (yield cancelled()) {
yield put(actions.LOGOUT)
yield put(push(routes.login.path))
}
}
}
function* updatePhone({ phoneNumber, countryCode, verify }) {
try {
yield put({ type: actions.UPDATE_PHONE_REQUEST })
const response = yield call(authService.updatePhoneNumber, {
phoneNumber,
countryCode
})
yield put({ type: actions.UPDATE_PHONE_SUCCESS, userData: response.data })
if (verify) {
yield put(push(routes.otpProfile.path))
}
} catch (e) {
const message = get(
e,
'response.data.message',
'Phone number update error!'
)
yield put(actionMethods.showSnackBar({ defaultMessage: message }))
yield put({ type: actions.UPDATE_PHONE_ERROR, error: message })
}
}
function* updateEmail({ email, verify, redirect }) {
try {
yield put({ type: actions.UPDATE_EMAIL_REQUEST })
const response = yield call(authService.updateEmail, { email, redirect })
yield put({ type: actions.UPDATE_EMAIL_SUCCESS, userData: response.data })
if (verify) {
yield put(push(routes.verifyEmailProfile.path))
}
} catch (e) {
const message = get(e, 'response.data.message', 'Email update error!')
yield put(actionMethods.showSnackBar({ defaultMessage: message }))
yield put({ type: actions.UPDATE_EMAIL_ERROR, error: message })
}
}
function* verifyOTP({ otp, redirect }) {
try {
yield put({ type: actions.VERIFY_OTP_REQUEST })
const response = yield call(authService.verifyOTP, { otp })
yield put({ type: actions.VERIFY_OTP_SUCCESS, userData: response.data })
if (redirect) {
yield put(push(routes.profile.path))
} else {
yield put(push(routes.verifyEmail.path))
}
} catch (e) {
const message = get(e, 'response.data.message', 'OTP does not match!')
yield put(actionMethods.showSnackBar({ defaultMessage: message }))
yield put({ type: actions.VERIFY_OTP_ERROR, error: message })
}
}
function* verifyEmail({ redirect }) {
try {
yield put({ type: actions.VERIFY_EMAIL_REQUEST })
const response = yield call(authService.verifyEmail)
const userData = response.data
if (userData.isEmailVerified) {
yield put({ type: actions.VERIFY_EMAIL_SUCCESS, userData })
if (redirect) yield put(push(redirect))
} else {
yield put(actionMethods.showSnackBar({ message: 'message.verifyemail' }))
}
} catch (e) {
const message = get(
e,
'response.data.message',
'Email could not be verified!'
)
yield put(actionMethods.showSnackBar({ defaultMessage: message }))
yield put({ type: actions.VERIFY_EMAIL_ERROR, error: message })
}
}
function* resendOTP() {
try {
yield put({ type: actions.RESEND_OTP_REQUEST })
const response = yield call(authService.resendOTP)
yield put(actionMethods.showSnackBar({ message: 'message.resendOtp' }))
yield put({ type: actions.RESEND_OTP_SUCCESS, userData: response.data })
} catch (e) {
const message = get(e, 'response.data.message', 'Could not send OTP!')
yield put(actionMethods.showSnackBar({ defaultMessage: message }))
yield put({ type: actions.RESEND_OTP_ERROR, error: message })
}
}
function* updateUserDetails({
name = undefined,
emeregencyContact = undefined,
avatar = undefined,
push: pushRoute = true
}) {
try {
yield put({ type: actions.UPDATE_USER_DETAILS_REQUEST })
const data = createFormData({ name, emeregencyContact, avatar })
const response = yield call(authService.updateUserDetails, data)
yield put({
type: actions.UPDATE_USER_DETAILS_SUCCESS,
userData: response.data
})
if (pushRoute === true) {
yield put(push(routes.profile.path))
} else if (pushRoute) {
yield put(push(pushRoute))
}
if (emeregencyContact === null) {
yield put(
actionMethods.showSnackBar({
message: 'message.emergencyContactDelete'
})
)
} else {
yield put(
actionMethods.showSnackBar({ message: 'message.updateSuccessful' })
)
}
} catch (e) {
const message = get(e, 'response.data.message', 'Could not update!')
yield put(actionMethods.showSnackBar({ defaultMessage: message }))
yield put({ type: actions.UPDATE_USER_DETAILS_ERROR, error: message })
}
}
function* updateCommunicationLanguage({ language }) {
try {
yield put({ type: actions.UPDATE_COMM_LANG_REQUEST })
const response = yield call(
authService.changeCommunicationLanguage,
language
)
yield put({
type: actions.UPDATE_COMM_LANG_SUCCESS,
userData: response.data
})
yield put(push(routes.settings.path))
yield put(actionMethods.showSnackBar({ message: 'message.languageUpdate' }))
} catch (e) {
const message = get(e, 'response.data.message', '')
yield put(actionMethods.showSnackBar({ defaultMessage: message }))
yield put({ type: actions.UPDATE_COMM_LANG_ERROR, error: message })
}
}
function* logout() {
localStorage.removeItem('token')
yield put(push(routes.login.path))
}
function* watchSignUp() {
yield takeLatest(actions.SIGNUP, signUp)
}
function* watchLogin() {
while (true) {
const { phoneNumber, password, countryCode } = yield take(
actions.LOGIN,
login
)
yield put({ type: actions.LOGIN_REQUEST })
const loginTask = yield fork(login, { phoneNumber, password, countryCode })
const action = yield take([actions.LOGOUT, actions.LOGIN_ERROR])
if (action.type === actions.LOGOUT) yield cancel(loginTask)
yield call(logout)
}
}
function* watchUpdatePhone() {
yield takeLatest(actions.UPDATE_PHONE, updatePhone)
}
function* watchUpdateEmail() {
yield takeLatest(actions.UPDATE_EMAIL, updateEmail)
}
function* watchVerifyOTP() {
yield takeLatest(actions.VERIFY_OTP, verifyOTP)
}
function* watchVerifyEmail() {
yield takeLatest(actions.VERIFY_EMAIL, verifyEmail)
}
function* watchResendOTP() {
yield takeLatest(actions.RESEND_OTP, resendOTP)
}
function* watchUpdateUserDetails() {
yield takeLatest(actions.UPDATE_USER_DETAILS, updateUserDetails)
}
function* watchUpdateCommunicationLanguage() {
yield takeLatest(actions.UPDATE_COMM_LANG, updateCommunicationLanguage)
}
export default [
fork(watchSignUp),
fork(watchLogin),
fork(watchUpdatePhone),
fork(watchUpdateEmail),
fork(watchVerifyOTP),
fork(watchResendOTP),
fork(watchVerifyEmail),
fork(watchUpdateUserDetails),
fork(watchUpdateCommunicationLanguage)
]
import { createStore, combineReducers, compose, applyMiddleware } from 'redux'
import { persistReducer, persistStore } from 'redux-persist'
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'
import localforage from 'localforage'
import { routerReducer, routerMiddleware } from 'react-router-redux'
import {
setToken,
removeToken,
setLanguage,
logoutUserOnTokenExpire
} from './middleware'
import createHistory from 'history/createBrowserHistory'
import createSageMiddleware from 'redux-saga'
import sagas from 'sagas'
import { isProduction } from 'config/utils'
import auth from './auth'
import ui from './ui'
export const history = createHistory()
const rootReducer = combineReducers({
auth,
ui,
router: routerReducer
})
const persistedReducer = persistReducer(
{
key: 'root',
storage: localforage,
stateReconciler: autoMergeLevel2,
blacklist: ['router']
},
rootReducer
)
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const sagaMiddleware = createSageMiddleware()
const middleware = [
routerMiddleware(history),
sagaMiddleware,
setToken,
setLanguage,
removeToken,
logoutUserOnTokenExpire
]
export const store = createStore(
persistedReducer,
isProduction()
? compose(applyMiddleware(...middleware))
: composeEnhancers(applyMiddleware(...middleware))
)
export const persistor = persistStore(store)
sagaMiddleware.run(sagas)
import axios from 'services'
import { actions } from 'rdx/auth'
export const setToken = ({ getState }) => next => async action => {
const state = getState()
let token = action && action.userData && action.userData.token
if (!token) {
if (state.auth.user.token) {
token = state.auth.user.token
} else if (localStorage.getItem('token')) {
token = localStorage.getItem('token')
}
token && localStorage.setItem('token', token)
}
return next(action)
}
export const removeToken = () => next => action => {
if (action && action.type === actions.LOGOUT) {
localStorage.removeItem('token')
}
return next(action)
}
export const setLanguage = ({ getState }) => next => action => {
if (localStorage.getItem('lang')) {
const state = getState()
localStorage.setItem('lang', state.auth.user.appLanguage)
}
if (action && action.type === actions.LANGUAGE_CHANGE) {
localStorage.setItem('lang', action.language)
}
axios.defaults.headers.common['language'] = localStorage.getItem('lang')
return next(action)
}
export const logoutUserOnTokenExpire = () => next => async action => {
if (action.error === 'jwt expired') {
console.log('log out')
return next({ type: actions.LOGOUT })
}
next(action)
}
import { all } from 'redux-saga/effects'
import authSagas from './auth'
export default function* watchAll() {
yield all([...authSagas])
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment