Merge pull request #380 from BEDOLAGA-DEV/dev

fix: 100% discount display + Telegram widget fix + sparkles icon
This commit is contained in:
c0mrade
2026-04-03 19:11:43 +03:00
committed by GitHub
9 changed files with 44 additions and 37 deletions

View File

@@ -104,7 +104,7 @@ export default function TrialOfferCard({
aria-hidden="true" aria-hidden="true"
> >
<path <path
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 00-2.455 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />

View File

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

View File

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

View File

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

View File

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

View File

@@ -157,12 +157,35 @@ function TelegramLinkWidget() {
isOIDC, isOIDC,
widgetConfig?.oidc_client_id, widgetConfig?.oidc_client_id,
widgetConfig?.request_access, widgetConfig?.request_access,
showToast,
t,
scriptLoaded, scriptLoaded,
handleScriptFailed, handleScriptFailed,
]); ]);
// 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({
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 // Legacy widget effect (only when NOT OIDC) with timeout
useEffect(() => { useEffect(() => {
if (isOIDC || !containerRef.current || !botUsername) return; if (isOIDC || !containerRef.current || !botUsername) return;
@@ -173,29 +196,10 @@ function TelegramLinkWidget() {
} }
const callbackName = '__onTelegramLinkAuth'; const callbackName = '__onTelegramLinkAuth';
(window as unknown as Record<string, unknown>)[callbackName] = async ( (window as unknown as Record<string, unknown>)[callbackName] = (
user: Record<string, unknown>, user: Record<string, unknown>,
) => { ) => {
if (!mountedRef.current) return; handleWidgetAuthRef.current?.(user);
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'),
});
}
}
}; };
const script = document.createElement('script'); const script = document.createElement('script');
@@ -227,16 +231,7 @@ function TelegramLinkWidget() {
container.removeChild(container.firstChild); container.removeChild(container.firstChild);
} }
}; };
}, [ }, [isOIDC, botUsername, handleScriptFailed]);
isOIDC,
botUsername,
navigate,
showToast,
t,
queryClient,
handleLinkResult,
handleScriptFailed,
]);
if (!botUsername && !isOIDC) { if (!botUsername && !isOIDC) {
return null; return null;

View File

@@ -177,7 +177,9 @@ export default function RenewSubscription() {
</div> </div>
<div className="text-right"> <div className="text-right">
<div className="text-base font-semibold" style={{ color: g.text }}> <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> </div>
{months > 1 && ( {months > 1 && (
<div className="text-[11px]" style={{ color: g.textSecondary }}> <div className="text-[11px]" style={{ color: g.textSecondary }}>

View File

@@ -183,7 +183,10 @@ export default function Subscription() {
const destructiveConfirm = useDestructiveConfirm(); const destructiveConfirm = useDestructiveConfirm();
// Helper to format price from kopeks // 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 // Device/traffic topup state
const [showDeviceTopup, setShowDeviceTopup] = useState(false); const [showDeviceTopup, setShowDeviceTopup] = useState(false);

View File

@@ -37,7 +37,10 @@ export default function SubscriptionPurchase() {
const { isDark } = useTheme(); const { isDark } = useTheme();
const g = getGlassColors(isDark); 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) // Subscription query (shares cache with /subscription page)
const { data: subscriptionResponse, isLoading } = useQuery({ const { data: subscriptionResponse, isLoading } = useQuery({