Initial Expo base app config with Redux
This commit is contained in:
28
redux/root.reducer.js
Normal file
28
redux/root.reducer.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { combineReducers } from "redux";
|
||||
import { persistReducer } from "redux-persist";
|
||||
import AsyncStorage from "@react-native-community/async-storage";
|
||||
|
||||
import userReducer from "./user/user.reducer";
|
||||
// import messagingReducer from "./messaging/messaging.reducer";
|
||||
// import emailReducer from "./email/email.reducer";
|
||||
// import modalsReducer from "./modals/modals.reducer";
|
||||
// import applicationReducer from "./application/application.reducer";
|
||||
// import techReducer from "./tech/tech.reducer";
|
||||
|
||||
const persistConfig = {
|
||||
key: "root",
|
||||
storage: AsyncStorage,
|
||||
// whitelist: ["messaging", "tech", "application"],
|
||||
// blacklist: ["user", "email", "modals"],
|
||||
};
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
user: userReducer,
|
||||
// messaging: messagingReducer,
|
||||
// email: emailReducer,
|
||||
// modals: modalsReducer,
|
||||
// application: applicationReducer,
|
||||
// tech: techReducer,
|
||||
});
|
||||
|
||||
export default persistReducer(persistConfig, rootReducer);
|
||||
19
redux/root.saga.js
Normal file
19
redux/root.saga.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { all, call } from "redux-saga/effects";
|
||||
|
||||
// import { userSagas } from "./user/user.sagas";
|
||||
// import { messagingSagas } from "./messaging/messaging.sagas";
|
||||
// import { emailSagas } from "./email/email.sagas";
|
||||
// import { modalsSagas } from "./modals/modals.sagas";
|
||||
// import { applicationSagas } from "./application/application.sagas";
|
||||
// import { techSagas } from "./tech/tech.sagas";
|
||||
|
||||
export default function* rootSaga() {
|
||||
yield all([
|
||||
// call(userSagas),
|
||||
// call(messagingSagas),
|
||||
// call(emailSagas),
|
||||
// call(modalsSagas),
|
||||
// call(applicationSagas),
|
||||
// call(techSagas),
|
||||
]);
|
||||
}
|
||||
26
redux/store.js
Normal file
26
redux/store.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createStore, applyMiddleware, compose } from "redux";
|
||||
import { persistStore } from "redux-persist";
|
||||
import { createLogger } from "redux-logger";
|
||||
import createSagaMiddleware from "redux-saga";
|
||||
|
||||
import rootReducer from "./root.reducer";
|
||||
import rootSaga from "./root.saga";
|
||||
|
||||
const sagaMiddleWare = createSagaMiddleware();
|
||||
|
||||
const middlewares = [sagaMiddleWare];
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
middlewares.push(createLogger({ collapsed: true, diff: true }));
|
||||
}
|
||||
|
||||
const enhancer = compose(
|
||||
applyMiddleware(...middlewares)
|
||||
// other store enhancers if any
|
||||
);
|
||||
|
||||
export const store = createStore(rootReducer, enhancer);
|
||||
sagaMiddleWare.run(rootSaga);
|
||||
|
||||
export const persistor = persistStore(store);
|
||||
|
||||
export default { store, persistStore };
|
||||
100
redux/user/user.actions.js
Normal file
100
redux/user/user.actions.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import UserActionTypes from "./user.types";
|
||||
|
||||
export const signInSuccess = (user) => ({
|
||||
type: UserActionTypes.SIGN_IN_SUCCESS,
|
||||
payload: user,
|
||||
});
|
||||
export const signInFailure = (errorMsg) => ({
|
||||
type: UserActionTypes.SIGN_IN_FAILURE,
|
||||
payload: errorMsg,
|
||||
});
|
||||
|
||||
export const emailSignInStart = (emailAndPassword) => ({
|
||||
type: UserActionTypes.EMAIL_SIGN_IN_START,
|
||||
payload: emailAndPassword,
|
||||
});
|
||||
|
||||
export const checkUserSession = () => ({
|
||||
type: UserActionTypes.CHECK_USER_SESSION,
|
||||
});
|
||||
|
||||
export const signOutStart = () => ({
|
||||
type: UserActionTypes.SIGN_OUT_START,
|
||||
});
|
||||
export const signOutSuccess = () => ({
|
||||
type: UserActionTypes.SIGN_OUT_SUCCESS,
|
||||
});
|
||||
|
||||
export const signOutFailure = (error) => ({
|
||||
type: UserActionTypes.SIGN_OUT_FAILURE,
|
||||
payload: error,
|
||||
});
|
||||
|
||||
export const unauthorizedUser = () => ({
|
||||
type: UserActionTypes.UNAUTHORIZED_USER,
|
||||
});
|
||||
|
||||
export const setUserLanguage = (language) => ({
|
||||
type: UserActionTypes.SET_USER_LANGUAGE,
|
||||
payload: language,
|
||||
});
|
||||
|
||||
export const updateUserDetails = (userDetails) => ({
|
||||
type: UserActionTypes.UPDATE_USER_DETAILS,
|
||||
payload: userDetails,
|
||||
});
|
||||
|
||||
export const updateUserDetailsSuccess = (userDetails) => ({
|
||||
type: UserActionTypes.UPDATE_USER_DETAILS_SUCCESS,
|
||||
payload: userDetails,
|
||||
});
|
||||
|
||||
export const setBodyshop = (bodyshop) => ({
|
||||
type: UserActionTypes.SET_SHOP_DETAILS,
|
||||
payload: bodyshop,
|
||||
});
|
||||
|
||||
export const setInstanceId = (userInfo) => ({
|
||||
type: UserActionTypes.SET_INSTANCE_ID,
|
||||
payload: userInfo,
|
||||
});
|
||||
|
||||
export const checkInstanceId = (uid) => ({
|
||||
type: UserActionTypes.CHECK_INSTANCE_ID,
|
||||
payload: uid,
|
||||
});
|
||||
|
||||
export const setInstanceConflict = () => ({
|
||||
type: UserActionTypes.SET_INSTANCE_CONFLICT,
|
||||
});
|
||||
|
||||
export const setLocalFingerprint = (fingerprint) => ({
|
||||
type: UserActionTypes.SET_LOCAL_FINGERPRINT,
|
||||
payload: fingerprint,
|
||||
});
|
||||
|
||||
export const sendPasswordReset = (email) => ({
|
||||
type: UserActionTypes.SEND_PASSWORD_RESET_EMAIL_START,
|
||||
payload: email,
|
||||
});
|
||||
export const sendPasswordResetFailure = (error) => ({
|
||||
type: UserActionTypes.SEND_PASSWORD_RESET_EMAIL_FAILURE,
|
||||
payload: error,
|
||||
});
|
||||
export const sendPasswordResetSuccess = () => ({
|
||||
type: UserActionTypes.SEND_PASSWORD_RESET_EMAIL_SUCCESS,
|
||||
});
|
||||
|
||||
export const validatePasswordResetStart = (emailAndPin) => ({
|
||||
type: UserActionTypes.VALIDATE_PASSWORD_RESET_START,
|
||||
payload: emailAndPin,
|
||||
});
|
||||
|
||||
export const validatePasswordResetSuccess = () => ({
|
||||
type: UserActionTypes.VALIDATE_PASSWORD_RESET_SUCCESS,
|
||||
});
|
||||
|
||||
export const validatePasswordResetFailure = (error) => ({
|
||||
type: UserActionTypes.VALIDATE_PASSWORD_RESET_FAILURE,
|
||||
payload: error,
|
||||
});
|
||||
91
redux/user/user.reducer.js
Normal file
91
redux/user/user.reducer.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// import UserActionTypes from "./user.types";
|
||||
|
||||
const INITIAL_STATE = {
|
||||
currentUser: {
|
||||
authorized: null,
|
||||
},
|
||||
bodyshop: null,
|
||||
fingerprint: null,
|
||||
error: null,
|
||||
conflict: false,
|
||||
passwordreset: {
|
||||
email: null,
|
||||
error: null,
|
||||
success: false,
|
||||
},
|
||||
};
|
||||
|
||||
const userReducer = (state = INITIAL_STATE, action) => {
|
||||
switch (action.type) {
|
||||
// case UserActionTypes.SET_LOCAL_FINGERPRINT:
|
||||
// return { ...state, fingerprint: action.payload };
|
||||
// case UserActionTypes.SET_INSTANCE_ID:
|
||||
// return { ...state, conflict: false };
|
||||
// case UserActionTypes.SET_INSTANCE_CONFLICT:
|
||||
// return { ...state, conflict: true };
|
||||
// case UserActionTypes.VALIDATE_PASSWORD_RESET_START:
|
||||
// case UserActionTypes.SEND_PASSWORD_RESET_EMAIL_START:
|
||||
// return {
|
||||
// ...state,
|
||||
// passwordreset: {
|
||||
// email: action.payload,
|
||||
// error: null,
|
||||
// success: false,
|
||||
// },
|
||||
// };
|
||||
// case UserActionTypes.VALIDATE_PASSWORD_RESET_FAILURE:
|
||||
// case UserActionTypes.SEND_PASSWORD_RESET_EMAIL_FAILURE:
|
||||
// return { ...state, passwordreset: { error: action.payload } };
|
||||
// case UserActionTypes.VALIDATE_PASSWORD_RESET_SUCCESS:
|
||||
// case UserActionTypes.SEND_PASSWORD_RESET_EMAIL_SUCCESS:
|
||||
// return {
|
||||
// ...state,
|
||||
// passwordreset: { ...state.passwordreset, success: true },
|
||||
// };
|
||||
// case UserActionTypes.SIGN_IN_SUCCESS:
|
||||
// return {
|
||||
// ...state,
|
||||
// currentUser: action.payload,
|
||||
// error: null,
|
||||
// };
|
||||
// case UserActionTypes.SIGN_OUT_SUCCESS:
|
||||
// return {
|
||||
// ...state,
|
||||
// currentUser: { authorized: false },
|
||||
// error: null,
|
||||
// };
|
||||
// case UserActionTypes.UNAUTHORIZED_USER:
|
||||
// return {
|
||||
// ...state,
|
||||
// error: null,
|
||||
// currentUser: { authorized: false },
|
||||
// };
|
||||
// case UserActionTypes.SET_USER_LANGUAGE:
|
||||
// return {
|
||||
// ...state,
|
||||
// language: action.payload,
|
||||
// };
|
||||
// case UserActionTypes.UPDATE_USER_DETAILS_SUCCESS:
|
||||
// return {
|
||||
// ...state,
|
||||
// currentUser: {
|
||||
// ...state.currentUser,
|
||||
// ...action.payload, //Spread current user details in.
|
||||
// },
|
||||
// };
|
||||
|
||||
// case UserActionTypes.SET_SHOP_DETAILS:
|
||||
// return { ...state, bodyshop: action.payload };
|
||||
// case UserActionTypes.SIGN_IN_FAILURE:
|
||||
// case UserActionTypes.SIGN_OUT_FAILURE:
|
||||
// case UserActionTypes.EMAIL_SIGN_UP_FAILURE:
|
||||
// return {
|
||||
// ...state,
|
||||
// error: action.payload,
|
||||
// };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default userReducer;
|
||||
214
redux/user/user.sagas.js
Normal file
214
redux/user/user.sagas.js
Normal file
@@ -0,0 +1,214 @@
|
||||
import Fingerprint2 from "fingerprintjs2";
|
||||
import LogRocket from "logrocket";
|
||||
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
||||
import {
|
||||
auth,
|
||||
firestore,
|
||||
getCurrentUser,
|
||||
logImEXEvent,
|
||||
updateCurrentUser,
|
||||
} from "../../firebase/firebase.utils";
|
||||
import {
|
||||
checkInstanceId,
|
||||
setInstanceConflict,
|
||||
setInstanceId,
|
||||
setLocalFingerprint,
|
||||
signInFailure,
|
||||
signInSuccess,
|
||||
signOutFailure,
|
||||
signOutSuccess,
|
||||
unauthorizedUser,
|
||||
updateUserDetailsSuccess,
|
||||
sendPasswordResetFailure,
|
||||
sendPasswordResetSuccess,
|
||||
validatePasswordResetSuccess,
|
||||
validatePasswordResetFailure,
|
||||
} from "./user.actions";
|
||||
import UserActionTypes from "./user.types";
|
||||
|
||||
export function* onEmailSignInStart() {
|
||||
yield takeLatest(UserActionTypes.EMAIL_SIGN_IN_START, signInWithEmail);
|
||||
}
|
||||
export function* signInWithEmail({ payload: { email, password } }) {
|
||||
try {
|
||||
logImEXEvent("redux_sign_in_attempt", { user: email });
|
||||
|
||||
const { user } = yield auth.signInWithEmailAndPassword(email, password);
|
||||
|
||||
yield put(
|
||||
signInSuccess({
|
||||
uid: user.uid,
|
||||
email: user.email,
|
||||
displayName: user.displayName,
|
||||
photoURL: user.photoURL,
|
||||
authorized: true,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
yield put(signInFailure(error));
|
||||
logImEXEvent("redux_sign_in_failure", { user: email, error });
|
||||
}
|
||||
}
|
||||
|
||||
export function* onCheckUserSession() {
|
||||
yield takeLatest(UserActionTypes.CHECK_USER_SESSION, isUserAuthenticated);
|
||||
}
|
||||
export function* isUserAuthenticated() {
|
||||
try {
|
||||
logImEXEvent("redux_auth_check");
|
||||
|
||||
const user = yield getCurrentUser();
|
||||
if (!user) {
|
||||
yield put(unauthorizedUser());
|
||||
return;
|
||||
}
|
||||
|
||||
LogRocket.identify(user.email);
|
||||
yield put(
|
||||
signInSuccess({
|
||||
uid: user.uid,
|
||||
email: user.email,
|
||||
displayName: user.displayName,
|
||||
photoURL: user.photoURL,
|
||||
authorized: true,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
yield put(signInFailure(error));
|
||||
}
|
||||
}
|
||||
export function* onSignOutStart() {
|
||||
yield takeLatest(UserActionTypes.SIGN_OUT_START, signOutStart);
|
||||
}
|
||||
export function* signOutStart() {
|
||||
try {
|
||||
logImEXEvent("redux_sign_out");
|
||||
|
||||
yield auth.signOut();
|
||||
yield put(signOutSuccess());
|
||||
localStorage.removeItem("token");
|
||||
} catch (error) {
|
||||
yield put(signOutFailure(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
export function* onUpdateUserDetails() {
|
||||
yield takeLatest(UserActionTypes.UPDATE_USER_DETAILS, updateUserDetails);
|
||||
}
|
||||
export function* updateUserDetails(userDetails) {
|
||||
try {
|
||||
yield updateCurrentUser(userDetails.payload);
|
||||
yield put(updateUserDetailsSuccess(userDetails.payload));
|
||||
} catch (error) {
|
||||
//yield put(signOutFailure(error.message));
|
||||
//TODO error handling
|
||||
}
|
||||
}
|
||||
export function* onSetInstanceId() {
|
||||
yield takeLatest(UserActionTypes.SET_INSTANCE_ID, setInstanceIdSaga);
|
||||
}
|
||||
export function* setInstanceIdSaga({ payload: uid }) {
|
||||
try {
|
||||
const userInstanceRef = firestore.doc(`userInstance/${uid}`);
|
||||
|
||||
const fingerprint = Fingerprint2.x64hash128(
|
||||
(yield Fingerprint2.getPromise({})).map((c) => c.value).join(""),
|
||||
31
|
||||
);
|
||||
|
||||
yield userInstanceRef.set({
|
||||
timestamp: new Date(),
|
||||
fingerprint,
|
||||
});
|
||||
|
||||
yield put(setLocalFingerprint(fingerprint));
|
||||
yield delay(5 * 60 * 1000);
|
||||
yield put(checkInstanceId(uid));
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
//yield put(signOutFailure(error.message));
|
||||
//TODO error handling
|
||||
}
|
||||
}
|
||||
|
||||
export function* onCheckInstanceId() {
|
||||
yield takeLatest(UserActionTypes.CHECK_INSTANCE_ID, checkInstanceIdSaga);
|
||||
}
|
||||
export function* checkInstanceIdSaga({ payload: uid }) {
|
||||
try {
|
||||
const userInstanceRef = firestore.doc(`userInstance/${uid}`);
|
||||
|
||||
const snapshot = yield userInstanceRef.get();
|
||||
let fingerprint = yield select((state) => state.user.fingerprint);
|
||||
|
||||
if (snapshot.data().fingerprint === fingerprint) {
|
||||
yield delay(5 * 60 * 1000);
|
||||
yield put(checkInstanceId(uid));
|
||||
} else {
|
||||
console.log("ERROR: Fingerprints do not match. Conflict detected.");
|
||||
logImEXEvent("instance_confict");
|
||||
yield put(setInstanceConflict());
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
//TODO error handling
|
||||
}
|
||||
}
|
||||
|
||||
export function* onSignInSuccess() {
|
||||
yield takeLatest(UserActionTypes.SIGN_IN_SUCCESS, signInSuccessSaga);
|
||||
}
|
||||
|
||||
export function* signInSuccessSaga({ payload }) {
|
||||
LogRocket.identify(payload.email);
|
||||
yield put(setInstanceId(payload.uid));
|
||||
yield logImEXEvent("redux_sign_in_success");
|
||||
}
|
||||
|
||||
export function* onSendPasswordResetStart() {
|
||||
yield takeLatest(
|
||||
UserActionTypes.SEND_PASSWORD_RESET_EMAIL_START,
|
||||
sendPasswordResetEmail
|
||||
);
|
||||
}
|
||||
export function* sendPasswordResetEmail({ payload }) {
|
||||
try {
|
||||
yield auth.sendPasswordResetEmail(payload, {
|
||||
url: "https://imex.online/passwordreset",
|
||||
});
|
||||
console.log("Good should send.");
|
||||
yield put(sendPasswordResetSuccess());
|
||||
} catch (error) {
|
||||
yield put(sendPasswordResetFailure(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
export function* onValidatePasswordResetStart() {
|
||||
yield takeLatest(
|
||||
UserActionTypes.VALIDATE_PASSWORD_RESET_START,
|
||||
validatePasswordResetStart
|
||||
);
|
||||
}
|
||||
export function* validatePasswordResetStart({ payload: { password, code } }) {
|
||||
try {
|
||||
yield auth.confirmPasswordReset(code, password);
|
||||
yield put(validatePasswordResetSuccess());
|
||||
} catch (error) {
|
||||
console.log("function*validatePasswordResetStart -> error", error);
|
||||
yield put(validatePasswordResetFailure(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
export function* userSagas() {
|
||||
yield all([
|
||||
call(onEmailSignInStart),
|
||||
call(onCheckUserSession),
|
||||
call(onSignOutStart),
|
||||
call(onUpdateUserDetails),
|
||||
call(onSetInstanceId),
|
||||
call(onCheckInstanceId),
|
||||
call(onSignInSuccess),
|
||||
call(onSendPasswordResetStart),
|
||||
call(onValidatePasswordResetStart),
|
||||
]);
|
||||
}
|
||||
28
redux/user/user.selectors.js
Normal file
28
redux/user/user.selectors.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { createSelector } from "reselect";
|
||||
|
||||
const selectUser = (state) => state.user;
|
||||
|
||||
export const selectCurrentUser = createSelector(
|
||||
[selectUser],
|
||||
(user) => user.currentUser
|
||||
);
|
||||
|
||||
export const selectSignInError = createSelector(
|
||||
[selectUser],
|
||||
(user) => user.error
|
||||
);
|
||||
|
||||
export const selectBodyshop = createSelector(
|
||||
[selectUser],
|
||||
(user) => user.bodyshop
|
||||
);
|
||||
|
||||
export const selectInstanceConflict = createSelector(
|
||||
[selectUser],
|
||||
(user) => user.conflict
|
||||
);
|
||||
|
||||
export const selectPasswordReset = createSelector(
|
||||
[selectUser],
|
||||
(user) => user.passwordreset
|
||||
);
|
||||
30
redux/user/user.types.js
Normal file
30
redux/user/user.types.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const UserActionTypes = {
|
||||
SET_CURRENT_USER: "SET_CURRENT_USER",
|
||||
GOOGLE_SIGN_IN_START: "GOOGLE_SIGN_IN_START",
|
||||
SIGN_IN_SUCCESS: "SIGN_IN_SUCCESS",
|
||||
SIGN_IN_FAILURE: "SIGN_IN_FAILURE",
|
||||
EMAIL_SIGN_IN_START: "EMAIL_SIGN_IN_START",
|
||||
CHECK_USER_SESSION: "CHECK_USER_SESSION",
|
||||
SIGN_OUT_START: "SIGN_OUT_START",
|
||||
SIGN_OUT_SUCCESS: "SIGN_OUT_SUCCESS",
|
||||
SIGN_OUT_FAILURE: "SIGN_OUT_FAILURE",
|
||||
EMAIL_SIGN_UP_START: "EMAIL_SIGN_UP_START",
|
||||
EMAIL_SIGN_UP_SUCCESS: "EMAIL_SIGN_UP_SUCCESS",
|
||||
EMAIL_SIGN_UP_FAILURE: "EMAIL_SIGN_UP_FAILURE",
|
||||
UNAUTHORIZED_USER: "UNAUTHORIZED_USER",
|
||||
SET_USER_LANGUAGE: "SET_USER_LANGUAGE",
|
||||
UPDATE_USER_DETAILS: "UPDATE_USER_DETAILS",
|
||||
UPDATE_USER_DETAILS_SUCCESS: "UPDATE_USER_DETAILS_SUCCESS",
|
||||
SET_SHOP_DETAILS: "SET_SHOP_DETAILS",
|
||||
SET_INSTANCE_ID: "SET_INSTANCE_ID",
|
||||
CHECK_INSTANCE_ID: "CHECK_INSTANCE_ID",
|
||||
SET_INSTANCE_CONFLICT: "SET_INSTANCE_CONFLICT",
|
||||
SET_LOCAL_FINGERPRINT: "SET_LOCAL_FINGERPRINT",
|
||||
SEND_PASSWORD_RESET_EMAIL_START: "SEND_PASSWORD_RESET_EMAIL_START",
|
||||
SEND_PASSWORD_RESET_EMAIL_FAILURE: "SEND_PASSWORD_RESET_EMAIL_FAILURE",
|
||||
SEND_PASSWORD_RESET_EMAIL_SUCCESS: "SEND_PASSWORD_RESET_EMAIL_SUCCESS",
|
||||
VALIDATE_PASSWORD_RESET_START: "VALIDATE_PASSWORD_RESET_START",
|
||||
VALIDATE_PASSWORD_RESET_SUCCESS: "VALIDATE_PASSWORD_RESET_SUCCESS",
|
||||
VALIDATE_PASSWORD_RESET_FAILURE: "VALIDATE_PASSWORD_RESET_FAILURE",
|
||||
};
|
||||
export default UserActionTypes;
|
||||
Reference in New Issue
Block a user