payment
add payment
This commit is contained in:
parent
19039ead3a
commit
21bedbcc53
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<HTMLDivElement | null>(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<string, string> = {
|
||||
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 (
|
||||
<div className="w-full min-h-dvh max-w-[560px] mx-auto flex items-center justify-center">
|
||||
<Spinner className="size-8" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TemplateLayout
|
||||
{...layoutProps}
|
||||
@ -347,20 +444,26 @@ export function TrialPaymentTemplate({
|
||||
{screen.tryForDays && (
|
||||
<TryForDays
|
||||
className="mt-[46px]"
|
||||
title={buildTypographyProps(screen.tryForDays.title, {
|
||||
as: "h3",
|
||||
defaults: { font: "inter", weight: "bold", size: "xl" },
|
||||
})}
|
||||
title={buildTypographyProps(
|
||||
{
|
||||
...screen.tryForDays.title,
|
||||
text: replacePlaceholders(screen.tryForDays.title?.text),
|
||||
},
|
||||
{
|
||||
as: "h3",
|
||||
defaults: { font: "inter", weight: "bold", size: "xl" },
|
||||
}
|
||||
)}
|
||||
textListProps={
|
||||
screen.tryForDays.textList
|
||||
? {
|
||||
listStyleType: "none",
|
||||
items: screen.tryForDays.textList.items.map(
|
||||
(it) =>
|
||||
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 && (
|
||||
<Policy
|
||||
className="mt-4"
|
||||
text={buildTypographyProps(screen.policy.text, {
|
||||
as: "p",
|
||||
defaults: { font: "inter", size: "xs" },
|
||||
})}
|
||||
text={buildTypographyProps(
|
||||
screen.policy.text
|
||||
? {
|
||||
...screen.policy.text,
|
||||
text: replacePlaceholders(screen.policy.text.text),
|
||||
}
|
||||
: undefined,
|
||||
{ as: "p", defaults: { font: "inter", size: "xs" } }
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
"use server";
|
||||
|
||||
import { http } from "@/shared/api/httpClient";
|
||||
import { API_ROUTES } from "@/shared/constants/api-routes";
|
||||
|
||||
|
||||
0
src/entities/session/funnel/serverActions.ts
Normal file
0
src/entities/session/funnel/serverActions.ts
Normal file
@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -106,6 +106,7 @@ class HttpClient {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`);
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
|
||||
@ -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=")
|
||||
);
|
||||
|
||||
|
||||
8
src/shared/constants/currency.ts
Normal file
8
src/shared/constants/currency.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Currency } from "@/shared/types";
|
||||
|
||||
export const symbolByCurrency: Record<string, string> = {
|
||||
[Currency.USD]: "$",
|
||||
[Currency.EUR]: "€",
|
||||
[Currency.USD.toLowerCase()]: "$",
|
||||
[Currency.EUR.toLowerCase()]: "€",
|
||||
};
|
||||
18
src/shared/utils/price.ts
Normal file
18
src/shared/utils/price.ts
Normal file
@ -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);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user