Compare commits

...

17 Commits

Author SHA1 Message Date
gofnnp
aef1a14bbc Merge branch 'main' of gitlab.com:witapp/aura-webapp into develop 2025-12-26 22:23:17 +04:00
gofnnp
cb56c45c2c develop
update version of packages
2025-12-26 22:18:03 +04:00
dev.daminik00
3197fb4ca2 update lib 2025-12-26 13:40:29 +03:00
dev.daminik00
630fd2209d update lib 2025-12-26 13:15:13 +03:00
Daniil Chemerkin
62a5002fa6 Merge branch 'develop' into 'main'
Develop

See merge request witapp/aura-webapp!839
2025-12-24 19:13:10 +00:00
Daniil Chemerkin
cfab5421af Develop 2025-12-24 19:13:10 +00:00
dev.daminik00
016241aec7 fix utm unicode 2025-12-24 22:02:12 +03:00
dev.daminik00
b4c4067b5e utm 2025-12-24 21:42:47 +03:00
Daniil Chemerkin
385c8315ba Merge branch 'develop' into 'main'
Develop

See merge request witapp/aura-webapp!838
2025-12-24 00:29:54 +00:00
Daniil Chemerkin
49c0b3121a Develop 2025-12-24 00:29:54 +00:00
dev.daminik00
05fdd07dd5 push 2025-12-24 03:13:11 +03:00
dev.daminik00
2e77f8ced7 push 2025-12-24 03:00:42 +03:00
dev.daminik00
e3dd721bea push 2025-12-24 02:45:32 +03:00
dev.daminik00
5b89cfd573 push 2025-12-24 02:06:31 +03:00
dev.daminik00
41f622125b fb update 2025-12-15 01:28:28 +03:00
dev.daminik00
8e21217f7c email error 2025-12-07 04:14:58 +03:00
dev.daminik00
6ee2affd5a add email error 2025-12-07 02:27:34 +03:00
12 changed files with 3113 additions and 5878 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}

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 = {

8527
package-lock.json generated

File diff suppressed because it is too large Load Diff

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.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"
}
}

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

@ -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} />;
}

View File

@ -86,7 +86,7 @@ function OutOfCreditsModal({
fill: "#7ED8F8",
fontSize: "30px",
fontWeight: "bold",
dominantBaseline: "no-change",
dominantBaseline: "middle",
},
}}
maxValue={timerSeconds}

View File

@ -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;

View File

@ -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)}&currency=${currency}&${getTrackingCookiesForRedirect()}`;
).toFixed(2)}&currency=${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}`;

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>

View File

@ -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;