mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
fix: resolve all 14 ESLint warnings across the codebase
- Add varsIgnorePattern to no-unused-vars for destructuring patterns - Fix all react-hooks/exhaustive-deps by adding missing dependencies - Refactor useAnimatedNumber to use ref instead of stale state closure - Wrap handleLinkResult in useCallback for stable deps - Extract OAuth utilities from OAuthCallback.tsx to utils/oauth.ts - Extract background config utilities to utils/backgroundConfig.ts - Remove unused catch parameter in GiftSubscription
This commit is contained in:
@@ -144,8 +144,7 @@ function SortableTariffCard({
|
||||
: 'border-dark-700/50 opacity-60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Drag handle */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
@@ -155,106 +154,113 @@ function SortableTariffCard({
|
||||
<GripIcon />
|
||||
</button>
|
||||
|
||||
{/* Content */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<h3 className="truncate font-medium text-dark-100">{tariff.name}</h3>
|
||||
{tariff.is_daily ? (
|
||||
<span className="rounded bg-warning-500/20 px-2 py-0.5 text-xs text-warning-400">
|
||||
{t('admin.tariffs.dailyType')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="rounded bg-accent-500/20 px-2 py-0.5 text-xs text-accent-400">
|
||||
{t('admin.tariffs.periodType')}
|
||||
</span>
|
||||
)}
|
||||
{tariff.is_trial_available && (
|
||||
<span className="rounded bg-success-500/20 px-2 py-0.5 text-xs text-success-400">
|
||||
{t('admin.tariffs.trial')}
|
||||
</span>
|
||||
)}
|
||||
{tariff.show_in_gift && (
|
||||
<span className="inline-flex items-center gap-1 rounded bg-purple-500/20 px-2 py-0.5 text-xs text-purple-400">
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21 11.25v8.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 109.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1114.625 7.5H12m0 0V21m-8.625-9.75h18c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125h-18c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
|
||||
/>
|
||||
</svg>
|
||||
{t('admin.tariffs.giftBadge')}
|
||||
</span>
|
||||
)}
|
||||
{!tariff.is_active && (
|
||||
<span className="rounded bg-dark-600 px-2 py-0.5 text-xs text-dark-400">
|
||||
{t('admin.tariffs.inactive')}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 flex flex-wrap items-center gap-2">
|
||||
<h3 className="truncate font-medium text-dark-100">{tariff.name}</h3>
|
||||
{tariff.is_daily ? (
|
||||
<span className="rounded bg-warning-500/20 px-2 py-0.5 text-xs text-warning-400">
|
||||
{t('admin.tariffs.dailyType')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="rounded bg-accent-500/20 px-2 py-0.5 text-xs text-accent-400">
|
||||
{t('admin.tariffs.periodType')}
|
||||
</span>
|
||||
)}
|
||||
{tariff.is_trial_available && (
|
||||
<span className="rounded bg-success-500/20 px-2 py-0.5 text-xs text-success-400">
|
||||
{t('admin.tariffs.trial')}
|
||||
</span>
|
||||
)}
|
||||
{tariff.show_in_gift && (
|
||||
<span className="inline-flex items-center gap-1 rounded bg-purple-500/20 px-2 py-0.5 text-xs text-purple-400">
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21 11.25v8.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 109.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1114.625 7.5H12m0 0V21m-8.625-9.75h18c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125h-18c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
|
||||
/>
|
||||
</svg>
|
||||
{t('admin.tariffs.giftBadge')}
|
||||
</span>
|
||||
)}
|
||||
{!tariff.is_active && (
|
||||
<span className="rounded bg-dark-600 px-2 py-0.5 text-xs text-dark-400">
|
||||
{t('admin.tariffs.inactive')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-dark-400">
|
||||
{tariff.is_daily && tariff.daily_price_kopeks > 0 && (
|
||||
<span className="text-warning-400">
|
||||
{(tariff.daily_price_kopeks / 100).toFixed(2)}{' '}
|
||||
{t('admin.tariffs.currencyPerDay')}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{tariff.traffic_limit_gb === 0
|
||||
? t('admin.tariffs.unlimited')
|
||||
: `${tariff.traffic_limit_gb} GB`}
|
||||
</span>
|
||||
<span>{t('admin.tariffs.devices', { count: tariff.device_limit })}</span>
|
||||
<span>{t('admin.tariffs.servers', { count: tariff.servers_count })}</span>
|
||||
<span>
|
||||
{t('admin.tariffs.subscriptions', { count: tariff.subscriptions_count })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 sm:flex-shrink-0">
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`rounded-lg p-2 transition-colors ${
|
||||
tariff.is_active
|
||||
? 'bg-success-500/20 text-success-400 hover:bg-success-500/30'
|
||||
: 'bg-dark-700 text-dark-400 hover:bg-dark-600'
|
||||
}`}
|
||||
title={
|
||||
tariff.is_active ? t('admin.tariffs.deactivate') : t('admin.tariffs.activate')
|
||||
}
|
||||
>
|
||||
{tariff.is_active ? <CheckIcon /> : <XIcon />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onToggleTrial}
|
||||
className={`rounded-lg p-2 transition-colors ${
|
||||
tariff.is_trial_available
|
||||
? 'bg-accent-500/20 text-accent-400 hover:bg-accent-500/30'
|
||||
: 'bg-dark-700 text-dark-400 hover:bg-dark-600'
|
||||
}`}
|
||||
title={t('admin.tariffs.toggleTrial')}
|
||||
>
|
||||
<GiftIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-dark-600 hover:text-dark-100"
|
||||
title={t('admin.tariffs.edit')}
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-error-500/20 hover:text-error-400"
|
||||
title={t('admin.tariffs.delete')}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-dark-400">
|
||||
{tariff.is_daily && tariff.daily_price_kopeks > 0 && (
|
||||
<span className="text-warning-400">
|
||||
{(tariff.daily_price_kopeks / 100).toFixed(2)} {t('admin.tariffs.currencyPerDay')}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{tariff.traffic_limit_gb === 0
|
||||
? t('admin.tariffs.unlimited')
|
||||
: `${tariff.traffic_limit_gb} GB`}
|
||||
</span>
|
||||
<span>{t('admin.tariffs.devices', { count: tariff.device_limit })}</span>
|
||||
<span>{t('admin.tariffs.servers', { count: tariff.servers_count })}</span>
|
||||
<span>{t('admin.tariffs.subscriptions', { count: tariff.subscriptions_count })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`rounded-lg p-2 transition-colors ${
|
||||
tariff.is_active
|
||||
? 'bg-success-500/20 text-success-400 hover:bg-success-500/30'
|
||||
: 'bg-dark-700 text-dark-400 hover:bg-dark-600'
|
||||
}`}
|
||||
title={tariff.is_active ? t('admin.tariffs.deactivate') : t('admin.tariffs.activate')}
|
||||
>
|
||||
{tariff.is_active ? <CheckIcon /> : <XIcon />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onToggleTrial}
|
||||
className={`rounded-lg p-2 transition-colors ${
|
||||
tariff.is_trial_available
|
||||
? 'bg-accent-500/20 text-accent-400 hover:bg-accent-500/30'
|
||||
: 'bg-dark-700 text-dark-400 hover:bg-dark-600'
|
||||
}`}
|
||||
title={t('admin.tariffs.toggleTrial')}
|
||||
>
|
||||
<GiftIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-dark-600 hover:text-dark-100"
|
||||
title={t('admin.tariffs.edit')}
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-error-500/20 hover:text-error-400"
|
||||
title={t('admin.tariffs.delete')}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
@@ -10,7 +10,7 @@ import { Card } from '@/components/data-display/Card';
|
||||
import { Button } from '@/components/primitives/Button';
|
||||
import { staggerContainer, staggerItem } from '@/components/motion/transitions';
|
||||
import ProviderIcon from '../components/ProviderIcon';
|
||||
import { LINK_OAUTH_STATE_KEY, LINK_OAUTH_PROVIDER_KEY, getErrorDetail } from './OAuthCallback';
|
||||
import { LINK_OAUTH_STATE_KEY, LINK_OAUTH_PROVIDER_KEY, getErrorDetail } from '../utils/oauth';
|
||||
import { getTelegramInitData } from '../hooks/useTelegramSDK';
|
||||
import { usePlatform, useIsTelegram } from '@/platform/hooks/usePlatform';
|
||||
import type { LinkedProvider } from '../types';
|
||||
@@ -53,15 +53,17 @@ function TelegramLinkWidget() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Shared handler for link result
|
||||
const handleLinkResult = async (response: Awaited<ReturnType<typeof authApi.linkTelegram>>) => {
|
||||
if (response.merge_required && response.merge_token) {
|
||||
navigate(`/merge/${response.merge_token}`, { replace: true });
|
||||
} else {
|
||||
queryClient.invalidateQueries({ queryKey: ['linked-providers'] });
|
||||
showToast({ type: 'success', message: t('profile.accounts.linkSuccess') });
|
||||
}
|
||||
};
|
||||
const handleLinkResult = useCallback(
|
||||
async (response: Awaited<ReturnType<typeof authApi.linkTelegram>>) => {
|
||||
if (response.merge_required && response.merge_token) {
|
||||
navigate(`/merge/${response.merge_token}`, { replace: true });
|
||||
} else {
|
||||
queryClient.invalidateQueries({ queryKey: ['linked-providers'] });
|
||||
showToast({ type: 'success', message: t('profile.accounts.linkSuccess') });
|
||||
}
|
||||
},
|
||||
[navigate, queryClient, showToast, t],
|
||||
);
|
||||
|
||||
// OIDC callback handler (ref pattern to avoid stale closures)
|
||||
const handleOIDCCallbackRef =
|
||||
@@ -129,7 +131,7 @@ function TelegramLinkWidget() {
|
||||
} else {
|
||||
initTelegramLogin();
|
||||
}
|
||||
}, [isOIDC, widgetConfig?.oidc_client_id, widgetConfig?.request_access]);
|
||||
}, [isOIDC, widgetConfig?.oidc_client_id, widgetConfig?.request_access, showToast, t]);
|
||||
|
||||
// Legacy widget effect (only when NOT OIDC)
|
||||
useEffect(() => {
|
||||
@@ -183,7 +185,7 @@ function TelegramLinkWidget() {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
};
|
||||
}, [isOIDC, botUsername, navigate, showToast, t, queryClient]);
|
||||
}, [isOIDC, botUsername, navigate, showToast, t, queryClient, handleLinkResult]);
|
||||
|
||||
if (!botUsername && !isOIDC) {
|
||||
return null;
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function Connection() {
|
||||
hideLink: appConfig?.hideLink ?? false,
|
||||
},
|
||||
});
|
||||
}, [navigate, appConfig?.subscriptionUrl, appConfig?.hideLink]);
|
||||
}, [navigate, appConfig?.subscriptionUrl, appConfig?.hideLink, isTelegramWebApp]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
|
||||
@@ -611,7 +611,7 @@ function BuyTabContent({
|
||||
setSubmitError(t('gift.failedDesc'));
|
||||
}
|
||||
// 'cancelled' — user closed the invoice, do nothing
|
||||
} catch (e) {
|
||||
} catch {
|
||||
setSubmitError(t('gift.failedDesc'));
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { authApi } from '../api/auth';
|
||||
import { useToast } from '../components/Toast';
|
||||
import { LINK_TELEGRAM_STATE_KEY } from './ConnectedAccounts';
|
||||
import { getErrorDetail } from './OAuthCallback';
|
||||
import { getErrorDetail } from '../utils/oauth';
|
||||
|
||||
export default function LinkTelegramCallback() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -21,7 +21,7 @@ import { closeMiniApp } from '@telegram-apps/sdk-react';
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher';
|
||||
import TelegramLoginButton from '../components/TelegramLoginButton';
|
||||
import OAuthProviderIcon from '../components/OAuthProviderIcon';
|
||||
import { saveOAuthState } from './OAuthCallback';
|
||||
import { saveOAuthState } from '../utils/oauth';
|
||||
import { consumeReferralCode, getPendingReferralCode } from '../utils/referral';
|
||||
|
||||
export default function Login() {
|
||||
|
||||
@@ -3,50 +3,17 @@ import { useNavigate, useSearchParams } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuthStore } from '../store/auth';
|
||||
import { authApi } from '../api/auth';
|
||||
import {
|
||||
peekLinkOAuthState,
|
||||
clearLinkOAuthState,
|
||||
loadOAuthState,
|
||||
clearOAuthState,
|
||||
getErrorDetail,
|
||||
} from '../utils/oauth';
|
||||
import type { ServerCompleteResponse } from '../types';
|
||||
|
||||
// SessionStorage keys for OAuth LINK state (shared with ConnectedAccounts)
|
||||
export const LINK_OAUTH_STATE_KEY = 'link_oauth_state';
|
||||
export const LINK_OAUTH_PROVIDER_KEY = 'link_oauth_provider';
|
||||
|
||||
// SessionStorage helpers for OAuth LOGIN state
|
||||
const OAUTH_STATE_KEY = 'oauth_state';
|
||||
const OAUTH_PROVIDER_KEY = 'oauth_provider';
|
||||
|
||||
export function saveOAuthState(state: string, provider: string): void {
|
||||
sessionStorage.setItem(OAUTH_STATE_KEY, state);
|
||||
sessionStorage.setItem(OAUTH_PROVIDER_KEY, provider);
|
||||
}
|
||||
|
||||
/** Read link OAuth state without clearing (cleared only after successful match). */
|
||||
function peekLinkOAuthState(): { state: string; provider: string } | null {
|
||||
const state = sessionStorage.getItem(LINK_OAUTH_STATE_KEY);
|
||||
const provider = sessionStorage.getItem(LINK_OAUTH_PROVIDER_KEY);
|
||||
if (!state || !provider) return null;
|
||||
return { state, provider };
|
||||
}
|
||||
|
||||
function clearLinkOAuthState(): void {
|
||||
sessionStorage.removeItem(LINK_OAUTH_STATE_KEY);
|
||||
sessionStorage.removeItem(LINK_OAUTH_PROVIDER_KEY);
|
||||
}
|
||||
|
||||
type CallbackMode = 'login' | 'link-browser' | 'link-server';
|
||||
|
||||
export function getErrorDetail(err: unknown): string | null {
|
||||
if (err && typeof err === 'object' && 'response' in err) {
|
||||
const resp = (err as { response?: { data?: { detail?: unknown } } }).response;
|
||||
const detail = resp?.data?.detail;
|
||||
if (typeof detail === 'string') return detail;
|
||||
if (detail && typeof detail === 'object' && 'message' in detail) {
|
||||
const msg = (detail as Record<string, unknown>).message;
|
||||
if (typeof msg === 'string') return msg;
|
||||
}
|
||||
}
|
||||
if (err instanceof Error) return err.message;
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function OAuthCallback() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -96,15 +63,12 @@ export default function OAuthCallback() {
|
||||
provider = linkSaved.provider;
|
||||
state = linkSaved.state;
|
||||
} else {
|
||||
// Peek at login state first; only clear if it matches URL state
|
||||
const loginState = sessionStorage.getItem(OAUTH_STATE_KEY);
|
||||
const loginProvider = sessionStorage.getItem(OAUTH_PROVIDER_KEY);
|
||||
if (loginState && loginProvider && loginState === urlState) {
|
||||
sessionStorage.removeItem(OAUTH_STATE_KEY);
|
||||
sessionStorage.removeItem(OAUTH_PROVIDER_KEY);
|
||||
const loginSaved = loadOAuthState();
|
||||
if (loginSaved && loginSaved.state === urlState) {
|
||||
clearOAuthState();
|
||||
mode = 'login';
|
||||
provider = loginProvider;
|
||||
state = loginState;
|
||||
provider = loginSaved.provider;
|
||||
state = loginSaved.state;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user