mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
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:
@@ -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:'),
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user