/payment/success metrics
This commit is contained in:
gofnnp 2025-06-20 16:26:22 +04:00
parent b1c34bd5cd
commit a201f449b5
7 changed files with 282 additions and 18 deletions

View File

@ -6,7 +6,8 @@
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"build": "next build", "build": "next build",
"start": "next start -p 3001", "start": "next start -p 3001",
"lint": "next lint" "lint": "next lint",
"lint:fix": "next lint --fix"
}, },
"dependencies": { "dependencies": {
"@lottiefiles/dotlottie-react": "^0.14.1", "@lottiefiles/dotlottie-react": "^0.14.1",
@ -34,4 +35,4 @@
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5" "typescript": "^5"
} }
} }

View File

@ -0,0 +1,24 @@
(function (m, e, t, r, i, k, a) {
m[i] =
m[i] ||
function () {
(m[i].a = m[i].a || []).push(arguments);
};
m[i].l = 1 * new Date();
for (var j = 0; j < document.scripts.length; j++) {
if (document.scripts[j].src === r) {
return;
}
}
(k = e.createElement(t)),
(a = e.getElementsByTagName(t)[0]),
(k.async = 1),
(k.src = r),
a.parentNode.insertBefore(k, a);
})(
window,
document,
"script",
"https://cdn.jsdelivr.net/npm/yandex-metrica-watch/tag.js",
"ym"
);

View File

@ -0,0 +1,23 @@
.button {
position: fixed;
bottom: calc(0dvh + 64px);
left: 50%;
transform: translate(-50%, 0);
opacity: 0;
pointer-events: none;
max-width: 400px;
width: calc(100dvw - 32px);
animation: fadeIn 0.5s ease-in-out 2s forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
pointer-events: none;
}
to {
opacity: 1;
pointer-events: auto;
}
}

View File

@ -0,0 +1,185 @@
"use client";
import { useEffect, useState } from "react";
import Script from "next/script";
import { useTranslations } from "next-intl";
import { Button, Typography } from "@/components/ui";
import { ROUTES } from "@/shared/constants/client-routes";
import styles from "./Metrics.module.scss";
interface MetricsProps {
fbPixels: string[];
productPrice: string;
currency: string;
}
export default function Metrics({ fbPixels, productPrice, currency }: MetricsProps) {
const t = useTranslations("Payment.Success");
const [isButtonVisible, setIsButtonVisible] = useState(false);
const navigateToHome = () => {
window.location.href = ROUTES.home()
}
// Yandex Metrica
useEffect(() => {
const interval = setInterval(() => {
if (typeof window.ym === 'function' && typeof window.klaviyo === 'object' && typeof window.gtag === 'function') {
try {
window.gtag('event', 'PaymentSuccess')
window.klaviyo.push(['track', "PaymentSuccess"]);
window.ym(95799066, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true,
});
window.ym(95799066, 'reachGoal', "PaymentSuccess", {}, () => {
console.log("Запрос отправлен");
// deleteYm()
setIsButtonVisible(true);
})
} catch (e) {
console.error('YM error:', e)
} finally {
clearInterval(interval);
}
}
}, 200);
return () => clearInterval(interval)
}, []);
return <>
{/* Klaviyo */}
{/* <Script src="https://static.klaviyo.com/onsite/js/klaviyo.js?company_id=RM7w5r" /> */}
<Script id="klaviyo-script">
{`const script = document.createElement("script");
script.src = "https://static.klaviyo.com/onsite/js/klaviyo.js?company_id=RM7w5r";
script.type = "text/javascript";
script.async = "";
document.head.appendChild(script);`}
</Script>
<Script id="klaviyo-script-proxy">
{`
!(function () {
if (!window.klaviyo) {
window._klOnsite = window._klOnsite || [];
try {
window.klaviyo = new Proxy(
{},
{
get: function (n, i) {
return "push" === i
? function () {
var n;
(n = window._klOnsite).push.apply(n, arguments);
}
: function () {
for (
var n = arguments.length, o = new Array(n), w = 0;
w < n;
w++
)
o[w] = arguments[w];
var t =
"function" == typeof o[o.length - 1]
? o.pop()
: void 0,
e = new Promise(function (n) {
window._klOnsite.push(
[i].concat(o, [
function (i) {
t && t(i), n(i);
},
])
);
});
return e;
};
},
}
);
} catch (n) {
(window.klaviyo = window.klaviyo || []),
(window.klaviyo.push = function () {
var n;
(n = window._klOnsite).push.apply(n, arguments);
});
}
}
})();
`}
</Script>
{/* Yandex Metrica */}
<Script id="yandex-metrica-script">
{`(function (m, e, t, r, i, k, a) {
m[i] =
m[i] ||
function () {
(m[i].a = m[i].a || []).push(arguments);
};
m[i].l = 1 * new Date();
for (var j = 0; j < document.scripts.length; j++) {
if (document.scripts[j].src === r) {
return;
}
}
(k = e.createElement(t)),
(a = e.getElementsByTagName(t)[0]),
(k.async = 1),
(k.src = r),
a.parentNode.insertBefore(k, a);
})(
window,
document,
"script",
"https://cdn.jsdelivr.net/npm/yandex-metrica-watch/tag.js",
"ym"
);`}
</Script>
{/* Google Analytics */}
<Script id="google-analytics-script" async src="https://www.googletagmanager.com/gtag/js?id=G-4N17LL3BB5" />
<Script id="google-analytics-script-config">
{`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config','G-4N17LL3BB5');`}
</Script>
{/* Facebook Pixel */}
{fbPixels.map((pixel) => (
<Script id={`facebook-pixel-${pixel}`} key={pixel}>
{`!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '${pixel}');
fbq('track', 'PageView');
fbq('track', 'Purchase', { value: ${productPrice}, currency: "${currency}" });`}
</Script>
))}
{isButtonVisible &&
<Button onClick={navigateToHome} className={styles.button}>
<Typography color="white">
{t("button")}
</Typography>
</Button>
}
</>;
}

