fix: support 100% discount display + fix Telegram link widget race condition

- 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
This commit is contained in:
c0mrade
2026-04-03 17:23:38 +03:00
parent 7892630e3b
commit 207af81c95
8 changed files with 43 additions and 36 deletions

View File

@@ -324,6 +324,7 @@
"used": "Used",
"of": "of",
"renew": "Renew",
"free": "Free",
"extend": "Extend Subscription",
"buyTraffic": "Buy Traffic",
"buyDevices": "Buy Devices",

View File

@@ -285,6 +285,7 @@
"used": "استفاده شده",
"of": "از",
"renew": "تمدید",
"free": "رایگان",
"extend": "تمدید اشتراک",
"buyTraffic": "خرید ترافیک",
"buyDevices": "خرید دستگاه",

View File

@@ -339,6 +339,7 @@
"used": "Использовано",
"of": "из",
"renew": "Продлить",
"free": "Бесплатно",
"extend": "Продлить подписку",
"buyTraffic": "Докупить трафик",
"buyDevices": "Докупить устройства",

View File

@@ -285,6 +285,7 @@
"used": "已使用",
"of": "/",
"renew": "续费",
"free": "免费",
"extend": "延长订阅",
"buyTraffic": "购买流量",
"buyDevices": "增加设备",

View File

@@ -157,25 +157,13 @@ function TelegramLinkWidget() {
isOIDC,
widgetConfig?.oidc_client_id,
widgetConfig?.request_access,
showToast,
t,
scriptLoaded,
handleScriptFailed,
]);
// Legacy widget effect (only when NOT OIDC) with timeout
useEffect(() => {
if (isOIDC || !containerRef.current || !botUsername) return;
const container = containerRef.current;
while (container.firstChild) {
container.removeChild(container.firstChild);
}
const callbackName = '__onTelegramLinkAuth';
(window as unknown as Record<string, unknown>)[callbackName] = async (
user: Record<string, unknown>,
) => {
// Ref-based callback for legacy widget (avoids re-creating iframe on every render)
const handleWidgetAuthRef = useRef<(user: Record<string, unknown>) => void>(undefined);
handleWidgetAuthRef.current = async (user: Record<string, unknown>) => {
if (!mountedRef.current) return;
try {
const response = await authApi.linkTelegram({
@@ -198,6 +186,22 @@ function TelegramLinkWidget() {
}
};
// Legacy widget effect (only when NOT OIDC) with timeout
useEffect(() => {
if (isOIDC || !containerRef.current || !botUsername) return;
const container = containerRef.current;
while (container.firstChild) {
container.removeChild(container.firstChild);
}
const callbackName = '__onTelegramLinkAuth';
(window as unknown as Record<string, unknown>)[callbackName] = (
user: Record<string, unknown>,
) => {
handleWidgetAuthRef.current?.(user);
};
const script = document.createElement('script');
script.src = 'https://telegram.org/js/telegram-widget.js?23';
script.setAttribute('data-telegram-login', botUsername);
@@ -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;

View File

@@ -177,7 +177,9 @@ export default function RenewSubscription() {
</div>
<div className="text-right">
<div className="text-base font-semibold" style={{ color: g.text }}>
{formatAmount(option.price_kopeks / 100)} {currencySymbol}
{option.price_kopeks === 0
? t('subscription.free', 'Бесплатно')
: `${formatAmount(option.price_kopeks / 100)} ${currencySymbol}`}
</div>
{months > 1 && (
<div className="text-[11px]" style={{ color: g.textSecondary }}>

View File

@@ -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);

View File

@@ -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({