This commit is contained in:
dev.daminik00 2025-12-24 02:06:31 +03:00
parent 41f622125b
commit 5b89cfd573
6 changed files with 251 additions and 14 deletions

5
.npmrc
View File

@ -1 +1,4 @@
node-options=--experimental-vm-modules --no-warnings
node-options=--experimental-vm-modules --no-warnings
@wit-lab-llc:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

54
package-lock.json generated
View File

@ -17,6 +17,7 @@
"@reduxjs/toolkit": "^1.9.5",
"@smakss/react-scroll-direction": "^4.0.4",
"@unleash/proxy-client-react": "^4.5.2",
"@wit-lab-llc/frontend-shared": "^1.0.4",
"apng-js": "^1.1.1",
"core-js": "^3.37.1",
"framer-motion": "^11.0.8",
@ -66,6 +67,28 @@
"vite-plugin-svgr": "^4.2.0"
}
},
"../wit-frontend-shared": {
"name": "@wit-lab-llc/frontend-shared",
"version": "1.0.0",
"extraneous": true,
"license": "UNLICENSED",
"dependencies": {
"@fingerprintjs/fingerprintjs": "^5.0.1"
},
"devDependencies": {
"@types/node": "^25.0.2",
"tsup": "^8.5.1",
"typescript": "^5.9.3"
},
"peerDependencies": {
"react": ">=18.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
@ -1028,6 +1051,12 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@fingerprintjs/fingerprintjs": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-5.0.1.tgz",
"integrity": "sha512-KbaeE/rk2WL8MfpRP6jTI4lSr42SJPjvkyrjP3QU6uUDkOMWWYC2Ts1sNSYcegHC8avzOoYTHBj+2fTqvZWQBA==",
"license": "MIT"
},
"node_modules/@floating-ui/core": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz",
@ -2127,6 +2156,18 @@
"vite": "^4.2.0"
}
},
"node_modules/@wit-lab-llc/frontend-shared": {
"version": "1.0.4",
"resolved": "https://npm.pkg.github.com/download/@wit-lab-llc/frontend-shared/1.0.4/c342b071bc0716511d84bc3f3c9685aa7649ea5e",
"integrity": "sha512-9EZEpjWCdz+IP4RsgkVTPiUEKhm5LqnbgD/S/GJ07eG0Y10ulj3qXjDVXc7N9gURyZULshriDt350w6nhN7Z3w==",
"license": "UNLICENSED",
"dependencies": {
"@fingerprintjs/fingerprintjs": "^5.0.1"
},
"peerDependencies": {
"react": ">=18.0.0"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -6238,6 +6279,11 @@
"integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==",
"dev": true
},
"@fingerprintjs/fingerprintjs": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-5.0.1.tgz",
"integrity": "sha512-KbaeE/rk2WL8MfpRP6jTI4lSr42SJPjvkyrjP3QU6uUDkOMWWYC2Ts1sNSYcegHC8avzOoYTHBj+2fTqvZWQBA=="
},
"@floating-ui/core": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz",
@ -6903,6 +6949,14 @@
"react-refresh": "^0.14.0"
}
},
"@wit-lab-llc/frontend-shared": {
"version": "1.0.4",
"resolved": "https://npm.pkg.github.com/download/@wit-lab-llc/frontend-shared/1.0.4/c342b071bc0716511d84bc3f3c9685aa7649ea5e",
"integrity": "sha512-9EZEpjWCdz+IP4RsgkVTPiUEKhm5LqnbgD/S/GJ07eG0Y10ulj3qXjDVXc7N9gURyZULshriDt350w6nhN7Z3w==",
"requires": {
"@fingerprintjs/fingerprintjs": "^5.0.1"
}
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",

View File

@ -12,7 +12,9 @@
"start:local": "vite --host --mode localhost",
"start:prod": "vite --host --mode production",
"build:dev": "tsc && vite build --mode develop",
"build:prod": "tsc && vite build --mode production"
"build:prod": "tsc && vite build --mode production",
"link:shared": "npm link @wit-lab-llc/frontend-shared",
"unlink:shared": "npm unlink @wit-lab-llc/frontend-shared && npm install"
},
"dependencies": {
"@emotion/react": "^11.11.4",
@ -24,6 +26,7 @@
"@reduxjs/toolkit": "^1.9.5",
"@smakss/react-scroll-direction": "^4.0.4",
"@unleash/proxy-client-react": "^4.5.2",
"@wit-lab-llc/frontend-shared": "^1.0.4",
"apng-js": "^1.1.1",
"core-js": "^3.37.1",
"framer-motion": "^11.0.8",

View File

@ -5,6 +5,7 @@ import { ICreateAuthorizeUser } from "./User";
import { ELocalesPlacement } from "@/locales";
import { Currency } from "@/components/PaymentTable/Price";
import { PeriodType } from "@/hooks/translations";
import type { SessionFingerprintData, SessionFacebookData } from "@wit-lab-llc/frontend-shared";
export interface PayloadCreate {
feature: string, // Type: string
@ -14,7 +15,13 @@ export interface PayloadCreate {
sign: boolean, // Type: boolean
signDate: string | undefined, // Type: string, ISO Date
utm: IUTM, // Type: { [key: string]: string } - Optional
domain: string // Type: string
anonymousId?: string,
query?: Record<string, string>,
landingQuery?: Record<string, string>,
lastActivityAt?: string,
domain: string, // Type: string
fingerprint?: SessionFingerprintData, // Fingerprint data from library
facebookData?: SessionFacebookData // Facebook data for Conversions API
}
export interface PayloadUpdate {
@ -22,6 +29,10 @@ export interface PayloadUpdate {
data: {
feature: string, // Type: string
anonymousId?: string,
query?: Record<string, string>,
lastActivityAt?: string,
profile?: Partial<ICreateAuthorizeUser>;
partner?: Partial<Exclude<ICreateAuthorizeUser, "relationship_status">>;
answers?: Partial<IAnswersSessionPalmistry | IAnswersSessionChats | IAnswersSessionCompatibilityV2 | IAnswersSessionCompatibilityV3 | IAnswersSessionCompatibilityV4>;

View File

@ -3,9 +3,33 @@ 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, useMemo, useState } from "react"
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();
@ -19,16 +43,119 @@ export const useSession = () => {
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> => {
if (session[source]?.length) {
localStorage.setItem(`${source}_sessionId`, session[source]);
return {
sessionId: session[source],
status: "old"
}
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,
@ -37,7 +164,13 @@ export const useSession = () => {
sign: checked,
signDate: dateOfCheck.length ? dateOfCheck : undefined,
utm,
domain: window.location.hostname
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);
@ -48,6 +181,15 @@ export const useSession = () => {
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);
@ -64,7 +206,7 @@ export const useSession = () => {
sessionId: ""
}
}
}, [api, checked, dateOfCheck, dispatch, feature, session, timezone, utm])
}, [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 {
@ -73,22 +215,39 @@ export const useSession = () => {
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])
}, [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])
}, [dispatch, getVisitStorageKey])
return useMemo(() => ({
session,

View File

@ -26,12 +26,19 @@ import { InitializationProvider } from "./initialization";
import { getSourceByPathname } from "./utils/source.utils";
import { parseQueryParams } from "./services/url";
import { actions } from "./store";
import { initWitLib } from "@wit-lab-llc/frontend-shared";
pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.js`;
const environments = import.meta.env;
const init = async () => {
// Initialize WIT shared library for fingerprint and facebook data collection
initWitLib({
baseUrl: environments.AURA_API_URL || '',
debug: environments.MODE !== 'production',
});
// Parse UTM parameters from URL at initialization
const utm = parseQueryParams();
console.log('UTM parameters parsed at init:', utm);