Merge pull request #29 from WIT-LAB-LLC/payment-edits

payment-edits
This commit is contained in:
pennyteenycat 2025-10-07 20:09:49 +02:00 committed by GitHub
commit 63fe674d83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 88 additions and 141 deletions

View File

@ -1,11 +0,0 @@
export default function PaymentLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<main className="p-4 pb-220 max-w-[560px] mx-auto relative min-h-dvh">
{children}
</main>
);
}

View File

@ -1,24 +0,0 @@
// import { getTranslations } from "next-intl/server";
import AnimatedInfoScreen from "@/components/widgets/AnimatedInfoScreen/AnimatedInfoScreen";
import LottieAnimation from "@/components/widgets/LottieAnimation/LottieAnimation";
import { ROUTES } from "@/shared/constants/client-routes";
import { ELottieKeys } from "@/shared/constants/lottie";
export default async function PaymentFailed() {
// const t = await getTranslations("Payment.Error");
return (
<AnimatedInfoScreen
lottieAnimation={
<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />
}
// title={t("title")}
title="Payment failed"
animationTime={0}
animationTexts={[]}
buttonText="Try again"
nextRoute={ROUTES.home()}
/>
);
}

View File

@ -1,31 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { createPaymentCheckout } from "@/entities/payment/api";
import { ROUTES } from "@/shared/constants/client-routes";
export async function GET(req: NextRequest) {
const productId = req.nextUrl.searchParams.get("productId");
const placementId = req.nextUrl.searchParams.get("placementId");
const paywallId = req.nextUrl.searchParams.get("paywallId");
const fbPixels = req.nextUrl.searchParams.get("fb_pixels");
const productPrice = req.nextUrl.searchParams.get("price");
const currency = req.nextUrl.searchParams.get("currency");
console.log("PRODUCT ID:", productId);
const data = await createPaymentCheckout({
productId: productId || "",
placementId: placementId || "",
paywallId: paywallId || "",
});
console.log("DATA:", data);
let redirectUrl: URL = new URL(data?.paymentUrl || "", req.nextUrl.origin);
if (!redirectUrl) {
redirectUrl = new URL(`${ROUTES.paymentFailed()}`, origin);
}
if (fbPixels) redirectUrl.searchParams.set("fb_pixels", fbPixels);
if (productPrice) redirectUrl.searchParams.set("price", productPrice);
if (currency) redirectUrl.searchParams.set("currency", currency);
return NextResponse.redirect(redirectUrl, { status: 307 });
}

View File

@ -1,31 +0,0 @@
"use client";
import { ActionButton } from "@/components/ui/ActionButton/ActionButton";
// import { useRouter } from "next/navigation";
// import { useTranslations } from "next-intl";
import Typography from "@/components/ui/Typography/Typography";
// import { ROUTES } from "@/shared/constants/client-routes";
// import styles from "./Button.module.scss";
export default function PaymentSuccessButton() {
// const t = useTranslations("Payment.Success");
// const router = useRouter();
const handleNext = () => {
// router.push(ROUTES.additionalPurchases());
};
return (
<ActionButton
onClick={handleNext}
className="fixed bottom-[calc(0dvh+64px)] left-1/2 -translate-x-1/2 opacity-0 pointer-events-none max-w-[400px] w-[calc(100dvw-32px)] [animation:fadeIn_0.5s_ease-in-out_forwards] [animation-delay:2s]"
>
<Typography color="primary" size="xl" weight="bold">
{/* {t("button")} */}
Done
</Typography>
</ActionButton>
);
}

View File

@ -1,24 +0,0 @@
// import { getTranslations } from "next-intl/server";
import AnimatedInfoScreen from "@/components/widgets/AnimatedInfoScreen/AnimatedInfoScreen";
import LottieAnimation from "@/components/widgets/LottieAnimation/LottieAnimation";
import PaymentSuccessButton from "./Button";
import { ELottieKeys } from "@/shared/constants/lottie";
export default async function PaymentSuccess() {
// const t = await getTranslations("Payment.Success");
return (
<>
<AnimatedInfoScreen
lottieAnimation={
<LottieAnimation loadKey={ELottieKeys.loaderCheckMark} />
}
// title={t("title")}
title="Payment successful"
/>
<PaymentSuccessButton />
</>
);
}

View File

