diff --git a/src/App.tsx b/src/App.tsx index 46e0ea8..5b484ca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -406,9 +406,11 @@ function App() { - - + + + + + } /> setSuccessMessage(null), 5000); diff --git a/src/components/WebSocketNotifications.tsx b/src/components/WebSocketNotifications.tsx index 5d93cbe..d411263 100644 --- a/src/components/WebSocketNotifications.tsx +++ b/src/components/WebSocketNotifications.tsx @@ -41,6 +41,7 @@ export default function WebSocketNotifications() { }); // Refresh data queryClient.invalidateQueries({ queryKey: ['balance'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); queryClient.invalidateQueries({ queryKey: ['transactions'] }); refreshUser(); return; @@ -67,6 +68,7 @@ export default function WebSocketNotifications() { duration: 5000, }); queryClient.invalidateQueries({ queryKey: ['balance'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); queryClient.invalidateQueries({ queryKey: ['transactions'] }); refreshUser(); return; @@ -82,6 +84,7 @@ export default function WebSocketNotifications() { }); queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['balance'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); refreshUser(); return; } @@ -95,6 +98,7 @@ export default function WebSocketNotifications() { }); queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['balance'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); refreshUser(); return; } @@ -151,6 +155,7 @@ export default function WebSocketNotifications() { duration: 5000, }); queryClient.invalidateQueries({ queryKey: ['balance'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); refreshUser(); return; } @@ -180,6 +185,7 @@ export default function WebSocketNotifications() { }); queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['balance'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); queryClient.invalidateQueries({ queryKey: ['transactions'] }); refreshUser(); return; @@ -195,6 +201,7 @@ export default function WebSocketNotifications() { }); queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['balance'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); queryClient.invalidateQueries({ queryKey: ['transactions'] }); refreshUser(); return; @@ -220,6 +227,7 @@ export default function WebSocketNotifications() { }); queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['balance'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); refreshUser(); return; } @@ -329,6 +337,7 @@ export default function WebSocketNotifications() { duration: 8000, }); queryClient.invalidateQueries({ queryKey: ['balance'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); queryClient.invalidateQueries({ queryKey: ['referral-stats'] }); refreshUser(); return; @@ -375,6 +384,7 @@ export default function WebSocketNotifications() { duration: 6000, }); queryClient.invalidateQueries({ queryKey: ['balance'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); queryClient.invalidateQueries({ queryKey: ['transactions'] }); refreshUser(); return; diff --git a/src/components/admin/AdminBackButton.tsx b/src/components/admin/AdminBackButton.tsx index 5dc084b..6770f90 100644 --- a/src/components/admin/AdminBackButton.tsx +++ b/src/components/admin/AdminBackButton.tsx @@ -4,6 +4,7 @@ import { BackIcon } from './icons'; interface AdminBackButtonProps { to?: string; + replace?: boolean; className?: string; } @@ -11,7 +12,7 @@ interface AdminBackButtonProps { * Back button for admin pages. * 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(); // In Telegram Mini App, we use native back button @@ -22,6 +23,7 @@ export function AdminBackButton({ to = '/admin', className }: AdminBackButtonPro return ( { navigate('/connection/qr', { + replace: !isTelegramWebApp, state: { url: appConfig?.subscriptionUrl, hideLink: appConfig?.hideLink ?? false, diff --git a/src/pages/ConnectionQR.tsx b/src/pages/ConnectionQR.tsx index b09cffe..5671a2a 100644 --- a/src/pages/ConnectionQR.tsx +++ b/src/pages/ConnectionQR.tsx @@ -2,8 +2,8 @@ import { useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import { QRCodeSVG } from 'qrcode.react'; -import { useAuthStore } from '../store/auth'; import { useBranding } from '../hooks/useBranding'; +import { AdminBackButton } from '@/components/admin'; interface ConnectionQRState { url: string; @@ -20,96 +20,56 @@ export default function ConnectionQR() { const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); - const isAuthenticated = useAuthStore((state) => state.isAuthenticated); - const isLoading = useAuthStore((state) => state.isLoading); const { appName } = useBranding(); const state = location.state as unknown; const validState = isValidState(state) ? state : null; useEffect(() => { - if (!isLoading && !isAuthenticated) { - navigate('/login', { replace: true }); - } - }, [isAuthenticated, isLoading, navigate]); - - useEffect(() => { - if (!isLoading && isAuthenticated && !validState) { + if (!validState) { navigate('/connection', { replace: true }); } - }, [isLoading, isAuthenticated, validState, navigate]); + }, [validState, navigate]); - useEffect(() => { - 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) { + if (!validState) { return null; } return ( -
- {/* Close button */} - +
+
+ +

{t('subscription.connection.qrTitle')}

+
- {/* Content */} -
- {/* Branding name */} - {appName && ( -

- {appName} +

+
+ {appName && ( +

+ {appName} +

+ )} + +

+ {t('subscription.connection.qrScanHint')}

- )} - {/* Title */} -

- {t('subscription.connection.qrTitle')} -

