fix(gift): working bot deep-link + desktop layout + barrel icons

- Build the share deep-link as t.me/<bot>?start=GIFT_<code> (literal underscore). The old GIFT%5F encoding was passed to the bot verbatim by Telegram, so its startswith('GIFT_') never matched and activation silently failed.
- Source the bot username from the runtime branding config (env var is only a fallback), so the 'activate via bot' line no longer vanishes on builds without VITE_TELEGRAM_BOT_USERNAME.
- Widen the page on desktop (max-w-2xl) and show sent gifts in a 2-col grid; it was cramped to a mobile column.
- Replace the remaining hand-written SVG icons with the central barrel.
This commit is contained in:
c0mrade
2026-06-01 13:08:18 +03:00
parent 872f73157f
commit 6ea767253f
2 changed files with 53 additions and 149 deletions

View File

@@ -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<TelegramWidgetConfig>({
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:'),

View File

@@ -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 (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="8" width="18" height="4" rx="1" />
<rect x="5" y="12" width="14" height="8" rx="1" />
<line x1="12" y1="8" x2="12" y2="20" />
<path d="M12 8c-2-2-4-3-5-2s0 3 2 4h3" />
<path d="M12 8c2-2 4-3 5-2s0 3-2 4h-3" />
</svg>
);
}
function CheckIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={3}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 13l4 4L19 7" />
</svg>
);
}
function ShareIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4 12v8a2 2 0 002 2h12a2 2 0 002-2v-8" />
<polyline points="16 6 12 2 8 6" />
<line x1="12" y1="2" x2="12" y2="15" />
</svg>
);
}
function CheckCircleIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<path d="M8 12l3 3 5-5" />
</svg>
);
}
function KeyIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="15.5" cy="8.5" r="5.5" />
<path d="M11.5 12.5L3 21" />
<path d="M3 21l3-1 1-3" />
</svg>
);
}
function InboxIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
<path d="M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z" />
</svg>
);
}
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 }) {
<div className="flex min-h-dvh items-center justify-center px-4">
<div className="flex max-w-sm flex-col items-center gap-4 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-error-500/10">
<svg
className="h-8 w-8 text-error-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</svg>
<WarningCircleIcon className="h-8 w-8 text-error-400" />
</div>
<h2 className="text-lg font-semibold text-dark-50">{t('gift.failedTitle')}</h2>
<p className="text-sm text-dark-300">{message}</p>
@@ -228,19 +121,7 @@ function DisabledState() {
<div className="flex min-h-dvh items-center justify-center px-4">
<div className="flex max-w-sm flex-col items-center gap-4 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-dark-800/50">
<svg
className="h-8 w-8 text-dark-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
/>
</svg>
<BanIcon className="h-8 w-8 text-dark-400" />
</div>
<h2 className="text-lg font-semibold text-dark-50">{t('gift.featureDisabled')}</h2>
<p className="text-sm text-dark-300">{t('gift.redirecting')}</p>
@@ -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<TelegramWidgetConfig>({
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]"
>
<ShareIcon className="h-4 w-4" />
<ExportIcon className="h-4 w-4" />
{t('gift.shareGift')}
</button>
</>
@@ -1219,7 +1110,7 @@ function MyGiftsTabContent() {
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-dark-400">
{t('gift.activeGiftsTitle')}
</h2>
<div className="space-y-3">
<div className="grid gap-3 sm:grid-cols-2">
{activeGifts.map((gift) => (
<SentGiftCard key={gift.token} gift={gift} />
))}
@@ -1233,7 +1124,7 @@ function MyGiftsTabContent() {
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-dark-400">
{t('gift.activatedGiftsTitle')}
</h2>
<div className="space-y-3">
<div className="grid gap-3 sm:grid-cols-2">
{activatedGifts.map((gift) => (
<SentGiftCard key={gift.token} gift={gift} />
))}
@@ -1247,7 +1138,7 @@ function MyGiftsTabContent() {
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-dark-400">
{t('gift.receivedGiftsTitle')}
</h2>
<div className="space-y-3">
<div className="grid gap-3 sm:grid-cols-2">
{receivedGifts!.map((gift) => (
<ReceivedGiftCard key={gift.token} gift={gift} />
))}
@@ -1315,7 +1206,7 @@ export default function GiftSubscription() {
return (
<div className="min-h-dvh">
<div className="mx-auto max-w-lg px-4 py-6">
<div className="mx-auto max-w-2xl px-4 py-6">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}