mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 10:27:49 +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 { useTranslation } from 'react-i18next';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { giftApi } from '../api/gift';
|
import { giftApi } from '../api/gift';
|
||||||
|
import { brandingApi, type TelegramWidgetConfig } from '../api/branding';
|
||||||
import { Spinner } from '@/components/ui/Spinner';
|
import { Spinner } from '@/components/ui/Spinner';
|
||||||
import { AnimatedCheckmark } from '@/components/ui/AnimatedCheckmark';
|
import { AnimatedCheckmark } from '@/components/ui/AnimatedCheckmark';
|
||||||
import { AnimatedCrossmark } from '@/components/ui/AnimatedCrossmark';
|
import { AnimatedCrossmark } from '@/components/ui/AnimatedCrossmark';
|
||||||
@@ -54,13 +55,25 @@ function CodeOnlySuccessState({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [copied, setCopied] = useState(false);
|
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 shortCode = purchaseToken.slice(0, 12);
|
||||||
const giftCode = `GIFT-${shortCode}`;
|
const giftCode = `GIFT-${shortCode}`;
|
||||||
const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME as string | undefined;
|
// Telegram forwards the start parameter to the bot verbatim (no URL-decoding),
|
||||||
// Encode underscores as %5F so Telegram auto-link detection doesn't strip them
|
// and "_" is a valid start-param char — so use a literal "GIFT_" prefix to
|
||||||
const safeCode = shortCode.replace(/_/g, '%5F');
|
// match the bot's `start_parameter.startswith('GIFT_')` handler. Encoding the
|
||||||
const botLink = botUsername ? `https://t.me/${botUsername}?start=GIFT%5F${safeCode}` : null;
|
// underscore as %5F made the bot receive "GIFT%5F…" and silently fail.
|
||||||
const cabinetLink = `${window.location.origin}/gift?tab=activate&code=${safeCode}`;
|
const botLink = botUsername ? `https://t.me/${botUsername}?start=GIFT_${shortCode}` : null;
|
||||||
|
const cabinetLink = `${window.location.origin}/gift?tab=activate&code=${encodeURIComponent(shortCode)}`;
|
||||||
|
|
||||||
const fullMessage = [
|
const fullMessage = [
|
||||||
t('gift.shareText', 'I have a gift for you! Activate it here:'),
|
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 { useTranslation } from 'react-i18next';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { giftApi } from '../api/gift';
|
import { giftApi } from '../api/gift';
|
||||||
|
import { brandingApi, type TelegramWidgetConfig } from '../api/branding';
|
||||||
import type {
|
import type {
|
||||||
GiftConfig,
|
GiftConfig,
|
||||||
GiftTariff,
|
GiftTariff,
|
||||||
@@ -21,113 +22,17 @@ import { getApiErrorMessage } from '../utils/api-error';
|
|||||||
import { formatPrice } from '../utils/format';
|
import { formatPrice } from '../utils/format';
|
||||||
import { useCurrency } from '../hooks/useCurrency';
|
import { useCurrency } from '../hooks/useCurrency';
|
||||||
import { usePlatform, useHaptic } from '@/platform';
|
import { usePlatform, useHaptic } from '@/platform';
|
||||||
import { SparklesIcon } from '@/components/icons';
|
import {
|
||||||
|
SparklesIcon,
|
||||||
function GiftIcon({ className }: { className?: string }) {
|
GiftIcon,
|
||||||
return (
|
CheckIcon,
|
||||||
<svg
|
CheckCircleIcon,
|
||||||
className={className}
|
KeyIcon,
|
||||||
viewBox="0 0 24 24"
|
InboxIcon,
|
||||||
fill="none"
|
ExportIcon,
|
||||||
stroke="currentColor"
|
WarningCircleIcon,
|
||||||
strokeWidth={1.5}
|
BanIcon,
|
||||||
strokeLinecap="round"
|
} from '@/components/icons';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatPeriodLabel(
|
function formatPeriodLabel(
|
||||||
days: number,
|
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 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 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">
|
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-error-500/10">
|
||||||
<svg
|
<WarningCircleIcon className="h-8 w-8 text-error-400" />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-lg font-semibold text-dark-50">{t('gift.failedTitle')}</h2>
|
<h2 className="text-lg font-semibold text-dark-50">{t('gift.failedTitle')}</h2>
|
||||||
<p className="text-sm text-dark-300">{message}</p>
|
<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 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 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">
|
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-dark-800/50">
|
||||||
<svg
|
<BanIcon className="h-8 w-8 text-dark-400" />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-lg font-semibold text-dark-50">{t('gift.featureDisabled')}</h2>
|
<h2 className="text-lg font-semibold text-dark-50">{t('gift.featureDisabled')}</h2>
|
||||||
<p className="text-sm text-dark-300">{t('gift.redirecting')}</p>
|
<p className="text-sm text-dark-300">{t('gift.redirecting')}</p>
|
||||||
@@ -991,6 +872,16 @@ function SentGiftCard({ gift }: { gift: SentGift }) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [showToast, setShowToast] = useState(false);
|
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 shortCode = gift.token.slice(0, 12);
|
||||||
const giftCode = `GIFT-${shortCode}`;
|
const giftCode = `GIFT-${shortCode}`;
|
||||||
const isActivated = isGiftActivated(gift);
|
const isActivated = isGiftActivated(gift);
|
||||||
@@ -1003,11 +894,11 @@ function SentGiftCard({ gift }: { gift: SentGift }) {
|
|||||||
: t(getGiftStatusKey(gift.status));
|
: t(getGiftStatusKey(gift.status));
|
||||||
|
|
||||||
const buildShareMessage = useCallback(() => {
|
const buildShareMessage = useCallback(() => {
|
||||||
const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME as string | undefined;
|
// Literal "GIFT_" prefix: Telegram forwards the start param to the bot
|
||||||
// Encode underscores as %5F so Telegram auto-link detection doesn't strip them
|
// verbatim (no URL-decoding), so the previously-encoded "%5F" never matched
|
||||||
const safeCode = shortCode.replace(/_/g, '%5F');
|
// the bot's `start_parameter.startswith('GIFT_')` handler.
|
||||||
const botLink = botUsername ? `https://t.me/${botUsername}?start=GIFT%5F${safeCode}` : null;
|
const botLink = botUsername ? `https://t.me/${botUsername}?start=GIFT_${shortCode}` : null;
|
||||||
const cabinetLink = `${window.location.origin}/gift?tab=activate&code=${safeCode}`;
|
const cabinetLink = `${window.location.origin}/gift?tab=activate&code=${encodeURIComponent(shortCode)}`;
|
||||||
return [
|
return [
|
||||||
t('gift.shareText'),
|
t('gift.shareText'),
|
||||||
'',
|
'',
|
||||||
@@ -1016,7 +907,7 @@ function SentGiftCard({ gift }: { gift: SentGift }) {
|
|||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}, [shortCode, t]);
|
}, [shortCode, botUsername, t]);
|
||||||
|
|
||||||
const handleShare = useCallback(async () => {
|
const handleShare = useCallback(async () => {
|
||||||
const message = buildShareMessage();
|
const message = buildShareMessage();
|
||||||
@@ -1070,7 +961,7 @@ function SentGiftCard({ gift }: { gift: SentGift }) {
|
|||||||
onClick={handleShare}
|
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]"
|
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')}
|
{t('gift.shareGift')}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
@@ -1219,7 +1110,7 @@ function MyGiftsTabContent() {
|
|||||||
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-dark-400">
|
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-dark-400">
|
||||||
{t('gift.activeGiftsTitle')}
|
{t('gift.activeGiftsTitle')}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-3">
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
{activeGifts.map((gift) => (
|
{activeGifts.map((gift) => (
|
||||||
<SentGiftCard key={gift.token} gift={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">
|
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-dark-400">
|
||||||
{t('gift.activatedGiftsTitle')}
|
{t('gift.activatedGiftsTitle')}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-3">
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
{activatedGifts.map((gift) => (
|
{activatedGifts.map((gift) => (
|
||||||
<SentGiftCard key={gift.token} gift={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">
|
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-dark-400">
|
||||||
{t('gift.receivedGiftsTitle')}
|
{t('gift.receivedGiftsTitle')}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-3">
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
{receivedGifts!.map((gift) => (
|
{receivedGifts!.map((gift) => (
|
||||||
<ReceivedGiftCard key={gift.token} gift={gift} />
|
<ReceivedGiftCard key={gift.token} gift={gift} />
|
||||||
))}
|
))}
|
||||||
@@ -1315,7 +1206,7 @@ export default function GiftSubscription() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-dvh">
|
<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 */}
|
{/* Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
|||||||
Reference in New Issue
Block a user