+
+ +
- {/* Hint */} -

- {t('subscription.connection.qrScanHint')} -

- - {/* QR code container */} -
- + {!validState.hideLink && ( +

+ {validState.url} +

+ )}
- - {/* URL display */} - {!validState.hideLink && ( -

- {validState.url} -

- )}
); diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index b355b6d..2861117 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -87,6 +87,7 @@ export default function Dashboard() { queryClient.invalidateQueries({ queryKey: ['subscription'] }); queryClient.invalidateQueries({ queryKey: ['trial-info'] }); queryClient.invalidateQueries({ queryKey: ['balance'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); refreshUser(); }, onError: (error: { response?: { data?: { detail?: string } } }) => { diff --git a/src/pages/PurchaseSuccess.tsx b/src/pages/PurchaseSuccess.tsx index a4e9575..94ac3f5 100644 --- a/src/pages/PurchaseSuccess.tsx +++ b/src/pages/PurchaseSuccess.tsx @@ -218,6 +218,9 @@ function SuccessState({ periodDays, isGift, giftMessage, + recipientInBot, + botLink, + contactType, }: { subscriptionUrl: string | null; cryptoLink: string | null; @@ -227,6 +230,9 @@ function SuccessState({ periodDays: number | null; isGift: boolean; giftMessage: string | null; + recipientInBot: boolean | null; + botLink: string | null; + contactType: string | null; }) { const { t } = useTranslation(); const [copied, setCopied] = useState(false); @@ -299,7 +305,13 @@ function SuccessState({ {tariffName} — {periodDays} {t('landing.daysAccess')}

)} - {displayContact && ( + {isGift && contactType === 'telegram' && recipientInBot === true && ( +

{t('landing.giftTelegramSent')}

+ )} + {isGift && contactType === 'telegram' && recipientInBot !== true && ( +

{t('landing.giftTelegramNotInBot')}

+ )} + {!(isGift && contactType === 'telegram') && displayContact && (

{isGift ? t('landing.giftSentTo', { contact: displayContact }) @@ -313,6 +325,18 @@ function SuccessState({ )}

+ {/* Bot link for telegram gifts where recipient is not in bot */} + {isGift && contactType === 'telegram' && recipientInBot !== true && botLink && ( + + {t('landing.openBot')} + + )} + {/* QR Code */} {displayUrl && (
@@ -507,11 +531,17 @@ function GiftPendingActivationState({ periodDays, recipientContactValue, giftMessage, + recipientInBot, + botLink, + contactType, }: { tariffName: string | null; periodDays: number | null; recipientContactValue: string | null; giftMessage: string | null; + recipientInBot: boolean | null; + botLink: string | null; + contactType: string | null; }) { const { t } = useTranslation(); @@ -556,18 +586,38 @@ function GiftPendingActivationState({ {tariffName} — {periodDays} {t('landing.daysAccess')}

)} - {recipientContactValue && ( + {contactType === 'telegram' && recipientInBot === true && ( +

{t('landing.giftTelegramPendingSent')}

+ )} + {contactType === 'telegram' && recipientInBot !== true && ( +

{t('landing.giftTelegramPendingNotInBot')}

+ )} + {contactType !== 'telegram' && ( +

{t('landing.giftPendingActivationDesc')}

+ )} + {contactType !== 'telegram' && recipientContactValue && (

{t('landing.giftSentTo', { contact: recipientContactValue })}

)} -

{t('landing.giftPendingActivationDesc')}

{giftMessage && (

{t('landing.giftMessage')}: {giftMessage}

)}
+ + {/* Bot link for telegram gifts where recipient is not in bot */} + {contactType === 'telegram' && recipientInBot !== true && botLink && ( + + {t('landing.openBot')} + + )} ); } @@ -756,6 +806,9 @@ export default function PurchaseSuccess() { periodDays={purchaseStatus.period_days} isGift={purchaseStatus.is_gift} giftMessage={purchaseStatus.gift_message} + recipientInBot={purchaseStatus.recipient_in_bot} + botLink={purchaseStatus.bot_link} + contactType={purchaseStatus.contact_type} /> ) : isGiftPendingActivation ? ( ) : isPendingActivation ? (
diff --git a/src/pages/QuickPurchase.tsx b/src/pages/QuickPurchase.tsx index 537f03e..2e18611 100644 --- a/src/pages/QuickPurchase.tsx +++ b/src/pages/QuickPurchase.tsx @@ -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" /> - {tariff.traffic_limit_gb} {t('landing.gb', 'GB')} + {tariff.traffic_limit_gb === 0 ? '∞' : tariff.traffic_limit_gb} {t('landing.gb', 'GB')} ; + return ( +
+ ); } function SummaryCard({ @@ -1000,7 +1002,7 @@ export default function QuickPurchase() { initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.1 }} - className="space-y-6" + className="min-w-0 space-y-6" > {/* Period tabs */} {allPeriods.length > 0 && ( @@ -1104,7 +1106,7 @@ export default function QuickPurchase() { initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} 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" >