From 207af81c953eccb2e474cca47dfe9434e8a58527 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Fri, 3 Apr 2026 17:23:38 +0300 Subject: [PATCH 1/2] fix: support 100% discount display + fix Telegram link widget race condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - formatPrice shows "Бесплатно/Free" when price is 0 (SubscriptionPurchase, Subscription, RenewSubscription) - Add subscription.free locale key (ru/en/zh/fa) - Fix TelegramLinkWidget NS_BINDING_ABORTED: move legacy widget callback to ref pattern, remove unstable deps (showToast, t, navigate) from useEffect — prevents iframe destruction on re-render - Clean up OIDC effect deps --- src/locales/en.json | 1 + src/locales/fa.json | 1 + src/locales/ru.json | 1 + src/locales/zh.json | 1 + src/pages/ConnectedAccounts.tsx | 61 ++++++++++++++---------------- src/pages/RenewSubscription.tsx | 4 +- src/pages/Subscription.tsx | 5 ++- src/pages/SubscriptionPurchase.tsx | 5 ++- 8 files changed, 43 insertions(+), 36 deletions(-) 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({ From 1cf95267138908e2da79e1da3ebb25c2d0ae03be Mon Sep 17 00:00:00 2001 From: c0mrade Date: Fri, 3 Apr 2026 18:36:11 +0300 Subject: [PATCH 2/2] fix: complete sparkles icon in trial offer card (was showing only 1 of 3 sparkle paths) --- src/components/dashboard/TrialOfferCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" >