diff --git a/src/components/dashboard/SubscriptionCardExpired.tsx b/src/components/dashboard/SubscriptionCardExpired.tsx index 361e727..4709ded 100644 --- a/src/components/dashboard/SubscriptionCardExpired.tsx +++ b/src/components/dashboard/SubscriptionCardExpired.tsx @@ -228,7 +228,11 @@ export default function SubscriptionCardExpired({ >
- {isLimited ? t('dashboard.expired.activeUntil') : t('dashboard.expired.expiredDate')} + {isLimited + ? t('dashboard.expired.activeUntil') + : t('dashboard.expired.expiredDate', { + context: subscription.is_trial ? 'trial' : '', + })}
{formattedDate} diff --git a/src/components/layout/AppShell/AppHeader.tsx b/src/components/layout/AppShell/AppHeader.tsx index 7d2c89f..3cac5df 100644 --- a/src/components/layout/AppShell/AppHeader.tsx +++ b/src/components/layout/AppShell/AppHeader.tsx @@ -5,6 +5,7 @@ import { useState, useEffect } from 'react'; import { initDataUser } from '@telegram-apps/sdk-react'; import { useAuthStore } from '@/store/auth'; +import { displayName } from '@/utils/displayName'; import { useShallow } from 'zustand/shallow'; import { useTheme } from '@/hooks/useTheme'; import { usePlatform } from '@/platform'; @@ -340,9 +341,7 @@ export function AppHeader({
-
- {user?.first_name || user?.username} -
+
{displayName(user)}
@{user?.username || `ID: ${user?.telegram_id}`}
diff --git a/src/components/layout/AppShell/DesktopSidebar.tsx b/src/components/layout/AppShell/DesktopSidebar.tsx index 7799435..99b9ad9 100644 --- a/src/components/layout/AppShell/DesktopSidebar.tsx +++ b/src/components/layout/AppShell/DesktopSidebar.tsx @@ -4,6 +4,7 @@ import { motion } from 'framer-motion'; import { useQuery } from '@tanstack/react-query'; import { useAuthStore } from '@/store/auth'; +import { displayName } from '@/utils/displayName'; import { brandingApi, getCachedBranding, @@ -188,9 +189,7 @@ export function DesktopSidebar({
-

- {user?.first_name || user?.username || `#${user?.telegram_id}`} -

+

{displayName(user)}

@{user?.username || `ID: ${user?.telegram_id}`}

diff --git a/src/hooks/useAnalyticsCounters.ts b/src/hooks/useAnalyticsCounters.ts index 2b5ee7f..f2e9f51 100644 --- a/src/hooks/useAnalyticsCounters.ts +++ b/src/hooks/useAnalyticsCounters.ts @@ -136,21 +136,21 @@ function syncYandexCid(counterId: string) { if (!token) return; // Route through brandingApi (apiClient) so baseURL, auth refresh, and // error handling all flow through the same interceptors as every other - // cabinet API call. - import('../api/branding').then(({ brandingApi: api }) => { - api - .storeYandexCid(cid) - .then(() => { - try { - localStorage.setItem(SENT_KEY, '1'); - } catch { - /* ignore */ - } - }) - .catch(() => { - /* swallow -- non-critical, will retry on next login */ - }); - }); + // cabinet API call. brandingApi уже импортирован статически — динамический + // import('../api/branding') ломал code-splitting (Vite warning о том, что + // модуль не может быть вынесен в отдельный chunk при mixed static+dynamic). + brandingApi + .storeYandexCid(cid) + .then(() => { + try { + localStorage.setItem(SENT_KEY, '1'); + } catch { + /* ignore */ + } + }) + .catch(() => { + /* swallow -- non-critical, will retry on next login */ + }); }); } catch { /* ignore */ diff --git a/src/hooks/useCurrency.ts b/src/hooks/useCurrency.ts index c58462d..b7ed8e7 100644 --- a/src/hooks/useCurrency.ts +++ b/src/hooks/useCurrency.ts @@ -1,7 +1,8 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { currencyApi, type ExchangeRates } from '../api/currency'; +import { setExchangeRates as setGlobalExchangeRates } from '../utils/format'; // Map language to currency const LANGUAGE_CURRENCY_MAP: Record = { @@ -30,6 +31,12 @@ export function useCurrency() { retry: 2, }); + // Делимся курсом с синхронным formatPrice из utils/format.ts — + // у него нет своего источника rates, а на лендинге его дёргают subcomponents. + useEffect(() => { + setGlobalExchangeRates(exchangeRates); + }, [exchangeRates]); + // Get current language and currency const currentLanguage = i18n.language; const targetCurrency = LANGUAGE_CURRENCY_MAP[currentLanguage] || 'USD'; diff --git a/src/locales/en.json b/src/locales/en.json index 2af6e2e..dbb137c 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1515,6 +1515,8 @@ "noMethods": "No payment methods configured", "methodEnabled": "Method enabled", "providerNotConfigured": "Provider not configured in env", + "openUrlDirect": "Open payment page directly", + "openUrlDirectHint": "Skip the link panel — the provider opens inside the MiniApp/tab right after the click. After payment the user returns to /balance/top-up/result. Ignored for Stars/CryptoBot and other t.me/ links.", "displayName": "Display name", "displayNameHint": "Leave empty for default name", "subOptions": "Payment options", diff --git a/src/locales/fa.json b/src/locales/fa.json index 854ed89..c26fb6d 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -2082,6 +2082,8 @@ "noMethods": "هیچ روش پرداخت پیکربندی‌شده‌ای نیست", "methodEnabled": "روش فعال است", "providerNotConfigured": "ارائه‌دهنده در env تنظیم نشده", + "openUrlDirect": "صفحه پرداخت را مستقیماً باز کن", + "openUrlDirectHint": "بدون پنل لینک — ارائه‌دهنده داخل MiniApp/تب بلافاصله پس از کلیک باز می‌شود. پس از پرداخت کاربر به /balance/top-up/result بازمی‌گردد. برای Stars/CryptoBot و سایر لینک‌های t.me/ این پرچم نادیده گرفته می‌شود.", "displayName": "نام نمایشی", "displayNameHint": "برای نام پیش‌فرض خالی بگذارید", "subOptions": "گزینه‌های پرداخت", diff --git a/src/locales/ru.json b/src/locales/ru.json index 22b67af..394b2a0 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -285,6 +285,7 @@ "traffic": "Трафик", "devices": "Устройства", "expiredDate": "Истекла", + "expiredDate_trial": "Истёк", "activeUntil": "Активна до" }, "suspended": { @@ -1536,6 +1537,8 @@ "noMethods": "Нет настроенных платёжных методов", "methodEnabled": "Метод включён", "providerNotConfigured": "Провайдер не настроен в env", + "openUrlDirect": "Открывать страницу оплаты сразу", + "openUrlDirectHint": "Без панели со ссылкой — провайдер открывается внутри MiniApp/вкладки сразу после клика. После оплаты юзер возвращается на /balance/top-up/result. Для Stars/CryptoBot и других t.me/-ссылок флаг игнорируется.", "displayName": "Отображаемое имя", "displayNameHint": "Оставьте пустым для имени по умолчанию", "subOptions": "Варианты оплаты", diff --git a/src/locales/zh.json b/src/locales/zh.json index 38ad838..dcac98d 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1280,6 +1280,8 @@ "noMethods": "没有配置的支付方法", "methodEnabled": "方法已启用", "providerNotConfigured": "提供商未在env中配置", + "openUrlDirect": "直接打开支付页面", + "openUrlDirectHint": "跳过链接面板 — 点击后提供商在 MiniApp/标签页内直接打开。支付完成后用户返回 /balance/top-up/result。Stars/CryptoBot 和其他 t.me/ 链接忽略此标志。", "displayName": "显示名称", "displayNameHint": "留空以使用默认名称", "subOptions": "支付选项", diff --git a/src/pages/AdminLandingEditor.tsx b/src/pages/AdminLandingEditor.tsx index ef9f395..a4d18a7 100644 --- a/src/pages/AdminLandingEditor.tsx +++ b/src/pages/AdminLandingEditor.tsx @@ -13,6 +13,7 @@ import { } from '../api/landings'; import { tariffsApi, TariffListItem, PeriodPrice } from '../api/tariffs'; import { formatPrice } from '../utils/format'; +import { useCurrency } from '../hooks/useCurrency'; import { adminPaymentMethodsApi } from '../api/adminPaymentMethods'; import { Toggle, LocaleTabs, LocalizedInput } from '../components/admin'; import { BackgroundConfigEditor } from '../components/admin/BackgroundConfigEditor'; @@ -97,6 +98,9 @@ export default function AdminLandingEditor() { const { capabilities } = usePlatform(); const isEdit = !!id; + // Прогреваем кэш курсов валют для formatPrice (preview лендинга в не-RU локали). + useCurrency(); + // Section visibility const [openSections, setOpenSections] = useState>({ general: true, diff --git a/src/pages/AdminPaymentMethodEdit.tsx b/src/pages/AdminPaymentMethodEdit.tsx index 2f76918..782feab 100644 --- a/src/pages/AdminPaymentMethodEdit.tsx +++ b/src/pages/AdminPaymentMethodEdit.tsx @@ -66,6 +66,7 @@ export default function AdminPaymentMethodEdit() { const [firstTopupFilter, setFirstTopupFilter] = useState<'any' | 'yes' | 'no'>('any'); const [promoGroupFilterMode, setPromoGroupFilterMode] = useState<'all' | 'selected'>('all'); const [selectedPromoGroupIds, setSelectedPromoGroupIds] = useState([]); + const [openUrlDirect, setOpenUrlDirect] = useState(false); // Initialize state when config loads useEffect(() => { @@ -79,6 +80,8 @@ export default function AdminPaymentMethodEdit() { setFirstTopupFilter(config.first_topup_filter); setPromoGroupFilterMode(config.promo_group_filter_mode); setSelectedPromoGroupIds(config.allowed_promo_group_ids); + // ?? false — защита от stale-config (backend ещё не пришёл с миграцией) + setOpenUrlDirect(config.open_url_direct ?? false); } }, [config]); @@ -100,6 +103,7 @@ export default function AdminPaymentMethodEdit() { first_topup_filter: firstTopupFilter, promo_group_filter_mode: promoGroupFilterMode, allowed_promo_group_ids: promoGroupFilterMode === 'selected' ? selectedPromoGroupIds : [], + open_url_direct: openUrlDirect, }; // Display name @@ -215,6 +219,34 @@ export default function AdminPaymentMethodEdit() {
+ {/* Open URL directly toggle */} +
+
+
+ {t('admin.paymentMethods.openUrlDirect', 'Открывать страницу оплаты сразу')} +
+
+ {t( + 'admin.paymentMethods.openUrlDirectHint', + 'Без панели со ссылкой — провайдер открывается внутри MiniApp/вкладки сразу после клика. После оплаты юзер возвращается на /balance/result.', + )} +
+
+ +
+ {/* Display name */}