Files
bodyshop/client/src/redux/user/user.sagas.js
2025-10-06 08:19:43 -07:00

432 lines
13 KiB
JavaScript

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 {
analytics,
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 * as amplitude from '@amplitude/analytics-browser';
import posthog from 'posthog-js';
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.open({
type: "success",
message: 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]]);
const currentUserSegment = InstanceRenderManager({
imex: "imex-online-user",
rome: "rome-online-user"
});
window.$crisp.push(["set", "session:segments", [[currentUserSegment]]]);
InstanceRenderManager({
executeFunction: true,
args: [],
imex: () => {
window.$crisp.push(["set", "session:segments", [["imex"]]]);
},
rome: () => {
window.$zoho.salesiq.visitor.name(payload.displayName || payload.email);
window.$zoho.salesiq.visitor.email(payload.email);
window.$crisp.push(["set", "session:segments", [["rome"]]]);
}
});
// 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", rome: "rome" });
// Always ensure segments include instance + user, and append partsManagement if applicable
const segs = [
currentUserSegment,
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", rome: "rome" });
let featureSegments;
if (payload.features?.allAccess === true) {
featureSegments = ["allAccess"];
} else {
const featureKeys = Object.keys(payload.features).filter(
(key) =>
payload.features[key] === true ||
(typeof payload.features[key] === "string" && !isNaN(Date.parse(payload.features[key])))
);
featureSegments = ["basic", ...featureKeys];
}
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)
]);
}