import FingerprintJS from "@fingerprintjs/fingerprintjs"; //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 * as Sentry from "@sentry/react"; import { notification } from "antd"; import axios from "axios"; import i18next from "i18next"; //import LogRocket from "logrocket"; import { all, call, delay, put, select, takeLatest } from "redux-saga/effects"; import { auth, firestore, getCurrentUser, logImEXEvent, messaging, updateCurrentUser } from "../../firebase/firebase.utils"; import { QUERY_EULA } from "../../graphql/bodyshop.queries"; import cleanAxios from "../../utils/CleanAxios"; import client from "../../utils/GraphQLClient"; import dayjs from "../../utils/day"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; import { checkInstanceId, sendPasswordResetFailure, sendPasswordResetSuccess, setAuthlevel, setImexShopId, setInstanceConflict, setInstanceId, setLocalFingerprint, setPartsManagementOnly, signInFailure, signInSuccess, signOutFailure, signOutSuccess, unauthorizedUser, updateUserDetailsSuccess, validatePasswordResetFailure, validatePasswordResetSuccess } from "./user.actions"; import UserActionTypes from "./user.types"; //import posthog from "posthog-js"; import { bodyshopHasDmsKey, determineDMSTypeByBodyshop, DMS_MAP } from "../../utils/dmsUtils"; 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 { const user = yield getCurrentUser(); if (!user) { yield put(unauthorizedUser()); return; } //LogRocket.identify(user.email); //amplitude.setUserId(user.email); //posthog.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" }); //amplitude.reset(); } catch { 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.success({ title: i18next.t("profile.successes.updated") }); } catch { //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 { window.$crisp?.push(["set", "user:nickname", [payload.displayName || payload.email]]); InstanceRenderManager({ executeFunction: true, args: [], rome: () => { window.$zoho.salesiq.visitor.name(payload.displayName || payload.email); window.$zoho.salesiq.visitor.email(payload.email); } }); // Hide Crisp if currently in parts-entry mode (pre-shop-details) try { const state = yield select(); const isParts = state?.application?.isPartsEntry === true; const instanceSeg = InstanceRenderManager({ imex: ["imex-online-user", "imex"], rome: ["rome-online-user", "rome"] }); // Always ensure segments include instance + user, and append partsManagement if applicable const segs = [ ...instanceSeg, ...(isParts ? [ InstanceRenderManager({ imex: "ImexPartsManagement", rome: "RomePartsManagement" }) ] : []) ]; window.$crisp?.push(["set", "session:segments", [segs]]); if (isParts) { window.$crisp?.push(["do", "chat:hide"]); } } catch { // no-op } } 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; } 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" }) }); 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); } // Dispatch the imexshopid to Redux store yield put(setImexShopId(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."))) yield put(setInstanceId(user.uid)); } try { //amplitude.setGroup('Shop', payload.shopname); 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]]); } // Build consolidated Crisp segments including instance, region, features, and parts mode const isParts = yield select((state) => state.application.isPartsEntry === true); const instanceSeg = InstanceRenderManager({ imex: ["imex-online-user", "imex"], rome: ["rome-online-user", "rome"] }); const featureSegments = payload.features?.allAccess === true ? ["allAccess"] : [ "basic", ...Object.keys(payload.features).filter( (key) => payload.features[key] === true || (typeof payload.features[key] === "string" && !isNaN(Date.parse(payload.features[key]))) ) ]; const hasDmsKey = bodyshopHasDmsKey(payload); const dmsType = hasDmsKey ? determineDMSTypeByBodyshop(payload) : null; const additionalSegments = [ dmsType === DMS_MAP.cdk && DMS_MAP.cdk.toUpperCase(), dmsType === DMS_MAP.pbs && DMS_MAP.pbs.toUpperCase(), dmsType === DMS_MAP.reynolds && DMS_MAP.reynolds.toUpperCase(), payload.accountingconfig?.qbo === true && "QBO", payload.accountingconfig?.qbo === false && !hasDmsKey && "QBD" ].filter(Boolean); featureSegments.push(...additionalSegments); const regionSeg = payload.region_config ? `region:${payload.region_config}` : null; const segments = [...instanceSeg, ...(regionSeg ? [regionSeg] : []), ...featureSegments]; if (isParts) { segments.push(InstanceRenderManager({ imex: "ImexPartsManagement", rome: "RomePartsManagement" })); } window.$crisp?.push(["set", "session:segments", [segments]]); // Hide/show Crisp chat based on parts mode or features window.$crisp?.push(["do", isParts ? "chat:hide" : "chat:show"]); InstanceRenderManager({ executeFunction: true, args: [], rome: () => { window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname }); } }); } catch (error) { console.warn("Couldnt find $crisp.", error.message); } yield put(setPartsManagementOnly(payload.features.partsManagementOnly)); } 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) ]); }