diff --git a/src/App.tsx b/src/App.tsx
index 6df73fb..f0c973f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -425,21 +425,25 @@ function App() {
-
-
-
-
+
+
+
+
+
+
+
}
/>
-
-
-
-
+
+
+
+
+
+
+
}
/>
(`/cabinet/gift/purchase/${token}`);
return data;
},
+
+ getPendingGifts: async (): Promise => {
+ const { data } = await apiClient.get('/cabinet/gift/pending');
+ return data;
+ },
};
diff --git a/src/components/dashboard/PendingGiftCard.tsx b/src/components/dashboard/PendingGiftCard.tsx
new file mode 100644
index 0000000..85240ad
--- /dev/null
+++ b/src/components/dashboard/PendingGiftCard.tsx
@@ -0,0 +1,80 @@
+import { Link } from 'react-router';
+import { useTranslation } from 'react-i18next';
+import { motion } from 'framer-motion';
+import type { PendingGift } from '../../api/gift';
+
+interface PendingGiftCardProps {
+ gifts: PendingGift[];
+ className?: string;
+}
+
+export default function PendingGiftCard({ gifts, className }: PendingGiftCardProps) {
+ const { t } = useTranslation();
+
+ if (gifts.length === 0) return null;
+
+ return (
+
+ {gifts.map((gift) => (
+
+ {/* Subtle glow effect */}
+
+
+
+ {/* Gift icon */}
+
+
+ {/* Content */}
+
+
{t('gift.pending.title')}
+
+ {gift.tariff_name && (
+
+ {gift.tariff_name} — {gift.period_days} {t('gift.days')}
+
+ )}
+ {gift.sender_display && (
+
+ {t('gift.pending.from', { sender: gift.sender_display })}
+
+ )}
+
+ {gift.gift_message && (
+
+ “{gift.gift_message}”
+
+ )}
+
+
+ {/* Activate button */}
+
+ {t('gift.pending.activate')}
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/locales/en.json b/src/locales/en.json
index c1554a6..ad055ea 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -4152,13 +4152,26 @@
"tryAgain": "Try again",
"pollTimeout": "Processing is taking longer than usual",
"pollTimeoutDesc": "Try checking the status later",
+ "pollErrorTitle": "Could not check gift status",
+ "pollErrorDesc": "Your purchase was successful. Check your dashboard for details.",
"retry": "Check again",
+ "notFound": "Gift configuration not found",
"noToken": "Invalid link",
+ "noTokenDesc": "This gift link is invalid or has expired.",
+ "days": "days",
"tariff": "Tariff",
"period": "Period",
"giftMessageLabel": "Message",
"recipientLabel": "Recipient",
"featureDisabled": "Gift feature is temporarily unavailable",
- "redirecting": "Redirecting..."
+ "redirecting": "Redirecting...",
+ "pending": {
+ "title": "You received a gift!",
+ "from": "from {{sender}}",
+ "activate": "Activate"
+ },
+ "warning": {
+ "telegram_unresolvable": "Could not verify this Telegram username. The recipient may not receive a notification about the gift."
+ }
}
}
diff --git a/src/locales/fa.json b/src/locales/fa.json
index de87e73..f94bde1 100644
--- a/src/locales/fa.json
+++ b/src/locales/fa.json
@@ -3647,13 +3647,26 @@
"tryAgain": "تلاش مجدد",
"pollTimeout": "پردازش بیش از حد معمول طول کشیده",
"pollTimeoutDesc": "بعداً وضعیت را بررسی کنید",
+ "pollErrorTitle": "بررسی وضعیت هدیه امکانپذیر نیست",
+ "pollErrorDesc": "خرید شما موفقیتآمیز بود. وضعیت را در داشبورد بررسی کنید.",
"retry": "بررسی مجدد",
+ "notFound": "تنظیمات هدیه یافت نشد",
"noToken": "لینک نامعتبر",
+ "noTokenDesc": "این لینک هدیه نامعتبر است یا منقضی شده.",
+ "days": "روز",
"tariff": "طرح",
"period": "مدت",
"giftMessageLabel": "پیام",
"recipientLabel": "گیرنده",
"featureDisabled": "قابلیت هدیه موقتاً در دسترس نیست",
- "redirecting": "در حال انتقال..."
+ "redirecting": "در حال انتقال...",
+ "pending": {
+ "title": "شما یک هدیه دریافت کردید!",
+ "from": "از {{sender}}",
+ "activate": "فعالسازی"
+ },
+ "warning": {
+ "telegram_unresolvable": "نام کاربری تلگرام قابل تأیید نبود. ممکن است گیرنده اعلان هدیه را دریافت نکند."
+ }
}
}
diff --git a/src/locales/ru.json b/src/locales/ru.json
index a755e3d..6abfecc 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -4714,13 +4714,26 @@
"tryAgain": "Попробовать снова",
"pollTimeout": "Обработка занимает больше времени, чем обычно",
"pollTimeoutDesc": "Попробуйте проверить статус позже",
+ "pollErrorTitle": "Не удалось проверить статус подарка",
+ "pollErrorDesc": "Ваша покупка прошла успешно. Проверьте статус на главной странице.",
"retry": "Проверить снова",
+ "notFound": "Конфигурация подарков не найдена",
"noToken": "Ссылка недействительна",
+ "noTokenDesc": "Ссылка на подарок недействительна или просрочена.",
+ "days": "дн.",
"tariff": "Тариф",
"period": "Период",
"giftMessageLabel": "Сообщение",
"recipientLabel": "Получатель",
"featureDisabled": "Функция подарков временно недоступна",
- "redirecting": "Перенаправляем..."
+ "redirecting": "Перенаправляем...",
+ "pending": {
+ "title": "Вам подарили подписку!",
+ "from": "от {{sender}}",
+ "activate": "Активировать"
+ },
+ "warning": {
+ "telegram_unresolvable": "Не удалось проверить этот Telegram-юзернейм. Получатель может не получить уведомление о подарке."
+ }
}
}
diff --git a/src/locales/zh.json b/src/locales/zh.json
index a53b03c..c7eb3b1 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -3646,13 +3646,26 @@
"tryAgain": "重试",
"pollTimeout": "处理时间超出预期",
"pollTimeoutDesc": "请稍后再查看状态",
+ "pollErrorTitle": "无法检查礼物状态",
+ "pollErrorDesc": "您的购买已成功。请在仪表板上查看详情。",
"retry": "再次检查",
+ "notFound": "未找到礼物配置",
"noToken": "无效链接",
+ "noTokenDesc": "此礼物链接无效或已过期。",
+ "days": "天",
"tariff": "套餐",
"period": "时长",
"giftMessageLabel": "消息",
"recipientLabel": "收件人",
"featureDisabled": "礼物功能暂时不可用",
- "redirecting": "正在跳转..."
+ "redirecting": "正在跳转...",
+ "pending": {
+ "title": "您收到了一份礼物!",
+ "from": "来自 {{sender}}",
+ "activate": "激活"
+ },
+ "warning": {
+ "telegram_unresolvable": "无法验证此 Telegram 用户名。收件人可能不会收到礼物通知。"
+ }
}
}
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx
index 0cc715c..7564778 100644
--- a/src/pages/Dashboard.tsx
+++ b/src/pages/Dashboard.tsx
@@ -14,6 +14,8 @@ import SubscriptionCardActive from '../components/dashboard/SubscriptionCardActi
import SubscriptionCardExpired from '../components/dashboard/SubscriptionCardExpired';
import TrialOfferCard from '../components/dashboard/TrialOfferCard';
import StatsGrid from '../components/dashboard/StatsGrid';
+import { giftApi } from '../api/gift';
+import PendingGiftCard from '../components/dashboard/PendingGiftCard';
import { API } from '../config/constants';
const ChevronRightIcon = () => (
@@ -80,6 +82,13 @@ export default function Dashboard() {
retry: false,
});
+ const { data: pendingGifts } = useQuery({
+ queryKey: ['pending-gifts'],
+ queryFn: giftApi.getPendingGifts,
+ staleTime: 30_000,
+ retry: false,
+ });
+
const activateTrialMutation = useMutation({
mutationFn: subscriptionApi.activateTrial,
onSuccess: () => {
@@ -221,6 +230,9 @@ export default function Dashboard() {
{t('dashboard.yourSubscription')}
+ {/* Pending Gift Activations */}
+ {pendingGifts && pendingGifts.length > 0 && }
+
{/* Subscription Status Card */}
{subLoading ? (
diff --git a/src/pages/GiftResult.tsx b/src/pages/GiftResult.tsx
index a1361b8..7083d60 100644
--- a/src/pages/GiftResult.tsx
+++ b/src/pages/GiftResult.tsx
@@ -10,6 +10,8 @@ import { AnimatedCrossmark } from '@/components/ui/AnimatedCrossmark';
const MAX_POLL_MS = 10 * 60 * 1000; // 10 minutes
+const KNOWN_WARNINGS = new Set(['telegram_unresolvable']);
+
// ============================================================
// Sub-components
// ============================================================
@@ -29,7 +31,7 @@ function PendingState() {
{t('gift.processing', 'Processing your gift...')}
- {t('gift.processingDesc', 'Please wait while we process your payment')}
+ {t('gift.pendingDesc', 'Please wait while we process your payment')}
@@ -41,11 +43,13 @@ function DeliveredState({
tariffName,
periodDays,
giftMessage,
+ warning,
}: {
recipientContact: string | null;
tariffName: string | null;
periodDays: number | null;
giftMessage: string | null;
+ warning: string | null;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
@@ -59,7 +63,7 @@ function DeliveredState({
-
{t('gift.sent', 'Gift sent!')}
+
{t('gift.successTitle', 'Gift sent!')}
{tariffName && periodDays !== null && (
{tariffName} — {periodDays} {t('gift.days', 'days')}
@@ -67,7 +71,7 @@ function DeliveredState({
)}
{recipientContact && (
- {t('gift.sentTo', {
+ {t('gift.successDesc', {
contact: recipientContact,
defaultValue: `Sent to ${recipientContact}`,
})}
@@ -78,6 +82,12 @@ function DeliveredState({
)}
+ {warning && (
+
+
{t(`gift.warning.${warning}`)}
+
+ )}
+
@@ -650,7 +650,6 @@ export default function GiftSubscription() {
const [paymentMode, setPaymentMode] = useState<'balance' | 'gateway'>('balance');
const [selectedMethod, setSelectedMethod] = useState(null);
const [selectedSubOption, setSelectedSubOption] = useState(null);
- const [isSubmitting, setIsSubmitting] = useState(false);
const [submitError, setSubmitError] = useState(null);
// Collect ALL unique periods across ALL tariffs
@@ -749,24 +748,27 @@ export default function GiftSubscription() {
// Balance mode - show success
queryClient.invalidateQueries({ queryKey: ['balance'] });
queryClient.invalidateQueries({ queryKey: ['gift-config'] });
- navigate('/gift/result?token=' + result.purchase_token + '&mode=balance');
+ const params = new URLSearchParams({ token: result.purchase_token, mode: 'balance' });
+ if (result.warning) {
+ params.set('warning', result.warning);
+ }
+ navigate('/gift/result?' + params.toString());
}
},
onError: (err) => {
const msg = getApiErrorMessage(
err,
- t('gift.purchaseError', 'Something went wrong. Please try again.'),
+ t('gift.failedDesc', 'Something went wrong. Please try again.'),
);
setSubmitError(msg);
- setIsSubmitting(false);
},
});
// Submit handler
const handleSubmit = () => {
- if (!canSubmit || isSubmitting) return;
+ if (!selectedTariffId || !selectedPeriodDays || !canSubmit || purchaseMutation.isPending)
+ return;
- setIsSubmitting(true);
setSubmitError(null);
let paymentMethod: string | undefined;
@@ -778,8 +780,8 @@ export default function GiftSubscription() {
}
const data: GiftPurchaseRequest = {
- tariff_id: selectedTariffId!,
- period_days: selectedPeriodDays!,
+ tariff_id: selectedTariffId,
+ period_days: selectedPeriodDays,
recipient_type: detectContactType(recipientValue),
recipient_value: recipientValue.trim(),
gift_message: giftMessage.trim() || undefined,
@@ -885,7 +887,7 @@ export default function GiftSubscription() {
{/* Recipient */}
- {t('gift.recipientSection', 'Recipient')}
+ {t('gift.recipientLabel', 'Recipient')}
- {t('gift.messageSection', 'Personal message')}
+ {t('gift.giftMessageLabel', 'Message')}
@@ -907,7 +909,7 @@ export default function GiftSubscription() {
{/* Payment mode toggle */}
- {t('gift.paymentModeSection', 'Payment method')}
+ {t('gift.paymentMode', 'Payment method')}