Merge pull request #261 from BEDOLAGA-DEV/dev

Dev
This commit is contained in:
Egor
2026-03-08 15:46:07 +03:00
committed by GitHub
15 changed files with 144 additions and 84 deletions

View File

@@ -406,9 +406,11 @@ function App() {
<Route <Route
path="/connection/qr" path="/connection/qr"
element={ element={
<ProtectedRoute>
<LazyPage> <LazyPage>
<ConnectionQR /> <ConnectionQR />
</LazyPage> </LazyPage>
</ProtectedRoute>
} }
/> />
<Route <Route

View File

@@ -126,6 +126,8 @@ export interface PurchaseStatus {
cabinet_email: string | null; cabinet_email: string | null;
cabinet_password: string | null; cabinet_password: string | null;
auto_login_token: string | null; auto_login_token: string | null;
recipient_in_bot: boolean | null;
bot_link: string | null;
} }
// ============================================================ // ============================================================

View File

@@ -116,6 +116,8 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
queryClient.invalidateQueries({ queryKey: ['promo-offers'] }); queryClient.invalidateQueries({ queryKey: ['promo-offers'] });
queryClient.invalidateQueries({ queryKey: ['active-discount'] }); queryClient.invalidateQueries({ queryKey: ['active-discount'] });
queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['subscription'] });
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
queryClient.invalidateQueries({ queryKey: ['balance'] });
setSuccessMessage(result.message); setSuccessMessage(result.message);
setClaimingId(null); setClaimingId(null);
setTimeout(() => setSuccessMessage(null), 5000); setTimeout(() => setSuccessMessage(null), 5000);

View File

@@ -41,6 +41,7 @@ export default function WebSocketNotifications() {
}); });
// Refresh data // Refresh data
queryClient.invalidateQueries({ queryKey: ['balance'] }); queryClient.invalidateQueries({ queryKey: ['balance'] });
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
queryClient.invalidateQueries({ queryKey: ['transactions'] }); queryClient.invalidateQueries({ queryKey: ['transactions'] });
refreshUser(); refreshUser();
return; return;
@@ -67,6 +68,7 @@ export default function WebSocketNotifications() {
duration: 5000, duration: 5000,
}); });
queryClient.invalidateQueries({ queryKey: ['balance'] }); queryClient.invalidateQueries({ queryKey: ['balance'] });
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
queryClient.invalidateQueries({ queryKey: ['transactions'] }); queryClient.invalidateQueries({ queryKey: ['transactions'] });
refreshUser(); refreshUser();
return; return;
@@ -82,6 +84,7 @@ export default function WebSocketNotifications() {
}); });
queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['subscription'] });
queryClient.invalidateQueries({ queryKey: ['balance'] }); queryClient.invalidateQueries({ queryKey: ['balance'] });
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
refreshUser(); refreshUser();
return; return;
} }
@@ -95,6 +98,7 @@ export default function WebSocketNotifications() {
}); });
queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['subscription'] });
queryClient.invalidateQueries({ queryKey: ['balance'] }); queryClient.invalidateQueries({ queryKey: ['balance'] });
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
refreshUser(); refreshUser();
return; return;
} }
@@ -151,6 +155,7 @@ export default function WebSocketNotifications() {
duration: 5000, duration: 5000,
}); });
queryClient.invalidateQueries({ queryKey: ['balance'] }); queryClient.invalidateQueries({ queryKey: ['balance'] });
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
refreshUser(); refreshUser();
return; return;
} }
@@ -180,6 +185,7 @@ export default function WebSocketNotifications() {
}); });
queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['subscription'] });
queryClient.invalidateQueries({ queryKey: ['balance'] }); queryClient.invalidateQueries({ queryKey: ['balance'] });
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
queryClient.invalidateQueries({ queryKey: ['transactions'] }); queryClient.invalidateQueries({ queryKey: ['transactions'] });
refreshUser(); refreshUser();
return; return;
@@ -195,6 +201,7 @@ export default function WebSocketNotifications() {
}); });
queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['subscription'] });
queryClient.invalidateQueries({ queryKey: ['balance'] }); queryClient.invalidateQueries({ queryKey: ['balance'] });
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
queryClient.invalidateQueries({ queryKey: ['transactions'] }); queryClient.invalidateQueries({ queryKey: ['transactions'] });
refreshUser(); refreshUser();
return; return;
@@ -220,6 +227,7 @@ export default function WebSocketNotifications() {
}); });
queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['subscription'] });
queryClient.invalidateQueries({ queryKey: ['balance'] }); queryClient.invalidateQueries({ queryKey: ['balance'] });
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
refreshUser(); refreshUser();
return; return;
} }
@@ -329,6 +337,7 @@ export default function WebSocketNotifications() {
duration: 8000, duration: 8000,
}); });
queryClient.invalidateQueries({ queryKey: ['balance'] }); queryClient.invalidateQueries({ queryKey: ['balance'] });
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
queryClient.invalidateQueries({ queryKey: ['referral-stats'] }); queryClient.invalidateQueries({ queryKey: ['referral-stats'] });
refreshUser(); refreshUser();
return; return;
@@ -375,6 +384,7 @@ export default function WebSocketNotifications() {
duration: 6000, duration: 6000,
}); });
queryClient.invalidateQueries({ queryKey: ['balance'] }); queryClient.invalidateQueries({ queryKey: ['balance'] });
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
queryClient.invalidateQueries({ queryKey: ['transactions'] }); queryClient.invalidateQueries({ queryKey: ['transactions'] });
refreshUser(); refreshUser();
return; return;

