265 lines
10 KiB
TypeScript
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
|
|
])
|
|
} |