diff --git a/src/app/(payment)/payment/route.ts b/src/app/(payment)/payment/route.ts index a1dde6d..69ae3d2 100644 --- a/src/app/(payment)/payment/route.ts +++ b/src/app/(payment)/payment/route.ts @@ -11,11 +11,13 @@ export async function GET(req: NextRequest) { 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) { diff --git a/src/components/funnel/FunnelRuntime.tsx b/src/components/funnel/FunnelRuntime.tsx index ee77706..2005fc8 100644 --- a/src/components/funnel/FunnelRuntime.tsx +++ b/src/components/funnel/FunnelRuntime.tsx @@ -31,7 +31,11 @@ function estimatePathLength( const currentScreen = funnel.screens.find((s) => s.id === currentScreenId); if (!currentScreen) break; - const resolvedScreen = resolveScreenVariant(currentScreen, answers, funnel.screens); + const resolvedScreen = resolveScreenVariant( + currentScreen, + answers, + funnel.screens + ); const nextScreenId = resolveNextScreenId( resolvedScreen, answers, @@ -86,13 +90,6 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) { createSession(); }, [createSession]); - // useEffect(() => { - // // updateSession({ - // // answers: answers, - // // }); - // console.log("answers", answers); - // }, [answers]); - useEffect(() => { registerScreen(currentScreen.id); }, [currentScreen.id, registerScreen]); @@ -130,10 +127,7 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) { }; const handleContinue = () => { - console.log({ - [currentScreen.id]: answers[currentScreen.id], - }); - if (answers[currentScreen.id]) { + if (answers[currentScreen.id] && currentScreen.template !== "email") { updateSession({ answers: { [currentScreen.id]: answers[currentScreen.id], @@ -223,9 +217,6 @@ export function FunnelRuntime({ funnel, initialScreenId }: FunnelRuntimeProps) { // Auto-advance for single selection without action button if (shouldAutoAdvance) { - console.log({ - [currentScreen.id]: ids, - }); updateSession({ answers: { [currentScreen.id]: ids, diff --git a/src/components/funnel/templates/EmailTemplate/EmailTemplate.tsx b/src/components/funnel/templates/EmailTemplate/EmailTemplate.tsx index 416c6e6..73bee33 100644 --- a/src/components/funnel/templates/EmailTemplate/EmailTemplate.tsx +++ b/src/components/funnel/templates/EmailTemplate/EmailTemplate.tsx @@ -78,8 +78,10 @@ export function EmailTemplate({ } try { - await authorization(email); - onContinue(); + const token = await authorization(email); + if (token) { + onContinue(); + } } catch (err) { console.error("Authorization failed:", err); } diff --git a/src/components/funnel/templates/TrialPaymentTemplate/TrialPaymentTemplate.tsx b/src/components/funnel/templates/TrialPaymentTemplate/TrialPaymentTemplate.tsx index cab26ad..057884f 100644 --- a/src/components/funnel/templates/TrialPaymentTemplate/TrialPaymentTemplate.tsx +++ b/src/components/funnel/templates/TrialPaymentTemplate/TrialPaymentTemplate.tsx @@ -33,15 +33,16 @@ import { } 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 { useRouter } from "next/navigation"; -// import { ROUTES } from "@/shared/constants/client-routes"; +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"; interface TrialPaymentTemplateProps { funnel: FunnelDefinition; screen: TrialPaymentScreenDefinition; - // selectedEmail: string; - // onEmailChange: (email: string) => void; onContinue: () => void; canGoBack: boolean; onBack: () => void; @@ -50,36 +51,44 @@ interface TrialPaymentTemplateProps { } export function TrialPaymentTemplate({ -// funnel, + funnel, screen, - // selectedEmail, - // onEmailChange, -// onContinue, canGoBack, onBack, screenProgress, defaultTexts, }: TrialPaymentTemplateProps) { -// const router = useRouter(); - // TODO: выбрать корректный paymentId для этого экрана (ключ из backend), временно "main_secret_discount" -// const paymentId = "main_secret_discount"; -// const { placement } = usePaymentPlacement({ funnel, paymentId }); + const router = useRouter(); + + // 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; + + console.log({ placement }); const handlePayClick = () => { - // const productId = - // placement?.variants?.[0]?.id || placement?.variants?.[0]?.key || ""; - // const placementId = placement?.placementId || ""; - // const paywallId = placement?.paywallId || ""; - // if (productId && placementId && paywallId) { - // router.push( - // ROUTES.payment({ - // productId, - // placementId, - // paywallId, - // }) - // ); - // } + router.push( + ROUTES.payment({ + productId, + placementId, + paywallId, + }) + ); }; + const paymentSectionRef = useRef(null); const scrollToPayment = () => { @@ -91,6 +100,86 @@ export function TrialPaymentTemplate({ } }; + 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, @@ -126,6 +215,14 @@ export function TrialPaymentTemplate({ } ); + if (isLoading || !placement) { + return ( +
+ +
+ ); + } + return ( - buildTypographyProps(it, { - as: "li", - defaults: { font: "inter", size: "sm" }, - })! + buildTypographyProps( + { ...it, text: replacePlaceholders(it.text) }, + { as: "li", defaults: { font: "inter", size: "sm" } } + )! ), } : undefined @@ -397,21 +500,36 @@ export function TrialPaymentTemplate({ } ), price: buildTypographyProps( - screen.totalPrice.priceContainer.price, + { + ...screen.totalPrice.priceContainer.price, + text: replacePlaceholders( + screen.totalPrice.priceContainer.price?.text + ), + }, { as: "span", defaults: { font: "inter", weight: "black" }, } ), oldPrice: buildTypographyProps( - screen.totalPrice.priceContainer.oldPrice, + { + ...screen.totalPrice.priceContainer.oldPrice, + text: replacePlaceholders( + screen.totalPrice.priceContainer.oldPrice?.text + ), + }, { as: "span", defaults: { font: "inter" }, } ), discount: buildTypographyProps( - screen.totalPrice.priceContainer.discount, + { + ...screen.totalPrice.priceContainer.discount, + text: replacePlaceholders( + screen.totalPrice.priceContainer.discount?.text + ), + }, { as: "span", defaults: { font: "inter", weight: "bold", size: "sm" }, @@ -530,10 +648,15 @@ export function TrialPaymentTemplate({ {screen.policy && ( )} diff --git a/src/entities/session/funnel/api.ts b/src/entities/session/funnel/api.ts index 1f75d41..7bf1758 100644 --- a/src/entities/session/funnel/api.ts +++ b/src/entities/session/funnel/api.ts @@ -1,3 +1,5 @@ +"use server"; + import { http } from "@/shared/api/httpClient"; import { API_ROUTES } from "@/shared/constants/api-routes"; diff --git a/src/entities/session/funnel/serverActions.ts b/src/entities/session/funnel/serverActions.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/entities/user/types.ts b/src/entities/user/types.ts index 05a2c2a..0591c47 100644 --- a/src/entities/user/types.ts +++ b/src/entities/user/types.ts @@ -39,7 +39,7 @@ export const CreateAuthorizeResponseSchema = z.object({ token: z.string(), userId: z.string().optional(), generatingVideo: z.boolean().optional(), - videoId: z.string().optional(), + videoId: z.string().nullable().optional(), authCode: z.string().optional(), }); diff --git a/src/hooks/auth/useAuth.ts b/src/hooks/auth/useAuth.ts index 9616e06..4945b4d 100644 --- a/src/hooks/auth/useAuth.ts +++ b/src/hooks/auth/useAuth.ts @@ -39,8 +39,8 @@ export const useAuth = ({ funnelId }: IUseAuthProps) => { timezone, locale, email, - // source: funnelId, - source: "aura.compatibility.v2", + source: funnelId, + // source: "aura.compatibility.v2", // profile: { // name: username || "", // gender: EGender[gender as keyof typeof EGender] || null, @@ -62,20 +62,7 @@ export const useAuth = ({ funnelId }: IUseAuthProps) => { // feature: feature.includes("black") ? "ios" : feature, }); }, - [ - // birthPlace, - // birthdate, - // gender, - // locale, - // partnerBirthPlace, - // partnerBirthdate, - // partnerGender, - // partnerName, - // username, - // birthtime, - // checked, - // dateOfCheck, - ] + [funnelId] ); const authorization = useCallback( @@ -106,6 +93,7 @@ export const useAuth = ({ funnelId }: IUseAuthProps) => { // authCode, } = await createAuthorization(payload); await setAuthTokenToCookie(token); + return token; // const { user: userMe } = await api.getMe({ token }); // const userId = userIdFromApi || userMe?._id; // if (userId?.length) { diff --git a/src/hooks/payment/usePaymentPlacement.ts b/src/hooks/payment/usePaymentPlacement.ts index 76c6553..eec6d58 100644 --- a/src/hooks/payment/usePaymentPlacement.ts +++ b/src/hooks/payment/usePaymentPlacement.ts @@ -51,7 +51,8 @@ export function usePaymentPlacement({ setPlacement(normalized); } catch (e) { if (!isMounted) return; - const message = e instanceof Error ? e.message : "Failed to load payment placement"; + const message = + e instanceof Error ? e.message : "Failed to load payment placement"; setError(message); } finally { if (isMounted) setIsLoading(false); @@ -65,5 +66,3 @@ export function usePaymentPlacement({ return { placement, isLoading, error }; } - - diff --git a/src/shared/api/httpClient.ts b/src/shared/api/httpClient.ts index 46f4741..d0f3482 100644 --- a/src/shared/api/httpClient.ts +++ b/src/shared/api/httpClient.ts @@ -106,6 +106,7 @@ class HttpClient { // ignore } } + if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`); headers.set("Content-Type", "application/json"); diff --git a/src/shared/auth/token.ts b/src/shared/auth/token.ts index 10b28e4..cef0e20 100644 --- a/src/shared/auth/token.ts +++ b/src/shared/auth/token.ts @@ -9,7 +9,7 @@ export function getClientAccessToken(): string | undefined { if (typeof window === "undefined") return undefined; const cookies = document.cookie.split(";"); - const accessTokenCookie = cookies.find(cookie => + const accessTokenCookie = cookies.find((cookie) => cookie.trim().startsWith("accessToken=") ); diff --git a/src/shared/constants/currency.ts b/src/shared/constants/currency.ts new file mode 100644 index 0000000..5f7bcd9 --- /dev/null +++ b/src/shared/constants/currency.ts @@ -0,0 +1,8 @@ +import { Currency } from "@/shared/types"; + +export const symbolByCurrency: Record = { + [Currency.USD]: "$", + [Currency.EUR]: "€", + [Currency.USD.toLowerCase()]: "$", + [Currency.EUR.toLowerCase()]: "€", +}; diff --git a/src/shared/utils/price.ts b/src/shared/utils/price.ts new file mode 100644 index 0000000..fc5f5c7 --- /dev/null +++ b/src/shared/utils/price.ts @@ -0,0 +1,18 @@ +import { Currency } from "@/shared/types"; +import { symbolByCurrency } from "../constants/currency"; + +const addCurrency = (price: number | string, currency: Currency) => { + const symbol = symbolByCurrency[currency]; + if ([Currency.EUR].includes(currency)) { + return `${price} ${symbol}`; + } + return `${symbol}${price}`; +}; + +export const getFormattedPrice = ( + price: number, + currency: Currency, + precision = 2 +) => { + return addCurrency((price / 100).toFixed(precision), currency); +};