"use client"; import type { TrialPaymentScreenDefinition, DefaultTexts, FunnelDefinition, } from "@/lib/funnel/types"; import { TemplateLayout } from "../layouts/TemplateLayout"; import { createTemplateLayoutProps } from "@/lib/funnel/templateHelpers"; import { cn } from "@/lib/utils"; import { useRef, useState } from "react"; import { Header, JoinedToday, TrustedByOver, JoinedTodayWithAvatars, } from "@/components/domains/TrialPayment"; import { UnlockYourSketch } from "@/components/domains/TrialPayment/Cards"; import { FindingOneGuide, TryForDays, TotalPrice, PaymentButtons, UsersPortraits, } from "@/components/domains/TrialPayment/Cards"; import { MoneyBackGuarantee, Policy } from "@/components/domains/TrialPayment"; import { StepsToSeeSoulmate, Reviews, CommonQuestions, StillHaveQuestions, Footer, } from "@/components/domains/TrialPayment/Cards"; import ProgressToSeeSoulmate from "@/components/domains/TrialPayment/ProgressToSeeSoulmate/ProgressToSeeSoulmate"; import { buildTypographyProps } from "@/lib/funnel/mappers"; import { usePaymentPlacement } from "@/hooks/payment/usePaymentPlacement"; import { Spinner } from "@/components/ui/spinner"; import { Currency } from "@/shared/types"; import { getFormattedPrice } from "@/shared/utils/price"; 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; screen: TrialPaymentScreenDefinition; onContinue: () => void; canGoBack: boolean; onBack: () => void; screenProgress?: { current: number; total: number }; defaultTexts?: DefaultTexts; } export function TrialPaymentTemplate({ funnel, screen, canGoBack, onBack, screenProgress, defaultTexts, }: TrialPaymentTemplateProps) { const token = useClientToken(); // TODO: выбрать корректный paymentId для этого экрана (ключ из backend), временно "main" const paymentId = "main"; const { placement, isLoading } = usePaymentPlacement({ funnel, paymentId }); const trialInterval = placement?.trialInterval || 7; const trialPeriod = placement?.trialPeriod; const variant = placement?.variants?.[0]; const productId = variant?.id || ""; const placementId = placement?.placementId || ""; const paywallId = placement?.paywallId || ""; const trialPrice = variant?.trialPrice || 0; const price = variant?.price || 0; const oldPrice = variant?.price || 0; const billingPeriod = placement?.billingPeriod; const billingInterval = placement?.billingInterval || 1; const currency = placement?.currency || Currency.USD; const paymentUrl = placement?.paymentUrl || ""; const [loadingButtonIndex, setLoadingButtonIndex] = useState(); const handlePayClick = (buttonIndex: number) => { if (!!loadingButtonIndex || loadingButtonIndex === 0) { return; } setLoadingButtonIndex(buttonIndex); const redirectUrl = `${paymentUrl}?paywallId=${paywallId}&placementId=${placementId}&productId=${productId}&jwtToken=${token}&price=${( (trialPrice || 100) / 100 ).toFixed(2)}¤cy=${currency}&${getTrackingCookiesForRedirect()}`; return window.location.replace(redirectUrl); }; const paymentSectionRef = useRef(null); const scrollToPayment = () => { if (paymentSectionRef.current) { paymentSectionRef.current.scrollIntoView({ behavior: "smooth", block: "end", // Policy text at bottom of screen }); } }; const formatPeriod = ( period: "DAY" | "WEEK" | "MONTH" | "YEAR" | undefined, interval: number ) => { if (!period) return `${interval} days`; const unit = period === "DAY" ? interval === 1 ? "day" : "days" : period === "WEEK" ? interval === 1 ? "week" : "weeks" : period === "MONTH" ? interval === 1 ? "month" : "months" : interval === 1 ? "year" : "years"; return `${interval} ${unit}`; }; const formattedTrialPrice = getFormattedPrice(trialPrice, currency); const formattedBillingPrice = getFormattedPrice(price, currency); const trialPeriodText = formatPeriod(trialPeriod, trialInterval); const billingPeriodText = formatPeriod(billingPeriod, billingInterval); const computeDiscountPercent = () => { if (!oldPrice || !trialPrice || oldPrice <= 0) return undefined; const ratio = 1 - trialPrice / oldPrice; const percent = Math.max(0, Math.min(100, Math.round(ratio * 100))); return String(percent); }; const formatPeriodHyphen = ( period: "DAY" | "WEEK" | "MONTH" | "YEAR" | undefined, interval: number ) => { if (!period) return `${interval}-day`; const unit = period === "DAY" ? interval === 1 ? "day" : "days" : period === "WEEK" ? interval === 1 ? "week" : "weeks" : period === "MONTH" ? interval === 1 ? "month" : "months" : interval === 1 ? "year" : "years"; return `${interval}-${unit}`; }; const trialPeriodHyphenText = formatPeriodHyphen(trialPeriod, trialInterval); const replacePlaceholders = (text: string | undefined) => { if (!text) return ""; const values: Record = { trialPrice: formattedTrialPrice, billingPrice: formattedBillingPrice, oldPrice: getFormattedPrice(oldPrice || 0, currency), discountPercent: computeDiscountPercent() ?? "", trialPeriod: trialPeriodText, billingPeriod: billingPeriodText, trialPeriodHyphen: trialPeriodHyphenText, }; let result = text; for (const [key, value] of Object.entries(values)) { result = result.replaceAll(`{{${key}}}`, value); } return result; }; // Отключаем общий Header в TemplateLayout для этого экрана const screenWithoutHeader = { ...screen, header: { ...(screen.header || {}), show: false, showBackButton: false, showProgress: false, }, } as typeof screen; // Убираем title/subtitle из общего Layout для этого экрана const screenForLayout = { ...screenWithoutHeader, title: undefined, subtitle: undefined, } as typeof screenWithoutHeader; const layoutProps = createTemplateLayoutProps( screenForLayout, { canGoBack, onBack }, screenProgress, { preset: "center", actionButton: screen.bottomActionButton?.show === false ? undefined : { defaultText: defaultTexts?.nextButton || "Continue", disabled: false, onClick: scrollToPayment, }, } ); if (isLoading || !placement || !token) { return (
); } return (
{/* Header block */} {screen.headerBlock && (
)} {/* UnlockYourSketch section */} {screen.unlockYourSketch && ( ) : undefined, }} button={ screen.unlockYourSketch.buttonText ? { children: screen.unlockYourSketch.buttonText, onClick: scrollToPayment, } : undefined } /> )} {screen.joinedToday && ( } /> )} {screen.trustedByOver && ( } /> )} {screen.findingOneGuide && ( ) : undefined, }} /> )} {screen.tryForDays && ( buildTypographyProps( { ...it, text: replacePlaceholders(it.text) }, { as: "li", defaults: { font: "inter", size: "sm" } } )! ), } : undefined } /> )} {screen.totalPrice && ( )} {screen.paymentButtons && (
{ const icon = b.icon === "pay" ? ( ) : b.icon === "google" ? ( ) : b.icon === "card" ? ( ) : undefined; const className = b.primary ? "bg-primary" : undefined; return { children: index === loadingButtonIndex ? ( ) : ( b.text ), icon: loadingButtonIndex === index ? undefined : icon, className, onClick: () => handlePayClick(index), }; })} />
)} {screen.moneyBackGuarantee && ( )} {screen.policy && (
By clicking Continue, you agree to our{" "} Terms of Use & Service {" "} and{" "} Privacy Policy . You also acknowledge that your 1-week introductory plan to Wit Lab LLC, billed at $1.00, will automatically renew at $14.99 every 1 week unless canceled before the end of the trial period.
)} {screen.usersPortraits && ( ({ src: img.src, alt: "user portrait", }))} button={ screen.usersPortraits.buttonText ? { children: screen.usersPortraits.buttonText, onClick: scrollToPayment, } : undefined } /> )} {screen.joinedTodayWithAvatars && ( ({ imageProps: { src: img.src, alt: "avatar" }, }) ), } : undefined } count={buildTypographyProps(screen.joinedTodayWithAvatars.count, { as: "span", defaults: { font: "inter", weight: "bold", size: "sm" }, })} text={buildTypographyProps(screen.joinedTodayWithAvatars.text, { as: "p", defaults: { font: "inter", weight: "semiBold", size: "sm" }, })} /> )} {screen.progressToSeeSoulmate && ( )} {screen.stepsToSeeSoulmate && ( ({ title: buildTypographyProps(s.title, { as: "h4", defaults: { font: "inter", weight: "semiBold", size: "sm" }, })!, description: buildTypographyProps(s.description, { as: "p", defaults: { font: "inter", size: "xs" }, })!, icon: s.icon === "questions" ? ( ) : s.icon === "profile" ? ( ) : s.icon === "sketch" ? ( ) : s.icon === "astro" ? ( ) : s.icon === "chat" ? ( ) : null, isActive: s.isActive ?? false, }))} button={ screen.stepsToSeeSoulmate.buttonText ? { children: screen.stepsToSeeSoulmate.buttonText, onClick: scrollToPayment, } : undefined } /> )} {screen.reviews && ( ({ name: buildTypographyProps(r.name, { as: "span", defaults: { font: "inter", weight: "semiBold", size: "sm" }, }), text: buildTypographyProps(r.text, { as: "p", defaults: { font: "inter", size: "sm" }, }), date: buildTypographyProps(r.date, { as: "span", defaults: { font: "inter", size: "xs" }, }), avatar: r.avatar ? { imageProps: { src: r.avatar.src, alt: "avatar" }, } : undefined, stars: r.rating ? { value: r.rating } : undefined, portrait: r.portrait ? { src: r.portrait.src, alt: "Portrait" } : undefined, photo: r.photo ? { src: r.photo.src, alt: "Photo" } : undefined, }))} /> )} {screen.commonQuestions && ( ({ value: `q-${index}`, trigger: { children: q.question }, content: { children: q.answer }, }))} accordionProps={{ defaultValue: "q-0", type: "single" }} /> )} {screen.stillHaveQuestions && ( { // Open contact link from footer config const contactUrl = screen.footer?.contacts?.email?.href; if (contactUrl) { window.open(contactUrl, "_blank", "noopener,noreferrer"); } }, } : undefined } /> )} {screen.footer && (
); }