Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aef1a14bbc | ||
|
|
cb56c45c2c | ||
|
|
3197fb4ca2 | ||
|
|
630fd2209d | ||
|
|
62a5002fa6 | ||
|
|
cfab5421af | ||
|
|
016241aec7 | ||
|
|
b4c4067b5e | ||
|
|
385c8315ba | ||
|
|
49c0b3121a | ||
|
|
05fdd07dd5 | ||
|
|
2e77f8ced7 | ||
|
|
e3dd721bea | ||
|
|
5b89cfd573 | ||
|
|
41f622125b | ||
|
|
8e21217f7c | ||
|
|
6ee2affd5a |
3
.npmrc
3
.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>
|
</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 = {
|
||||||
|
|||||||
8525
package-lock.json
generated
8525
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: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",
|
||||||
@ -24,6 +26,7 @@
|
|||||||
"@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",
|
||||||
@ -39,7 +42,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": "8.0.2",
|
"react-pdf": "^10.2.0",
|
||||||
"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",
|
||||||
@ -69,7 +72,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": "^4.3.8",
|
"vite": "^7.3.0",
|
||||||
"vite-plugin-svgr": "^4.2.0"
|
"vite-plugin-svgr": "^4.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ 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
|
||||||
@ -14,7 +15,13 @@ 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
|
||||||
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 {
|
export interface PayloadUpdate {
|
||||||
@ -22,6 +29,10 @@ 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>;
|
||||||
@ -160,7 +171,9 @@ export interface ResponseGetLocale {
|
|||||||
export interface ResponseGetPixels {
|
export interface ResponseGetPixels {
|
||||||
status: "success" | string,
|
status: "success" | string,
|
||||||
data: {
|
data: {
|
||||||
fb?: string[];
|
facebook_pixel?: string[];
|
||||||
|
google_analytics?: string[];
|
||||||
|
yandex_metrica?: string[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { useState, useEffect, useRef, useMemo, useLayoutEffect } from "react";
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useMemo,
|
|
||||||
useLayoutEffect
|
|
||||||
} from "react";
|
|
||||||
import {
|
import {
|
||||||
Routes,
|
Routes,
|
||||||
Route,
|
Route,
|
||||||
@ -150,7 +144,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";
|
||||||
@ -184,16 +178,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]);
|
||||||
@ -214,10 +208,14 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
if (isPageAvailable) {
|
if (isPageAvailable) {
|
||||||
const utm = parseQueryParams();
|
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
|
// 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -293,18 +291,12 @@ function App(): JSX.Element {
|
|||||||
<CookieYesController isDelete={subscriptionStatus === "subscribed"} />
|
<CookieYesController isDelete={subscriptionStatus === "subscribed"} />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route
|
<Route path={`${profilePrefix}/*`} element={<ProfileRoutes />} />
|
||||||
path={`${profilePrefix}/*`}
|
|
||||||
element={<ProfileRoutes />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path={`${retainingFunnelPrefix}/*`}
|
path={`${retainingFunnelPrefix}/*`}
|
||||||
element={<RetainingFunnelRoutes />}
|
element={<RetainingFunnelRoutes />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route path={`${anonymousPrefix}/*`} element={<AnonymousRoutes />} />
|
||||||
path={`${anonymousPrefix}/*`}
|
|
||||||
element={<AnonymousRoutes />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path={`${palmistryV1Prefix}/*`}
|
path={`${palmistryV1Prefix}/*`}
|
||||||
element={<PalmistryV1Routes />}
|
element={<PalmistryV1Routes />}
|
||||||
@ -321,7 +313,10 @@ function App(): JSX.Element {
|
|||||||
path={`${compatibilityV4Prefix}/*`}
|
path={`${compatibilityV4Prefix}/*`}
|
||||||
element={<CompatibilityV4Routes />}
|
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 element={<AuthorizedUserOutlet />}>
|
||||||
<Route
|
<Route
|
||||||
path={`${palmistryV2Prefix}/*`}
|
path={`${palmistryV2Prefix}/*`}
|
||||||
@ -339,9 +334,22 @@ 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={<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>
|
||||||
</Route>
|
</Route>
|
||||||
{/* Additional Purchases Main End */}
|
{/* Additional Purchases Main End */}
|
||||||
@ -377,14 +385,8 @@ function App(): JSX.Element {
|
|||||||
element={<FreePeriodInfoPage />}
|
element={<FreePeriodInfoPage />}
|
||||||
/>
|
/>
|
||||||
<Route path={routes.client.feedback()} element={<FeedbackPage />} />
|
<Route path={routes.client.feedback()} element={<FeedbackPage />} />
|
||||||
<Route
|
<Route path={routes.client.birthtime()} element={<BirthtimePage />} />
|
||||||
path={routes.client.birthtime()}
|
<Route path={routes.client.createProfile()} element={<SkipStep />} />
|
||||||
element={<BirthtimePage />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path={routes.client.createProfile()}
|
|
||||||
element={<SkipStep />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path={routes.client.emailEnter()}
|
path={routes.client.emailEnter()}
|
||||||
element={<EmailEnterPage />}
|
element={<EmailEnterPage />}
|
||||||
@ -398,10 +400,7 @@ function App(): JSX.Element {
|
|||||||
element={<AuthResultPage />}
|
element={<AuthResultPage />}
|
||||||
/>
|
/>
|
||||||
{/* <Route path={routes.client.static()} element={<StaticPage />} /> */}
|
{/* <Route path={routes.client.static()} element={<StaticPage />} /> */}
|
||||||
<Route
|
<Route path={routes.client.priceList()} element={<PriceListPage />} />
|
||||||
path={routes.client.priceList()}
|
|
||||||
element={<PriceListPage />}
|
|
||||||
/>
|
|
||||||
</Route>
|
</Route>
|
||||||
{/* <Route element={<AuthorizedUserOutlet />}>
|
{/* <Route element={<AuthorizedUserOutlet />}>
|
||||||
<Route
|
<Route
|
||||||
@ -419,9 +418,7 @@ function App(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route element={<PrivateSubscriptionOutlet />}>
|
<Route element={<PrivateSubscriptionOutlet />}>
|
||||||
<Route
|
<Route element={<Layout />}>
|
||||||
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()}
|
||||||
@ -481,7 +478,10 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
{/* <Route path="*" element={<ABDesignV1Routes />} /> */}
|
{/* <Route path="*" element={<ABDesignV1Routes />} /> */}
|
||||||
|
|
||||||
<Route path="*" element={<Navigate to={getRouteBy(subscriptionStatus)} />} />
|
<Route
|
||||||
|
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 ? (
|
{showHeader ? <Header openMenu={() => setIsMenuOpen(true)} /> : null}
|
||||||
<Header
|
|
||||||
openMenu={() => setIsMenuOpen(true)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{isRouteFullDataModal && (
|
{isRouteFullDataModal && (
|
||||||
<Modal open={isShowFullDataModal} isCloseButtonVisible={false} onClose={() => setIsShowFullDataModal(false)}>
|
<Modal
|
||||||
|
open={isShowFullDataModal}
|
||||||
|
isCloseButtonVisible={false}
|
||||||
|
onClose={() => setIsShowFullDataModal(false)}
|
||||||
|
>
|
||||||
<FullDataModal onClose={onCloseFullDataModal} />
|
<FullDataModal onClose={onCloseFullDataModal} />
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
@ -1288,8 +1288,9 @@ 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 = window.location.hostname === 'witlab.app' ||
|
const isWitlabDomain =
|
||||||
window.location.hostname.endsWith('.witlab.app');
|
window.location.hostname === "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: "no-change",
|
dominantBaseline: "middle",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
maxValue={timerSeconds}
|
maxValue={timerSeconds}
|
||||||
|
|||||||
@ -2,12 +2,11 @@ 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?: File;
|
file?: DocumentProps["file"];
|
||||||
}
|
}
|
||||||
|
|
||||||
const pagesOfPaginationPageLength = 1;
|
const pagesOfPaginationPageLength = 1;
|
||||||
|
|||||||
@ -19,6 +19,8 @@ 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;
|
||||||
|
|
||||||
@ -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
|
activeProduct?.id
|
||||||
}&jwtToken=${token}&price=${(
|
}&jwtToken=${token}&price=${(
|
||||||
(activeProduct?.trialPrice || 100) / 100
|
(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) {
|
if (fbPixels.length) {
|
||||||
redirectUrl += `&fb_pixels=${fbPixels}`;
|
redirectUrl += `&fb_pixels=${fbPixels}`;
|
||||||
|
|||||||
@ -3,9 +3,33 @@ 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, useMemo, useState } from "react"
|
import { useCallback, useEffect, useMemo, useRef, 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();
|
||||||
@ -19,16 +43,119 @@ 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> => {
|
||||||
if (session[source]?.length) {
|
const nowMs = Date.now();
|
||||||
localStorage.setItem(`${source}_sessionId`, session[source]);
|
const existingVisit = readVisit(source);
|
||||||
return {
|
if (existingVisit && nowMs - existingVisit.lastActivityAt <= visitTtlMs) {
|
||||||
sessionId: session[source],
|
const nextQuery = parseQueryParams();
|
||||||
status: "old"
|
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 {
|
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,
|
||||||
@ -37,7 +164,13 @@ export const useSession = () => {
|
|||||||
sign: checked,
|
sign: checked,
|
||||||
signDate: dateOfCheck.length ? dateOfCheck : undefined,
|
signDate: dateOfCheck.length ? dateOfCheck : undefined,
|
||||||
utm,
|
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);
|
console.log('Creating session with parameters:', sessionParams);
|
||||||
const sessionFromServer = await api.createSession(sessionParams);
|
const sessionFromServer = await api.createSession(sessionParams);
|
||||||
@ -48,6 +181,15 @@ 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);
|
||||||
@ -64,7 +206,7 @@ export const useSession = () => {
|
|||||||
sessionId: ""
|
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) => {
|
const updateSession = useCallback(async (data: Omit<PayloadUpdate["data"], "feature">, source: ESourceAuthorization, sessionId?: string) => {
|
||||||
try {
|
try {
|
||||||
@ -73,22 +215,39 @@ 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])
|
}, [api, feature, session, readVisit, writeVisit])
|
||||||
|
|
||||||
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])
|
}, [dispatch, getVisitStorageKey])
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
session,
|
session,
|
||||||
|
|||||||
11
src/init.tsx
11
src/init.tsx
@ -26,12 +26,19 @@ 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);
|
||||||
@ -105,11 +112,11 @@ const init = async () => {
|
|||||||
locale: getDefaultLocaleByLanguage(language),
|
locale: getDefaultLocaleByLanguage(language),
|
||||||
});
|
});
|
||||||
|
|
||||||
localStorage.setItem('fb_pixels', JSON.stringify(pixels?.data?.fb || []));
|
localStorage.setItem('fb_pixels', JSON.stringify(pixels?.data?.facebook_pixel || []));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<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}>
|
<I18nextProvider i18n={i18nextInstance}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|||||||
@ -14,3 +14,94 @@ 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