From 2dab25c5a036fb90f75c80e4e28f2a53885f9038 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Fri, 13 Mar 2026 17:50:49 +0300 Subject: [PATCH] fix: resolve telegram auth token expiration and clean up codebase - Fix stale initData comparison in clearStaleSessionIfNeeded that destroyed valid refresh tokens on mobile WebView reopens - Restrict X-Telegram-Init-Data header to auth endpoints only - Close Mini App on auth retry to force fresh initData from Telegram - Merge Connection page error/not-configured states for better UX - Remove unnecessary comments across 40+ files (section dividers, restating comments, noise catch block comments) - Configure ESLint allowEmptyCatch to properly handle intentional empty catch blocks (62 warnings resolved) --- eslint.config.js | 2 +- src/AppWithNavigator.tsx | 12 +- src/api/admin.ts | 6 - src/api/adminRemnawave.ts | 4 - src/api/adminUpdates.ts | 4 - src/api/adminUsers.ts | 4 - src/api/auth.ts | 25 ---- src/api/branding.ts | 4 +- src/api/client.ts | 37 ++--- src/api/landings.ts | 35 +---- src/api/rbac.ts | 4 - src/api/subscription.ts | 38 ----- src/api/wheel.ts | 22 --- src/components/PromoOffersSection.tsx | 2 - src/components/SuccessNotificationModal.tsx | 1 - src/components/admin/MenuEditorTab.tsx | 14 -- .../backgrounds/BackgroundRenderer.tsx | 4 +- .../connection/InstallationGuide.tsx | 10 -- src/config/constants.ts | 1 - src/hooks/useTelegramSDK.ts | 48 +------ src/hooks/useThemeColors.ts | 3 - src/main.tsx | 34 ++--- src/pages/AdminPaymentMethods.tsx | 8 -- src/pages/AdminPolicies.tsx | 8 -- src/pages/AdminRemnawave.tsx | 10 -- src/pages/AdminRoles.tsx | 4 - src/pages/AdminUserDetail.tsx | 16 +-- src/pages/AdminUsers.tsx | 8 -- src/pages/AdminWheel.tsx | 4 - src/pages/Connection.tsx | 16 +-- src/pages/GiftSubscription.tsx | 36 ----- src/pages/Login.tsx | 30 +--- src/pages/PurchaseSuccess.tsx | 8 -- src/pages/QuickPurchase.tsx | 12 -- src/pages/Referral.tsx | 4 +- src/pages/Wheel.tsx | 1 - src/platform/adapters/TelegramAdapter.ts | 47 ++---- src/platform/adapters/WebAdapter.ts | 2 - src/store/auth.ts | 31 +--- src/utils/campaign.ts | 8 +- src/utils/referral.ts | 8 +- src/utils/token.ts | 136 ++---------------- src/utils/topUpStorage.ts | 8 +- 43 files changed, 72 insertions(+), 647 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 6f488b5..097d8b4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -31,7 +31,7 @@ export default tseslint.config( 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], - 'no-empty': 'warn', + 'no-empty': ['warn', { allowEmptyCatch: true }], 'no-eval': 'error', 'no-implied-eval': 'error', 'no-new-func': 'error', diff --git a/src/AppWithNavigator.tsx b/src/AppWithNavigator.tsx index bd5b6f2..4a4cbe3 100644 --- a/src/AppWithNavigator.tsx +++ b/src/AppWithNavigator.tsx @@ -39,9 +39,7 @@ function TelegramBackButton() { } else { showBackButton(); } - } catch { - // Back button not mounted - } + } catch {} }, [location]); // Stable handler — ref prevents re-subscription on every render @@ -52,15 +50,11 @@ function TelegramBackButton() { useEffect(() => { try { onBackButtonClick(handler); - } catch { - // Back button not mounted - } + } catch {} return () => { try { offBackButtonClick(handler); - } catch { - // Back button not mounted - } + } catch {} }; }, [handler]); diff --git a/src/api/admin.ts b/src/api/admin.ts index 5439352..bf09059 100644 --- a/src/api/admin.ts +++ b/src/api/admin.ts @@ -147,8 +147,6 @@ export const adminApi = { }, }; -// ============ Dashboard Stats Types ============ - export interface NodeStatus { uuid: string; name: string; @@ -239,8 +237,6 @@ export interface DashboardStats { tariff_stats?: TariffStats; } -// ============ Extended Stats Types ============ - export interface TopReferrerItem { user_id: number; telegram_id: number | null; @@ -318,8 +314,6 @@ export interface SystemInfo { subscriptions_active: number; } -// ============ Dashboard Stats API ============ - export const statsApi = { // Get system info getSystemInfo: async (): Promise => { diff --git a/src/api/adminRemnawave.ts b/src/api/adminRemnawave.ts index 2043925..ab6e5fa 100644 --- a/src/api/adminRemnawave.ts +++ b/src/api/adminRemnawave.ts @@ -1,7 +1,5 @@ import { apiClient } from './client'; -// ============ Types ============ - // Status & Connection export interface ConnectionStatus { status: string; @@ -224,8 +222,6 @@ export interface SyncResponse { data?: Record; } -// ============ API ============ - export const adminRemnawaveApi = { // Status & Connection getStatus: async (): Promise => { diff --git a/src/api/adminUpdates.ts b/src/api/adminUpdates.ts index d8f157c..e7f0425 100644 --- a/src/api/adminUpdates.ts +++ b/src/api/adminUpdates.ts @@ -1,7 +1,5 @@ import apiClient from './client'; -// ============ Types ============ - export interface ReleaseItem { tag_name: string; name: string; @@ -22,8 +20,6 @@ export interface ReleasesResponse { cabinet: ProjectReleasesInfo; } -// ============ API ============ - export const adminUpdatesApi = { getReleases: async (): Promise => { const response = await apiClient.get('/cabinet/admin/updates/releases'); diff --git a/src/api/adminUsers.ts b/src/api/adminUsers.ts index f64b685..294bfcf 100644 --- a/src/api/adminUsers.ts +++ b/src/api/adminUsers.ts @@ -1,7 +1,5 @@ import apiClient from './client'; -// ============ Types ============ - export interface TrafficPurchaseInfo { id: number; traffic_gb: number; @@ -381,8 +379,6 @@ export interface AdminUserGiftsResponse { received_total: number; } -// ============ API ============ - export const adminUsersApi = { // List users getUsers: async ( diff --git a/src/api/auth.ts b/src/api/auth.ts index 4a66f2d..8ccaa8b 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -13,7 +13,6 @@ import type { } from '../types'; export const authApi = { - // Telegram WebApp authentication loginTelegram: async ( initData: string, campaignSlug?: string | null, @@ -27,7 +26,6 @@ export const authApi = { return response.data; }, - // Telegram Login Widget authentication loginTelegramWidget: async ( data: { id: number; @@ -49,7 +47,6 @@ export const authApi = { return response.data; }, - // Telegram OIDC authentication (popup flow with id_token) loginTelegramOIDC: async ( idToken: string, campaignSlug?: string | null, @@ -63,7 +60,6 @@ export const authApi = { return response.data; }, - // Email login loginEmail: async ( email: string, password: string, @@ -79,7 +75,6 @@ export const authApi = { return response.data; }, - // Register email (link to existing Telegram account) registerEmail: async ( email: string, password: string, @@ -91,8 +86,6 @@ export const authApi = { return response.data; }, - // Register standalone email account (no Telegram required) - // Returns message - user must verify email before login registerEmailStandalone: async (data: { email: string; password: string; @@ -107,7 +100,6 @@ export const authApi = { return response.data; }, - // Verify email and get auth tokens verifyEmail: async (token: string, campaignSlug?: string | null): Promise => { const response = await apiClient.post('/cabinet/auth/email/verify', { token, @@ -116,13 +108,11 @@ export const authApi = { return response.data; }, - // Resend verification email resendVerification: async (): Promise<{ message: string }> => { const response = await apiClient.post('/cabinet/auth/email/resend'); return response.data; }, - // Refresh token refreshToken: async (refreshToken: string): Promise => { const response = await apiClient.post('/cabinet/auth/refresh', { refresh_token: refreshToken, @@ -130,20 +120,17 @@ export const authApi = { return response.data; }, - // Logout logout: async (refreshToken: string): Promise => { await apiClient.post('/cabinet/auth/logout', { refresh_token: refreshToken, }); }, - // Forgot password forgotPassword: async (email: string): Promise<{ message: string }> => { const response = await apiClient.post('/cabinet/auth/password/forgot', { email }); return response.data; }, - // Reset password resetPassword: async (token: string, password: string): Promise<{ message: string }> => { const response = await apiClient.post('/cabinet/auth/password/reset', { token, @@ -152,13 +139,11 @@ export const authApi = { return response.data; }, - // Get current user getMe: async (): Promise => { const response = await apiClient.get('/cabinet/auth/me'); return response.data; }, - // Request email change - sends verification code to new email requestEmailChange: async ( newEmail: string, ): Promise<{ message: string; new_email: string; expires_in_minutes: number }> => { @@ -168,7 +153,6 @@ export const authApi = { return response.data; }, - // Verify email change with code verifyEmailChange: async (code: string): Promise<{ message: string; email: string }> => { const response = await apiClient.post('/cabinet/auth/email/change/verify', { code, @@ -176,7 +160,6 @@ export const authApi = { return response.data; }, - // OAuth: get enabled providers getOAuthProviders: async (): Promise<{ providers: OAuthProvider[] }> => { const response = await apiClient.get<{ providers: OAuthProvider[] }>( '/cabinet/auth/oauth/providers', @@ -184,7 +167,6 @@ export const authApi = { return response.data; }, - // OAuth: get authorization URL getOAuthAuthorizeUrl: async ( provider: string, ): Promise<{ authorize_url: string; state: string }> => { @@ -194,7 +176,6 @@ export const authApi = { return response.data; }, - // OAuth: callback (exchange code for tokens) oauthCallback: async ( provider: string, code: string, @@ -216,7 +197,6 @@ export const authApi = { return response.data; }, - // Account linking getLinkedProviders: async (): Promise => { const response = await apiClient.get( '/cabinet/auth/account/linked-providers', @@ -248,7 +228,6 @@ export const authApi = { return response.data; }, - // Link Telegram account (Mini App initData, OIDC id_token, or Login Widget data) linkTelegram: async ( data: | { init_data: string } @@ -270,8 +249,6 @@ export const authApi = { return response.data; }, - // Server-side OAuth linking completion (no JWT needed — auth via state token) - // Used when OAuth opens in external browser from Telegram Mini App linkServerComplete: async ( code: string, state: string, @@ -295,13 +272,11 @@ export const authApi = { return response.data; }, - // Auto-login from guest purchase success page autoLogin: async (token: string): Promise => { const response = await apiClient.post('/cabinet/auth/login/auto', { token }); return response.data; }, - // Account merge (no JWT required) getMergePreview: async (mergeToken: string): Promise => { const response = await apiClient.get( `/cabinet/auth/merge/${encodeURIComponent(mergeToken)}`, diff --git a/src/api/branding.ts b/src/api/branding.ts index 748041d..7f97a61 100644 --- a/src/api/branding.ts +++ b/src/api/branding.ts @@ -88,9 +88,7 @@ export const getCachedBranding = (): BrandingInfo | null => { export const setCachedBranding = (branding: BrandingInfo) => { try { sessionStorage.setItem(BRANDING_CACHE_KEY, JSON.stringify(branding)); - } catch { - // sessionStorage not available - } + } catch {} }; // Preload logo image as blob to hide backend URL diff --git a/src/api/client.ts b/src/api/client.ts index e433589..abe6eb8 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -11,10 +11,8 @@ import { API } from '../config/constants'; const API_BASE_URL = import.meta.env.VITE_API_URL || '/api'; -// Настраиваем endpoint для refresh tokenRefreshManager.setRefreshEndpoint(`${API_BASE_URL}/cabinet/auth/refresh`); -// CSRF token management const CSRF_COOKIE_NAME = 'csrf_token'; const CSRF_HEADER_NAME = 'X-CSRF-Token'; @@ -34,7 +32,6 @@ function ensureCsrfToken(): string { let token = getCsrfToken(); if (!token) { token = generateCsrfToken(); - // Set cookie with SameSite=Strict for CSRF protection document.cookie = `${CSRF_COOKIE_NAME}=${token}; path=/; SameSite=Strict; Secure`; } return token; @@ -49,9 +46,7 @@ const getTelegramInitData = (): string | null => { tokenStorage.setTelegramInitData(raw); return raw; } - } catch { - // Not in Telegram or SDK not initialized - } + } catch {} return tokenStorage.getTelegramInitData(); }; @@ -64,7 +59,6 @@ export const apiClient = axios.create({ }, }); -// Auth endpoints that don't need Bearer token or token refresh const AUTH_ENDPOINTS = [ '/cabinet/auth/telegram', '/cabinet/auth/telegram/widget', @@ -85,15 +79,11 @@ function isAuthEndpoint(url: string | undefined): boolean { return AUTH_ENDPOINTS.some((endpoint) => url.startsWith(endpoint)); } -// Request interceptor - add auth token with expiration check apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => { - // Skip token refresh and Bearer header for auth endpoints - // These endpoints authenticate via init_data/credentials, not Bearer tokens if (!isAuthEndpoint(config.url)) { let token = tokenStorage.getAccessToken(); if (token && isTokenExpired(token)) { - // Access token expired — try refresh const newToken = await tokenRefreshManager.refreshAccessToken(); if (newToken) { token = newToken; @@ -103,7 +93,6 @@ apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => return config; } } else if (!token && tokenStorage.getRefreshToken()) { - // No access token (e.g. tab reopen) but refresh token exists — restore session const newToken = await tokenRefreshManager.refreshAccessToken(); if (newToken) { token = newToken; @@ -115,12 +104,16 @@ apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => } } - const telegramInitData = getTelegramInitData(); - if (telegramInitData && config.headers) { - config.headers['X-Telegram-Init-Data'] = telegramInitData; + const isTelegramAuthEndpoint = + config.url?.startsWith('/cabinet/auth/telegram') || + config.url?.startsWith('/cabinet/auth/account/link/telegram'); + if (isTelegramAuthEndpoint) { + const telegramInitData = getTelegramInitData(); + if (telegramInitData && config.headers) { + config.headers['X-Telegram-Init-Data'] = telegramInitData; + } } - // Add CSRF token for state-changing methods const method = config.method?.toUpperCase(); if (method && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method) && config.headers) { config.headers[CSRF_HEADER_NAME] = ensureCsrfToken(); @@ -129,7 +122,6 @@ apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => return config; }); -// Custom error types for special handling export interface MaintenanceError { code: 'maintenance'; message: string; @@ -180,13 +172,11 @@ export function isBlacklistedError( return err.response?.status === 403 && err.response?.data?.detail?.code === 'blacklisted'; } -// Response interceptor - handle 401, 503 (maintenance), 403 (channel subscription) apiClient.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; - // Handle maintenance mode (503) if (isMaintenanceError(error)) { const detail = (error.response?.data as { detail: MaintenanceError }).detail; useBlockingStore.getState().setMaintenance({ @@ -196,7 +186,6 @@ apiClient.interceptors.response.use( return Promise.reject(error); } - // Handle channel subscription required (403) if (isChannelSubscriptionError(error)) { const detail = (error.response?.data as { detail: ChannelSubscriptionError }).detail; useBlockingStore.getState().setChannelSubscription({ @@ -207,7 +196,6 @@ apiClient.interceptors.response.use( return Promise.reject(error); } - // Handle blacklisted user (403) if (isBlacklistedError(error)) { const detail = (error.response?.data as { detail: BlacklistedError }).detail; useBlockingStore.getState().setBlacklisted({ @@ -216,14 +204,10 @@ apiClient.interceptors.response.use( return Promise.reject(error); } - // Если получили 401 и ещё не пробовали refresh (на случай если проверка exp не сработала) if (error.response?.status === 401 && !originalRequest._retry) { - // Не обрабатываем 401 для авторизационных endpoints - пусть ошибка дойдет до компонента const requestUrl = originalRequest.url || ''; - const isLoginEndpoint = isAuthEndpoint(requestUrl); - if (isLoginEndpoint) { - // Пробрасываем ошибку в компонент для показа сообщения пользователю + if (isAuthEndpoint(requestUrl)) { return Promise.reject(error); } @@ -236,7 +220,6 @@ apiClient.interceptors.response.use( } return apiClient(originalRequest); } else { - // Refresh не удался tokenStorage.clearTokens(); safeRedirectToLogin(); } diff --git a/src/api/landings.ts b/src/api/landings.ts index f557de0..d6cdb8a 100644 --- a/src/api/landings.ts +++ b/src/api/landings.ts @@ -1,10 +1,6 @@ import apiClient from './client'; import type { AnimationConfig } from '@/components/ui/backgrounds/types'; -// ============================================================ -// Public types -// ============================================================ - export interface LandingFeature { icon: string; title: string; @@ -130,10 +126,6 @@ export interface PurchaseStatus { bot_link: string | null; } -// ============================================================ -// Locale helpers -// ============================================================ - /** Locale dict for multi-language text fields (admin API) */ export type LocaleDict = Record; @@ -148,10 +140,6 @@ export const LOCALE_META: Record; -/** - * Normalize a value that might be a plain string (old API) or a LocaleDict. - * If it's a string, wraps it as `{ ru: value }`. - * If null/undefined, returns the fallback. - */ -/** Extract best display string from a LocaleDict: ru → en → first available → '' */ +/** Extract best display string from a LocaleDict: ru -> en -> first available -> '' */ export function resolveLocaleDisplay(dict: LocaleDict | string | null | undefined): string { if (!dict) return ''; if (typeof dict === 'string') return dict; @@ -253,10 +236,6 @@ export function toLocaleDict( return value; } -// ============================================================ -// Public API -// ============================================================ - export const landingApi = { getConfig: async (slug: string, lang?: string): Promise => { const params = lang ? `?lang=${lang}` : ''; @@ -280,10 +259,6 @@ export const landingApi = { }, }; -// ============================================================ -// Admin stats types -// ============================================================ - export interface LandingDailyStat { date: string; purchases: number; @@ -311,10 +286,6 @@ export interface LandingStatsResponse { tariff_stats: LandingTariffStat[]; } -// ============================================================ -// Admin purchase list types -// ============================================================ - export type PurchaseItemStatus = | 'pending' | 'paid' @@ -346,10 +317,6 @@ export interface LandingPurchaseListResponse { total: number; } -// ============================================================ -// Admin API -// ============================================================ - export const adminLandingsApi = { list: async (): Promise => { const response = await apiClient.get('/cabinet/admin/landings'); diff --git a/src/api/rbac.ts b/src/api/rbac.ts index 39cbd45..880e907 100644 --- a/src/api/rbac.ts +++ b/src/api/rbac.ts @@ -1,7 +1,5 @@ import apiClient from './client'; -// === Types === - export interface AdminRole { id: number; name: string; @@ -144,8 +142,6 @@ export interface PermissionSection { actions: string[]; } -// === API === - const BASE = '/cabinet/admin/rbac'; export const rbacApi = { diff --git a/src/api/subscription.ts b/src/api/subscription.ts index 99fe63d..c1f676b 100644 --- a/src/api/subscription.ts +++ b/src/api/subscription.ts @@ -12,19 +12,16 @@ import type { } from '../types'; export const subscriptionApi = { - // Get current subscription status getSubscription: async (): Promise => { const response = await apiClient.get('/cabinet/subscription'); return response.data; }, - // Get renewal options getRenewalOptions: async (): Promise => { const response = await apiClient.get('/cabinet/subscription/renewal-options'); return response.data; }, - // Renew subscription renewSubscription: async ( periodDays: number, ): Promise<{ @@ -38,7 +35,6 @@ export const subscriptionApi = { return response.data; }, - // Get traffic packages getTrafficPackages: async (): Promise => { const response = await apiClient.get( '/cabinet/subscription/traffic-packages', @@ -46,7 +42,6 @@ export const subscriptionApi = { return response.data; }, - // Purchase traffic purchaseTraffic: async ( gb: number, ): Promise<{ @@ -58,7 +53,6 @@ export const subscriptionApi = { return response.data; }, - // Purchase devices purchaseDevices: async ( devices: number, ): Promise<{ @@ -75,7 +69,6 @@ export const subscriptionApi = { return response.data; }, - // Get device purchase price getDevicePrice: async ( devices: number = 1, ): Promise<{ @@ -103,12 +96,10 @@ export const subscriptionApi = { return response.data; }, - // Save devices cart for later purchase after top-up saveDevicesCart: async (devices: number): Promise => { await apiClient.post('/cabinet/subscription/devices/save-cart', { devices }); }, - // Get device reduction info (for reducing device limit) getDeviceReductionInfo: async (): Promise<{ available: boolean; reason?: string; @@ -121,7 +112,6 @@ export const subscriptionApi = { return response.data; }, - // Reduce device limit reduceDevices: async ( newDeviceLimit: number, ): Promise<{ @@ -136,12 +126,10 @@ export const subscriptionApi = { return response.data; }, - // Save traffic cart for later purchase after top-up saveTrafficCart: async (trafficGb: number): Promise => { await apiClient.post('/cabinet/subscription/traffic/save-cart', { gb: trafficGb }); }, - // Update autopay settings updateAutopay: async ( enabled: boolean, daysBefore?: number, @@ -157,25 +145,21 @@ export const subscriptionApi = { return response.data; }, - // Get trial info getTrialInfo: async (): Promise => { const response = await apiClient.get('/cabinet/subscription/trial'); return response.data; }, - // Activate trial activateTrial: async (): Promise => { const response = await apiClient.post('/cabinet/subscription/trial'); return response.data; }, - // Get purchase options (periods, servers, traffic, devices) getPurchaseOptions: async (): Promise => { const response = await apiClient.get('/cabinet/subscription/purchase-options'); return response.data; }, - // Preview purchase price previewPurchase: async (selection: PurchaseSelection): Promise => { const response = await apiClient.post( '/cabinet/subscription/purchase-preview', @@ -186,7 +170,6 @@ export const subscriptionApi = { return response.data; }, - // Submit purchase submitPurchase: async ( selection: PurchaseSelection, ): Promise<{ @@ -201,7 +184,6 @@ export const subscriptionApi = { return response.data; }, - // Purchase tariff (for tariffs mode) purchaseTariff: async ( tariffId: number, periodDays: number, @@ -223,13 +205,11 @@ export const subscriptionApi = { return response.data; }, - // Get app config for connection getAppConfig: async (): Promise => { const response = await apiClient.get('/cabinet/subscription/app-config'); return response.data; }, - // Get available countries/servers getCountries: async (): Promise<{ countries: Array<{ uuid: string; @@ -253,7 +233,6 @@ export const subscriptionApi = { return response.data; }, - // Update countries/servers updateCountries: async ( countries: string[], ): Promise<{ @@ -267,7 +246,6 @@ export const subscriptionApi = { return response.data; }, - // Get connection link and instructions getConnectionLink: async (): Promise<{ subscription_url: string | null; display_link: string | null; @@ -283,7 +261,6 @@ export const subscriptionApi = { return response.data; }, - // Get hApp download links getHappDownloads: async (): Promise<{ platforms: Record< string, @@ -299,9 +276,6 @@ export const subscriptionApi = { return response.data; }, - // ============ Device Management ============ - - // Get connected devices getDevices: async (): Promise<{ devices: Array<{ hwid: string; @@ -316,7 +290,6 @@ export const subscriptionApi = { return response.data; }, - // Delete a specific device deleteDevice: async ( hwid: string, ): Promise<{ @@ -330,7 +303,6 @@ export const subscriptionApi = { return response.data; }, - // Delete all devices deleteAllDevices: async (): Promise<{ success: boolean; message: string; @@ -340,9 +312,6 @@ export const subscriptionApi = { return response.data; }, - // ============ Tariff Switch ============ - - // Preview tariff switch cost previewTariffSwitch: async ( tariffId: number, ): Promise<{ @@ -372,7 +341,6 @@ export const subscriptionApi = { return response.data; }, - // Switch to a different tariff switchTariff: async ( tariffId: number, ): Promise<{ @@ -393,9 +361,6 @@ export const subscriptionApi = { return response.data; }, - // ============ Subscription Pause (Daily Tariffs) ============ - - // Toggle pause/resume for daily subscription togglePause: async (): Promise<{ success: boolean; message: string; @@ -407,9 +372,6 @@ export const subscriptionApi = { return response.data; }, - // ============ Traffic Switch ============ - - // Switch to a different traffic package switchTraffic: async ( gb: number, ): Promise<{ diff --git a/src/api/wheel.ts b/src/api/wheel.ts index 1c36d94..d8bac9b 100644 --- a/src/api/wheel.ts +++ b/src/api/wheel.ts @@ -1,7 +1,5 @@ import apiClient from './client'; -// ==================== TYPES ==================== - export interface WheelPrize { id: number; display_name: string; @@ -79,7 +77,6 @@ export interface StarsInvoiceResponse { stars_amount: number; } -// Admin types export interface WheelPrizeAdmin { id: number; config_id: number; @@ -99,7 +96,6 @@ export interface WheelPrizeAdmin { updated_at: string | null; } -// Type for creating a new prize (excludes id, config_id which are auto-generated) export interface CreateWheelPrizeData { prize_type: string; prize_value: number; @@ -180,22 +176,17 @@ export interface AdminSpinsResponse { pages: number; } -// ==================== USER API ==================== - export const wheelApi = { - // Get wheel config getConfig: async (): Promise => { const response = await apiClient.get('/cabinet/wheel/config'); return response.data; }, - // Check spin availability checkAvailability: async (): Promise => { const response = await apiClient.get('/cabinet/wheel/availability'); return response.data; }, - // Spin the wheel spin: async (paymentType: 'telegram_stars' | 'subscription_days'): Promise => { const response = await apiClient.post('/cabinet/wheel/spin', { payment_type: paymentType, @@ -203,7 +194,6 @@ export const wheelApi = { return response.data; }, - // Get spin history getHistory: async (page = 1, perPage = 20): Promise => { const response = await apiClient.get('/cabinet/wheel/history', { params: { page, per_page: perPage }, @@ -211,41 +201,33 @@ export const wheelApi = { return response.data; }, - // Create Stars invoice for Mini App payment createStarsInvoice: async (): Promise => { const response = await apiClient.post('/cabinet/wheel/stars-invoice'); return response.data; }, }; -// ==================== ADMIN API ==================== - export const adminWheelApi = { - // Get full config getConfig: async (): Promise => { const response = await apiClient.get('/cabinet/admin/wheel/config'); return response.data; }, - // Update config updateConfig: async (data: Partial): Promise => { const response = await apiClient.put('/cabinet/admin/wheel/config', data); return response.data; }, - // Get prizes getPrizes: async (): Promise => { const response = await apiClient.get('/cabinet/admin/wheel/prizes'); return response.data; }, - // Create prize createPrize: async (data: CreateWheelPrizeData): Promise => { const response = await apiClient.post('/cabinet/admin/wheel/prizes', data); return response.data; }, - // Update prize updatePrize: async ( prizeId: number, data: Partial, @@ -257,17 +239,14 @@ export const adminWheelApi = { return response.data; }, - // Delete prize deletePrize: async (prizeId: number): Promise => { await apiClient.delete(`/cabinet/admin/wheel/prizes/${prizeId}`); }, - // Reorder prizes reorderPrizes: async (prizeIds: number[]): Promise => { await apiClient.post('/cabinet/admin/wheel/prizes/reorder', { prize_ids: prizeIds }); }, - // Get statistics getStatistics: async (dateFrom?: string, dateTo?: string): Promise => { const response = await apiClient.get('/cabinet/admin/wheel/statistics', { params: { date_from: dateFrom, date_to: dateTo }, @@ -275,7 +254,6 @@ export const adminWheelApi = { return response.data; }, - // Get all spins getSpins: async (params?: { user_id?: number; date_from?: string; diff --git a/src/components/PromoOffersSection.tsx b/src/components/PromoOffersSection.tsx index a8accdf..998ccb3 100644 --- a/src/components/PromoOffersSection.tsx +++ b/src/components/PromoOffersSection.tsx @@ -80,8 +80,6 @@ const XCircleIcon = () => ( ); -// ------- Main Component ------- - interface PromoOffersSectionProps { className?: string; } diff --git a/src/components/SuccessNotificationModal.tsx b/src/components/SuccessNotificationModal.tsx index 9682579..08f1d74 100644 --- a/src/components/SuccessNotificationModal.tsx +++ b/src/components/SuccessNotificationModal.tsx @@ -108,7 +108,6 @@ export default function SuccessNotificationModal() { return () => document.removeEventListener('keydown', handleKeyDown); }, [isOpen, handleClose]); - // Haptic feedback on open useEffect(() => { if (isOpen) { haptic.notification('success'); diff --git a/src/components/admin/MenuEditorTab.tsx b/src/components/admin/MenuEditorTab.tsx index 0daa1dc..15baf1e 100644 --- a/src/components/admin/MenuEditorTab.tsx +++ b/src/components/admin/MenuEditorTab.tsx @@ -30,8 +30,6 @@ import { import { Toggle } from './Toggle'; import { useNotify } from '../../platform/hooks/useNotify'; -// ============ Icons ============ - const GripIcon = () => ( ( ); -// ============ Helpers ============ - function generateId(): string { return `custom_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`; } @@ -126,8 +122,6 @@ function configsEqual(a: MenuConfig, b: MenuConfig): boolean { const DEFAULT_CONFIG: MenuConfig = { rows: [] }; -// ============ MaxPerRowSelector ============ - interface MaxPerRowSelectorProps { value: number; onChange: (value: number) => void; @@ -153,8 +147,6 @@ function MaxPerRowSelector({ value, onChange }: MaxPerRowSelectorProps) { ); } -// ============ ButtonChip ============ - interface ButtonChipProps { button: MenuButtonConfig; isExpanded: boolean; @@ -358,8 +350,6 @@ function ButtonChip({ ); } -// ============ SortableRow ============ - interface SortableRowProps { row: MenuRowConfig; rowIndex: number; @@ -471,8 +461,6 @@ function SortableRow({ ); } -// ============ InlineAddPanel ============ - interface InlineAddPanelProps { rowId: string; usedBuiltinIds: Set; @@ -540,8 +528,6 @@ function InlineAddPanel({ rowId, usedBuiltinIds, onAddBuiltin, onAddCustom }: In ); } -// ============ MenuEditorTab ============ - export function MenuEditorTab() { const { t } = useTranslation(); const queryClient = useQueryClient(); diff --git a/src/components/backgrounds/BackgroundRenderer.tsx b/src/components/backgrounds/BackgroundRenderer.tsx index 2638c3d..20d2b07 100644 --- a/src/components/backgrounds/BackgroundRenderer.tsx +++ b/src/components/backgrounds/BackgroundRenderer.tsx @@ -66,9 +66,7 @@ if (cachedConfig?.enabled && cachedConfig.type && cachedConfig.type !== 'none') function setCachedConfig(config: AnimationConfig) { try { localStorage.setItem(ANIMATION_CACHE_KEY, JSON.stringify(config)); - } catch { - // localStorage not available - } + } catch {} } export function setCachedAnimationConfig(config: AnimationConfig) { diff --git a/src/components/connection/InstallationGuide.tsx b/src/components/connection/InstallationGuide.tsx index 558512a..f825bd4 100644 --- a/src/components/connection/InstallationGuide.tsx +++ b/src/components/connection/InstallationGuide.tsx @@ -62,8 +62,6 @@ export default function InstallationGuide({ const [activePlatformKey, setActivePlatformKey] = useState(null); const [selectedApp, setSelectedApp] = useState(null); - // --- Helpers --- - const getLocalizedText = useCallback( (text: LocalizedText | undefined): string => { if (!text) return ''; @@ -96,8 +94,6 @@ export default function InstallationGuide({ [appConfig.svgLibrary], ); - // --- Available platforms --- - const availablePlatforms = useMemo(() => { if (!appConfig.platforms) return []; const available = platformOrder.filter((key) => { @@ -110,8 +106,6 @@ export default function InstallationGuide({ return available; }, [appConfig.platforms, detectedPlatform]); - // --- Auto-select platform & app --- - useEffect(() => { if (selectedApp || !availablePlatforms.length) return; const platform = availablePlatforms[0]; @@ -124,8 +118,6 @@ export default function InstallationGuide({ } }, [appConfig.platforms, availablePlatforms, selectedApp]); - // --- Button renderer (delegates to BlockButtons component) --- - const renderBlockButtons = useCallback( (buttons: RemnawaveButtonClient[] | undefined, variant: 'light' | 'subtle') => ( { try { return localStorage.getItem(FULLSCREEN_CACHE_KEY) === 'true'; @@ -29,18 +26,12 @@ export const getCachedFullscreenEnabled = (): boolean => { } }; -/** - * Set cached fullscreen setting - */ export const setCachedFullscreenEnabled = (enabled: boolean) => { try { localStorage.setItem(FULLSCREEN_CACHE_KEY, String(enabled)); - } catch { - // localStorage not available - } + } catch {} }; -// Cached detection result (evaluated once at module load) let _isInTelegram: boolean | null = null; function detectTelegram(): boolean { if (_isInTelegram === null) { @@ -54,16 +45,10 @@ function detectTelegram(): boolean { return _isInTelegram; } -/** - * Check if we're actually running inside Telegram Mini App - */ export function isInTelegramWebApp(): boolean { return detectTelegram(); } -/** - * Check if running on mobile Telegram client (iOS/Android) - */ export function isTelegramMobile(): boolean { try { const { tgWebAppPlatform } = retrieveLaunchParams(); @@ -73,9 +58,6 @@ export function isTelegramMobile(): boolean { } } -/** - * Get Telegram init data for authentication - */ export function getTelegramInitData(): string | null { try { return retrieveRawInitData() || null; @@ -84,9 +66,6 @@ export function getTelegramInitData(): string | null { } } -/** - * Type for platform values - */ export type TelegramPlatform = | 'android' | 'ios' @@ -98,13 +77,8 @@ export type TelegramPlatform = | 'unknown' | undefined; -// Default values const defaultInsets = { top: 0, bottom: 0, left: 0, right: 0 }; -/** - * Hook for Telegram WebApp integration - * Uses @telegram-apps/sdk-react v3 signals - */ export function useTelegramSDK() { const inTelegram = detectTelegram(); @@ -156,18 +130,14 @@ export function useTelegramSDK() { if (!inTelegram) return; try { sdkRequestFullscreen(); - } catch { - // Not supported - } + } catch {} }, [inTelegram]); const exitFullscreen = useCallback(() => { if (!inTelegram) return; try { sdkExitFullscreen(); - } catch { - // Not supported - } + } catch {} }, [inTelegram]); const toggleFullscreen = useCallback(() => { @@ -182,27 +152,21 @@ export function useTelegramSDK() { if (!inTelegram) return; try { expandViewport(); - } catch { - // Not supported - } + } catch {} }, [inTelegram]); const disableVerticalSwipes = useCallback(() => { if (!inTelegram) return; try { sdkDisableVerticalSwipes(); - } catch { - // Not supported - } + } catch {} }, [inTelegram]); const enableVerticalSwipes = useCallback(() => { if (!inTelegram) return; try { sdkEnableVerticalSwipes(); - } catch { - // Not supported - } + } catch {} }, [inTelegram]); const isFullscreenSupported = inTelegram; diff --git a/src/hooks/useThemeColors.ts b/src/hooks/useThemeColors.ts index 1fb29d9..88c32d2 100644 --- a/src/hooks/useThemeColors.ts +++ b/src/hooks/useThemeColors.ts @@ -64,7 +64,6 @@ export function applyThemeColors(colors: ThemeColors): void { const warningPalette = generatePalette(colors.warning); const errorPalette = generatePalette(colors.error); - // === DARK THEME PALETTE === // Convert hex colors to RGB const darkBgRgb = hexToRgb(colors.darkBackground); const darkSurfaceRgb = hexToRgb(colors.darkSurface); @@ -104,7 +103,6 @@ export function applyThemeColors(colors: ThemeColors): void { root.style.setProperty('--color-dark-900', interpolateRgb(darkSurfaceRgb, darkBgRgb, 0.7)); root.style.setProperty('--color-dark-950', rgbToString(darkBgRgb.r, darkBgRgb.g, darkBgRgb.b)); - // === LIGHT THEME PALETTE === const lightBgRgb = hexToRgb(colors.lightBackground); const lightSurfaceRgb = hexToRgb(colors.lightSurface); const lightTextRgb = hexToRgb(colors.lightText); @@ -149,7 +147,6 @@ export function applyThemeColors(colors: ThemeColors): void { rgbToString(lightTextRgb.r, lightTextRgb.g, lightTextRgb.b), ); - // === STATUS COLOR PALETTES === for (const shade of SHADE_LEVELS) { root.style.setProperty(`--color-accent-${shade}`, accentPalette[shade]); root.style.setProperty(`--color-success-${shade}`, successPalette[shade]); diff --git a/src/main.tsx b/src/main.tsx index 4a8af2e..788e9e4 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -38,40 +38,29 @@ if (!alreadyInitialized) { init(); restoreInitData(); - // Сбрасываем старые токены если init data изменился (новая сессия Telegram) clearStaleSessionIfNeeded(retrieveRawInitData() || null); - // Mount components — each in its own try/catch so one failure doesn't block others - // Note: mountMiniApp() internally mounts themeParams in SDK v3, - // so we don't call mountThemeParams() separately to avoid ConcurrentCallError + // Each mount in its own try/catch so one failure doesn't block others. + // mountMiniApp() internally mounts themeParams in SDK v3, + // so we don't call mountThemeParams() separately to avoid ConcurrentCallError. try { mountMiniApp(); - } catch { - /* already mounted */ - } + } catch {} try { bindThemeParamsCssVars(); - } catch { - /* theme params not yet available */ - } + } catch {} try { mountSwipeBehavior(); disableVerticalSwipes(); - } catch { - /* already mounted */ - } + } catch {} try { mountClosingBehavior(); disableClosingConfirmation(); - } catch { - /* already mounted */ - } + } catch {} try { mountBackButton(); - } catch { - /* already mounted */ - } - // Viewport — async, fullscreen зависит от смонтированного viewport + } catch {} + // Viewport must be mounted before requesting fullscreen mountViewport() .then(() => { bindViewportCssVars(); @@ -87,12 +76,9 @@ if (!alreadyInitialized) { .catch(() => {}); miniAppReady(); - } catch { - // Not in Telegram — ok - } + } catch {} } -// Preload logo from cache — defer to idle time so it doesn't compete with LCP if ('requestIdleCallback' in window) { requestIdleCallback(() => initLogoPreload()); } else { diff --git a/src/pages/AdminPaymentMethods.tsx b/src/pages/AdminPaymentMethods.tsx index ad58180..10aa5b6 100644 --- a/src/pages/AdminPaymentMethods.tsx +++ b/src/pages/AdminPaymentMethods.tsx @@ -23,8 +23,6 @@ import { import { CSS } from '@dnd-kit/utilities'; import { adminPaymentMethodsApi } from '../api/adminPaymentMethods'; import type { PaymentMethodConfig } from '../types'; -// ============ Icons ============ - const BackIcon = () => ( ( ); -// ============ Sortable Card ============ - interface SortableCardProps { config: PaymentMethodConfig; onClick: () => void; @@ -184,10 +180,6 @@ function SortablePaymentCard({ config, onClick }: SortableCardProps) { ); } -// ============ Toast ============ - -// ============ Main Page ============ - export default function AdminPaymentMethods() { const { t } = useTranslation(); const navigate = useNavigate(); diff --git a/src/pages/AdminPolicies.tsx b/src/pages/AdminPolicies.tsx index b83c4f2..68d6a47 100644 --- a/src/pages/AdminPolicies.tsx +++ b/src/pages/AdminPolicies.tsx @@ -6,8 +6,6 @@ import { rbacApi, AccessPolicy, AdminRole } from '@/api/rbac'; import { PermissionGate } from '@/components/auth/PermissionGate'; import { usePlatform } from '@/platform/hooks/usePlatform'; -// === Icons === - const BackIcon = () => ( ( ); -// === Helpers === - interface PolicyConditions { time_range?: { start: string; end: string }; ip_whitelist?: string[]; @@ -130,8 +126,6 @@ function parseConditions(raw: Record): PolicyConditions { return result; } -// === Sub-components === - interface EffectBadgeProps { effect: 'allow' | 'deny'; className?: string; @@ -154,8 +148,6 @@ function EffectBadge({ effect, className }: EffectBadgeProps) { ); } -// === Main Page === - export default function AdminPolicies() { const { t } = useTranslation(); const navigate = useNavigate(); diff --git a/src/pages/AdminRemnawave.tsx b/src/pages/AdminRemnawave.tsx index 35b6126..a9b4c7c 100644 --- a/src/pages/AdminRemnawave.tsx +++ b/src/pages/AdminRemnawave.tsx @@ -23,8 +23,6 @@ import { RemnawaveIcon, } from '../components/icons'; -// ============ Icons ============ - const BackIcon = () => ( ( ); -// ============ Helpers ============ - const formatBytes = (bytes: number): string => { if (bytes === 0) return '0 B'; const k = 1024; @@ -100,8 +96,6 @@ const getCountryFlag = (code: string | null | undefined): string => { return codeMap[code.toUpperCase()] || code; }; -// ============ Sub-Components ============ - interface StatCardProps { label: string; value: string | number; @@ -333,8 +327,6 @@ function SyncCard({ title, description, onAction, isLoading, lastResult }: SyncC ); } -// ============ Tab Components ============ - interface OverviewTabProps { stats: SystemStatsResponse | undefined; isLoading: boolean; @@ -873,8 +865,6 @@ function SyncTab({ ); } -// ============ Main Component ============ - type TabType = 'overview' | 'nodes' | 'squads' | 'sync'; export default function AdminRemnawave() { diff --git a/src/pages/AdminRoles.tsx b/src/pages/AdminRoles.tsx index ad622fd..4586c9c 100644 --- a/src/pages/AdminRoles.tsx +++ b/src/pages/AdminRoles.tsx @@ -7,8 +7,6 @@ import { PermissionGate } from '@/components/auth/PermissionGate'; import { usePermissionStore } from '@/store/permissions'; import { usePlatform } from '@/platform/hooks/usePlatform'; -// === Icons === - const BackIcon = () => ( ( ); -// === Main Page === - export default function AdminRoles() { const { t } = useTranslation(); const navigate = useNavigate(); diff --git a/src/pages/AdminUserDetail.tsx b/src/pages/AdminUserDetail.tsx index 202c96e..50776b1 100644 --- a/src/pages/AdminUserDetail.tsx +++ b/src/pages/AdminUserDetail.tsx @@ -433,7 +433,6 @@ export default function AdminUserDetail() { const data = await adminUsersApi.getReferrals(userId, 0, 50); setReferrals(data.users); } catch { - // ignore } finally { setReferralsLoading(false); } @@ -446,7 +445,6 @@ export default function AdminUserDetail() { const data = await adminUsersApi.getPanelInfo(userId); setPanelInfo(data); } catch { - // ignore } finally { setPanelInfoLoading(false); } @@ -457,9 +455,7 @@ export default function AdminUserDetail() { try { const data = await adminUsersApi.getNodeUsage(userId); setNodeUsage(data); - } catch { - // ignore - } + } catch {} }, [userId]); const loadDevices = useCallback(async () => { @@ -471,7 +467,6 @@ export default function AdminUserDetail() { setDevicesTotal(data.total); setDeviceLimit(data.device_limit); } catch { - // ignore } finally { setDevicesLoading(false); } @@ -488,7 +483,6 @@ export default function AdminUserDetail() { const data = await adminUsersApi.getUserGifts(userId); setGiftsData(data); } catch { - // ignore } finally { setGiftsLoading(false); } @@ -498,9 +492,7 @@ export default function AdminUserDetail() { try { const data = await promocodesApi.getPromoGroups({ limit: 100 }); setPromoGroups(data.items); - } catch { - // ignore - } + } catch {} }, []); const handleTicketReply = async () => { @@ -954,9 +946,7 @@ export default function AdminUserDetail() { try { await navigator.clipboard.writeText(text); notify.success(t('admin.users.detail.copied')); - } catch { - // ignore - } + } catch {} }; if (loading) { diff --git a/src/pages/AdminUsers.tsx b/src/pages/AdminUsers.tsx index d869e53..cb22df3 100644 --- a/src/pages/AdminUsers.tsx +++ b/src/pages/AdminUsers.tsx @@ -5,8 +5,6 @@ import { useCurrency } from '../hooks/useCurrency'; import { adminUsersApi, type UserListItem, type UsersStatsResponse } from '../api/adminUsers'; import { usePlatform } from '../platform/hooks/usePlatform'; -// ============ Icons ============ - const BackIcon = () => ( ( ); -// ============ Components ============ - interface StatCardProps { title: string; value: string | number; @@ -101,8 +97,6 @@ function StatusBadge({ status }: { status: string }) { ); } -// ============ User List Component ============ - interface UserRowProps { user: UserListItem; onClick: () => void; @@ -181,8 +175,6 @@ function UserRow({ user, onClick, formatAmount }: UserRowProps) { ); } -// ============ Main Page ============ - export default function AdminUsers() { const { t } = useTranslation(); const { formatWithCurrency } = useCurrency(); diff --git a/src/pages/AdminWheel.tsx b/src/pages/AdminWheel.tsx index 51c1ed5..bb1c6b2 100644 --- a/src/pages/AdminWheel.tsx +++ b/src/pages/AdminWheel.tsx @@ -29,8 +29,6 @@ import { ColorPicker } from '@/components/ColorPicker'; import { usePlatform } from '../platform/hooks/usePlatform'; import { toNumber } from '../utils/inputHelpers'; -// Icons - const BackIcon = () => ( @@ -107,20 +106,7 @@ export default function Connection() { ); } - // Error - if (error || !appConfig) { - return ( -
-

{t('common.error')}

- -
- ); - } - - // No apps configured — check before subscription since empty config also has no subscription - if (!hasApps) { + if (error || !appConfig || !hasApps) { return (
diff --git a/src/pages/GiftSubscription.tsx b/src/pages/GiftSubscription.tsx index 0e8543c..fa1fa4a 100644 --- a/src/pages/GiftSubscription.tsx +++ b/src/pages/GiftSubscription.tsx @@ -21,10 +21,6 @@ import { getApiErrorMessage } from '../utils/api-error'; import { formatPrice } from '../utils/format'; import { usePlatform, useHaptic } from '@/platform'; -// ============================================================ -// SVG Icons -// ============================================================ - function GiftIcon({ className }: { className?: string }) { return ( ) => string, @@ -181,16 +173,8 @@ function formatGiftDate(dateStr: string | null): string { }); } -// ============================================================ -// Tab type -// ============================================================ - type TabId = 'buy' | 'activate' | 'myGifts'; -// ============================================================ -// Sub-components: Shared -// ============================================================ - function LoadingSkeleton() { return (
@@ -263,10 +247,6 @@ function DisabledState() { ); } -// ============================================================ -// Sub-components: Buy Tab -// ============================================================ - function TariffCard({ tariff, isSelected, @@ -904,10 +884,6 @@ function BuyTabContent({ ); } -// ============================================================ -// Sub-components: Activate Tab -// ============================================================ - function ActivateTabContent({ initialCode }: { initialCode?: string | null }) { const { t } = useTranslation(); const queryClient = useQueryClient(); @@ -1023,10 +999,6 @@ function ActivateTabContent({ initialCode }: { initialCode?: string | null }) { ); } -// ============================================================ -// Sub-components: My Gifts Tab -// ============================================================ - function CopiedToast({ onDismiss }: { onDismiss: () => void }) { const { t } = useTranslation(); @@ -1320,20 +1292,12 @@ function MyGiftsTabContent() { ); } -// ============================================================ -// Tab animation variants -// ============================================================ - const tabContentVariants = { initial: { opacity: 0, x: 20 }, animate: { opacity: 1, x: 0 }, exit: { opacity: 0, x: -20 }, }; -// ============================================================ -// Main Component -// ============================================================ - export default function GiftSubscription() { const { t } = useTranslation(); const [searchParams] = useSearchParams(); diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 0b06de2..084a952 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -17,6 +17,7 @@ import { } from '../api/branding'; import { getAndClearReturnUrl } from '../utils/token'; import { isInTelegramWebApp, getTelegramInitData, useTelegramSDK } from '../hooks/useTelegramSDK'; +import { closeMiniApp } from '@telegram-apps/sdk-react'; import LanguageSwitcher from '../components/LanguageSwitcher'; import TelegramLoginButton from '../components/TelegramLoginButton'; import OAuthProviderIcon from '../components/OAuthProviderIcon'; @@ -213,30 +214,13 @@ export default function Login() { tryTelegramAuth(); }, [isAuthInitializing, loginWithTelegram, navigate, t, getReturnUrl]); - // Manual retry for Telegram Mini App auth - const handleRetryTelegramAuth = async () => { - const initData = getTelegramInitData(); - if (!initData) { - setError(t('auth.telegramRequired')); - return; - } - - setError(''); - setIsLoading(true); + const handleRetryTelegramAuth = () => { try { - await loginWithTelegram(initData); - navigate(getReturnUrl(), { replace: true }); - } catch (err) { - const error = err as { response?: { status?: number; data?: { detail?: string } } }; - const status = error.response?.status; - const detail = error.response?.data?.detail; - if (import.meta.env.DEV) console.warn('Telegram auth retry failed:', status, detail); - setError( - detail || - t('auth.telegramRetryFailed', 'Authorization failed. Close the app and try again.'), - ); - } finally { - setIsLoading(false); + sessionStorage.removeItem('tapps/launchParams'); + sessionStorage.removeItem('telegram_init_data'); + closeMiniApp(); + } catch { + window.location.reload(); } }; diff --git a/src/pages/PurchaseSuccess.tsx b/src/pages/PurchaseSuccess.tsx index 9474b12..cc9deb5 100644 --- a/src/pages/PurchaseSuccess.tsx +++ b/src/pages/PurchaseSuccess.tsx @@ -15,10 +15,6 @@ import { cn } from '../lib/utils'; const MAX_POLL_MS = 10 * 60 * 1000; // 10 minutes -// ============================================================ -// Sub-components -// ============================================================ - function PendingState() { const { t } = useTranslation(); @@ -600,10 +596,6 @@ function PollTimedOutState({ onRetry }: { onRetry: () => void }) { ); } -// ============================================================ -// Main Component -// ============================================================ - export default function PurchaseSuccess() { const { t } = useTranslation(); const { token } = useParams<{ token: string }>(); diff --git a/src/pages/QuickPurchase.tsx b/src/pages/QuickPurchase.tsx index 2e18611..f981d13 100644 --- a/src/pages/QuickPurchase.tsx +++ b/src/pages/QuickPurchase.tsx @@ -48,10 +48,6 @@ function formatPeriodLabel( return t('landing.periodLabels.nDays', { count: days }); } -// ============================================================ -// Sub-components -// ============================================================ - function LoadingSkeleton() { return (
@@ -611,10 +607,6 @@ function SummaryCard({ ); } -// ============================================================ -// Discount Countdown -// ============================================================ - function TimeUnit({ value, label }: { value: number; label: string }) { return (
@@ -715,10 +707,6 @@ function DiscountBanner({ ); } -// ============================================================ -// Main Component -// ============================================================ - export default function QuickPurchase() { const { slug } = useParams<{ slug: string }>(); const { t, i18n } = useTranslation(); diff --git a/src/pages/Referral.tsx b/src/pages/Referral.tsx index 99b2381..f414ec4 100644 --- a/src/pages/Referral.tsx +++ b/src/pages/Referral.tsx @@ -239,9 +239,7 @@ export default function Referral() { text: shareText, url: referralLink, }) - .catch(() => { - // ignore cancellation errors - }); + .catch(() => {}); return; } diff --git a/src/pages/Wheel.tsx b/src/pages/Wheel.tsx index 9540793..6a625cf 100644 --- a/src/pages/Wheel.tsx +++ b/src/pages/Wheel.tsx @@ -425,7 +425,6 @@ export default function Wheel() { // Use the pending result from polling, or show a fallback if (pendingStarsResultRef.current) { setSpinResult(pendingStarsResultRef.current); - // Haptic feedback based on result if (pendingStarsResultRef.current.prize_type === 'nothing') { haptic.notification('warning'); } else { diff --git a/src/platform/adapters/TelegramAdapter.ts b/src/platform/adapters/TelegramAdapter.ts index 1765bb0..3eddbb8 100644 --- a/src/platform/adapters/TelegramAdapter.ts +++ b/src/platform/adapters/TelegramAdapter.ts @@ -73,18 +73,14 @@ function createBackButtonController(): BackButtonController { if (currentCallback) { try { offBackButtonClick(currentCallback); - } catch { - // ignore - } + } catch {} } currentCallback = onClick; try { onBackButtonClick(onClick); showBackButton(); - } catch { - // Back button not mounted - } + } catch {} }, hide() { @@ -93,16 +89,12 @@ function createBackButtonController(): BackButtonController { if (currentCallback) { try { offBackButtonClick(currentCallback); - } catch { - // ignore - } + } catch {} currentCallback = null; } try { hideBackButton(); - } catch { - // Back button not mounted - } + } catch {} }, }; } @@ -115,27 +107,21 @@ function createHapticController(): HapticController { if (!inTelegram) return; try { hapticFeedbackImpactOccurred(style); - } catch { - // Haptic not available - } + } catch {} }, notification(type: HapticNotificationType) { if (!inTelegram) return; try { hapticFeedbackNotificationOccurred(type); - } catch { - // Haptic not available - } + } catch {} }, selection() { if (!inTelegram) return; try { hapticFeedbackSelectionChanged(); - } catch { - // Haptic not available - } + } catch {} }, }; } @@ -183,8 +169,6 @@ function createDialogController(): DialogController { } try { const buttons = options.buttons?.map((btn) => { - // For 'ok', 'close', 'cancel' types: do NOT include text - // For 'default', 'destructive' types: text is required if (btn.type === 'ok' || btn.type === 'close' || btn.type === 'cancel') { return { type: btn.type, id: btn.id }; } @@ -212,18 +196,14 @@ function createThemeController(): ThemeController { if (!inTelegram) return; try { setMiniAppHeaderColor(color as `#${string}`); - } catch { - // Not supported - } + } catch {} }, setBottomBarColor(color: string) { if (!inTelegram) return; try { setMiniAppBottomBarColor(color as `#${string}`); - } catch { - // Not supported - } + } catch {} }, getThemeParams() { @@ -231,7 +211,6 @@ function createThemeController(): ThemeController { try { const params = themeParamsState(); if (!params) return null; - // SDK v3 uses camelCase — convert to snake_case for our interface return { bg_color: params.bgColor, text_color: params.textColor, @@ -330,9 +309,7 @@ export function createTelegramAdapter(): PlatformContext { try { await navigator.share({ text: shareText, url }); return true; - } catch { - // User cancelled or share failed - } + } catch {} } try { @@ -350,9 +327,7 @@ export function createTelegramAdapter(): PlatformContext { } else { disableClosingConfirmation(); } - } catch { - // Not supported - } + } catch {} }, }; } diff --git a/src/platform/adapters/WebAdapter.ts b/src/platform/adapters/WebAdapter.ts index 0e90ff4..4817722 100644 --- a/src/platform/adapters/WebAdapter.ts +++ b/src/platform/adapters/WebAdapter.ts @@ -12,7 +12,6 @@ import type { HapticNotificationType, } from '@/platform/types'; -// Storage key for local storage fallback const STORAGE_PREFIX = 'bedolaga_'; function createCapabilities(): PlatformCapabilities { @@ -154,7 +153,6 @@ function createCloudStorageController(): CloudStorageController { try { localStorage.setItem(STORAGE_PREFIX + key, value); } catch { - // Storage might be full or disabled console.warn('Failed to save to localStorage:', key); } }, diff --git a/src/store/auth.ts b/src/store/auth.ts index c147b31..ef4eb6f 100644 --- a/src/store/auth.ts +++ b/src/store/auth.ts @@ -53,8 +53,6 @@ interface AuthState { ) => Promise; } -// Блокировка для предотвращения race condition при инициализации -// Используем объект для атомарности операций const initState = { promise: null as Promise | null, isInitializing: false, @@ -92,12 +90,9 @@ export const useAuthStore = create()( }, logout: () => { - // Get refresh token from secure storage, not zustand state const refreshToken = tokenStorage.getRefreshToken(); if (refreshToken) { - authApi.logout(refreshToken).catch(() => { - // Logout API call failed - ignore silently - }); + authApi.logout(refreshToken).catch(() => {}); } tokenStorage.clearTokens(); usePermissionStore.getState().reset(); @@ -118,7 +113,6 @@ export const useAuthStore = create()( usePermissionStore.getState().reset(); return; } - // Используем apiClient для единообразной обработки ошибок const response = await apiClient.get<{ is_admin: boolean }>('/cabinet/auth/me/is-admin'); set({ isAdmin: response.data.is_admin }); if (response.data.is_admin) { @@ -136,18 +130,14 @@ export const useAuthStore = create()( try { const user = await authApi.getMe(); set({ user }); - } catch { - // Failed to refresh user - ignore silently - } + } catch {} }, initialize: async () => { - // Защита от race condition - если уже инициализировано, выходим if (initState.isInitialized) { return; } - // Если уже идёт инициализация, ждём её завершения if (initState.isInitializing && initState.promise) { return initState.promise; } @@ -157,7 +147,6 @@ export const useAuthStore = create()( try { set({ isLoading: true }); - // Миграция токенов из localStorage (для обратной совместимости) tokenStorage.migrateFromLocalStorage(); const accessToken = tokenStorage.getAccessToken(); @@ -168,15 +157,10 @@ export const useAuthStore = create()( return; } - // No access token or it's expired — try refresh - // This handles Mini App reopens where sessionStorage was cleared - // but refresh token persists in localStorage if (!isTokenValid(accessToken)) { - // Используем централизованный менеджер для refresh const newToken = await tokenRefreshManager.refreshAccessToken(); if (newToken) { const user = await authApi.getMe(); - // Сначала проверяем admin статус, потом снимаем isLoading await get().checkAdminStatus(); set({ accessToken: newToken, @@ -200,7 +184,6 @@ export const useAuthStore = create()( try { const user = await authApi.getMe(); - // Сначала проверяем admin статус, потом снимаем isLoading await get().checkAdminStatus(); set({ accessToken, @@ -210,12 +193,10 @@ export const useAuthStore = create()( isLoading: false, }); } catch { - // Token might be invalid on server, try to refresh const newToken = await tokenRefreshManager.refreshAccessToken(); if (newToken) { try { const user = await authApi.getMe(); - // Сначала проверяем admin статус, потом снимаем isLoading await get().checkAdminStatus(); set({ accessToken: newToken, @@ -235,7 +216,6 @@ export const useAuthStore = create()( }); } } else { - // Refresh failed, logout tokenStorage.clearTokens(); set({ accessToken: null, @@ -339,9 +319,6 @@ export const useAuthStore = create()( }, registerWithEmail: async (email, password, firstName, referralCode) => { - // Registration now returns message, not tokens - // User must verify email before they can login - // Campaign slug stays in localStorage — consumed during verify_email step const code = referralCode || consumeReferralCode() || undefined; const response = await authApi.registerEmailStandalone({ email, @@ -355,8 +332,6 @@ export const useAuthStore = create()( }), { name: 'cabinet-auth', - // Only persist user info for UI caching - // Tokens are stored securely in sessionStorage via tokenStorage partialize: (state) => ({ user: state.user, }), @@ -364,9 +339,7 @@ export const useAuthStore = create()( ), ); -// Capture campaign slug and referral code from URL before auth initialization captureCampaignFromUrl(); captureReferralFromUrl(); -// Initialize auth on app load useAuthStore.getState().initialize(); diff --git a/src/utils/campaign.ts b/src/utils/campaign.ts index 4fa3d2c..2aee0b9 100644 --- a/src/utils/campaign.ts +++ b/src/utils/campaign.ts @@ -28,9 +28,7 @@ function clearSlug(): void { try { localStorage.removeItem(CAMPAIGN_KEY); localStorage.removeItem(CAMPAIGN_TTL_KEY); - } catch { - // localStorage unavailable - } + } catch {} } /** @@ -52,9 +50,7 @@ export function captureCampaignFromUrl(): void { const newUrl = window.location.pathname + (newSearch ? `?${newSearch}` : '') + window.location.hash; window.history.replaceState(null, '', newUrl); - } catch { - // localStorage or history API unavailable - } + } catch {} } /** diff --git a/src/utils/referral.ts b/src/utils/referral.ts index b0dfbb1..7fb4a73 100644 --- a/src/utils/referral.ts +++ b/src/utils/referral.ts @@ -28,9 +28,7 @@ function clearCode(): void { try { localStorage.removeItem(REFERRAL_KEY); localStorage.removeItem(REFERRAL_TTL_KEY); - } catch { - // localStorage unavailable - } + } catch {} } /** @@ -52,9 +50,7 @@ export function captureReferralFromUrl(): void { const newUrl = window.location.pathname + (newSearch ? `?${newSearch}` : '') + window.location.hash; window.history.replaceState(null, '', newUrl); - } catch { - // localStorage or history API unavailable - } + } catch {} } /** diff --git a/src/utils/token.ts b/src/utils/token.ts index fd68bb5..11cc932 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -1,7 +1,3 @@ -/** - * Утилиты для безопасной работы с JWT токенами - */ - import axios from 'axios'; const TOKEN_KEYS = { @@ -18,10 +14,6 @@ interface JWTPayload { [key: string]: unknown; } -/** - * Декодирует JWT токен без верификации подписи - * Используется только для чтения payload на клиенте - */ export function decodeJWT(token: string): JWTPayload | null { try { const parts = token.split('.'); @@ -35,11 +27,6 @@ export function decodeJWT(token: string): JWTPayload | null { } } -/** - * Проверяет, истёк ли срок действия токена - * @param token JWT токен - * @param bufferSeconds Буфер в секундах до истечения (по умолчанию 30 сек) - */ export function isTokenExpired(token: string | null, bufferSeconds = 30): boolean { if (!token) return true; @@ -50,19 +37,11 @@ export function isTokenExpired(token: string | null, bufferSeconds = 30): boolea return payload.exp <= now + bufferSeconds; } -/** - * Проверяет, валиден ли токен (не истёк и корректный формат) - */ export function isTokenValid(token: string | null): boolean { if (!token) return false; return !isTokenExpired(token); } -/** - * Безопасное хранилище токенов - * Access token: sessionStorage (short-lived, cleared on tab close) - * Refresh token: localStorage (persistent, survives Mini App reopens, server-validated) - */ export const tokenStorage = { getAccessToken(): string | null { try { @@ -74,7 +53,6 @@ export const tokenStorage = { getRefreshToken(): string | null { try { - // Refresh token in localStorage for persistence across Mini App reopens return localStorage.getItem(TOKEN_KEYS.REFRESH) || sessionStorage.getItem(TOKEN_KEYS.REFRESH); } catch { return null; @@ -84,13 +62,9 @@ export const tokenStorage = { setTokens(accessToken: string, refreshToken: string): void { try { sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken); - // Refresh token in localStorage — survives Mini App tab close/reopen localStorage.setItem(TOKEN_KEYS.REFRESH, refreshToken); - // Clean up old sessionStorage refresh token (migration) sessionStorage.removeItem(TOKEN_KEYS.REFRESH); - } catch { - // Storage unavailable - } + } catch {} }, setAccessToken(accessToken: string): void { @@ -106,39 +80,26 @@ export const tokenStorage = { sessionStorage.removeItem(TOKEN_KEYS.ACCESS); sessionStorage.removeItem(TOKEN_KEYS.REFRESH); sessionStorage.removeItem(TOKEN_KEYS.USER); - // Также очищаем localStorage для миграции со старой версии localStorage.removeItem(TOKEN_KEYS.ACCESS); localStorage.removeItem(TOKEN_KEYS.REFRESH); localStorage.removeItem(TOKEN_KEYS.USER); - } catch { - // ignore - } + } catch {} }, - /** - * Миграция токенов для обратной совместимости. - * Access token: sessionStorage (short-lived, OK to lose on tab close) - * Refresh token: localStorage (persistent, survives Mini App reopens) - */ migrateFromLocalStorage(): void { try { const accessToken = localStorage.getItem(TOKEN_KEYS.ACCESS); - - // Migrate access token to sessionStorage if (accessToken && !sessionStorage.getItem(TOKEN_KEYS.ACCESS)) { sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken); } localStorage.removeItem(TOKEN_KEYS.ACCESS); - // Migrate refresh token from sessionStorage to localStorage const refreshInSession = sessionStorage.getItem(TOKEN_KEYS.REFRESH); if (refreshInSession && !localStorage.getItem(TOKEN_KEYS.REFRESH)) { localStorage.setItem(TOKEN_KEYS.REFRESH, refreshInSession); } sessionStorage.removeItem(TOKEN_KEYS.REFRESH); - } catch { - // ignore - } + } catch {} }, getTelegramInitData(): string | null { @@ -152,16 +113,10 @@ export const tokenStorage = { setTelegramInitData(data: string): void { try { sessionStorage.setItem(TOKEN_KEYS.TELEGRAM_INIT, data); - } catch { - // ignore - } + } catch {} }, }; -/** - * Extract Telegram user ID from raw initData string. - * Does NOT validate cryptographic signature — used only for client-side identity comparison. - */ function extractTelegramUserId(initData: string): string | null { try { const params = new URLSearchParams(initData); @@ -176,29 +131,14 @@ function extractTelegramUserId(initData: string): string | null { const TG_USER_ID_KEY = 'tg_user_id'; -/** - * Detect Telegram account switch and clear stale auth data. - * - * Telegram Mini App WebView shares localStorage across accounts on the same device - * (confirmed bug on Desktop, likely on mobile too). This means that when user A logs in, - * then user B opens the same Mini App, user A's refresh_token persists in localStorage. - * - * The old approach stored initData in sessionStorage for comparison, but sessionStorage - * is cleared on tab close — so the comparison never triggers between separate openings. - * - * Fix: store the Telegram user ID in localStorage (survives tab close) and compare it - * with the fresh initData on every app launch. - */ export function clearStaleSessionIfNeeded(freshInitData: string | null): void { if (!freshInitData) return; try { const currentTgUserId = extractTelegramUserId(freshInitData); - - // PRIMARY CHECK: compare Telegram user ID stored in localStorage (survives tab close) const storedTgUserId = localStorage.getItem(TG_USER_ID_KEY); + if (storedTgUserId && currentTgUserId && storedTgUserId !== currentTgUserId) { - // Account switch detected — purge all auth data sessionStorage.removeItem(TOKEN_KEYS.ACCESS); sessionStorage.removeItem(TOKEN_KEYS.REFRESH); sessionStorage.removeItem(TOKEN_KEYS.USER); @@ -206,32 +146,15 @@ export function clearStaleSessionIfNeeded(freshInitData: string | null): void { localStorage.removeItem('cabinet-auth'); } - // SECONDARY CHECK: same-session switch (initData changed without tab close) - const storedInitData = sessionStorage.getItem(TOKEN_KEYS.TELEGRAM_INIT); - if (storedInitData && storedInitData !== freshInitData) { - sessionStorage.removeItem(TOKEN_KEYS.ACCESS); - sessionStorage.removeItem(TOKEN_KEYS.REFRESH); - sessionStorage.removeItem(TOKEN_KEYS.USER); - localStorage.removeItem(TOKEN_KEYS.REFRESH); - localStorage.removeItem('cabinet-auth'); - } - - // Persist current Telegram user ID for future comparisons if (currentTgUserId) { localStorage.setItem(TG_USER_ID_KEY, currentTgUserId); } sessionStorage.setItem(TOKEN_KEYS.TELEGRAM_INIT, freshInitData); localStorage.removeItem(TOKEN_KEYS.TELEGRAM_INIT); - } catch { - // Storage not available - } + } catch {} } -/** - * Централизованный менеджер обновления токенов - * Предотвращает множественные параллельные refresh запросы - */ class TokenRefreshManager { private isRefreshing = false; private refreshPromise: Promise | null = null; @@ -242,12 +165,7 @@ class TokenRefreshManager { this.refreshEndpoint = endpoint; } - /** - * Обновляет access token используя refresh token - * При множественных вызовах возвращает один и тот же Promise - */ async refreshAccessToken(): Promise { - // Если уже идёт refresh - возвращаем существующий Promise if (this.isRefreshing && this.refreshPromise) { return this.refreshPromise; } @@ -270,9 +188,9 @@ class TokenRefreshManager { } } + // Uses plain axios (not apiClient) to avoid circular dependency private async doRefresh(refreshToken: string): Promise { try { - // Используем чистый axios (не apiClient) чтобы избежать циклической зависимости const response = await axios.post<{ access_token?: string }>( this.refreshEndpoint, { refresh_token: refreshToken }, @@ -288,14 +206,10 @@ class TokenRefreshManager { return null; } catch { - // Token refresh failed - don't log sensitive error details return null; } } - /** - * Подписка на результат refresh (для ожидающих запросов) - */ subscribe(callback: (token: string | null) => void): () => void { this.subscribers.push(callback); return () => { @@ -308,16 +222,10 @@ class TokenRefreshManager { this.subscribers = []; } - /** - * Проверяет, идёт ли сейчас refresh - */ get isRefreshInProgress(): boolean { return this.isRefreshing; } - /** - * Ожидает завершения текущего refresh (если есть) - */ async waitForRefresh(): Promise { if (this.refreshPromise) { return this.refreshPromise; @@ -328,27 +236,17 @@ class TokenRefreshManager { export const tokenRefreshManager = new TokenRefreshManager(); -/** - * Ключ для сохранения URL для возврата после логина - */ const RETURN_URL_KEY = 'auth_return_url'; -/** - * Сохраняет URL для возврата после авторизации - */ export function saveReturnUrl(): void { if (typeof window !== 'undefined') { const currentPath = window.location.pathname + window.location.search; - // Не сохраняем /login как return URL if (currentPath && currentPath !== '/login') { sessionStorage.setItem(RETURN_URL_KEY, currentPath); } } } -/** - * Получает и очищает сохранённый URL для возврата - */ export function getAndClearReturnUrl(): string | null { if (typeof window !== 'undefined') { const url = sessionStorage.getItem(RETURN_URL_KEY); @@ -358,40 +256,22 @@ export function getAndClearReturnUrl(): string | null { return null; } -/** - * Безопасный редирект на страницу логина - * Валидирует URL чтобы предотвратить open redirect уязвимость - * Сохраняет текущий URL для возврата после авторизации - */ export function safeRedirectToLogin(): void { - // Разрешённые пути для редиректа (относительные пути нашего приложения) - const loginPath = '/login'; - - // Проверяем, что мы на том же origin if (typeof window !== 'undefined') { - // Сохраняем текущий URL для возврата после логина saveReturnUrl(); - // Используем только относительный путь для безопасности - window.location.href = loginPath; + window.location.href = '/login'; } } -/** - * Валидирует URL для редиректа - * Возвращает true только для безопасных URL (относительные пути или тот же origin) - */ export function isValidRedirectUrl(url: string): boolean { - // Пустой URL - небезопасен if (!url) return false; - // Относительные пути всегда безопасны if (url.startsWith('/') && !url.startsWith('//')) { return true; } try { const parsed = new URL(url, window.location.origin); - // Разрешаем только тот же origin return parsed.origin === window.location.origin; } catch { return false; diff --git a/src/utils/topUpStorage.ts b/src/utils/topUpStorage.ts index 7eee91d..5da258e 100644 --- a/src/utils/topUpStorage.ts +++ b/src/utils/topUpStorage.ts @@ -16,9 +16,7 @@ function isRecord(value: unknown): value is Record { export function saveTopUpPendingInfo(info: TopUpPendingInfo) { try { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(info)); - } catch { - // sessionStorage unavailable (private mode, quota, etc.) - } + } catch {} } export function loadTopUpPendingInfo(): TopUpPendingInfo | null { @@ -57,7 +55,5 @@ export function loadTopUpPendingInfo(): TopUpPendingInfo | null { export function clearTopUpPendingInfo() { try { sessionStorage.removeItem(STORAGE_KEY); - } catch { - // ignore - } + } catch {} }