import FingerprintJS from "@fingerprintjs/fingerprintjs"; import * as Sentry from "@sentry/browser"; import { notification } from "antd"; import axios from "axios"; import { setUserId, setUserProperties } from "firebase/analytics"; import { checkActionCode, confirmPasswordReset, sendPasswordResetEmail, signInWithEmailAndPassword, signOut } from "firebase/auth"; import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "firebase/firestore"; import { getToken } from "firebase/messaging"; import i18next from "i18next"; import LogRocket from "logrocket"; import { all, call, delay, put, select, takeLatest } from "redux-saga/effects"; import { Userpilot } from "userpilot"; import { factory } from "../../App/App.container"; import { analytics, auth, firestore, getCurrentUser, logImEXEvent, messaging, updateCurrentUser } from "../../firebase/firebase.utils"; import { QUERY_EULA } from "../../graphql/bodyshop.queries"; import client from "../../utils/GraphQLClient"; import dayjs from "../../utils/day"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; import { checkInstanceId, sendPasswordResetFailure, sendPasswordResetSuccess, setAuthlevel, setInstanceConflict, setInstanceId, setLocalFingerprint, signInFailure, signInSuccess, signOutFailure, signOutSuccess, unauthorizedUser, updateUserDetailsSuccess, validatePasswordResetFailure, validatePasswordResetSuccess } from "./user.actions"; import UserActionTypes from "./user.types"; import cleanAxios from "../../utils/CleanAxios"; const fpPromise = FingerprintJS.load(); 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 signInWithEmailAndPassword(auth, 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); const eulaQuery = yield client.query({ query: QUERY_EULA, variables: { now: dayjs() } }); const eulaIsAccepted = eulaQuery.data.eulas.length > 0 && eulaQuery.data.eulas[0].eula_acceptances.length > 0; yield put( signInSuccess({ uid: user.uid, email: user.email, displayName: user.displayName, photoURL: user.photoURL, authorized: true, eulaIsAccepted, currentEula: eulaIsAccepted ? null : eulaQuery.data.eulas[0] }) ); } catch (error) { yield put(signInFailure(error)); } } export function* onSignOutStart() { yield takeLatest(UserActionTypes.SIGN_OUT_START, signOutStart); } export function* signOutStart() { try { logImEXEvent("redux_sign_out"); const state = yield select(); //unsub from topic. try { const fcm_tokens = yield getToken(messaging); yield call(axios.post, "/notifications/unsubscribe", { fcm_tokens, imexshopid: state.user.bodyshop.imexshopid, type: "messaging" }); } catch (error) { console.log("No FCM token. Skipping unsubscribe."); } yield signOut(auth); 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 { const updatedDetails = yield updateCurrentUser(userDetails.payload); yield put(updateUserDetailsSuccess(updatedDetails)); notification.open({ type: "success", message: i18next.t("profile.successes.updated") }); } catch (error) { //yield put(signOutFailure(error.message)); } } export function* onSetInstanceId() { yield takeLatest(UserActionTypes.SET_INSTANCE_ID, setInstanceIdSaga); } export function* setInstanceIdSaga({ payload: uid }) { try { const userInstanceRef = doc(firestore, `userInstance/${uid}`); // Get the visitor identifier when you need it. const fp = yield fpPromise; const result = yield fp.get(); const res = yield cleanAxios.get("https://api.ipify.org/?format=json"); const udoc = yield getDoc(userInstanceRef); if (!udoc.data()) { yield setDoc(userInstanceRef, { timestamp: new Date(), fingerprint: result.visitorId, //totalFingerprint: result, ip: [res.data.ip] }); } else { yield updateDoc(userInstanceRef, { timestamp: new Date(), fingerprint: result.visitorId, //totalFingerprint: result, ip: arrayUnion(res.data.ip) }); } yield put(setLocalFingerprint(result.visitorId)); yield delay(5 * 60 * 1000); if (import.meta.env.PROD) yield put(checkInstanceId(uid)); } catch (error) { console.log("error", error); } } export function* onCheckInstanceId() { yield takeLatest(UserActionTypes.CHECK_INSTANCE_ID, checkInstanceIdSaga); } export function* checkInstanceIdSaga({ payload: uid }) { try { const snapshot = yield getDoc(doc(firestore, `userInstance/${uid}`)); let fingerprint = yield select((state) => state.user.fingerprint); yield put(setInstanceConflict()); 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); } } export function* onSignInSuccess() { yield takeLatest(UserActionTypes.SIGN_IN_SUCCESS, signInSuccessSaga); } export function* signInSuccessSaga({ payload }) { LogRocket.identify(payload.email); try { InstanceRenderManager({ executeFunction: true, args: [], imex: () => { window.$crisp.push(["set", "user:nickname", [payload.displayName || payload.email]]); window.$crisp.push(["set", "session:segments", [["user"]]]); }, rome: () => { window.$zoho.salesiq.visitor.name(payload.displayName || payload.email); window.$zoho.salesiq.visitor.email(payload.email); }, promanager: () => { Userpilot.identify(payload.email, { email: payload.email }); console.log("*** Userpilot identified."); } }); } catch (error) { console.log("Error updating Crisp settings.", error); } try { Sentry.setUser({ email: payload.email, username: payload.displayName || payload.email }); } catch (error) { console.log("Error setting Sentry user.", error); } setUserId(analytics, payload.email); setUserProperties(analytics, payload); yield logImEXEvent("redux_sign_in_success"); } export function* onSendPasswordResetStart() { yield takeLatest(UserActionTypes.SEND_PASSWORD_RESET_EMAIL_START, sendPasswordResetEmailSaga); yield takeLatest(UserActionTypes.SEND_PASSWORD_RESET_EMAIL_START_AGAIN, sendPasswordResetEmailSaga); } export function* sendPasswordResetEmailSaga({ payload }) { try { yield sendPasswordResetEmail(auth, payload, { url: InstanceRenderManager({ imex: "https://imex.online/passwordreset", rome: "https://romeonline.io/passwordreset", promanager: "https:promanager.web-est.com/passwordreset" }) }); 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 { checkActionCode(auth, code); yield confirmPasswordReset(auth, code, password); yield put(validatePasswordResetSuccess()); } catch (error) { yield put(validatePasswordResetFailure(error.message)); } } export function* onSetShopDetails() { yield takeLatest(UserActionTypes.SET_SHOP_DETAILS, SetAuthLevelFromShopDetails); } export function* SetAuthLevelFromShopDetails({ payload }) { try { const userEmail = yield select((state) => state.user.currentUser.email); try { dayjs.tz.setDefault(payload.timezone); } catch (error) { console.log(error); } factory.client(payload.imexshopid); const authRecord = payload.associations.filter((a) => a.useremail.toLowerCase() === userEmail.toLowerCase()); yield put(setAuthlevel(authRecord[0] ? authRecord[0].authlevel : 0)); yield put( updateUserDetailsSuccess(authRecord[0] ? { validemail: authRecord[0].user.validemail } : { validemail: false }) ); const user = yield select((state) => state.user.currentUser); if (payload.features.singleDeviceOnly) { if ( !( user.email.includes("@imex.") || user.email.includes("@rome.") || user.email.includes("@rometech.") || user.email.includes("@promanager.") ) ) yield put(setInstanceId(user.uid)); } //For Rome, check to make sure it's not a PM shop. try { InstanceRenderManager({ executeFunction: true, args: [], rome: () => { if ( payload.imexshopid.toLowerCase().startsWith("pm_") && !( user.email.includes("@imex.") || user.email.includes("@rome.") || user.email.includes("@rometech.") || user.email.includes("@promanager.") ) ) { throw new Error("You are not authorized to use this application."); } }, promanager: () => {} }); } catch (error) { yield put(setInstanceConflict()); } try { InstanceRenderManager({ executeFunction: true, args: [], imex: () => { window.$crisp.push(["set", "user:company", [payload.shopname]]); if (authRecord[0] && authRecord[0].user.validemail) { window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]); } }, rome: () => { window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname }); } }); } catch (error) { console.error("Couldnt find $crisp."); } } catch (error) { yield put(signInFailure(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), call(onSetShopDetails) ]); }