From 01e811bdfc5db4e0680c1b7d4bed5e91a66e503c Mon Sep 17 00:00:00 2001 From: Fringg Date: Thu, 12 Mar 2026 03:32:14 +0300 Subject: [PATCH 1/2] fix: handle Telegram Stars payment for gift subscriptions via openInvoice - Use openInvoice() with capabilities.hasInvoice guard for Stars payments - Fall back to redirect for web environments without invoice support - Add haptic feedback on payment success/failure --- src/pages/GiftSubscription.tsx | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/pages/GiftSubscription.tsx b/src/pages/GiftSubscription.tsx index 6ae22f2..0e8543c 100644 --- a/src/pages/GiftSubscription.tsx +++ b/src/pages/GiftSubscription.tsx @@ -19,6 +19,7 @@ import { cn } from '../lib/utils'; import { copyToClipboard } from '../utils/clipboard'; import { getApiErrorMessage } from '../utils/api-error'; import { formatPrice } from '../utils/format'; +import { usePlatform, useHaptic } from '@/platform'; // ============================================================ // SVG Icons @@ -524,6 +525,8 @@ function BuyTabContent({ }) { const { t } = useTranslation(); const queryClient = useQueryClient(); + const { openInvoice, capabilities } = usePlatform(); + const haptic = useHaptic(); // Selection state const [selectedTariffId, setSelectedTariffId] = useState(null); @@ -610,8 +613,29 @@ function BuyTabContent({ // Purchase mutation const purchaseMutation = useMutation({ mutationFn: (data: GiftPurchaseRequest) => giftApi.createPurchase(data), - onSuccess: (result) => { + onSuccess: async (result) => { if (result.payment_url) { + // Telegram Stars: open invoice natively instead of redirect + const isStars = selectedMethod === 'telegram_stars'; + if (isStars && capabilities.hasInvoice) { + try { + const status = await openInvoice(result.payment_url); + if (status === 'paid') { + haptic.notification('success'); + queryClient.invalidateQueries({ queryKey: ['balance'] }); + queryClient.invalidateQueries({ queryKey: ['gift-config'] }); + queryClient.invalidateQueries({ queryKey: ['gift-sent'] }); + onPurchaseComplete(); + } else if (status === 'failed') { + haptic.notification('error'); + setSubmitError(t('gift.failedDesc')); + } + // 'cancelled' — user closed the invoice, do nothing + } catch (e) { + setSubmitError(t('gift.failedDesc')); + } + return; + } window.location.href = result.payment_url; } else { // Balance purchase: switch to MyGifts tab so the new code is visible From 5a5a9878931234103761d40fb24893afdb16a817 Mon Sep 17 00:00:00 2001 From: Fringg Date: Thu, 12 Mar 2026 04:16:04 +0300 Subject: [PATCH 2/2] feat: add show_in_gift toggle UI for tariffs in admin panel Add pill toggle in tariff create/edit form and purple gift badge in tariff list. Includes i18n for ru, en, zh, fa locales. --- src/api/tariffs.ts | 4 ++++ src/locales/en.json | 3 +++ src/locales/fa.json | 3 +++ src/locales/ru.json | 3 +++ src/locales/zh.json | 3 +++ src/pages/AdminTariffCreate.tsx | 27 +++++++++++++++++++++++++++ src/pages/AdminTariffs.tsx | 18 ++++++++++++++++++ 7 files changed, 61 insertions(+) diff --git a/src/api/tariffs.ts b/src/api/tariffs.ts index 8664f24..d3bc1cb 100644 --- a/src/api/tariffs.ts +++ b/src/api/tariffs.ts @@ -32,6 +32,7 @@ export interface TariffListItem { description: string | null; is_active: boolean; is_trial_available: boolean; + show_in_gift: boolean; is_daily: boolean; daily_price_kopeks: number; traffic_limit_gb: number; @@ -54,6 +55,7 @@ export interface TariffDetail { description: string | null; is_active: boolean; is_trial_available: boolean; + show_in_gift: boolean; traffic_limit_gb: number; device_limit: number; device_price_kopeks: number | null; @@ -95,6 +97,7 @@ export interface TariffCreateRequest { name: string; description?: string; is_active?: boolean; + show_in_gift?: boolean; traffic_limit_gb?: number; device_limit?: number; device_price_kopeks?: number; @@ -137,6 +140,7 @@ export interface TariffUpdateRequest { name?: string; description?: string; is_active?: boolean; + show_in_gift?: boolean; traffic_limit_gb?: number; device_limit?: number; device_price_kopeks?: number; diff --git a/src/locales/en.json b/src/locales/en.json index 710abaf..601f0a3 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2044,6 +2044,9 @@ "statusTitle": "Tariff Status", "isActiveLabel": "Active", "isActiveHint": "Inactive tariffs are not shown to users", + "showInGiftLabel": "Show in gifts", + "showInGiftHint": "Tariff will be available for purchase in the gifts section", + "giftBadge": "Gift", "isTrialLabel": "Trial tariff", "isTrialHint": "Available for free trial period", "validation": { diff --git a/src/locales/fa.json b/src/locales/fa.json index 83d0a57..401bef2 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -1705,6 +1705,9 @@ "statusTitle": "وضعیت تعرفه", "isActiveLabel": "فعال", "isActiveHint": "تعرفه‌های غیرفعال به کاربران نمایش داده نمی‌شوند", + "showInGiftLabel": "نمایش در هدایا", + "showInGiftHint": "تعرفه برای خرید در بخش هدایا در دسترس خواهد بود", + "giftBadge": "هدیه", "isTrialLabel": "تعرفه آزمایشی", "isTrialHint": "برای دوره آزمایشی رایگان در دسترس است", "validation": { diff --git a/src/locales/ru.json b/src/locales/ru.json index f77d001..ff68633 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2558,6 +2558,9 @@ "statusTitle": "Статус тарифа", "isActiveLabel": "Активен", "isActiveHint": "Неактивные тарифы не отображаются пользователям", + "showInGiftLabel": "Видимость в подарках", + "showInGiftHint": "Тариф будет доступен для покупки в разделе подарков", + "giftBadge": "Подарок", "isTrialLabel": "Триальный тариф", "isTrialHint": "Доступен для бесплатного пробного периода", "validation": { diff --git a/src/locales/zh.json b/src/locales/zh.json index e9fad4d..639f69f 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1742,6 +1742,9 @@ "statusTitle": "套餐状态", "isActiveLabel": "已激活", "isActiveHint": "未激活的套餐不会显示给用户", + "showInGiftLabel": "在礼物中显示", + "showInGiftHint": "套餐将在礼物区域中可供购买", + "giftBadge": "礼物", "isTrialLabel": "试用套餐", "isTrialHint": "可用于免费试用期", "validation": { diff --git a/src/pages/AdminTariffCreate.tsx b/src/pages/AdminTariffCreate.tsx index f88ccbf..5c0f1e6 100644 --- a/src/pages/AdminTariffCreate.tsx +++ b/src/pages/AdminTariffCreate.tsx @@ -125,6 +125,9 @@ export default function AdminTariffCreate() { // Traffic reset mode const [trafficResetMode, setTrafficResetMode] = useState(null); + // Gift visibility + const [showInGift, setShowInGift] = useState(true); + // New period for adding const [newPeriodDays, setNewPeriodDays] = useState(30); const [newPeriodPrice, setNewPeriodPrice] = useState(300); @@ -182,6 +185,7 @@ export default function AdminTariffCreate() { setMaxTopupTrafficGb(data.max_topup_traffic_gb || 0); setTrafficTopupPackages(data.traffic_topup_packages || {}); setTrafficResetMode(data.traffic_reset_mode || null); + setShowInGift(data.show_in_gift ?? true); return data; }, []), }); @@ -212,6 +216,7 @@ export default function AdminTariffCreate() { name, description: description || undefined, is_active: isActive, + show_in_gift: showInGift, traffic_limit_gb: toNumber(trafficLimitGb, 100), device_limit: toNumber(deviceLimit, 1), device_price_kopeks: @@ -1113,6 +1118,28 @@ export default function AdminTariffCreate() { /> + {/* Show in gift toggle */} +
+
+ + {t('admin.tariffs.showInGiftLabel')} + +

{t('admin.tariffs.showInGiftHint')}

+
+ +
)} diff --git a/src/pages/AdminTariffs.tsx b/src/pages/AdminTariffs.tsx index c45831c..7817b99 100644 --- a/src/pages/AdminTariffs.tsx +++ b/src/pages/AdminTariffs.tsx @@ -173,6 +173,24 @@ function SortableTariffCard({ {t('admin.tariffs.trial')} )} + {tariff.show_in_gift && ( + + + + + {t('admin.tariffs.giftBadge')} + + )} {!tariff.is_active && ( {t('admin.tariffs.inactive')}