diff --git a/src/components/dashboard/TrialOfferCard.tsx b/src/components/dashboard/TrialOfferCard.tsx index 2b20af6..c930141 100644 --- a/src/components/dashboard/TrialOfferCard.tsx +++ b/src/components/dashboard/TrialOfferCard.tsx @@ -104,7 +104,7 @@ export default function TrialOfferCard({ aria-hidden="true" > diff --git a/src/locales/en.json b/src/locales/en.json index ef4ea57..1221050 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -324,6 +324,7 @@ "used": "Used", "of": "of", "renew": "Renew", + "free": "Free", "extend": "Extend Subscription", "buyTraffic": "Buy Traffic", "buyDevices": "Buy Devices", diff --git a/src/locales/fa.json b/src/locales/fa.json index 3cf08b8..0b3c251 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -285,6 +285,7 @@ "used": "استفاده شده", "of": "از", "renew": "تمدید", + "free": "رایگان", "extend": "تمدید اشتراک", "buyTraffic": "خرید ترافیک", "buyDevices": "خرید دستگاه", diff --git a/src/locales/ru.json b/src/locales/ru.json index 4fa428e..fdc4051 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -339,6 +339,7 @@ "used": "Использовано", "of": "из", "renew": "Продлить", + "free": "Бесплатно", "extend": "Продлить подписку", "buyTraffic": "Докупить трафик", "buyDevices": "Докупить устройства", diff --git a/src/locales/zh.json b/src/locales/zh.json index aa9e1d8..e851133 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -285,6 +285,7 @@ "used": "已使用", "of": "/", "renew": "续费", + "free": "免费", "extend": "延长订阅", "buyTraffic": "购买流量", "buyDevices": "增加设备", diff --git a/src/pages/ConnectedAccounts.tsx b/src/pages/ConnectedAccounts.tsx index e89e0a6..78467d3 100644 --- a/src/pages/ConnectedAccounts.tsx +++ b/src/pages/ConnectedAccounts.tsx @@ -157,12 +157,35 @@ function TelegramLinkWidget() { isOIDC, widgetConfig?.oidc_client_id, widgetConfig?.request_access, - showToast, - t, scriptLoaded, handleScriptFailed, ]); + // Ref-based callback for legacy widget (avoids re-creating iframe on every render) + const handleWidgetAuthRef = useRef<(user: Record) => void>(undefined); + handleWidgetAuthRef.current = async (user: Record) => { + if (!mountedRef.current) return; + try { + const response = await authApi.linkTelegram({ + id: user.id as number, + first_name: user.first_name as string, + last_name: (user.last_name as string) || undefined, + username: (user.username as string) || undefined, + photo_url: (user.photo_url as string) || undefined, + auth_date: user.auth_date as number, + hash: user.hash as string, + }); + if (mountedRef.current) await handleLinkResult(response); + } catch (err: unknown) { + if (mountedRef.current) { + showToast({ + type: 'error', + message: getErrorDetail(err) || t('profile.accounts.linkError'), + }); + } + } + }; + // Legacy widget effect (only when NOT OIDC) with timeout useEffect(() => { if (isOIDC || !containerRef.current || !botUsername) return; @@ -173,29 +196,10 @@ function TelegramLinkWidget() { } const callbackName = '__onTelegramLinkAuth'; - (window as unknown as Record)[callbackName] = async ( + (window as unknown as Record)[callbackName] = ( user: Record, ) => { - if (!mountedRef.current) return; - try { - const response = await authApi.linkTelegram({ - id: user.id as number, - first_name: user.first_name as string, - last_name: (user.last_name as string) || undefined, - username: (user.username as string) || undefined, - photo_url: (user.photo_url as string) || undefined, - auth_date: user.auth_date as number, - hash: user.hash as string, - }); - if (mountedRef.current) await handleLinkResult(response); - } catch (err: unknown) { - if (mountedRef.current) { - showToast({ - type: 'error', - message: getErrorDetail(err) || t('profile.accounts.linkError'), - }); - } - } + handleWidgetAuthRef.current?.(user); }; const script = document.createElement('script'); @@ -227,16 +231,7 @@ function TelegramLinkWidget() { container.removeChild(container.firstChild); } }; - }, [ - isOIDC, - botUsername, - navigate, - showToast, - t, - queryClient, - handleLinkResult, - handleScriptFailed, - ]); + }, [isOIDC, botUsername, handleScriptFailed]); if (!botUsername && !isOIDC) { return null; diff --git a/src/pages/RenewSubscription.tsx b/src/pages/RenewSubscription.tsx index 8dded97..591f03a 100644 --- a/src/pages/RenewSubscription.tsx +++ b/src/pages/RenewSubscription.tsx @@ -177,7 +177,9 @@ export default function RenewSubscription() {
- {formatAmount(option.price_kopeks / 100)} {currencySymbol} + {option.price_kopeks === 0 + ? t('subscription.free', 'Бесплатно') + : `${formatAmount(option.price_kopeks / 100)} ${currencySymbol}`}
{months > 1 && (
diff --git a/src/pages/Subscription.tsx b/src/pages/Subscription.tsx index d1e70d0..d73674a 100644 --- a/src/pages/Subscription.tsx +++ b/src/pages/Subscription.tsx @@ -183,7 +183,10 @@ export default function Subscription() { const destructiveConfirm = useDestructiveConfirm(); // Helper to format price from kopeks - const formatPrice = (kopeks: number) => `${formatAmount(kopeks / 100)} ${currencySymbol}`; + const formatPrice = (kopeks: number) => + kopeks === 0 + ? t('subscription.free', 'Бесплатно') + : `${formatAmount(kopeks / 100)} ${currencySymbol}`; // Device/traffic topup state const [showDeviceTopup, setShowDeviceTopup] = useState(false); diff --git a/src/pages/SubscriptionPurchase.tsx b/src/pages/SubscriptionPurchase.tsx index 13d29f9..0d9fd60 100644 --- a/src/pages/SubscriptionPurchase.tsx +++ b/src/pages/SubscriptionPurchase.tsx @@ -37,7 +37,10 @@ export default function SubscriptionPurchase() { const { isDark } = useTheme(); const g = getGlassColors(isDark); - const formatPrice = (kopeks: number) => `${formatAmount(kopeks / 100)} ${currencySymbol}`; + const formatPrice = (kopeks: number) => + kopeks === 0 + ? t('subscription.free', 'Бесплатно') + : `${formatAmount(kopeks / 100)} ${currencySymbol}`; // Subscription query (shares cache with /subscription page) const { data: subscriptionResponse, isLoading } = useQuery({