View File

@ -1,20 +1,34 @@
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { AnimatedInfoScreen, LottieAnimation } from "@/components/widgets"; import { AnimatedInfoScreen, LottieAnimation } from "@/components/widgets";
import { ROUTES } from "@/shared/constants/client-routes";
import { ELottieKeys } from "@/shared/constants/lottie"; import { ELottieKeys } from "@/shared/constants/lottie";
export default async function PaymentSuccess() { import Metrics from "./Metrics";
export default async function PaymentSuccess({ searchParams }: {
searchParams: Promise<{
[key: string]: string | undefined
}>;
}) {
const params = await searchParams;
const fbPixels = params?.fb_pixels?.split(",") || [];
const productPrice = params?.price || "0";
const currency = params?.currency || "USD";
const t = await getTranslations("Payment.Success"); const t = await getTranslations("Payment.Success");
return ( return (
<AnimatedInfoScreen <>
lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />} <AnimatedInfoScreen
title={t("title")} lottieAnimation={<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />}
animationTime={0} title={t("title")}
animationTexts={[]} />
buttonText={t("button")} <Metrics
nextRoute={ROUTES.home()} fbPixels={fbPixels}
/> productPrice={productPrice}
currency={currency}
/>
</>
); );
} }

View File

@ -8,10 +8,10 @@ import styles from "./AnimatedInfoScreen.module.scss";
interface AnimatedInfoScreenProps { interface AnimatedInfoScreenProps {
lottieAnimation: React.ReactNode; lottieAnimation: React.ReactNode;
title: string; title: string;
animationTime: number; animationTime?: number;
animationTexts?: string[]; animationTexts?: string[];
buttonText: string; buttonText?: string;
nextRoute: string; nextRoute?: string;
} }
export default async function AnimatedInfoScreen({ export default async function AnimatedInfoScreen({
@ -29,15 +29,15 @@ export default async function AnimatedInfoScreen({
<Typography as="h1" weight="bold" className={styles.title}> <Typography as="h1" weight="bold" className={styles.title}>
{title} {title}
</Typography> </Typography>
{!!animationTexts?.length && <GPTAnimationText {!!animationTexts?.length && animationTime && <GPTAnimationText
points={animationTexts} points={animationTexts}
totalAnimationTime={animationTime} totalAnimationTime={animationTime}
/>} />}
<Link className={styles.link} style={{ animationDelay: `${animationTime}ms` }} href={nextRoute}> {nextRoute && buttonText && <Link className={styles.link} style={{ animationDelay: `${animationTime}ms` }} href={nextRoute}>
<RetainingButton className={styles.button} active={true}> <RetainingButton className={styles.button} active={true}>
{buttonText} {buttonText}
</RetainingButton> </RetainingButton>
</Link> </Link>}
</div> </div>
) )
} }

View File

@ -20,4 +20,21 @@ export enum ERetainingFunnel {
Green = "green", Green = "green",
Purple = "purple", Purple = "purple",
Stay50 = "stay50", Stay50 = "stay50",
}
declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ym: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ymab: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
klaviyo: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fbq: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
CollectJS: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
gtag: any;
}
} }