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}` }; +}