Merge branch 'develop' into 'main'

Develop

See merge request witapp/aura-webapp!838
This commit is contained in:
Daniil Chemerkin 2025-12-24 00:29:54 +00:00
commit 385c8315ba
7 changed files with 256 additions and 32 deletions

3
.npmrc
View File

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

View File

@ -190,21 +190,6 @@
</head>
<body>
<!-- Load NMI Collect.js -->
<script type="module">
import { createApi } from "/src/api/api.ts"
const api = createApi();
api.getPaymentConfig(null).then((paymentConfig) => {
const nmiPublicKey = paymentConfig?.data?.nmi?.publicKey;
const scriptElement = document.createElement("script");
scriptElement.src = "https://hms.transactiongateway.com/token/Collect.js";
scriptElement.setAttribute("data-tokenization-key", nmiPublicKey);
scriptElement.setAttribute("data-variant", "inline");
document.head.appendChild(scriptElement);
})
</script>
<!-- Load NMI Collect.js -->
<!-- Klaviyo Metric -->
<script type="module">
const klaviyoKeys = {

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>;
@ -160,7 +171,9 @@ export interface ResponseGetLocale {
export interface ResponseGetPixels {
status: "success" | string,
data: {
fb?: string[];
facebook_pixel?: string[];
google_analytics?: string[];
yandex_metrica?: string[];
}
}

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_DAPI_HOST || '',
debug: environments.MODE !== 'production',
});
// Parse UTM parameters from URL at initialization
const utm = parseQueryParams();
console.log('UTM parameters parsed at init:', utm);
@ -105,11 +112,11 @@ const init = async () => {
locale: getDefaultLocaleByLanguage(language),
});
localStorage.setItem('fb_pixels', JSON.stringify(pixels?.data?.fb || []));
localStorage.setItem('fb_pixels', JSON.stringify(pixels?.data?.facebook_pixel || []));
return (
<React.Fragment>
{!!pixels?.data?.fb?.length && <HeadData pixels={pixels.data.fb} />}
{!!pixels?.data?.facebook_pixel?.length && <HeadData pixels={pixels.data.facebook_pixel} />}
<I18nextProvider i18n={i18nextInstance}>
<Provider store={store}>
<BrowserRouter>