Merge PR #508: различать устаревшего бота и ненастроенного на логине

404 на /cabinet/branding/telegram-widget и deeplink-роутах = сборка бота
старше v3.24/v3.33 — вместо «бот не настроен» показываем auth.botOutdated
(4 локали). README: фактический минимум v3.33.0. Закрывает #345.
Спасибо @kewldan.
This commit is contained in:
Fringg
2026-07-24 15:00:36 +03:00
9 changed files with 87 additions and 7 deletions

View File

@@ -1,4 +1,5 @@
import apiClient from './client';
import { isEndpointMissingError } from '../utils/api-error';
import type { AnimationConfig } from '@/components/ui/backgrounds/types';
import { DEFAULT_ANIMATION_CONFIG } from '@/components/ui/backgrounds/types';
@@ -36,6 +37,8 @@ export interface TelegramWidgetConfig {
request_access: boolean;
oidc_enabled: boolean;
oidc_client_id: string;
/** The bot answered 404 on the config route — build older than v3.24.0. */
endpoint_missing?: boolean;
}
export interface OfflineConvGoal {
@@ -321,7 +324,7 @@ export const brandingApi = {
'/cabinet/branding/telegram-widget',
);
return response.data;
} catch {
} catch (err) {
return {
bot_username: import.meta.env.VITE_TELEGRAM_BOT_USERNAME || '',
size: 'large',
@@ -330,6 +333,7 @@ export const brandingApi = {
request_access: true,
oidc_enabled: false,
oidc_client_id: '',
endpoint_missing: isEndpointMissingError(err),
};
}
},

View File

@@ -9,6 +9,7 @@ import { useAuthStore } from '../store/auth';
import { useNavigate } from 'react-router';
import { getPendingCampaignSlug } from '../utils/campaign';
import { copyToClipboard } from '../utils/clipboard';
import { isEndpointMissingError } from '../utils/api-error';
interface TelegramLoginButtonProps {
referralCode?: string;
@@ -159,7 +160,6 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
isOIDC,
widgetConfig?.oidc_client_id,
widgetConfig?.request_access,
t,
scriptLoaded,
handleScriptFailed,
]);
@@ -330,8 +330,9 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
},
(expires_in || 300) * 1000,
);
} catch {
setDeepLinkError(t('common.error'));
} catch (err) {
// 404 = the deep-link auth routes don't exist on this bot build (< v3.33.0)
setDeepLinkError(t(isEndpointMissingError(err) ? 'auth.botOutdated' : 'common.error'));
}
}, [botUsername, loginWithDeepLink, navigate, t]);
@@ -421,9 +422,11 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
}, [deepLinkPolling, deepLinkToken, loginWithDeepLink, navigate, t]);
if (!botUsername || botUsername === 'your_bot') {
// 404 on the config route = the bot build predates the cabinet endpoints,
// which reads as "not configured" but is actually a version mismatch (#345).
return (
<div className="py-4 text-center text-sm text-dark-400">
{t('auth.telegramNotConfigured')}
{t(widgetConfig?.endpoint_missing ? 'auth.botOutdated' : 'auth.telegramNotConfigured')}
</div>
);
}

View File

@@ -201,6 +201,7 @@
"telegramRetryFailed": "Authorization failed. Close the app and try again.",
"telegramReopenHint": "If the problem persists, close and reopen the app",
"telegramNotConfigured": "Telegram bot is not configured",
"botOutdated": "The bot version is not compatible with the cabinet. Update the bot to v3.33.0 or newer.",
"telegramUnavailable": "Telegram login is temporarily unavailable",
"authenticating": "Authenticating...",
"orOpenInApp": "Or open the bot in the app",

View File

@@ -193,6 +193,7 @@
"telegramRetryFailed": "تایید هویت ناموفق بود. برنامه را ببندید و دوباره باز کنید.",
"telegramReopenHint": "اگر مشکل ادامه دارد، برنامه را ببندید و دوباره باز کنید",
"telegramNotConfigured": "ربات تلگرام پیکربندی نشده",
"botOutdated": "نسخهٔ ربات با کابینت سازگار نیست. ربات را به v3.33.0 یا جدیدتر به‌روزرسانی کنید.",
"telegramUnavailable": "ورود از طریق تلگرام موقتاً در دسترس نیست",
"authenticating": "در حال تایید هویت...",
"orOpenInApp": "یا ربات را در برنامه باز کنید",

