Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aef1a14bbc | ||
|
|
cb56c45c2c | ||
|
|
3197fb4ca2 | ||
|
|
630fd2209d | ||
|
|
62a5002fa6 | ||
|
|
cfab5421af | ||
|
|
016241aec7 | ||
|
|
b4c4067b5e | ||
|
|
385c8315ba | ||
|
|
49c0b3121a | ||
|
|
05fdd07dd5 | ||
|
|
2e77f8ced7 | ||
|
|
e3dd721bea | ||
|
|
5b89cfd573 | ||
|
|
41f622125b | ||
|
|
8e21217f7c | ||
|
|
6ee2affd5a |
5
.npmrc
5
.npmrc
@ -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}
|
||||
15
index.html
15
index.html
@ -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 = {
|
||||
|
||||
8527
package-lock.json
generated
8527
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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.9",
|
||||
"apng-js": "^1.1.1",
|
||||
"core-js": "^3.37.1",
|
||||
"framer-motion": "^11.0.8",
|
||||
@ -39,7 +42,7 @@
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-i18next": "^12.3.1",
|
||||
"react-pdf": "8.0.2",
|
||||
"react-pdf": "^10.2.0",
|
||||
"react-player": "^2.16.0",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.11.2",
|
||||
@ -69,7 +72,7 @@
|
||||
"postcss": "^8.5.6",
|
||||
"terser": "^5.43.1",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.8",
|
||||
"vite": "^7.3.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useMemo,
|
||||
useLayoutEffect
|
||||
} from "react";
|
||||
import { useState, useEffect, useRef, useMemo, useLayoutEffect } from "react";
|
||||
import {
|
||||
Routes,
|
||||
Route,
|
||||
@ -150,7 +144,7 @@ import { useSession } from "@/hooks/session/useSession";
|
||||
import { getSourceByPathname } from "@/utils/source.utils";
|
||||
import { navigateToAuth } from "@/utils/auth-navigation";
|
||||
|
||||
import "../palmistry/palmistry-container/palmistry-container.css"
|
||||
import "../palmistry/palmistry-container/palmistry-container.css";
|
||||
import ProfileRoutes from "@/routerComponents/Profile";
|
||||
import RetainingFunnelRoutes from "@/routerComponents/RetainingFunnel";
|
||||
import { ELocalesPlacement } from "@/locales";
|
||||
@ -184,16 +178,16 @@ function App(): JSX.Element {
|
||||
unleashClient.updateContext({
|
||||
userId: user?.id || undefined,
|
||||
properties: {
|
||||
source
|
||||
}
|
||||
source,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (session?.[source]) {
|
||||
unleashClient.updateContext({
|
||||
sessionId: session?.[source] || undefined,
|
||||
properties: {
|
||||
source
|
||||
}
|
||||
source,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [user, session, source, unleashClient]);
|
||||
@ -214,10 +208,14 @@ function App(): JSX.Element {
|
||||
|
||||
if (isPageAvailable) {
|
||||
const utm = parseQueryParams();
|
||||
console.log('App component - parsed UTM on page:', location.pathname, utm);
|
||||
console.log(
|
||||
"App component - parsed UTM on page:",
|
||||
location.pathname,
|
||||
utm
|
||||
);
|
||||
// Only update UTM if there are new parameters and they're not empty
|
||||
if (Object.keys(utm).length > 0) {
|
||||
console.log('App component - dispatching UTM update:', utm);
|
||||
console.log("App component - dispatching UTM update:", utm);
|
||||
dispatch(actions.utm.update(utm));
|
||||
}
|
||||
}
|
||||
@ -293,18 +291,12 @@ function App(): JSX.Element {
|
||||
<CookieYesController isDelete={subscriptionStatus === "subscribed"} />
|
||||
}
|
||||
>
|
||||
<Route
|
||||
path={`${profilePrefix}/*`}
|
||||
element={<ProfileRoutes />}
|
||||
/>
|
||||
<Route path={`${profilePrefix}/*`} element={<ProfileRoutes />} />
|
||||
<Route
|
||||
path={`${retainingFunnelPrefix}/*`}
|
||||
element={<RetainingFunnelRoutes />}
|
||||
/>
|
||||
<Route
|
||||
path={`${anonymousPrefix}/*`}
|
||||
element={<AnonymousRoutes />}
|
||||
/>
|
||||
<Route path={`${anonymousPrefix}/*`} element={<AnonymousRoutes />} />
|
||||
<Route
|
||||
path={`${palmistryV1Prefix}/*`}
|
||||
element={<PalmistryV1Routes />}
|
||||
@ -321,7 +313,10 @@ function App(): JSX.Element {
|
||||
path={`${compatibilityV4Prefix}/*`}
|
||||
element={<CompatibilityV4Routes />}
|
||||
/>
|
||||
<Route path={routes.client.auth()} element={<Auth redirectUrl={routes.client.trialPaymentV1()} />} />
|
||||
<Route
|
||||
path={routes.client.auth()}
|
||||
element={<Auth redirectUrl={routes.client.trialPaymentV1()} />}
|
||||
/>
|
||||
<Route element={<AuthorizedUserOutlet />}>
|
||||
<Route
|
||||
path={`${palmistryV2Prefix}/*`}
|
||||
@ -339,9 +334,22 @@ function App(): JSX.Element {
|
||||
<Route path={routes.client.skipTrial()} element={<SkipTrial />} />
|
||||
<Route
|
||||
path={routes.client.addConsultant()}
|
||||
element={<AddConsultant funnel={ELocalesPlacement.V1} paymentPlacement="add_consultant" />}
|
||||
element={
|
||||
<AddConsultant
|
||||
funnel={ELocalesPlacement.V1}
|
||||
paymentPlacement="add_consultant"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.addGuides()}
|
||||
element={
|
||||
<AddGuides
|
||||
funnel={ELocalesPlacement.V1}
|
||||
paymentPlacement="add_guides"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path={routes.client.addGuides()} element={<AddGuides funnel={ELocalesPlacement.V1} paymentPlacement="add_guides" />} />
|
||||
</Route>
|
||||
</Route>
|
||||
{/* Additional Purchases Main End */}
|
||||
@ -377,14 +385,8 @@ function App(): JSX.Element {
|
||||
element={<FreePeriodInfoPage />}
|
||||
/>
|
||||
<Route path={routes.client.feedback()} element={<FeedbackPage />} />
|
||||
<Route
|
||||
path={routes.client.birthtime()}
|
||||
element={<BirthtimePage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.createProfile()}
|
||||
element={<SkipStep />}
|
||||
/>
|
||||
<Route path={routes.client.birthtime()} element={<BirthtimePage />} />
|
||||
<Route path={routes.client.createProfile()} element={<SkipStep />} />
|
||||
<Route
|
||||
path={routes.client.emailEnter()}
|
||||
element={<EmailEnterPage />}
|
||||
@ -398,10 +400,7 @@ function App(): JSX.Element {
|
||||
element={<AuthResultPage />}
|
||||
/>
|
||||
{/* <Route path={routes.client.static()} element={<StaticPage />} /> */}
|
||||
<Route
|
||||
path={routes.client.priceList()}
|
||||
element={<PriceListPage />}
|
||||
/>
|
||||
<Route path={routes.client.priceList()} element={<PriceListPage />} />
|
||||
</Route>
|
||||
{/* <Route element={<AuthorizedUserOutlet />}>
|
||||
<Route
|
||||
@ -419,9 +418,7 @@ function App(): JSX.Element {
|
||||
/>
|
||||
</Route>
|
||||
<Route element={<PrivateSubscriptionOutlet />}>
|
||||
<Route
|
||||
element={<Layout />}
|
||||
>
|
||||
<Route element={<Layout />}>
|
||||
<Route path={routes.client.home()} element={<HomePage />} />
|
||||
<Route
|
||||
path={routes.client.compatibility()}
|
||||
@ -481,7 +478,10 @@ function App(): JSX.Element {
|
||||
|
||||
{/* <Route path="*" element={<ABDesignV1Routes />} /> */}
|
||||
|
||||
<Route path="*" element={<Navigate to={getRouteBy(subscriptionStatus)} />} />
|
||||
<Route
|
||||
path="*"
|
||||
element={<Navigate to={getRouteBy(subscriptionStatus)} />}
|
||||
/>
|
||||
|
||||
{/* ROUTES OFF */}
|
||||
|
||||
@ -1055,13 +1055,13 @@ function Layout(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
{showHeader ? (
|
||||
<Header
|
||||
openMenu={() => setIsMenuOpen(true)}
|
||||
/>
|
||||
) : null}
|
||||
{showHeader ? <Header openMenu={() => setIsMenuOpen(true)} /> : null}
|
||||
{isRouteFullDataModal && (
|
||||
<Modal open={isShowFullDataModal} isCloseButtonVisible={false} onClose={() => setIsShowFullDataModal(false)}>
|
||||
<Modal
|
||||
open={isShowFullDataModal}
|
||||
isCloseButtonVisible={false}
|
||||
onClose={() => setIsShowFullDataModal(false)}
|
||||
>
|
||||
<FullDataModal onClose={onCloseFullDataModal} />
|
||||
</Modal>
|
||||
)}
|
||||
@ -1284,13 +1284,14 @@ function MainPage(): JSX.Element {
|
||||
const status = useSelector(selectors.selectStatus);
|
||||
const route = getRouteBy(status);
|
||||
const [shouldRedirect, setShouldRedirect] = useState(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// Check if we're navigating to auth and on witlab.app domain
|
||||
if (route === routes.client.auth()) {
|
||||
const isWitlabDomain = window.location.hostname === 'witlab.app' ||
|
||||
window.location.hostname.endsWith('.witlab.app');
|
||||
|
||||
const isWitlabDomain =
|
||||
window.location.hostname === "witlab.app" ||
|
||||
window.location.hostname.endsWith(".witlab.app");
|
||||
|
||||
// If we're on witlab.app domain, use server-side navigation
|
||||
if (isWitlabDomain) {
|
||||
navigateToAuth();
|
||||
@ -1298,12 +1299,12 @@ function MainPage(): JSX.Element {
|
||||
}
|
||||
}
|
||||
}, [route]);
|
||||
|
||||
|
||||
// If we're redirecting via server-side navigation, return empty fragment
|
||||
if (shouldRedirect) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
// For all other cases, use client-side navigation
|
||||
return <Navigate to={route} replace={true} />;
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ function OutOfCreditsModal({
|
||||
fill: "#7ED8F8",
|
||||
fontSize: "30px",
|
||||
fontWeight: "bold",
|
||||
dominantBaseline: "no-change",
|
||||
dominantBaseline: "middle",
|
||||
},
|
||||
}}
|
||||
maxValue={timerSeconds}
|
||||
|
||||
@ -2,12 +2,11 @@ import styles from "./styles.module.scss";
|
||||
import { useRef, useState } from "react";
|
||||
import { Document, DocumentProps, Page } from "react-pdf";
|
||||
import Loader, { LoaderColor } from "../Loader";
|
||||
import { File } from "node_modules/react-pdf/dist/esm/shared/types";
|
||||
import { Pagination } from "@mui/material";
|
||||
|
||||
interface IPDFViewerProps {
|
||||
width?: number;
|
||||
file?: File;
|
||||
file?: DocumentProps["file"];
|
||||
}
|
||||
|
||||
const pagesOfPaginationPageLength = 1;
|
||||
|
||||
@ -19,6 +19,8 @@ import { useNavigate } from "react-router-dom";
|
||||
import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash";
|
||||
import { IFunnelPaymentVariant } from "@/api/resources/Session";
|
||||
import { useFunnel } from "@/hooks/funnel/useFunnel";
|
||||
import { getSourceByPathname } from "@/utils/source.utils";
|
||||
import { getStateParamForRedirect } from "@/services/url";
|
||||
|
||||
const environments = import.meta.env;
|
||||
|
||||
@ -197,11 +199,26 @@ function PaymentPage({
|
||||
","
|
||||
);
|
||||
|
||||
let redirectUrl = `${paymentUrl}?paywallId=${paywallId}&placementId=${placementId}&productId=${
|
||||
// Build base params
|
||||
const baseParams = `paywallId=${paywallId}&placementId=${placementId}&productId=${
|
||||
activeProduct?.id
|
||||
}&jwtToken=${token}&price=${(
|
||||
(activeProduct?.trialPrice || 100) / 100
|
||||
).toFixed(2)}¤cy=${currency}&${getTrackingCookiesForRedirect()}`;
|
||||
).toFixed(2)}¤cy=${currency}`;
|
||||
|
||||
// Add sessionId
|
||||
const source = getSourceByPathname();
|
||||
const currentSessionId = localStorage.getItem(`${source}_sessionId`);
|
||||
const sessionParam = currentSessionId ? `&sessionId=${currentSessionId}` : "";
|
||||
|
||||
// Add state param with current UTM (base64 encoded JSON)
|
||||
const stateParam = getStateParamForRedirect();
|
||||
const stateStr = stateParam ? `&state=${stateParam}` : "";
|
||||
|
||||
// Add tracking cookies
|
||||
const trackingCookies = getTrackingCookiesForRedirect();
|
||||
|
||||
let redirectUrl = `${paymentUrl}?${baseParams}${sessionParam}${stateStr}&${trackingCookies}`;
|
||||
|
||||
if (fbPixels.length) {
|
||||
redirectUrl += `&fb_pixels=${fbPixels}`;
|
||||
|
||||
@ -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,
|
||||
|
||||
11
src/init.tsx
11
src/init.tsx
@ -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>
|
||||
|
||||
@ -13,4 +13,95 @@ export const parseQueryParams = () => {
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Params that should NOT be included in state (they are passed separately)
|
||||
const EXCLUDED_STATE_PARAMS = [
|
||||
"paywallId",
|
||||
"placementId",
|
||||
"productId",
|
||||
"jwtToken",
|
||||
"price",
|
||||
"currency",
|
||||
"fb_pixels",
|
||||
"sessionId",
|
||||
"state",
|
||||
// Tracking cookies (passed separately)
|
||||
"_fbc",
|
||||
"_fbp",
|
||||
"_ym_uid",
|
||||
"_ym_d",
|
||||
"_ym_isad",
|
||||
"_ym_visorc",
|
||||
"yandexuid",
|
||||
"ymex",
|
||||
];
|
||||
|
||||
/**
|
||||
* Get current query params that should be passed between screens and to payment
|
||||
* Includes ALL params except internal ones (productId, placementId, etc.)
|
||||
* Works with utm_*, fbclid, gclid, and any other marketing params
|
||||
*/
|
||||
export const getCurrentQueryParams = (): Record<string, string> => {
|
||||
if (typeof window === "undefined") return {};
|
||||
|
||||
const params = parseQueryParams();
|
||||
const utmParams: Record<string, string> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
const isExcluded =
|
||||
EXCLUDED_STATE_PARAMS.includes(key) ||
|
||||
key.startsWith("_ga") ||
|
||||
key.startsWith("_gid");
|
||||
|
||||
if (!isExcluded && value) {
|
||||
utmParams[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return utmParams;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert bytes to base64 string (handles UTF-8 properly)
|
||||
* MDN recommended approach: https://developer.mozilla.org/en-US/docs/Web/API/Window/btoa#unicode_strings
|
||||
*/
|
||||
const bytesToBase64 = (bytes: Uint8Array): string => {
|
||||
const binString = Array.from(bytes, (byte) =>
|
||||
String.fromCodePoint(byte)
|
||||
).join("");
|
||||
return btoa(binString);
|
||||
};
|
||||
|
||||
/**
|
||||
* Encode params as base64 JSON for state parameter
|
||||
* Uses URL-safe base64 encoding with UTF-8 support
|
||||
* Handles Unicode characters (e.g., utm_campaign=夏セール)
|
||||
*/
|
||||
export const encodeStateParam = (params: Record<string, string>): string => {
|
||||
if (Object.keys(params).length === 0) return "";
|
||||
|
||||
try {
|
||||
const json = JSON.stringify(params);
|
||||
// Encode string as UTF-8 bytes, then convert to base64
|
||||
const bytes = new TextEncoder().encode(json);
|
||||
const base64 = bytesToBase64(bytes)
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
return base64;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get base64-encoded state parameter with current query params
|
||||
*/
|
||||
export const getStateParamForRedirect = (): string => {
|
||||
const params = getCurrentQueryParams();
|
||||
return encodeStateParam(params);
|
||||
};
|
||||
|
||||
// Backward compatibility alias
|
||||
export const getCurrentUtmParams = getCurrentQueryParams;
|
||||
Loading…
Reference in New Issue
Block a user