View File

@@ -4,6 +4,7 @@ import { BackIcon } from './icons';
interface AdminBackButtonProps { interface AdminBackButtonProps {
to?: string; to?: string;
replace?: boolean;
className?: string; className?: string;
} }
@@ -11,7 +12,7 @@ interface AdminBackButtonProps {
* Back button for admin pages. * Back button for admin pages.
* Hidden in Telegram Mini App since native back button is used instead. * Hidden in Telegram Mini App since native back button is used instead.
*/ */
export function AdminBackButton({ to = '/admin', className }: AdminBackButtonProps) { export function AdminBackButton({ to = '/admin', replace, className }: AdminBackButtonProps) {
const { platform } = usePlatform(); const { platform } = usePlatform();
// In Telegram Mini App, we use native back button // In Telegram Mini App, we use native back button
@@ -22,6 +23,7 @@ export function AdminBackButton({ to = '/admin', className }: AdminBackButtonPro
return ( return (
<Link <Link
to={to} to={to}
replace={replace}
className={ className={
className || className ||
'flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600' 'flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600'

View File

@@ -4060,6 +4060,11 @@
"giftSentSuccess": "Gift sent!", "giftSentSuccess": "Gift sent!",
"giftSentDesc": "Recipient will be notified by email", "giftSentDesc": "Recipient will be notified by email",
"giftPendingActivationDesc": "The recipient already has an active subscription. They will receive a link to activate the gift.", "giftPendingActivationDesc": "The recipient already has an active subscription. They will receive a link to activate the gift.",
"giftTelegramSent": "Gift notification sent to recipient via Telegram",
"giftTelegramNotInBot": "Recipient hasn't started the bot yet. Send them this link:",
"giftTelegramPendingSent": "Activation request sent to recipient via Telegram",
"giftTelegramPendingNotInBot": "Recipient hasn't started the bot yet. Send them the bot link so they can activate the gift:",
"openBot": "Open bot",
"autoLoginFailed": "Auto-login failed", "autoLoginFailed": "Auto-login failed",
"autoLoginProcessing": "Signing in...", "autoLoginProcessing": "Signing in...",
"discount": { "discount": {

View File

@@ -3552,6 +3552,11 @@
"giftSentSuccess": "هدیه ارسال شد!", "giftSentSuccess": "هدیه ارسال شد!",
"giftSentDesc": "گیرنده از طریق ایمیل مطلع خواهد شد", "giftSentDesc": "گیرنده از طریق ایمیل مطلع خواهد شد",
"giftPendingActivationDesc": "گیرنده در حال حاضر اشتراک فعال دارد. لینک فعال‌سازی هدیه برای او ارسال خواهد شد.", "giftPendingActivationDesc": "گیرنده در حال حاضر اشتراک فعال دارد. لینک فعال‌سازی هدیه برای او ارسال خواهد شد.",
"giftTelegramSent": "اطلاعیه هدیه از طریق تلگرام به گیرنده ارسال شد",
"giftTelegramNotInBot": "گیرنده هنوز ربات را شروع نکرده است. این لینک را برای او ارسال کنید:",
"giftTelegramPendingSent": "درخواست فعال‌سازی از طریق تلگرام به گیرنده ارسال شد",
"giftTelegramPendingNotInBot": "گیرنده هنوز در ربات نیست. لینک ربات را برایش ارسال کنید تا بتواند هدیه را فعال کند:",
"openBot": "باز کردن ربات",
"autoLoginFailed": "ورود خودکار ناموفق بود", "autoLoginFailed": "ورود خودکار ناموفق بود",
"autoLoginProcessing": "در حال ورود...", "autoLoginProcessing": "در حال ورود...",
"periodLabels": { "periodLabels": {

View File

@@ -4620,6 +4620,11 @@
"giftSentSuccess": "Подарок отправлен!", "giftSentSuccess": "Подарок отправлен!",
"giftSentDesc": "Получатель получит уведомление на email", "giftSentDesc": "Получатель получит уведомление на email",
"giftPendingActivationDesc": "У получателя уже есть активная подписка. Ему будет отправлена ссылка для активации подарка.", "giftPendingActivationDesc": "У получателя уже есть активная подписка. Ему будет отправлена ссылка для активации подарка.",
"giftTelegramSent": "Мы отправили получателю уведомление о подарке в Telegram",
"giftTelegramNotInBot": "Получатель пока не пользуется ботом. Отправьте ему эту ссылку, чтобы он мог забрать подарок:",
"giftTelegramPendingSent": "Мы отправили получателю предложение активировать подарок в Telegram",
"giftTelegramPendingNotInBot": "Получатель пока не пользуется ботом. Отправьте ему ссылку, чтобы он мог активировать подарок:",
"openBot": "Ссылка на бота",
"autoLoginFailed": "Не удалось выполнить автоматический вход", "autoLoginFailed": "Не удалось выполнить автоматический вход",
"autoLoginProcessing": "Выполняется вход...", "autoLoginProcessing": "Выполняется вход...",
"discount": { "discount": {

View File

@@ -3551,6 +3551,11 @@
"giftSentSuccess": "礼物已发送!", "giftSentSuccess": "礼物已发送!",
"giftSentDesc": "收件人将通过邮件收到通知", "giftSentDesc": "收件人将通过邮件收到通知",
"giftPendingActivationDesc": "收件人已有活跃订阅。他们将收到激活礼物的链接。", "giftPendingActivationDesc": "收件人已有活跃订阅。他们将收到激活礼物的链接。",
"giftTelegramSent": "已通过 Telegram 向收件人发送通知",
"giftTelegramNotInBot": "收件人尚未启动机器人。请将此链接发送给他们:",
"giftTelegramPendingSent": "已通过 Telegram 向收件人发送激活请求",
"giftTelegramPendingNotInBot": "收件人尚未启动机器人。请将机器人链接发送给他们以激活礼物:",
"openBot": "打开机器人",
"autoLoginFailed": "自动登录失败", "autoLoginFailed": "自动登录失败",
"autoLoginProcessing": "正在登录...", "autoLoginProcessing": "正在登录...",
"periodLabels": { "periodLabels": {

View File

@@ -79,6 +79,7 @@ export default function Balance() {
refreshUser(); refreshUser();
queryClient.invalidateQueries({ queryKey: ['transactions'] }); queryClient.invalidateQueries({ queryKey: ['transactions'] });
queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['subscription'] });
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
showToast({ showToast({
type: 'success', type: 'success',
@@ -164,6 +165,7 @@ export default function Balance() {
await refetchBalance(); await refetchBalance();
await refreshUser(); await refreshUser();
queryClient.invalidateQueries({ queryKey: ['transactions'] }); queryClient.invalidateQueries({ queryKey: ['transactions'] });
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
} }
} catch (error: unknown) { } catch (error: unknown) {
const axiosError = error as { response?: { data?: { detail?: string } } }; const axiosError = error as { response?: { data?: { detail?: string } } };

View File

@@ -37,6 +37,7 @@ export default function Connection() {
const handleOpenQR = useCallback(() => { const handleOpenQR = useCallback(() => {
navigate('/connection/qr', { navigate('/connection/qr', {
replace: !isTelegramWebApp,
state: { state: {
url: appConfig?.subscriptionUrl, url: appConfig?.subscriptionUrl,
hideLink: appConfig?.hideLink ?? false, hideLink: appConfig?.hideLink ?? false,

View File

@@ -2,8 +2,8 @@ import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router'; import { useLocation, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
import { useAuthStore } from '../store/auth';
import { useBranding } from '../hooks/useBranding'; import { useBranding } from '../hooks/useBranding';
import { AdminBackButton } from '@/components/admin';
interface ConnectionQRState { interface ConnectionQRState {
url: string; url: string;
@@ -20,80 +20,40 @@ export default function ConnectionQR() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const isLoading = useAuthStore((state) => state.isLoading);
const { appName } = useBranding(); const { appName } = useBranding();
const state = location.state as unknown; const state = location.state as unknown;
const validState = isValidState(state) ? state : null; const validState = isValidState(state) ? state : null;
useEffect(() => { useEffect(() => {
if (!isLoading && !isAuthenticated) { if (!validState) {
navigate('/login', { replace: true });
}
}, [isAuthenticated, isLoading, navigate]);
useEffect(() => {
if (!isLoading && isAuthenticated && !validState) {
navigate('/connection', { replace: true }); navigate('/connection', { replace: true });
} }
}, [isLoading, isAuthenticated, validState, navigate]); }, [validState, navigate]);
useEffect(() => { if (!validState) {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
navigate(-1);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [navigate]);
if (isLoading || !validState) {
return null; return null;
} }
return ( return (
<div className="fixed inset-0 z-50 flex min-h-dvh flex-col items-center justify-center bg-gradient-to-b from-dark-900 to-dark-950"> <div className="animate-fade-in">
{/* Close button */} <div className="mb-6 flex items-center gap-3">
<button <AdminBackButton to="/connection" replace />
type="button" <h1 className="text-2xl font-bold text-dark-100">{t('subscription.connection.qrTitle')}</h1>
onClick={() => navigate(-1)} </div>
className="absolute left-4 top-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-dark-800/60 backdrop-blur-sm transition-colors hover:bg-dark-700/80"
aria-label={t('common.close')}
>
<svg
className="h-5 w-5 text-dark-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/* Content */} <div className="flex flex-col items-center">
<div className="flex w-full max-w-sm animate-scale-in flex-col items-center px-6"> <div className="flex w-full max-w-sm flex-col items-center px-6">
{/* Branding name */}
{appName && ( {appName && (
<p className="mb-3 text-sm font-medium uppercase tracking-wider text-dark-400"> <p className="mb-3 text-sm font-medium uppercase tracking-wider text-dark-400">
{appName} {appName}
</p> </p>
)} )}
{/* Title */}
<h1 className="mb-2 text-center text-xl font-bold text-dark-100">
{t('subscription.connection.qrTitle')}
</h1>
{/* Hint */}
<p className="mb-8 text-center text-sm text-dark-400"> <p className="mb-8 text-center text-sm text-dark-400">
{t('subscription.connection.qrScanHint')} {t('subscription.connection.qrScanHint')}
</p> </p>
{/* QR code container */}
<div className="rounded-3xl bg-white p-6"> <div className="rounded-3xl bg-white p-6">
<QRCodeSVG <QRCodeSVG
value={validState.url} value={validState.url}
@@ -104,7 +64,6 @@ export default function ConnectionQR() {
/> />
</div> </div>
{/* URL display */}
{!validState.hideLink && ( {!validState.hideLink && (
<p className="mt-6 max-w-full truncate text-center font-mono text-xs text-dark-500"> <p className="mt-6 max-w-full truncate text-center font-mono text-xs text-dark-500">
{validState.url} {validState.url}
@@ -112,5 +71,6 @@ export default function ConnectionQR() {
)} )}
</div> </div>
</div> </div>
</div>
); );
} }

View File

@@ -87,6 +87,7 @@ export default function Dashboard() {
queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['subscription'] });
queryClient.invalidateQueries({ queryKey: ['trial-info'] }); queryClient.invalidateQueries({ queryKey: ['trial-info'] });
queryClient.invalidateQueries({ queryKey: ['balance'] }); queryClient.invalidateQueries({ queryKey: ['balance'] });
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
refreshUser(); refreshUser();
}, },
onError: (error: { response?: { data?: { detail?: string } } }) => { onError: (error: { response?: { data?: { detail?: string } } }) => {

View File

@@ -218,6 +218,9 @@ function SuccessState({
periodDays, periodDays,
isGift, isGift,
giftMessage, giftMessage,
recipientInBot,
botLink,
contactType,
}: { }: {
subscriptionUrl: string | null; subscriptionUrl: string | null;
cryptoLink: string | null; cryptoLink: string | null;
@@ -227,6 +230,9 @@ function SuccessState({
periodDays: number | null; periodDays: number | null;
isGift: boolean; isGift: boolean;
giftMessage: string | null; giftMessage: string | null;
recipientInBot: boolean | null;
botLink: string | null;
contactType: string | null;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@@ -299,7 +305,13 @@ function SuccessState({
{tariffName} {periodDays} {t('landing.daysAccess')} {tariffName} {periodDays} {t('landing.daysAccess')}
</p> </p>
)} )}
{displayContact && ( {isGift && contactType === 'telegram' && recipientInBot === true && (
<p className="mt-2 text-sm text-dark-400">{t('landing.giftTelegramSent')}</p>
)}
{isGift && contactType === 'telegram' && recipientInBot !== true && (
<p className="mt-2 text-sm text-dark-400">{t('landing.giftTelegramNotInBot')}</p>
)}
{!(isGift && contactType === 'telegram') && displayContact && (
<p className="mt-2 text-sm text-dark-400"> <p className="mt-2 text-sm text-dark-400">
{isGift {isGift
? t('landing.giftSentTo', { contact: displayContact }) ? t('landing.giftSentTo', { contact: displayContact })
@@ -313,6 +325,18 @@ function SuccessState({
)} )}
</div> </div>
{/* Bot link for telegram gifts where recipient is not in bot */}
{isGift && contactType === 'telegram' && recipientInBot !== true && botLink && (
<a
href={botLink}
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
>
{t('landing.openBot')}
</a>
)}
{/* QR Code */} {/* QR Code */}
{displayUrl && ( {displayUrl && (
<div className="space-y-4"> <div className="space-y-4">
@@ -507,11 +531,17 @@ function GiftPendingActivationState({
periodDays, periodDays,
recipientContactValue, recipientContactValue,
giftMessage, giftMessage,
recipientInBot,
botLink,
contactType,
}: { }: {
tariffName: string | null; tariffName: string | null;
periodDays: number | null; periodDays: number | null;
recipientContactValue: string | null; recipientContactValue: string | null;
giftMessage: string | null; giftMessage: string | null;
recipientInBot: boolean | null;
botLink: string | null;
contactType: string | null;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -556,18 +586,38 @@ function GiftPendingActivationState({
{tariffName} {periodDays} {t('landing.daysAccess')} {tariffName} {periodDays} {t('landing.daysAccess')}
</p> </p>
)} )}
{recipientContactValue && ( {contactType === 'telegram' && recipientInBot === true && (
<p className="mt-2 text-sm text-dark-400">{t('landing.giftTelegramPendingSent')}</p>
)}
{contactType === 'telegram' && recipientInBot !== true && (
<p className="mt-2 text-sm text-dark-400">{t('landing.giftTelegramPendingNotInBot')}</p>
)}
{contactType !== 'telegram' && (
<p className="mt-2 text-sm text-dark-400">{t('landing.giftPendingActivationDesc')}</p>
)}
{contactType !== 'telegram' && recipientContactValue && (
<p className="mt-2 text-sm text-dark-400"> <p className="mt-2 text-sm text-dark-400">
{t('landing.giftSentTo', { contact: recipientContactValue })} {t('landing.giftSentTo', { contact: recipientContactValue })}
</p> </p>
)} )}
<p className="mt-2 text-sm text-dark-400">{t('landing.giftPendingActivationDesc')}</p>
{giftMessage && ( {giftMessage && (
<p className="mt-2 text-sm italic text-dark-400"> <p className="mt-2 text-sm italic text-dark-400">
{t('landing.giftMessage')}: {giftMessage} {t('landing.giftMessage')}: {giftMessage}
</p> </p>
)} )}
</div> </div>
{/* Bot link for telegram gifts where recipient is not in bot */}
{contactType === 'telegram' && recipientInBot !== true && botLink && (
<a
href={botLink}
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
>
{t('landing.openBot')}
</a>
)}
</motion.div> </motion.div>
); );
} }
@@ -756,6 +806,9 @@ export default function PurchaseSuccess() {
periodDays={purchaseStatus.period_days} periodDays={purchaseStatus.period_days}
isGift={purchaseStatus.is_gift} isGift={purchaseStatus.is_gift}
giftMessage={purchaseStatus.gift_message} giftMessage={purchaseStatus.gift_message}
recipientInBot={purchaseStatus.recipient_in_bot}
botLink={purchaseStatus.bot_link}
contactType={purchaseStatus.contact_type}
/> />
) : isGiftPendingActivation ? ( ) : isGiftPendingActivation ? (
<GiftPendingActivationState <GiftPendingActivationState
@@ -763,6 +816,9 @@ export default function PurchaseSuccess() {
periodDays={purchaseStatus.period_days} periodDays={purchaseStatus.period_days}
recipientContactValue={purchaseStatus.recipient_contact_value} recipientContactValue={purchaseStatus.recipient_contact_value}
giftMessage={purchaseStatus.gift_message} giftMessage={purchaseStatus.gift_message}
recipientInBot={purchaseStatus.recipient_in_bot}
botLink={purchaseStatus.bot_link}
contactType={purchaseStatus.contact_type}
/> />
) : isPendingActivation ? ( ) : isPendingActivation ? (
<div className="space-y-4"> <div className="space-y-4">

View File

@@ -316,7 +316,7 @@ function TariffCard({
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
/> />
</svg> </svg>
{tariff.traffic_limit_gb} {t('landing.gb', 'GB')} {tariff.traffic_limit_gb === 0 ? '∞' : tariff.traffic_limit_gb} {t('landing.gb', 'GB')}
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<svg <svg
@@ -462,7 +462,9 @@ function SanitizedHtml({ html, className }: { html: string; className?: string }
return container.innerHTML; return container.innerHTML;
}, [html]); }, [html]);
return <div className={className} dangerouslySetInnerHTML={{ __html: sanitized }} />; return (
<div className={cn('break-words', className)} dangerouslySetInnerHTML={{ __html: sanitized }} />
);
} }
function SummaryCard({ function SummaryCard({
@@ -1000,7 +1002,7 @@ export default function QuickPurchase() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }} transition={{ duration: 0.5, delay: 0.1 }}
className="space-y-6" className="min-w-0 space-y-6"
> >
{/* Period tabs */} {/* Period tabs */}
{allPeriods.length > 0 && ( {allPeriods.length > 0 && (
@@ -1104,7 +1106,7 @@ export default function QuickPurchase() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }} transition={{ duration: 0.5, delay: 0.2 }}
className="lg:sticky lg:top-8 lg:self-start" className="min-w-0 lg:sticky lg:top-8 lg:self-start"
> >
<SummaryCard <SummaryCard
config={config} config={config}