302 lines
9.9 KiB
TypeScript
302 lines
9.9 KiB
TypeScript
"use client";
|
|
|
|
import Typography from "@/components/ui/Typography/Typography";
|
|
|
|
import { buildTypographyProps } from "@/lib/funnel/mappers";
|
|
import type {
|
|
DefaultTexts,
|
|
FunnelDefinition,
|
|
SpecialOfferScreenDefinition,
|
|
} from "@/lib/funnel/types";
|
|
import { TemplateLayout } from "../layouts/TemplateLayout";
|
|
import { createTemplateLayoutProps } from "@/lib/funnel/templateHelpers";
|
|
import { usePaymentPlacement } from "@/hooks/payment/usePaymentPlacement";
|
|
import { Spinner } from "@/components/ui/spinner";
|
|
import { Currency } from "@/shared/types";
|
|
import { useClientToken } from "@/hooks/auth/useClientToken";
|
|
import { getFormattedPrice } from "@/shared/utils/price";
|
|
import { formatPeriod, formatPeriodHyphen } from "@/shared/utils/period";
|
|
import { useState } from "react";
|
|
import { getTrackingCookiesForRedirect } from "@/shared/utils/cookies";
|
|
|
|
interface SpecialOfferProps {
|
|
funnel: FunnelDefinition;
|
|
screen: SpecialOfferScreenDefinition;
|
|
onContinue: () => void;
|
|
canGoBack: boolean;
|
|
onBack: () => void;
|
|
screenProgress?: { current: number; total: number };
|
|
defaultTexts?: DefaultTexts;
|
|
}
|
|
|
|
export function SpecialOfferTemplate({
|
|
funnel,
|
|
screen,
|
|
canGoBack,
|
|
onBack,
|
|
screenProgress,
|
|
defaultTexts,
|
|
}: SpecialOfferProps) {
|
|
const token = useClientToken();
|
|
const paymentId = "main_secret_discount";
|
|
const { placement, isLoading } = usePaymentPlacement({ funnel, paymentId });
|
|
const [isLoadingRedirect, setIsLoadingRedirect] = useState(false);
|
|
|
|
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 formattedTrialPrice = getFormattedPrice(trialPrice, currency);
|
|
const formattedBillingPrice = getFormattedPrice(price, currency);
|
|
const trialPeriodText = formatPeriod(trialPeriod, trialInterval);
|
|
const billingPeriodText = formatPeriod(billingPeriod, billingInterval);
|
|
const trialPeriodHyphenText = formatPeriodHyphen(trialPeriod, trialInterval);
|
|
const oldTrialPeriodText = formatPeriod(trialPeriod, 7);
|
|
|
|
const handlePayClick = () => {
|
|
if (isLoadingRedirect) {
|
|
return;
|
|
}
|
|
setIsLoadingRedirect(true);
|
|
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 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 replacePlaceholders = (text: string | undefined) => {
|
|
if (!text) return "";
|
|
const values: Record<string, string> = {
|
|
trialPrice: formattedTrialPrice,
|
|
billingPrice: formattedBillingPrice,
|
|
oldTrialPrice: getFormattedPrice(oldPrice || 0, currency),
|
|
discountPercent: computeDiscountPercent() ?? "",
|
|
trialPeriod: trialPeriodText,
|
|
billingPeriod: billingPeriodText,
|
|
trialPeriodHyphen: trialPeriodHyphenText,
|
|
oldTrialPeriod: oldTrialPeriodText,
|
|
};
|
|
let result = text;
|
|
for (const [key, value] of Object.entries(values)) {
|
|
result = result.replaceAll(`{{${key}}}`, value);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
const textProps = {
|
|
title: buildTypographyProps<"h2">(
|
|
{
|
|
...screen.text.title,
|
|
text: replacePlaceholders(screen.text.title?.text),
|
|
},
|
|
{
|
|
as: "h2",
|
|
defaults: {
|
|
font: "inter",
|
|
weight: "bold",
|
|
size: "xl",
|
|
align: "center",
|
|
className: "mt-2.5 text-[#FF0707] leading-7",
|
|
},
|
|
}
|
|
),
|
|
subtitle: buildTypographyProps<"h3">(
|
|
{
|
|
...screen.text.subtitle,
|
|
text: replacePlaceholders(screen.text.subtitle?.text),
|
|
},
|
|
{
|
|
as: "h3",
|
|
defaults: {
|
|
font: "inter",
|
|
weight: "black",
|
|
align: "center",
|
|
className: "mt-[25px] text-[#1F2937] text-[36px]",
|
|
},
|
|
}
|
|
),
|
|
description: {
|
|
trialPrice: buildTypographyProps<"span">(
|
|
{
|
|
...screen.text.description,
|
|
text: replacePlaceholders(screen.text.description?.trialPrice?.text),
|
|
},
|
|
{
|
|
as: "span",
|
|
defaults: {
|
|
font: "inter",
|
|
weight: "extraBold",
|
|
align: "center",
|
|
className: "text-[26px] text-[#2A6AEE] inline-block",
|
|
},
|
|
}
|
|
),
|
|
text: buildTypographyProps<"p">(
|
|
{
|
|
...screen.text.description,
|
|
text: replacePlaceholders(screen.text.description?.text?.text),
|
|
},
|
|
{
|
|
as: "p",
|
|
defaults: {
|
|
font: "inter",
|
|
weight: "medium",
|
|
align: "center",
|
|
className: "mt-[11px] text-[22px] inline-block",
|
|
},
|
|
}
|
|
),
|
|
oldTrialPrice: buildTypographyProps<"span">(
|
|
{
|
|
...screen.text.description,
|
|
text: replacePlaceholders(
|
|
screen.text.description?.oldTrialPrice?.text
|
|
),
|
|
},
|
|
{
|
|
as: "span",
|
|
defaults: {
|
|
font: "inter",
|
|
weight: "medium",
|
|
align: "center",
|
|
className: "text-[22px] inline-block",
|
|
},
|
|
}
|
|
),
|
|
},
|
|
};
|
|
|
|
const advantagesProps = {
|
|
items: screen.advantages?.items.map((item) => {
|
|
return {
|
|
icon: buildTypographyProps<"span">(item.icon, {
|
|
as: "span",
|
|
defaults: {
|
|
font: "inter",
|
|
weight: "medium",
|
|
size: "md",
|
|
className: "text-[26px] leading-[39px] inline-block mr-1",
|
|
},
|
|
}),
|
|
text: buildTypographyProps<"p">(
|
|
{
|
|
...item.text,
|
|
text: replacePlaceholders(item.text?.text),
|
|
},
|
|
{
|
|
as: "p",
|
|
defaults: {
|
|
font: "inter",
|
|
weight: "medium",
|
|
size: "md",
|
|
className: "text-[17px] leading-[39px] inline-block",
|
|
},
|
|
}
|
|
),
|
|
};
|
|
}),
|
|
};
|
|
|
|
const layoutProps = createTemplateLayoutProps(
|
|
{
|
|
...screen,
|
|
header: {
|
|
...screen.header,
|
|
showProgress: false,
|
|
},
|
|
},
|
|
{ canGoBack, onBack },
|
|
screenProgress,
|
|
{
|
|
preset: "left",
|
|
actionButton: {
|
|
defaultText: replacePlaceholders(
|
|
defaultTexts?.nextButton || "GET {{trialPeriodHyphen}} TRIAL"
|
|
),
|
|
children: isLoadingRedirect ? (
|
|
<Spinner className="size-6" />
|
|
) : (
|
|
replacePlaceholders(
|
|
screen.bottomActionButton?.text ||
|
|
defaultTexts?.nextButton ||
|
|
"GET {{trialPeriodHyphen}} TRIAL"
|
|
)
|
|
),
|
|
disabled: false,
|
|
onClick: handlePayClick,
|
|
},
|
|
}
|
|
);
|
|
|
|
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" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<TemplateLayout
|
|
{...layoutProps}
|
|
contentProps={{ className: "pt-0" }}
|
|
childrenWrapperProps={{ className: "-mt-1" }}
|
|
>
|
|
<div className="w-full flex flex-col items-center justify-center">
|
|
<svg
|
|
width="104"
|
|
height="105"
|
|
viewBox="0 0 104 105"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
d="M38.6953 14.725L45.7641 26.75H45.5H30.875C26.3859 26.75 22.75 23.1141 22.75 18.625C22.75 14.1359 26.3859 10.5 30.875 10.5H31.3219C34.3484 10.5 37.1719 12.1047 38.6953 14.725ZM13 18.625C13 21.55 13.7109 24.3125 14.95 26.75H6.5C2.90469 26.75 0 29.6547 0 33.25V46.25C0 49.8453 2.90469 52.75 6.5 52.75H97.5C101.095 52.75 104 49.8453 104 46.25V33.25C104 29.6547 101.095 26.75 97.5 26.75H89.05C90.2891 24.3125 91 21.55 91 18.625C91 8.75313 82.9969 0.75 73.125 0.75H72.6781C66.1984 0.75 60.1859 4.18281 56.8953 9.76875L52 18.1172L47.1047 9.78906C43.8141 4.18281 37.8016 0.75 31.3219 0.75H30.875C21.0031 0.75 13 8.75313 13 18.625ZM81.25 18.625C81.25 23.1141 77.6141 26.75 73.125 26.75H58.5H58.2359L65.3047 14.725C66.8484 12.1047 69.6516 10.5 72.6781 10.5H73.125C77.6141 10.5 81.25 14.1359 81.25 18.625ZM6.5 59.25V95C6.5 100.383 10.8672 104.75 16.25 104.75H45.5V59.25H6.5ZM58.5 104.75H87.75C93.1328 104.75 97.5 100.383 97.5 95V59.25H58.5V104.75Z"
|
|
fill="#FF0000"
|
|
/>
|
|
</svg>
|
|
{textProps.title && <Typography {...textProps.title} />}
|
|
{textProps.subtitle && <Typography {...textProps.subtitle} />}
|
|
{textProps.description && (
|
|
<Typography {...textProps.description.text}>
|
|
{textProps.description.trialPrice && (
|
|
<Typography {...textProps.description.trialPrice} />
|
|
)}
|
|
{textProps.description.text?.children}
|
|
{textProps.description.oldTrialPrice && (
|
|
<Typography {...textProps.description.oldTrialPrice} />
|
|
)}
|
|
</Typography>
|
|
)}
|
|
{advantagesProps.items && (
|
|
<ul className="mt-[25px] flex flex-col gap-[11px]">
|
|
{advantagesProps.items.map((item, index) => (
|
|
<Typography key={index} as="li" enableMarkup>
|
|
{item.icon && <Typography as="span" {...item.icon} />}
|
|
{item.text && <Typography {...item.text} />}
|
|
</Typography>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</TemplateLayout>
|
|
);
|
|
}
|