diff --git a/src/pages/GiftResult.tsx b/src/pages/GiftResult.tsx index 6b78350..2fea2d8 100644 --- a/src/pages/GiftResult.tsx +++ b/src/pages/GiftResult.tsx @@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { motion } from 'framer-motion'; import { giftApi } from '../api/gift'; +import { brandingApi, type TelegramWidgetConfig } from '../api/branding'; import { Spinner } from '@/components/ui/Spinner'; import { AnimatedCheckmark } from '@/components/ui/AnimatedCheckmark'; import { AnimatedCrossmark } from '@/components/ui/AnimatedCrossmark'; @@ -54,13 +55,25 @@ function CodeOnlySuccessState({ const navigate = useNavigate(); const [copied, setCopied] = useState(false); + // Bot username comes from the runtime branding config; the build-time env var + // is only a fallback. Otherwise the "activate via bot" line silently vanishes + // on deployments that don't set VITE_TELEGRAM_BOT_USERNAME at build time. + const { data: widgetConfig } = useQuery({ + queryKey: ['telegram-widget-config'], + queryFn: brandingApi.getTelegramWidgetConfig, + staleTime: 60000, + }); + const botUsername = + widgetConfig?.bot_username || import.meta.env.VITE_TELEGRAM_BOT_USERNAME || ''; + const shortCode = purchaseToken.slice(0, 12); const giftCode = `GIFT-${shortCode}`; - const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME as string | undefined; - // Encode underscores as %5F so Telegram auto-link detection doesn't strip them - const safeCode = shortCode.replace(/_/g, '%5F'); - const botLink = botUsername ? `https://t.me/${botUsername}?start=GIFT%5F${safeCode}` : null; - const cabinetLink = `${window.location.origin}/gift?tab=activate&code=${safeCode}`; + // Telegram forwards the start parameter to the bot verbatim (no URL-decoding), + // and "_" is a valid start-param char — so use a literal "GIFT_" prefix to + // match the bot's `start_parameter.startswith('GIFT_')` handler. Encoding the + // underscore as %5F made the bot receive "GIFT%5F…" and silently fail. + const botLink = botUsername ? `https://t.me/${botUsername}?start=GIFT_${shortCode}` : null; + const cabinetLink = `${window.location.origin}/gift?tab=activate&code=${encodeURIComponent(shortCode)}`; const fullMessage = [ t('gift.shareText', 'I have a gift for you! Activate it here:'), diff --git a/src/pages/GiftSubscription.tsx b/src/pages/GiftSubscription.tsx index d1e4650..9e4fe4c 100644 --- a/src/pages/GiftSubscription.tsx +++ b/src/pages/GiftSubscription.tsx @@ -5,6 +5,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { motion, AnimatePresence } from 'framer-motion'; import { giftApi } from '../api/gift'; +import { brandingApi, type TelegramWidgetConfig } from '../api/branding'; import type { GiftConfig, GiftTariff, @@ -21,113 +22,17 @@ import { getApiErrorMessage } from '../utils/api-error'; import { formatPrice } from '../utils/format'; import { useCurrency } from '../hooks/useCurrency'; import { usePlatform, useHaptic } from '@/platform'; -import { SparklesIcon } from '@/components/icons'; - -function GiftIcon({ className }: { className?: string }) { - return ( - - - - - - - - ); -} - -function CheckIcon({ className }: { className?: string }) { - return ( - - - - ); -} - -function ShareIcon({ className }: { className?: string }) { - return ( - - - - - - ); -} - -function CheckCircleIcon({ className }: { className?: string }) { - return ( - - - - - ); -} - -function KeyIcon({ className }: { className?: string }) { - return ( - - - - - - ); -} - -function InboxIcon({ className }: { className?: string }) { - return ( - - - - - ); -} +import { + SparklesIcon, + GiftIcon, + CheckIcon, + CheckCircleIcon, + KeyIcon, + InboxIcon, + ExportIcon, + WarningCircleIcon, + BanIcon, +} from '@/components/icons'; function formatPeriodLabel( days: number, @@ -194,19 +99,7 @@ function ErrorState({ message }: { message: string }) {
- - - +

{t('gift.failedTitle')}

{message}

@@ -228,19 +121,7 @@ function DisabledState() {
- - - +

{t('gift.featureDisabled')}

{t('gift.redirecting')}

@@ -991,6 +872,16 @@ function SentGiftCard({ gift }: { gift: SentGift }) { const { t } = useTranslation(); const [showToast, setShowToast] = useState(false); + // Runtime bot username (env var is only a fallback) so the "activate via bot" + // line never silently disappears when VITE_TELEGRAM_BOT_USERNAME is unset. + const { data: widgetConfig } = useQuery({ + queryKey: ['telegram-widget-config'], + queryFn: brandingApi.getTelegramWidgetConfig, + staleTime: 60000, + }); + const botUsername = + widgetConfig?.bot_username || import.meta.env.VITE_TELEGRAM_BOT_USERNAME || ''; + const shortCode = gift.token.slice(0, 12); const giftCode = `GIFT-${shortCode}`; const isActivated = isGiftActivated(gift); @@ -1003,11 +894,11 @@ function SentGiftCard({ gift }: { gift: SentGift }) { : t(getGiftStatusKey(gift.status)); const buildShareMessage = useCallback(() => { - const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME as string | undefined; - // Encode underscores as %5F so Telegram auto-link detection doesn't strip them - const safeCode = shortCode.replace(/_/g, '%5F'); - const botLink = botUsername ? `https://t.me/${botUsername}?start=GIFT%5F${safeCode}` : null; - const cabinetLink = `${window.location.origin}/gift?tab=activate&code=${safeCode}`; + // Literal "GIFT_" prefix: Telegram forwards the start param to the bot + // verbatim (no URL-decoding), so the previously-encoded "%5F" never matched + // the bot's `start_parameter.startswith('GIFT_')` handler. + const botLink = botUsername ? `https://t.me/${botUsername}?start=GIFT_${shortCode}` : null; + const cabinetLink = `${window.location.origin}/gift?tab=activate&code=${encodeURIComponent(shortCode)}`; return [ t('gift.shareText'), '', @@ -1016,7 +907,7 @@ function SentGiftCard({ gift }: { gift: SentGift }) { ] .filter(Boolean) .join('\n'); - }, [shortCode, t]); + }, [shortCode, botUsername, t]); const handleShare = useCallback(async () => { const message = buildShareMessage(); @@ -1070,7 +961,7 @@ function SentGiftCard({ gift }: { gift: SentGift }) { onClick={handleShare} className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-4 py-3 text-sm font-bold uppercase tracking-wider text-white transition-colors hover:bg-accent-400 active:scale-[0.98]" > - + {t('gift.shareGift')} @@ -1219,7 +1110,7 @@ function MyGiftsTabContent() {

{t('gift.activeGiftsTitle')}

-
+
{activeGifts.map((gift) => ( ))} @@ -1233,7 +1124,7 @@ function MyGiftsTabContent() {

{t('gift.activatedGiftsTitle')}

-
+
{activatedGifts.map((gift) => ( ))} @@ -1247,7 +1138,7 @@ function MyGiftsTabContent() {

{t('gift.receivedGiftsTitle')}

-
+
{receivedGifts!.map((gift) => ( ))} @@ -1315,7 +1206,7 @@ export default function GiftSubscription() { return (
-
+
{/* Header */}