"use client"; import { useEffect, useId, useRef, useState, type CSSProperties } from "react"; import { PAY_METHODS_URL, PAY_ORDER_URL, type StoreCatalogEntry, } from "@/lib/store-catalog"; export interface LockedCourse { id: string; slug: string; title: string; description: string | null; coverImage: string | null; } interface PayMethod { id: string; label: string; provider: string; kind: "online" | "manual"; } interface Props { course: LockedCourse; entry: StoreCatalogEntry; buyer: { name: string; email: string }; onClose: () => void; } function formatRub(v: number) { return `${v.toLocaleString("ru-RU")} ₽`; } const inputStyle: CSSProperties = { width: "100%", border: "2px solid var(--border)", background: "var(--background)", color: "var(--foreground)", padding: "0.5rem 0.6rem", fontSize: "14px", fontFamily: "inherit", outline: "none", }; export function StoreCheckoutModal({ course, entry, buyer, onClose }: Props) { const [selectedSku, setSelectedSku] = useState(entry.tariffs[0].sku); const [methods, setMethods] = useState(null); const [method, setMethod] = useState("robokassa"); const [otherOpen, setOtherOpen] = useState(false); const [submitting, setSubmitting] = useState(false); const dialogRef = useRef(null); const titleId = useId(); const multiTariff = entry.tariffs.length > 1; const currentMethod = methods?.find((m) => m.id === method); // Payment methods dropdown, same source as the landings. On any failure the // hidden method=robokassa keeps the checkout fully functional. useEffect(() => { fetch(PAY_METHODS_URL, { signal: AbortSignal.timeout(5000) }) .then((r) => (r.ok ? r.json() : Promise.reject(new Error("bad status")))) .then((data: { methods?: PayMethod[] }) => { const ms = data?.methods ?? []; if (ms.length >= 2) { setMethods(ms); setMethod((cur) => (cur === "robokassa" ? ms[0].id : cur)); } }) .catch(() => {}); }, []); // Close on Escape useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [onClose]); // Lock body scroll while the modal is open useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, []); // Focus the dialog on open, restore focus on close useEffect(() => { const prev = document.activeElement as HTMLElement | null; dialogRef.current?.focus(); return () => prev?.focus(); }, []); // Coming back from the payment page via bfcache must not leave the // submit button stuck in the "submitting" state. useEffect(() => { const onPageShow = (e: PageTransitionEvent) => { if (e.persisted) setSubmitting(false); }; window.addEventListener("pageshow", onPageShow); return () => window.removeEventListener("pageshow", onPageShow); }, []); // Minimal focus trap: keep Tab cycling inside the dialog function handleKeyDown(e: React.KeyboardEvent) { if (e.key !== "Tab" || !dialogRef.current) return; const focusables = dialogRef.current.querySelectorAll( 'button, [href], input, select, [tabindex]:not([tabindex="-1"])' ); if (focusables.length === 0) return; const first = focusables[0]; const last = focusables[focusables.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } } const labelClass = "block text-xs font-bold uppercase tracking-widest mb-1.5"; const primary = methods?.[0]; const rest = methods?.slice(1) ?? []; return (
{ if (e.target === e.currentTarget) onClose(); }} >
{/* Header */}

Покупка курса

{course.coverImage ? ( // eslint-disable-next-line @next/next/no-img-element ) : (
📚
)}

{course.title}

{course.description && (

{course.description}

)}
setSubmitting(true)}> {!multiTariff && } {/* Tariff */}
Тариф {multiTariff ? (
{entry.tariffs.map((t) => ( ))}
) : (

{formatRub(entry.tariffs[0].priceRub)}

)}
{/* Payment method — rendered only when the list actually loaded */} {methods && primary && rest.length > 0 && (
Способ оплаты
{otherOpen && ( )} {currentMethod?.kind === "manual" && (

Счёт выставим вручную — доступ откроется после подтверждения оплаты.

)}
)} {/* Promo code */}
(e.currentTarget.style.borderColor = "var(--foreground)")} onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")} />

Доступ придёт на {buyer.email}

{/* Footer */}
Подробнее о курсе на сайте →

Сумма на странице оплаты отобразится в тенге (₸). Доступ откроется автоматически после оплаты.

); }