@ -37,8 +37,39 @@ import { usePaymentPlacement } from "@/hooks/payment/usePaymentPlacement";
import { Spinner } from "@/components/ui/spinner";
import { Currency } from "@/shared/types";
import { getFormattedPrice } from "@/shared/utils/price";
import { useRouter } from "next/navigation";
import { ROUTES } from "@/shared/constants/client-routes";
import { useClientToken } from "@/hooks/auth/useClientToken";
function getTrackingCookiesForRedirect() {
const cookieObj = Object.fromEntries(
document.cookie.split("; ").map((c) => c.split("="))
);
const result = Object.entries(cookieObj).filter(([key]) => {
return (
[
"_fbc",
"_fbp",
"_ym_uid",
"_ym_d",
"_ym_isad",
"_ym_visorc",
"yandexuid",
"ymex",
].includes(key) ||
key.startsWith("_ga") ||
key.startsWith("_gid")
);
});
const queryString = result
.map(
([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
)
.join("&");
return queryString;
}
interface TrialPaymentTemplateProps {
funnel: FunnelDefinition;
@ -58,7 +89,7 @@ export function TrialPaymentTemplate({
screenProgress,
defaultTexts,
}: TrialPaymentTemplateProps) {
const router = useRouter();
const token = useClientToken();
// TODO: выбрать корректный paymentId для этого экрана (ключ из backend), временно "main"
const paymentId = "main";
@ -76,17 +107,15 @@ export function TrialPaymentTemplate({
const billingPeriod = placement?.billingPeriod;
const billingInterval = placement?.billingInterval || 1;
const currency = placement?.currency || Currency.USD;
console.log({ placement });
const paymentUrl = placement?.paymentUrl || "";
const handlePayClick = () => {
router.push(
ROUTES.payment({
productId,
placementId,
paywallId,
})
);
const redirectUrl = `${paymentUrl}?paywallId=${paywallId}&placementId=${placementId}&productId=${productId}&jwtToken=${token}&price=${(
(trialPrice || 100) / 100
).toFixed(2)}&currency=${currency}&${getTrackingCookiesForRedirect()}`;
console.log("redirectUrl", redirectUrl);
// return window.location.replace(redirectUrl);
};
const paymentSectionRef = useRef<HTMLDivElement | null>(null);
@ -215,7 +244,7 @@ export function TrialPaymentTemplate({
}
);
if (isLoading || !placement) {
if (isLoading || !placement || !token) {
return (
<div className="w-full min-h-dvh max-w-[560px] mx-auto flex items-center justify-center">
<Spinner className="size-8" />
@ -239,7 +268,7 @@ export function TrialPaymentTemplate({
{/* Header block */}
{screen.headerBlock && (
<Header
className="mt-3"
className="mt-3 sticky top-[18px] z-30"
text={buildTypographyProps(screen.headerBlock.text, {
as: "p",
defaults: { font: "inter", weight: "semiBold", size: "sm" },

View File

@ -29,8 +29,7 @@ const buttonVariants = cva(
},
active: {
true: "bg-gradient-to-r from-[#EBF5FF] to-[#DBEAFE] border-primary shadow-blue-glow-2 text-primary",
false:
"bg-background border-border shadow-black-glow text-black",
false: "bg-background border-border shadow-black-glow text-black",
},
},
defaultVariants: {
@ -64,6 +63,16 @@ function MainButton({
data-slot="main-button"
className={cn(buttonVariants({ cornerRadius, active, className }))}
{...props}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
if (disabled) return;
const targetEl = e.target as HTMLElement | null;
const isCheckboxTarget = targetEl?.closest('[data-slot="checkbox"]');
if (isCheckboxTarget) {
return;
}
e.preventDefault();
props.onClick?.(e);
}}
asChild
>
<Label
@ -81,7 +90,13 @@ function MainButton({
{...checkboxProps}
checked={active ?? false}
disabled={disabled}
onClick={(e) => e.stopPropagation()}
onClick={(e) => {
if (disabled) return;
e.stopPropagation();
props.onClick?.(
e as unknown as React.MouseEvent<HTMLButtonElement>
);
}}
/>
)}
</Label>

View File

@ -76,7 +76,7 @@ const BottomActionButton = forwardRef<HTMLDivElement, BottomActionButtonProps>(
<div
ref={innerRef}
className={cn(
"fixed bottom-0 left-1/2 -translate-x-1/2 w-full",
"fixed bottom-0 left-1/2 -translate-x-1/2 w-full z-10",
className
)}
{...props}

View File

@ -12,3 +12,8 @@ export const setAuthTokenToCookie = async (token: string): Promise<void> => {
maxAge: 60 * 60 * 24 * 365,
});
};
export const getAuthTokenFromCookie = async (): Promise<string | undefined> => {
const cookieStore = await cookies();
return cookieStore.get("accessToken")?.value;
};

View File

@ -0,0 +1,17 @@
"use client";
import { getAuthTokenFromCookie } from "@/entities/user/serverActions";
import { useEffect, useMemo, useState } from "react";
export const useClientToken = () => {
const [token, setToken] = useState<string | undefined>(undefined);
useEffect(() => {
(async () => {
const token = await getAuthTokenFromCookie();
setToken(token);
})();
}, []);
return useMemo(() => token, [token]);
};

View File

@ -100,8 +100,10 @@ class HttpClient {
accessToken = await getServerAccessToken();
} else {
try {
const { getClientAccessToken } = await import("../auth/token");
accessToken = getClientAccessToken();
const { getAuthTokenFromCookie } = await import(
"@/entities/user/serverActions"
);
accessToken = await getAuthTokenFromCookie();
} catch {
// ignore
}