From f04675b2498abbbfb41ba48e0dd8cf7ee3a5a0a1 Mon Sep 17 00:00:00 2001 From: Fringg Date: Mon, 20 Jul 2026 21:41:08 +0300 Subject: [PATCH] =?UTF-8?q?fix(support):=20=D0=BD=D0=B5=20=D1=81=D0=BA?= =?UTF-8?q?=D0=BB=D0=B5=D0=B8=D0=B2=D0=B0=D1=82=D1=8C=20t.me=20=D1=81?= =?UTF-8?q?=D0=BE=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=BE=D0=B9=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B2=D0=BD=D0=B5=D1=88=D0=BD=D0=B8=D0=B9=20=D1=85?= =?UTF-8?q?=D0=B5=D0=BB=D0=BF=D0=B4=D0=B5=D1=81=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SUPPORT_USERNAME на бэке принимает и @username, и произвольный URL. Кабинет же во всех трёх ветках клеил `https://t.me/${support_username}`, из-за чего внешний хелпдеск превращался в https://t.me/https://help.example.com и не открывался. Бэк теперь отдаёт контакт разрезолвленным (support_url + contact_is_telegram) — клеить на клиенте нечего. Логика вынесена в resolveSupportContact: три копии склейки (profile, fallback и карточка режима both) заменены одним вызовом. Старый бэк не шлёт support_url, для него сохранено прежнее поведение — новый фронт не ломается на неподнятом бэкенде. Заодно ушли три non-null assertion на support_username. --- src/pages/Support.tsx | 48 +++++++++------------- src/types/index.ts | 2 + src/utils/supportContact.test.ts | 69 ++++++++++++++++++++++++++++++++ src/utils/supportContact.ts | 36 +++++++++++++++++ 4 files changed, 125 insertions(+), 30 deletions(-) create mode 100644 src/utils/supportContact.test.ts create mode 100644 src/utils/supportContact.ts diff --git a/src/pages/Support.tsx b/src/pages/Support.tsx index 4bda888..0f44cfc 100644 --- a/src/pages/Support.tsx +++ b/src/pages/Support.tsx @@ -1,5 +1,5 @@ import { uiLocale } from '@/utils/uiLocale'; -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect, useCallback } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { motion } from 'framer-motion'; @@ -9,13 +9,14 @@ import { infoApi } from '../api/info'; import { useAuthStore } from '../store/auth'; import { logger } from '../utils/logger'; import { checkRateLimit, getRateLimitResetTime, RATE_LIMIT_KEYS } from '../utils/rateLimit'; -import type { TicketDetail } from '../types'; +import type { SupportConfig, TicketDetail } from '../types'; import { Card } from '@/components/data-display/Card'; import { Button } from '@/components/primitives/Button'; import { staggerContainer, staggerItem } from '@/components/motion/transitions'; import { ChatIcon, CloseIcon, ImageIcon, PlusIcon, SendIcon } from '@/components/icons'; import { usePlatform } from '@/platform'; import { linkifyText } from '../utils/linkify'; +import { resolveSupportContact } from '../utils/supportContact'; const log = logger.createLogger('Support'); @@ -36,6 +37,18 @@ export default function Support() { const isAdmin = useAuthStore((state) => state.isAdmin); const queryClient = useQueryClient(); const { openTelegramLink, openLink } = usePlatform(); + + const openSupportContact = useCallback( + (config: SupportConfig) => { + const target = resolveSupportContact(config); + if (target.kind === 'external') { + openLink(target.url, { tryInstantView: false }); + } else { + openTelegramLink(target.url); + } + }, + [openLink, openTelegramLink], + ); const [selectedTicket, setSelectedTicket] = useState(null); const [showCreateForm, setShowCreateForm] = useState(false); const [newTitle, setNewTitle] = useState(''); @@ -211,17 +224,7 @@ export default function Support() { buttonText: t('support.contactUs'), buttonAction: () => { log.debug('Button clicked, opening:', supportUsername); - - // Extract username without @ - const username = supportUsername.startsWith('@') - ? supportUsername.slice(1) - : supportUsername; - - const webUrl = `https://t.me/${username}`; - log.debug('Web URL:', webUrl); - - // Use platform's openTelegramLink - openTelegramLink(webUrl); + openSupportContact(supportConfig); }, }; } @@ -246,17 +249,7 @@ export default function Support() { buttonText: t('support.contactUs'), buttonAction: () => { log.debug('Fallback button clicked, opening:', supportUsername); - - // Extract username without @ - const username = supportUsername.startsWith('@') - ? supportUsername.slice(1) - : supportUsername; - - const webUrl = `https://t.me/${username}`; - log.debug('Fallback opening URL:', webUrl); - - // Use platform's openTelegramLink - openTelegramLink(webUrl); + openSupportContact(supportConfig); }, }; }; @@ -367,12 +360,7 @@ export default function Support() { diff --git a/src/types/index.ts b/src/types/index.ts index 9181884..6f10bb7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -530,6 +530,8 @@ export interface SupportConfig { support_type: 'tickets' | 'profile' | 'url' | 'both'; support_url?: string | null; support_username?: string | null; + /** Резолвнутый контакт ведёт в Telegram, а не на внешний хелпдеск. */ + contact_is_telegram?: boolean; } // Paginated response diff --git a/src/utils/supportContact.test.ts b/src/utils/supportContact.test.ts new file mode 100644 index 0000000..e8e0a5e --- /dev/null +++ b/src/utils/supportContact.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest'; +import type { SupportConfig } from '../types'; +import { resolveSupportContact } from './supportContact'; + +/** + * Кабинет склеивал `https://t.me/${support_username}` и ломал внешний хелпдеск: + * SUPPORT_USERNAME принимает и `@user`, и произвольный URL, поэтому из + * `https://help.example.com` получалось `https://t.me/https://help.example.com`. + * Бэк отдаёт контакт уже разрезолвленным — клеить на клиенте больше нечего. + */ + +const config = (overrides: Partial): SupportConfig => ({ + tickets_enabled: true, + support_type: 'both', + ...overrides, +}); + +describe('resolveSupportContact', () => { + it('внешний хелпдеск открывается как обычная ссылка, без t.me', () => { + const target = resolveSupportContact( + config({ + support_url: 'https://help.example.com', + support_username: 'https://help.example.com', + contact_is_telegram: false, + }), + ); + + expect(target).toEqual({ kind: 'external', url: 'https://help.example.com' }); + expect(target.url).not.toContain('t.me'); + }); + + it('телеграм-контакт открывается через telegram-ссылку', () => { + const target = resolveSupportContact( + config({ + support_url: 'https://t.me/help', + support_username: '@help', + contact_is_telegram: true, + }), + ); + + expect(target).toEqual({ kind: 'telegram', url: 'https://t.me/help' }); + }); + + describe('старый бэк без support_url — прежнее поведение', () => { + it.each([ + ['@help', 'https://t.me/help'], + ['help', 'https://t.me/help'], + ])('%s -> %s', (username, expected) => { + const target = resolveSupportContact(config({ support_username: username })); + + expect(target).toEqual({ kind: 'telegram', url: expected }); + }); + + it('контакт вообще не задан — дефолтный @support', () => { + expect(resolveSupportContact(config({}))).toEqual({ + kind: 'telegram', + url: 'https://t.me/support', + }); + }); + }); + + it('support_url без contact_is_telegram трактуется как телеграм', () => { + // Комбинация недостижима с текущим бэком, но деградировать надо в старое + // поведение, а не в переход по внешней ссылке. + const target = resolveSupportContact(config({ support_url: 'https://t.me/help' })); + + expect(target.kind).toBe('telegram'); + }); +}); diff --git a/src/utils/supportContact.ts b/src/utils/supportContact.ts new file mode 100644 index 0000000..07ac663 --- /dev/null +++ b/src/utils/supportContact.ts @@ -0,0 +1,36 @@ +import type { SupportConfig } from '../types'; + +/** + * Куда вести пользователя по кнопке «Написать в поддержку». + * + * Telegram-ссылку открывают через openTelegramLink, внешнюю — обычным переходом. + */ +export type SupportContactTarget = { + kind: 'telegram' | 'external'; + url: string; +}; + +/** + * Резолвит контакт поддержки из конфига. + * + * SUPPORT_USERNAME на бэке принимает и `@username`, и произвольный URL, поэтому + * бэк отдаёт уже разрезолвленные `support_url` + `contact_is_telegram`. Клиент + * раньше склеивал `https://t.me/${support_username}` — на внешнем хелпдеске это + * давало `https://t.me/https://help.example.com`, и ссылка не открывалась. + * + * Старый бэк не отдаёт `support_url`; для него сохранено прежнее поведение, + * иначе новый фронт сломался бы на неподнятом бэкенде. + */ +export function resolveSupportContact(config: SupportConfig): SupportContactTarget { + if (config.support_url) { + // contact_is_telegram отсутствует только у старого бэка, а он не шлёт и + // support_url — так что явная проверка на false, а не на truthy. + const kind = config.contact_is_telegram === false ? 'external' : 'telegram'; + return { kind, url: config.support_url }; + } + + const raw = config.support_username || '@support'; + const username = raw.startsWith('@') ? raw.slice(1) : raw; + + return { kind: 'telegram', url: `https://t.me/${username}` }; +}