View File

@@ -204,6 +204,7 @@
"telegramRetryFailed": "Авторизация не удалась. Закройте приложение и откройте заново.",
"telegramReopenHint": "Если проблема повторяется, закройте и откройте приложение заново",
"telegramNotConfigured": "Telegram бот не настроен",
"botOutdated": "Версия бота несовместима с кабинетом. Обновите бота до v3.33.0 или новее.",
"telegramUnavailable": "Вход через Telegram временно недоступен",
"authenticating": "Авторизация...",
"orOpenInApp": "Или откройте бота в приложении",

View File

@@ -193,6 +193,7 @@
"telegramRetryFailed": "授权失败。请关闭应用后重新打开。",
"telegramReopenHint": "如果问题持续存在,请关闭并重新打开应用",
"telegramNotConfigured": "Telegram机器人未配置",
"botOutdated": "机器人版本与后台不兼容,请将机器人更新至 v3.33.0 或更高版本。",
"telegramUnavailable": "Telegram登录暂时不可用",
"authenticating": "正在验证...",
"orOpenInApp": "或在应用中打开机器人",

View File

@@ -0,0 +1,59 @@
import { AxiosError, AxiosHeaders } from 'axios';
import { describe, expect, it } from 'vitest';
import { getApiErrorMessage, isEndpointMissingError } from './api-error';
function axiosErrorWithStatus(status: number, detail?: unknown): AxiosError {
const headers = new AxiosHeaders();
const config = { headers };
return new AxiosError(
'Request failed',
'ERR_BAD_REQUEST',
config,
{},
{
status,
statusText: '',
headers,
config,
data: detail === undefined ? {} : { detail },
},
);
}
describe('isEndpointMissingError', () => {
it('returns true for an axios 404', () => {
expect(isEndpointMissingError(axiosErrorWithStatus(404))).toBe(true);
});
it('returns false for other axios statuses', () => {
expect(isEndpointMissingError(axiosErrorWithStatus(403))).toBe(false);
expect(isEndpointMissingError(axiosErrorWithStatus(500))).toBe(false);
});
it('returns false for non-axios errors', () => {
expect(isEndpointMissingError(new Error('boom'))).toBe(false);
expect(isEndpointMissingError(undefined)).toBe(false);
expect(isEndpointMissingError(null)).toBe(false);
});
});
describe('getApiErrorMessage', () => {
it('returns string detail from an axios error', () => {
expect(getApiErrorMessage(axiosErrorWithStatus(400, 'Code must not be empty'), 'fb')).toBe(
'Code must not be empty',
);
});
it('joins pydantic validation errors', () => {
const err = axiosErrorWithStatus(422, [
{ loc: ['body', 'name'], msg: 'field required' },
{ loc: ['body', 'days'], msg: 'must be positive' },
]);
expect(getApiErrorMessage(err, 'fb')).toBe('name: field required; days: must be positive');
});
it('falls back for non-axios errors and missing detail', () => {
expect(getApiErrorMessage(new Error('boom'), 'fb')).toBe('fb');
expect(getApiErrorMessage(axiosErrorWithStatus(500), 'fb')).toBe('fb');
});
});

View File

@@ -1,5 +1,15 @@
import axios from 'axios';
/**
* True when the backend answered 404 on the route itself — for cabinet
* endpoints that means the bot build is too old to have them (e.g. the
* deep-link auth routes exist only since bot v3.33.0), as opposed to a
* missing entity inside a handler.
*/
export function isEndpointMissingError(err: unknown): boolean {
return axios.isAxiosError(err) && err.response?.status === 404;
}
export function getApiErrorMessage(err: unknown, fallback: string): string {
if (axios.isAxiosError(err)) {
const detail = err.response?.data?.detail;