diff --git a/src/api/gift.ts b/src/api/gift.ts
index a009b5a..72c7d89 100644
--- a/src/api/gift.ts
+++ b/src/api/gift.ts
@@ -70,6 +70,8 @@ export type GiftPurchaseStatusValue =
export interface GiftPurchaseStatus {
status: GiftPurchaseStatusValue;
is_gift: boolean;
+ is_code_only: boolean;
+ purchase_token: string | null;
recipient_contact_value: string | null;
gift_message: string | null;
tariff_name: string | null;
diff --git a/src/locales/en.json b/src/locales/en.json
index 6d9951d..e5b53d8 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -4233,9 +4233,21 @@
"codeCopied": "Code copied!",
"shareGift": "SHARE",
"shareText": "I have a gift for you! Activate it here:",
+ "codeReadyTitle": "Gift code is ready!",
+ "codeLabel": "Gift code",
+ "sharePreview": "Message to share",
+ "copyMessage": "Copy",
"activatedBy": "Activated by {{username}}",
"sentTo": "Sent to {{recipient}}",
"daysShort": "days",
- "devicesShort": "dev."
+ "devicesShort": "dev.",
+ "gbShort": "GB",
+ "unlimitedTraffic": "Unlimited",
+ "shareModalTitle": "Share gift",
+ "shareModalDesc": "Send this message to the gift recipient",
+ "shareModalActivateVia": "Activate via bot:",
+ "shareModalActivateViaCabinet": "Or via website:",
+ "shareModalCopyAll": "Copy message",
+ "shareModalCopied": "Copied!"
}
}
diff --git a/src/locales/ru.json b/src/locales/ru.json
index fb98fb2..0e12e78 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -4770,8 +4770,10 @@
"tabMyGifts": "Мои подарки",
"selectTariff": "ВЫБЕРИТЕ ТАРИФ",
"selectPeriod": "ПЕРИОД ПОДПИСКИ",
- "deviceCount": "{{count}} устройство",
- "deviceCount_other": "{{count}} устройств",
+ "deviceCount": "{{count}} устройств",
+ "deviceCount_one": "{{count}} устройство",
+ "deviceCount_few": "{{count}} устройства",
+ "deviceCount_many": "{{count}} устройств",
"activateTitle": "Введите код подарка",
"activateDescription": "Введите код подарка, полученный от друга или знакомого",
"activateCodePlaceholder": "GIFT-XXXXXXXXXXXX",
@@ -4795,9 +4797,24 @@
"codeCopied": "Код скопирован!",
"shareGift": "ПОДЕЛИТЬСЯ",
"shareText": "У меня есть подарок для тебя! Активируй его здесь:",
+ "codeReadyTitle": "Код подарка готов!",
+ "codeLabel": "Код подарка",
+ "sharePreview": "Сообщение для отправки",
+ "copyMessage": "Копировать",
"activatedBy": "Активирован пользователем {{username}}",
"sentTo": "Отправлен {{recipient}}",
"daysShort": "дн.",
- "devicesShort": "устр."
+ "devicesShort": "устр.",
+ "devicesShort_one": "устр.",
+ "devicesShort_few": "устр.",
+ "devicesShort_many": "устр.",
+ "gbShort": "ГБ",
+ "unlimitedTraffic": "Безлимит",
+ "shareModalTitle": "Поделиться подарком",
+ "shareModalDesc": "Отправьте это сообщение получателю подарка",
+ "shareModalActivateVia": "Активировать через бота:",
+ "shareModalActivateViaCabinet": "Или через сайт:",
+ "shareModalCopyAll": "Скопировать сообщение",
+ "shareModalCopied": "Скопировано!"
}
}
diff --git a/src/pages/GiftResult.tsx b/src/pages/GiftResult.tsx
index 6193e16..4828145 100644
--- a/src/pages/GiftResult.tsx
+++ b/src/pages/GiftResult.tsx
@@ -7,6 +7,11 @@ import { giftApi } from '../api/gift';
import { Spinner } from '@/components/ui/Spinner';
import { AnimatedCheckmark } from '@/components/ui/AnimatedCheckmark';
import { AnimatedCrossmark } from '@/components/ui/AnimatedCrossmark';
+import { cn } from '@/lib/utils';
+
+function formatGiftCode(token: string): string {
+ return `GIFT-${token.slice(0, 12).toUpperCase()}`;
+}
const MAX_POLL_MS = 10 * 60 * 1000; // 10 minutes
@@ -38,6 +43,154 @@ function PendingState() {
);
}
+function CodeOnlySuccessState({
+ purchaseToken,
+ tariffName,
+ periodDays,
+}: {
+ purchaseToken: string;
+ tariffName: string | null;
+ periodDays: number | null;
+}) {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const [copied, setCopied] = useState(false);
+
+ const giftCode = formatGiftCode(purchaseToken);
+ const botUsername = import.meta.env.VITE_BOT_USERNAME as string | undefined;
+ const botLink = botUsername
+ ? `https://t.me/${botUsername}?start=GIFTCODE_${purchaseToken}`
+ : null;
+ const cabinetLink = `${window.location.origin}/gift?tab=activate&code=${purchaseToken}`;
+
+ const fullMessage = [
+ t('gift.shareText', 'I have a gift for you! Activate it here:'),
+ '',
+ botLink ? `${t('gift.shareModalActivateVia', 'Activate via bot:')} ${botLink}` : null,
+ `${t('gift.shareModalActivateViaCabinet', 'Or via website:')} ${cabinetLink}`,
+ ]
+ .filter(Boolean)
+ .join('\n');
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(fullMessage);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch {
+ // fallback
+ }
+ };
+
+ return (
+
+
+
+
+
+ {t('gift.codeReadyTitle', 'Gift code is ready!')}
+
+ {tariffName && periodDays !== null && (
+
+ {tariffName} — {periodDays} {t('gift.days', 'days')}
+
+ )}
+
+
+ {/* Gift code display */}
+
+
+ {t('gift.codeLabel', 'Gift code')}
+
+
{giftCode}
+
+
+ {/* Share message preview */}
+
+
+ {t('gift.shareText', 'I have a gift for you! Activate it here:')}
+
+
+ {botLink && (
+
+
+ {t('gift.shareModalActivateVia', 'Activate via bot:')}
+
+
+ {botLink}
+
+
+ )}
+
+
+
+ {t('gift.shareModalActivateViaCabinet', 'Or via website:')}
+
+
+ {cabinetLink}
+
+
+
+
+ {/* Copy button */}
+
+
+
+
+ );
+}
+
function DeliveredState({
recipientContact,
tariffName,
@@ -375,9 +528,12 @@ export default function GiftResult() {
// Balance mode: fetch once, no polling
if (isBalanceMode) return false;
- const s = query.state.data?.status;
+ const d = query.state.data;
+ const s = d?.status;
if (s === 'delivered' || s === 'failed' || s === 'pending_activation' || s === 'expired')
return false;
+ // Code-only gifts stay in 'paid' status — stop polling
+ if (s === 'paid' && d?.is_code_only) return false;
// Check poll timeout
if (Date.now() - pollStart.current > MAX_POLL_MS) {
@@ -411,6 +567,7 @@ export default function GiftResult() {
);
}
+ const isCodeOnlyPaid = status?.status === 'paid' && status?.is_code_only;
const isDelivered = status?.status === 'delivered';
const isPendingActivation = status?.status === 'pending_activation';
const isFailed = status?.status === 'failed' || status?.status === 'expired';
@@ -429,6 +586,12 @@ export default function GiftResult() {
>
{isError ? (
+ ) : isCodeOnlyPaid ? (
+
) : isDelivered ? (
+ {tariff.traffic_limit_gb > 0
+ ? `${tariff.traffic_limit_gb} ${t('gift.gbShort')}`
+ : t('gift.unlimitedTraffic')}
+ {' \u2022 '}
{t('gift.deviceCount', { count: tariff.device_limit })}
@@ -699,6 +703,13 @@ function BuyTabContent({
)}
+ {/* Selected tariff description */}
+ {selectedTariff?.description && (
+
+
{selectedTariff.description}
+
+ )}
+
{/* Period selection */}
{periodsForDisplay.length > 0 && (
@@ -839,10 +850,15 @@ function BuyTabContent({
// Sub-components: Activate Tab
// ============================================================
-function ActivateTabContent() {
+function ActivateTabContent({ initialCode }: { initialCode?: string | null }) {
const { t } = useTranslation();
const queryClient = useQueryClient();
- const [code, setCode] = useState('');
+ const [code, setCode] = useState(initialCode?.toUpperCase() ?? '');
+
+ // Sync when initialCode changes (e.g. URL param update while tab is active)
+ useEffect(() => {
+ if (initialCode) setCode(initialCode.toUpperCase());
+ }, [initialCode]);
const [activateError, setActivateError] = useState
(null);
const activateMutation = useMutation({
@@ -949,10 +965,154 @@ function ActivateTabContent() {
// Sub-components: My Gifts Tab
// ============================================================
-function SentGiftCard({ gift }: { gift: SentGift }) {
+function ShareModal({ gift, onClose }: { gift: SentGift; onClose: () => void }) {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
+ const botUsername = import.meta.env.VITE_BOT_USERNAME as string | undefined;
+ const botLink = botUsername ? `https://t.me/${botUsername}?start=GIFTCODE_${gift.token}` : null;
+ const cabinetLink = `${window.location.origin}/gift?tab=activate&code=${gift.token}`;
+
+ const fullMessage = [
+ t('gift.shareText'),
+ '',
+ botLink ? `${t('gift.shareModalActivateVia')} ${botLink}` : null,
+ `${t('gift.shareModalActivateViaCabinet')} ${cabinetLink}`,
+ ]
+ .filter(Boolean)
+ .join('\n');
+
+ const handleCopyAll = async () => {
+ try {
+ await navigator.clipboard.writeText(fullMessage);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch {
+ // fallback
+ }
+ };
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Modal content */}
+ e.stopPropagation()}
+ >
+ {/* Header */}
+
+
+
+
+
+
+
{t('gift.shareModalTitle')}
+
{t('gift.shareModalDesc')}
+
+
+
+
+
+ {/* Message preview */}
+
+
+
{t('gift.shareText')}
+
+ {botLink && (
+
+ )}
+
+
+
+
+
+ {/* Actions */}
+
+
+
+
+
+ );
+}
+
+function SentGiftCard({ gift }: { gift: SentGift }) {
+ const { t } = useTranslation();
+ const [showShareModal, setShowShareModal] = useState(false);
+
const giftCode = `GIFT-${gift.token.slice(0, 12).toUpperCase()}`;
const isActivated = isGiftActivated(gift);
const isAvailable = !isActivated && isGiftAvailable(gift.status);
@@ -963,40 +1123,6 @@ function SentGiftCard({ gift }: { gift: SentGift }) {
? t('gift.statusAvailable')
: t(getGiftStatusKey(gift.status));
- const handleCopy = async () => {
- try {
- await navigator.clipboard.writeText(gift.token);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- } catch {
- // Clipboard API not available
- }
- };
-
- const handleShare = async () => {
- const botUsername = import.meta.env.VITE_BOT_USERNAME as string | undefined;
- if (!botUsername) return;
- const shareUrl = `https://t.me/${botUsername}?start=GIFTCODE_${gift.token}`;
- const shareText = t('gift.shareText');
-
- if (navigator.share) {
- try {
- await navigator.share({
- text: `${shareText} ${shareUrl}`,
- });
- return;
- } catch {
- // User cancelled or not available — fallback to clipboard
- }
- }
-
- try {
- await navigator.clipboard.writeText(shareUrl);
- } catch {
- // Clipboard API not available
- }
- };
-
return (
{/* Header: tariff name + status badge */}
@@ -1022,7 +1148,7 @@ function SentGiftCard({ gift }: { gift: SentGift }) {
{' \u2022 '}
{gift.period_days} {t('gift.daysShort')}
{' \u2022 '}
- {gift.device_limit} {t('gift.devicesShort')}
+ {gift.device_limit} {t('gift.devicesShort', { count: gift.device_limit })}
{/* Gift code + actions (only when not activated) */}
@@ -1035,20 +1161,10 @@ function SentGiftCard({ gift }: { gift: SentGift }) {
- {/* Copy button */}
+ {/* Share button — opens modal */}
-
- {/* Share button */}
-
);
}
@@ -1096,7 +1217,7 @@ function ReceivedGiftCard({ gift }: { gift: ReceivedGift }) {
{' \u2022 '}
{gift.period_days} {t('gift.daysShort')}
{' \u2022 '}
- {gift.device_limit} {t('gift.devicesShort')}
+ {gift.device_limit} {t('gift.devicesShort', { count: gift.device_limit })}
{/* Sender */}
@@ -1221,8 +1342,14 @@ const tabContentVariants = {
export default function GiftSubscription() {
const { t } = useTranslation();
+ const [searchParams] = useSearchParams();
- const [activeTab, setActiveTab] = useState('buy');
+ // URL params: ?tab=activate&code=TOKEN for auto-activation
+ const urlTab = searchParams.get('tab') as TabId | null;
+ const urlCode = searchParams.get('code');
+ const [activeTab, setActiveTab] = useState(
+ urlTab === 'activate' || urlTab === 'myGifts' ? urlTab : 'buy',
+ );
// Fetch config
const {
@@ -1319,7 +1446,7 @@ export default function GiftSubscription() {
{activeTab === 'buy' && (
setActiveTab('myGifts')} />
)}
- {activeTab === 'activate' && }
+ {activeTab === 'activate' && }
{activeTab === 'myGifts' && }