mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -3552,6 +3552,11 @@
|
|||||||
"giftSentSuccess": "هدیه ارسال شد!",
|
"giftSentSuccess": "هدیه ارسال شد!",
|
||||||
"giftSentDesc": "گیرنده از طریق ایمیل مطلع خواهد شد",
|
"giftSentDesc": "گیرنده از طریق ایمیل مطلع خواهد شد",
|
||||||
"giftPendingActivationDesc": "گیرنده در حال حاضر اشتراک فعال دارد. لینک فعالسازی هدیه برای او ارسال خواهد شد.",
|
"giftPendingActivationDesc": "گیرنده در حال حاضر اشتراک فعال دارد. لینک فعالسازی هدیه برای او ارسال خواهد شد.",
|
||||||
|
"giftTelegramSent": "اطلاعیه هدیه از طریق تلگرام به گیرنده ارسال شد",
|
||||||
|
"giftTelegramNotInBot": "گیرنده هنوز ربات را شروع نکرده است. این لینک را برای او ارسال کنید:",
|
||||||
|
"giftTelegramPendingSent": "درخواست فعالسازی از طریق تلگرام به گیرنده ارسال شد",
|
||||||
|
"giftTelegramPendingNotInBot": "گیرنده هنوز در ربات نیست. لینک ربات را برایش ارسال کنید تا بتواند هدیه را فعال کند:",
|
||||||
|
"openBot": "باز کردن ربات",
|
||||||
"autoLoginFailed": "ورود خودکار ناموفق بود",
|
"autoLoginFailed": "ورود خودکار ناموفق بود",
|
||||||
"autoLoginProcessing": "در حال ورود...",
|
"autoLoginProcessing": "در حال ورود...",
|
||||||
"periodLabels": {
|
"periodLabels": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -3551,6 +3551,11 @@
|
|||||||
"giftSentSuccess": "礼物已发送!",
|
"giftSentSuccess": "礼物已发送!",
|
||||||
"giftSentDesc": "收件人将通过邮件收到通知",
|
"giftSentDesc": "收件人将通过邮件收到通知",
|
||||||
"giftPendingActivationDesc": "收件人已有活跃订阅。他们将收到激活礼物的链接。",
|
"giftPendingActivationDesc": "收件人已有活跃订阅。他们将收到激活礼物的链接。",
|
||||||
|
"giftTelegramSent": "已通过 Telegram 向收件人发送通知",
|
||||||
|
"giftTelegramNotInBot": "收件人尚未启动机器人。请将此链接发送给他们:",
|
||||||
|
"giftTelegramPendingSent": "已通过 Telegram 向收件人发送激活请求",
|
||||||
|
"giftTelegramPendingNotInBot": "收件人尚未启动机器人。请将机器人链接发送给他们以激活礼物:",
|
||||||
|
"openBot": "打开机器人",
|
||||||
"autoLoginFailed": "自动登录失败",
|
"autoLoginFailed": "自动登录失败",
|
||||||
"autoLoginProcessing": "正在登录...",
|
"autoLoginProcessing": "正在登录...",
|
||||||
"periodLabels": {
|
"periodLabels": {
|
||||||
|
|||||||
@@ -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 } } };
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 } } }) => {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user