Compare commits
No commits in common. "develop" and "main" have entirely different histories.
3
.npmrc
3
.npmrc
@ -1,4 +1 @@
|
|||||||
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,6 +190,21 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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 -->
|
<!-- Klaviyo Metric -->
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const klaviyoKeys = {
|
const klaviyoKeys = {
|
||||||
|
|||||||
8239
package-lock.json
generated
8239
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,9 +12,7 @@
|
|||||||
"start:local": "vite --host --mode localhost",
|
"start:local": "vite --host --mode localhost",
|
||||||
"start:prod": "vite --host --mode production",
|
"start:prod": "vite --host --mode production",
|
||||||
"build:dev": "tsc && vite build --mode develop",
|
"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": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
@ -26,7 +24,6 @@
|
|||||||
"@reduxjs/toolkit": "^1.9.5",
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
"@smakss/react-scroll-direction": "^4.0.4",
|
"@smakss/react-scroll-direction": "^4.0.4",
|
||||||
"@unleash/proxy-client-react": "^4.5.2",
|
"@unleash/proxy-client-react": "^4.5.2",
|
||||||
"@wit-lab-llc/frontend-shared": "^1.0.9",
|
|
||||||
"apng-js": "^1.1.1",
|
"apng-js": "^1.1.1",
|
||||||
"core-js": "^3.37.1",
|
"core-js": "^3.37.1",
|
||||||
"framer-motion": "^11.0.8",
|
"framer-motion": "^11.0.8",
|
||||||
@ -42,7 +39,7 @@
|
|||||||
"react-ga4": "^2.1.0",
|
"react-ga4": "^2.1.0",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-i18next": "^12.3.1",
|
"react-i18next": "^12.3.1",
|
||||||
"react-pdf": "^10.2.0",
|
"react-pdf": "8.0.2",
|
||||||
"react-player": "^2.16.0",
|
"react-player": "^2.16.0",
|
||||||
"react-redux": "^8.0.5",
|
"react-redux": "^8.0.5",
|
||||||
"react-router-dom": "^6.11.2",
|
"react-router-dom": "^6.11.2",
|
||||||
@ -72,7 +69,7 @@
|
|||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"terser": "^5.43.1",
|
"terser": "^5.43.1",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"vite": "^7.3.0",
|
"vite": "^4.3.8",
|
||||||
"vite-plugin-svgr": "^4.2.0"
|
"vite-plugin-svgr": "^4.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { ICreateAuthorizeUser } from "./User";
|
|||||||
import { ELocalesPlacement } from "@/locales";
|
import { ELocalesPlacement } from "@/locales";
|
||||||
import { Currency } from "@/components/PaymentTable/Price";
|
import { Currency } from "@/components/PaymentTable/Price";
|
||||||
import { PeriodType } from "@/hooks/translations";
|
import { PeriodType } from "@/hooks/translations";
|
||||||
import type { SessionFingerprintData, SessionFacebookData } from "@wit-lab-llc/frontend-shared";
|
|
||||||
|
|
||||||
export interface PayloadCreate {
|
export interface PayloadCreate {
|
||||||
feature: string, // Type: string
|
feature: string, // Type: string
|
||||||
@ -15,13 +14,7 @@ export interface PayloadCreate {
|
|||||||
sign: boolean, // Type: boolean
|
sign: boolean, // Type: boolean
|
||||||
signDate: string | undefined, // Type: string, ISO Date
|
signDate: string | undefined, // Type: string, ISO Date
|
||||||
utm: IUTM, // Type: { [key: string]: string } - Optional
|
utm: IUTM, // Type: { [key: string]: string } - Optional
|
||||||
anonymousId?: string,
|
domain: string // Type: 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 {
|
export interface PayloadUpdate {
|
||||||
@ -29,10 +22,6 @@ export interface PayloadUpdate {
|
|||||||
data: {
|
data: {
|
||||||
feature: string, // Type: string
|
feature: string, // Type: string
|
||||||
|
|
||||||
anonymousId?: string,
|
|
||||||
query?: Record<string, string>,
|
|
||||||
lastActivityAt?: string,
|
|
||||||
|
|
||||||
profile?: Partial<ICreateAuthorizeUser>;
|
profile?: Partial<ICreateAuthorizeUser>;
|
||||||
partner?: Partial<Exclude<ICreateAuthorizeUser, "relationship_status">>;
|
partner?: Partial<Exclude<ICreateAuthorizeUser, "relationship_status">>;
|
||||||
answers?: Partial<IAnswersSessionPalmistry | IAnswersSessionChats | IAnswersSessionCompatibilityV2 | IAnswersSessionCompatibilityV3 | IAnswersSessionCompatibilityV4>;
|
answers?: Partial<IAnswersSessionPalmistry | IAnswersSessionChats | IAnswersSessionCompatibilityV2 | IAnswersSessionCompatibilityV3 | IAnswersSessionCompatibilityV4>;
|
||||||
@ -171,9 +160,7 @@ export interface ResponseGetLocale {
|
|||||||
export interface ResponseGetPixels {
|
export interface ResponseGetPixels {
|
||||||
status: "success" | string,
|
status: "success" | string,
|
||||||
data: {
|
data: {
|
||||||
facebook_pixel?: string[];
|
fb?: string[];
|
||||||
google_analytics?: string[];
|
|
||||||
yandex_metrica?: string[];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
import { useState, useEffect, useRef, useMemo, useLayoutEffect } from "react";
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useMemo,
|
||||||
|
useLayoutEffect
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Routes,
|
Routes,
|
||||||
Route,
|
Route,
|
||||||
@ -144,7 +150,7 @@ import { useSession } from "@/hooks/session/useSession";
|
|||||||
import { getSourceByPathname } from "@/utils/source.utils";
|
import { getSourceByPathname } from "@/utils/source.utils";
|
||||||
import { navigateToAuth } from "@/utils/auth-navigation";
|
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 ProfileRoutes from "@/routerComponents/Profile";
|
||||||
import RetainingFunnelRoutes from "@/routerComponents/RetainingFunnel";
|
import RetainingFunnelRoutes from "@/routerComponents/RetainingFunnel";
|
||||||
import { ELocalesPlacement } from "@/locales";
|
import { ELocalesPlacement } from "@/locales";
|
||||||
@ -178,16 +184,16 @@ function App(): JSX.Element {
|
|||||||
unleashClient.updateContext({
|
unleashClient.updateContext({
|
||||||
userId: user?.id || undefined,
|
userId: user?.id || undefined,
|
||||||
properties: {
|
properties: {
|
||||||
source,
|
source
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (session?.[source]) {
|
if (session?.[source]) {
|
||||||
unleashClient.updateContext({
|
unleashClient.updateContext({
|
||||||
sessionId: session?.[source] || undefined,
|
sessionId: session?.[source] || undefined,
|
||||||
properties: {
|
properties: {
|
||||||
source,
|
source
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [user, session, source, unleashClient]);
|
}, [user, session, source, unleashClient]);
|
||||||
@ -208,14 +214,10 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
if (isPageAvailable) {
|
if (isPageAvailable) {
|
||||||
const utm = parseQueryParams();
|
const utm = parseQueryParams();
|
||||||
console.log(
|
console.log('App component - parsed UTM on page:', location.pathname, utm);
|
||||||
"App component - parsed UTM on page:",
|
|
||||||
location.pathname,
|
|
||||||
utm
|
|
||||||
);
|
|
||||||
// Only update UTM if there are new parameters and they're not empty
|
// Only update UTM if there are new parameters and they're not empty
|
||||||
if (Object.keys(utm).length > 0) {
|
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));
|
dispatch(actions.utm.update(utm));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -291,12 +293,18 @@ function App(): JSX.Element {
|
|||||||
<CookieYesController isDelete={subscriptionStatus === "subscribed"} />
|
<CookieYesController isDelete={subscriptionStatus === "subscribed"} />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route path={`${profilePrefix}/*`} element={<ProfileRoutes />} />
|
<Route
|
||||||
|
path={`${profilePrefix}/*`}
|
||||||
|
element={<ProfileRoutes />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={`${retainingFunnelPrefix}/*`}
|
path={`${retainingFunnelPrefix}/*`}
|
||||||
element={<RetainingFunnelRoutes />}
|
element={<RetainingFunnelRoutes />}
|
||||||
/>
|
/>
|
||||||
<Route path={`${anonymousPrefix}/*`} element={<AnonymousRoutes />} />
|
<Route
|
||||||
|
path={`${anonymousPrefix}/*`}
|
||||||
|
element={<AnonymousRoutes />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={`${palmistryV1Prefix}/*`}
|
path={`${palmistryV1Prefix}/*`}
|
||||||
element={<PalmistryV1Routes />}
|
element={<PalmistryV1Routes />}
|
||||||
@ -313,10 +321,7 @@ function App(): JSX.Element {
|
|||||||
path={`${compatibilityV4Prefix}/*`}
|
path={`${compatibilityV4Prefix}/*`}
|
||||||
element={<CompatibilityV4Routes />}
|
element={<CompatibilityV4Routes />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route path={routes.client.auth()} element={<Auth redirectUrl={routes.client.trialPaymentV1()} />} />
|
||||||
path={routes.client.auth()}
|
|
||||||
element={<Auth redirectUrl={routes.client.trialPaymentV1()} />}
|
|
||||||
/>
|
|
||||||
<Route element={<AuthorizedUserOutlet />}>
|
<Route element={<AuthorizedUserOutlet />}>
|
||||||
<Route
|
<Route
|
||||||
path={`${palmistryV2Prefix}/*`}
|
path={`${palmistryV2Prefix}/*`}
|
||||||
@ -334,22 +339,9 @@ function App(): JSX.Element {
|
|||||||
<Route path={routes.client.skipTrial()} element={<SkipTrial />} />
|
<Route path={routes.client.skipTrial()} element={<SkipTrial />} />
|
||||||
<Route
|
<Route
|
||||||
path={routes.client.addConsultant()}
|
path={routes.client.addConsultant()}
|
||||||
element={
|
element={<AddConsultant funnel={ELocalesPlacement.V1} paymentPlacement="add_consultant" />}
|
||||||
<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>
|
||||||
</Route>
|
</Route>
|
||||||
{/* Additional Purchases Main End */}
|
{/* Additional Purchases Main End */}
|
||||||
@ -385,8 +377,14 @@ function App(): JSX.Element {
|
|||||||
element={<FreePeriodInfoPage />}
|
element={<FreePeriodInfoPage />}
|
||||||
/>
|
/>
|
||||||
<Route path={routes.client.feedback()} element={<FeedbackPage />} />
|
<Route path={routes.client.feedback()} element={<FeedbackPage />} />
|
||||||
<Route path={routes.client.birthtime()} element={<BirthtimePage />} />
|
<Route
|
||||||
<Route path={routes.client.createProfile()} element={<SkipStep />} />
|
path={routes.client.birthtime()}
|
||||||
|
element={<BirthtimePage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={routes.client.createProfile()}
|
||||||
|
element={<SkipStep />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={routes.client.emailEnter()}
|
path={routes.client.emailEnter()}
|
||||||
element={<EmailEnterPage />}
|
element={<EmailEnterPage />}
|
||||||
@ -400,7 +398,10 @@ function App(): JSX.Element {
|
|||||||
element={<AuthResultPage />}
|
element={<AuthResultPage />}
|
||||||
/>
|
/>
|
||||||
{/* <Route path={routes.client.static()} element={<StaticPage />} /> */}
|
{/* <Route path={routes.client.static()} element={<StaticPage />} /> */}
|
||||||
<Route path={routes.client.priceList()} element={<PriceListPage />} />
|
<Route
|
||||||
|
path={routes.client.priceList()}
|
||||||
|
element={<PriceListPage />}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
{/* <Route element={<AuthorizedUserOutlet />}>
|
{/* <Route element={<AuthorizedUserOutlet />}>
|
||||||
<Route
|
<Route
|
||||||
@ -418,7 +419,9 @@ function App(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route element={<PrivateSubscriptionOutlet />}>
|
<Route element={<PrivateSubscriptionOutlet />}>
|
||||||
<Route element={<Layout />}>
|
<Route
|
||||||
|
element={<Layout />}
|
||||||
|
>
|
||||||
<Route path={routes.client.home()} element={<HomePage />} />
|
<Route path={routes.client.home()} element={<HomePage />} />
|
||||||
<Route
|
<Route
|
||||||
path={routes.client.compatibility()}
|
path={routes.client.compatibility()}
|
||||||
@ -478,10 +481,7 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
{/* <Route path="*" element={<ABDesignV1Routes />} /> */}
|
{/* <Route path="*" element={<ABDesignV1Routes />} /> */}
|
||||||
|
|
||||||
<Route
|
<Route path="*" element={<Navigate to={getRouteBy(subscriptionStatus)} />} />
|
||||||
path="*"
|
|
||||||
element={<Navigate to={getRouteBy(subscriptionStatus)} />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* ROUTES OFF */}
|
{/* ROUTES OFF */}
|
||||||
|
|
||||||
@ -1055,13 +1055,13 @@ function Layout(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
{showHeader ? <Header openMenu={() => setIsMenuOpen(true)} /> : null}
|
{showHeader ? (
|
||||||
|
<Header
|
||||||
|
openMenu={() => setIsMenuOpen(true)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{isRouteFullDataModal && (
|
{isRouteFullDataModal && (
|
||||||
<Modal
|
<Modal open={isShowFullDataModal} isCloseButtonVisible={false} onClose={() => setIsShowFullDataModal(false)}>
|
||||||
open={isShowFullDataModal}
|
|
||||||
isCloseButtonVisible={false}
|
|
||||||
onClose={() => setIsShowFullDataModal(false)}
|
|
||||||
>
|
|
||||||
<FullDataModal onClose={onCloseFullDataModal} />
|
<FullDataModal onClose={onCloseFullDataModal} />
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
@ -1288,9 +1288,8 @@ function MainPage(): JSX.Element {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if we're navigating to auth and on witlab.app domain
|
// Check if we're navigating to auth and on witlab.app domain
|
||||||
if (route === routes.client.auth()) {
|
if (route === routes.client.auth()) {
|
||||||
const isWitlabDomain =
|
const isWitlabDomain = window.location.hostname === 'witlab.app' ||
|
||||||
window.location.hostname === "witlab.app" ||
|
window.location.hostname.endsWith('.witlab.app');
|
||||||
window.location.hostname.endsWith(".witlab.app");
|
|
||||||
|
|
||||||
// If we're on witlab.app domain, use server-side navigation
|
// If we're on witlab.app domain, use server-side navigation
|
||||||
if (isWitlabDomain) {
|
if (isWitlabDomain) {
|
||||||
|
|||||||
@ -86,7 +86,7 @@ function OutOfCreditsModal({
|
|||||||
fill: "#7ED8F8",
|
fill: "#7ED8F8",
|
||||||
fontSize: "30px",
|
fontSize: "30px",
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
dominantBaseline: "middle",
|
dominantBaseline: "no-change",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
maxValue={timerSeconds}
|
maxValue={timerSeconds}
|
||||||
|
|||||||
@ -2,11 +2,12 @@ import styles from "./styles.module.scss";
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { Document, DocumentProps, Page } from "react-pdf";
|
import { Document, DocumentProps, Page } from "react-pdf";
|
||||||
import Loader, { LoaderColor } from "../Loader";
|
import Loader, { LoaderColor } from "../Loader";
|
||||||
|
import { File } from "node_modules/react-pdf/dist/esm/shared/types";
|
||||||
import { Pagination } from "@mui/material";
|
import { Pagination } from "@mui/material";
|
||||||
|
|
||||||
interface IPDFViewerProps {
|
interface IPDFViewerProps {
|
||||||
width?: number;
|
width?: number;
|
||||||
file?: DocumentProps["file"];
|
file?: File;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pagesOfPaginationPageLength = 1;
|
const pagesOfPaginationPageLength = 1;
|
||||||
|
|||||||
@ -19,8 +19,6 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash";
|
import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash";
|
||||||
import { IFunnelPaymentVariant } from "@/api/resources/Session";
|
import { IFunnelPaymentVariant } from "@/api/resources/Session";
|
||||||
import { useFunnel } from "@/hooks/funnel/useFunnel";
|
import { useFunnel } from "@/hooks/funnel/useFunnel";
|
||||||
import { getSourceByPathname } from "@/utils/source.utils";
|
|
||||||
import { getStateParamForRedirect } from "@/services/url";
|
|
||||||
|
|
||||||
const environments = import.meta.env;
|
const environments = import.meta.env;
|
||||||
|
|
||||||
@ -199,26 +197,11 @@ function PaymentPage({
|
|||||||
","
|
","
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build base params
|
let redirectUrl = `${paymentUrl}?paywallId=${paywallId}&placementId=${placementId}&productId=${
|
||||||
const baseParams = `paywallId=${paywallId}&placementId=${placementId}&productId=${
|
|
||||||
activeProduct?.id
|
activeProduct?.id
|
||||||
}&jwtToken=${token}&price=${(
|
}&jwtToken=${token}&price=${(
|
||||||
(activeProduct?.trialPrice || 100) / 100
|
(activeProduct?.trialPrice || 100) / 100
|
||||||
).toFixed(2)}¤cy=${currency}`;
|
).toFixed(2)}¤cy=${currency}&${getTrackingCookiesForRedirect()}`;
|
||||||
|
|
||||||
// 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) {
|
if (fbPixels.length) {
|
||||||
redirectUrl += `&fb_pixels=${fbPixels}`;
|
redirectUrl += `&fb_pixels=${fbPixels}`;
|
||||||
|
|||||||
@ -3,33 +3,9 @@ import { PayloadUpdate, ResponseCreate } from "@/api/resources/Session";
|
|||||||
import { ESourceAuthorization } from "@/api/resources/User";
|
import { ESourceAuthorization } from "@/api/resources/User";
|
||||||
import { getClientTimezone, language } from "@/locales";
|
import { getClientTimezone, language } from "@/locales";
|
||||||
import { actions, selectors } from "@/store";
|
import { actions, selectors } from "@/store";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
import { useCallback, useMemo, useState } from "react"
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
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 = () => {
|
export const useSession = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -43,119 +19,16 @@ export const useSession = () => {
|
|||||||
const timezone = getClientTimezone();
|
const timezone = getClientTimezone();
|
||||||
|
|
||||||
const [isError, setIsError] = useState(false);
|
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 createSession = useCallback(async (source: ESourceAuthorization): Promise<ResponseCreate> => {
|
||||||
const nowMs = Date.now();
|
if (session[source]?.length) {
|
||||||
const existingVisit = readVisit(source);
|
localStorage.setItem(`${source}_sessionId`, session[source]);
|
||||||
if (existingVisit && nowMs - existingVisit.lastActivityAt <= visitTtlMs) {
|
return {
|
||||||
const nextQuery = parseQueryParams();
|
sessionId: session[source],
|
||||||
const nextAnonymousId = existingVisit.anonymousId ?? getOrCreateAnonymousId();
|
status: "old"
|
||||||
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 {
|
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 = {
|
const sessionParams = {
|
||||||
feature,
|
feature,
|
||||||
locale: language,
|
locale: language,
|
||||||
@ -164,13 +37,7 @@ export const useSession = () => {
|
|||||||
sign: checked,
|
sign: checked,
|
||||||
signDate: dateOfCheck.length ? dateOfCheck : undefined,
|
signDate: dateOfCheck.length ? dateOfCheck : undefined,
|
||||||
utm,
|
utm,
|
||||||
anonymousId: getOrCreateAnonymousId(),
|
domain: window.location.hostname
|
||||||
query: parseQueryParams(),
|
|
||||||
landingQuery: parseQueryParams(),
|
|
||||||
lastActivityAt: new Date().toISOString(),
|
|
||||||
domain: window.location.hostname,
|
|
||||||
fingerprint,
|
|
||||||
facebookData,
|
|
||||||
};
|
};
|
||||||
console.log('Creating session with parameters:', sessionParams);
|
console.log('Creating session with parameters:', sessionParams);
|
||||||
const sessionFromServer = await api.createSession(sessionParams);
|
const sessionFromServer = await api.createSession(sessionParams);
|
||||||
@ -181,15 +48,6 @@ export const useSession = () => {
|
|||||||
source
|
source
|
||||||
}));
|
}));
|
||||||
localStorage.setItem(`${source}_sessionId`, sessionFromServer.sessionId);
|
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
|
return sessionFromServer
|
||||||
}
|
}
|
||||||
console.error('Session creation failed - invalid response:', sessionFromServer);
|
console.error('Session creation failed - invalid response:', sessionFromServer);
|
||||||
@ -206,7 +64,7 @@ export const useSession = () => {
|
|||||||
sessionId: ""
|
sessionId: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [api, checked, dateOfCheck, dispatch, feature, session, timezone, utm, fingerprintCollector, facebookCollector, readVisit, visitTtlMs, writeVisit])
|
}, [api, checked, dateOfCheck, dispatch, feature, session, timezone, utm])
|
||||||
|
|
||||||
const updateSession = useCallback(async (data: Omit<PayloadUpdate["data"], "feature">, source: ESourceAuthorization, sessionId?: string) => {
|
const updateSession = useCallback(async (data: Omit<PayloadUpdate["data"], "feature">, source: ESourceAuthorization, sessionId?: string) => {
|
||||||
try {
|
try {
|
||||||
@ -215,39 +73,22 @@ export const useSession = () => {
|
|||||||
sessionId: _sessionId,
|
sessionId: _sessionId,
|
||||||
data: {
|
data: {
|
||||||
feature,
|
feature,
|
||||||
anonymousId: getOrCreateAnonymousId(),
|
|
||||||
query: parseQueryParams(),
|
|
||||||
lastActivityAt: new Date().toISOString(),
|
|
||||||
...data
|
...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;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
}, [api, feature, session, readVisit, writeVisit])
|
}, [api, feature, session])
|
||||||
|
|
||||||
const deleteSession = useCallback(async (source: ESourceAuthorization) => {
|
const deleteSession = useCallback(async (source: ESourceAuthorization) => {
|
||||||
localStorage.removeItem(`${source}_sessionId`);
|
localStorage.removeItem(`${source}_sessionId`);
|
||||||
localStorage.removeItem(getVisitStorageKey(source));
|
|
||||||
dispatch(actions.session.update({
|
dispatch(actions.session.update({
|
||||||
session: "",
|
session: "",
|
||||||
source
|
source
|
||||||
}))
|
}))
|
||||||
}, [dispatch, getVisitStorageKey])
|
}, [dispatch])
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
session,
|
session,
|
||||||
|
|||||||
11
src/init.tsx
11
src/init.tsx
@ -26,19 +26,12 @@ import { InitializationProvider } from "./initialization";
|
|||||||
import { getSourceByPathname } from "./utils/source.utils";
|
import { getSourceByPathname } from "./utils/source.utils";
|
||||||
import { parseQueryParams } from "./services/url";
|
import { parseQueryParams } from "./services/url";
|
||||||
import { actions } from "./store";
|
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`;
|
pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.js`;
|
||||||
|
|
||||||
const environments = import.meta.env;
|
const environments = import.meta.env;
|
||||||
|
|
||||||
const init = async () => {
|
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
|
// Parse UTM parameters from URL at initialization
|
||||||
const utm = parseQueryParams();
|
const utm = parseQueryParams();
|
||||||
console.log('UTM parameters parsed at init:', utm);
|
console.log('UTM parameters parsed at init:', utm);
|
||||||
@ -112,11 +105,11 @@ const init = async () => {
|
|||||||
locale: getDefaultLocaleByLanguage(language),
|
locale: getDefaultLocaleByLanguage(language),
|
||||||
});
|
});
|
||||||
|
|
||||||
localStorage.setItem('fb_pixels', JSON.stringify(pixels?.data?.facebook_pixel || []));
|
localStorage.setItem('fb_pixels', JSON.stringify(pixels?.data?.fb || []));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!!pixels?.data?.facebook_pixel?.length && <HeadData pixels={pixels.data.facebook_pixel} />}
|
{!!pixels?.data?.fb?.length && <HeadData pixels={pixels.data.fb} />}
|
||||||
<I18nextProvider i18n={i18nextInstance}>
|
<I18nextProvider i18n={i18nextInstance}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|||||||
@ -14,94 +14,3 @@ export const parseQueryParams = () => {
|
|||||||
|
|
||||||
return result;
|
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