w-aura/src/hooks/session/useSession.ts
Daniil Chemerkin 49c0b3121a Develop
2025-12-24 00:29:54 +00:00

265 lines
10 KiB
TypeScript

import { useApi } from "@/api";
import { PayloadUpdate, ResponseCreate } from "@/api/resources/Session";
import { ESourceAuthorization } from "@/api/resources/User";
import { getClientTimezone, language } from "@/locales";
import { actions, selectors } from "@/store";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useDispatch, useSelector } from "react-redux";
import {
FingerprintCollector,
FacebookCollector,
type SessionFingerprintData,
type SessionFacebookData,
getOrCreateAnonymousId,
} from "@wit-lab-llc/frontend-shared";
import { parseQueryParams } from "@/services/url";
let fingerprintCollectorInstance: FingerprintCollector | null = null;
let facebookCollectorInstance: FacebookCollector | null = null;
function getFingerprintCollector(): FingerprintCollector {
if (!fingerprintCollectorInstance) {
fingerprintCollectorInstance = new FingerprintCollector();
}
return fingerprintCollectorInstance;
}
function getFacebookCollector(): FacebookCollector {
if (!facebookCollectorInstance) {
facebookCollectorInstance = new FacebookCollector();
}
return facebookCollectorInstance;
}
export const useSession = () => {
const dispatch = useDispatch();
const api = useApi();
const session = useSelector(selectors.selectSession);
const feature = useSelector(selectors.selectFeature)
const { checked, dateOfCheck } = useSelector(selectors.selectPrivacyPolicy);
const utm = useSelector(selectors.selectUTM);
const timezone = getClientTimezone();
const [isError, setIsError] = useState(false);
const [fingerprintCollector] = useState(() => getFingerprintCollector());
const [facebookCollector] = useState(() => getFacebookCollector());
const dataCollectedRef = useRef(false);
const visitTtlMs = 30 * 60 * 1000;
const getVisitStorageKey = useCallback((source: ESourceAuthorization) => {
return `${source}_visit`;
}, []);
const readVisit = useCallback((source: ESourceAuthorization) => {
try {
const raw = localStorage.getItem(getVisitStorageKey(source));
if (!raw) return null;
return JSON.parse(raw) as {
sessionId: string;
startedAt: number;
lastActivityAt: number;
anonymousId?: string;
landingQuery?: Record<string, string>;
lastQuery?: Record<string, string>;
};
} catch {
return null;
}
}, [getVisitStorageKey]);
const writeVisit = useCallback((source: ESourceAuthorization, visit: {
sessionId: string;
startedAt: number;
lastActivityAt: number;
anonymousId?: string;
landingQuery?: Record<string, string>;
lastQuery?: Record<string, string>;
}) => {
localStorage.setItem(getVisitStorageKey(source), JSON.stringify(visit));
localStorage.setItem(`${source}_sessionId`, visit.sessionId);
}, [getVisitStorageKey]);
useEffect(() => {
if (typeof window === "undefined" || dataCollectedRef.current) return;
dataCollectedRef.current = true;
Promise.all([
fingerprintCollector.collect().catch(() => null),
Promise.resolve(facebookCollector.collect()),
]).then(() => {
console.log("[useSession] Fingerprint and Facebook data collected");
});
}, [fingerprintCollector, facebookCollector]);
const createSession = useCallback(async (source: ESourceAuthorization): Promise<ResponseCreate> => {
const nowMs = Date.now();
const existingVisit = readVisit(source);
if (existingVisit && nowMs - existingVisit.lastActivityAt <= visitTtlMs) {
const nextQuery = parseQueryParams();
const nextAnonymousId = existingVisit.anonymousId ?? getOrCreateAnonymousId();
writeVisit(source, {
...existingVisit,
lastActivityAt: nowMs,
lastQuery: nextQuery,
anonymousId: nextAnonymousId,
});
dispatch(actions.session.update({ session: existingVisit.sessionId, source }));
localStorage.setItem(`${source}_sessionId`, existingVisit.sessionId);
return { sessionId: existingVisit.sessionId, status: "old" };
}
try {
let fingerprint: SessionFingerprintData | undefined;
try {
const fpData = await fingerprintCollector.getOrCollect();
if (fpData) {
const payload = fingerprintCollector.toServerPayload();
fingerprint = {
visitorId: fpData.visitorId,
confidence: fpData.confidence,
collectedAt: fpData.collectedAt,
...payload,
} as SessionFingerprintData;
}
} catch (e) {
console.warn("[useSession] Failed to collect fingerprint:", e);
}
let facebookData: SessionFacebookData | undefined;
try {
const fbData = facebookCollector.getData() || facebookCollector.collect();
if (fbData) {
facebookData = {
fbp: fbData.fbp ?? undefined,
fbc: fbData.fbc ?? undefined,
fbclid: fbData.fbclid ?? undefined,
externalId: fingerprint?.visitorId,
landingPage: fbData.landingPage ?? undefined,
referrer: fbData.referrer,
clientUserAgent: fbData.userAgent,
eventSourceUrl: fbData.currentUrl,
browserLanguage: fbData.browserLanguage,
screenResolution: fbData.screenResolution,
viewportSize: fbData.viewportSize,
colorDepth: fbData.colorDepth,
devicePixelRatio: fbData.devicePixelRatio,
touchSupport: fbData.touchSupport,
cookiesEnabled: fbData.cookiesEnabled,
doNotTrack: fbData.doNotTrack,
collectedAt: fbData.collectedAt,
};
}
} catch (e) {
console.warn("[useSession] Failed to collect Facebook data:", e);
}
const sessionParams = {
feature,
locale: language,
timezone,
source,
sign: checked,
signDate: dateOfCheck.length ? dateOfCheck : undefined,
utm,
anonymousId: getOrCreateAnonymousId(),
query: parseQueryParams(),
landingQuery: parseQueryParams(),
lastActivityAt: new Date().toISOString(),
domain: window.location.hostname,
fingerprint,
facebookData,
};
console.log('Creating session with parameters:', sessionParams);
const sessionFromServer = await api.createSession(sessionParams);
console.log('Session creation response:', sessionFromServer);
if (sessionFromServer?.sessionId?.length && sessionFromServer?.status === "success") {
dispatch(actions.session.update({
session: sessionFromServer.sessionId,
source
}));
localStorage.setItem(`${source}_sessionId`, sessionFromServer.sessionId);
const q = parseQueryParams();
writeVisit(source, {
sessionId: sessionFromServer.sessionId,
startedAt: Date.now(),
lastActivityAt: Date.now(),
anonymousId: sessionParams.anonymousId,
landingQuery: q,
lastQuery: q,
});
return sessionFromServer
}
console.error('Session creation failed - invalid response:', sessionFromServer);
setIsError(true);
return {
status: "error",
sessionId: ""
}
} catch (error) {
console.error('Session creation failed with error:', error);
setIsError(true);
return {
status: "error",
sessionId: ""
}
}
}, [api, checked, dateOfCheck, dispatch, feature, session, timezone, utm, fingerprintCollector, facebookCollector, readVisit, visitTtlMs, writeVisit])
const updateSession = useCallback(async (data: Omit<PayloadUpdate["data"], "feature">, source: ESourceAuthorization, sessionId?: string) => {
try {
const _sessionId = sessionId || session[source];
const result = await api.updateSession({
sessionId: _sessionId,
data: {
feature,
anonymousId: getOrCreateAnonymousId(),
query: parseQueryParams(),
lastActivityAt: new Date().toISOString(),
...data
}
});
if (_sessionId) {
const nowMs = Date.now();
const existingVisit = readVisit(source);
if (existingVisit && existingVisit.sessionId === _sessionId) {
writeVisit(source, {
...existingVisit,
lastActivityAt: nowMs,
lastQuery: parseQueryParams(),
anonymousId: getOrCreateAnonymousId(),
});
}
}
return result;
} catch (error) {
console.log(error)
}
}, [api, feature, session, readVisit, writeVisit])
const deleteSession = useCallback(async (source: ESourceAuthorization) => {
localStorage.removeItem(`${source}_sessionId`);
localStorage.removeItem(getVisitStorageKey(source));
dispatch(actions.session.update({
session: "",
source
}))
}, [dispatch, getVisitStorageKey])
return useMemo(() => ({
session,
isError,
createSession,
updateSession,
deleteSession
}), [
session,
isError,
createSession,
deleteSession,
updateSession
])
}