mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix(support): не склеивать t.me со ссылкой на внешний хелпдеск
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.
This commit is contained in:
@@ -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<TicketDetail | null>(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() {
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="shrink-0 whitespace-nowrap"
|
||||
onClick={() => {
|
||||
const username = supportConfig.support_username!.startsWith('@')
|
||||
? supportConfig.support_username!.slice(1)
|
||||
: supportConfig.support_username!;
|
||||
openTelegramLink(`https://t.me/${username}`);
|
||||
}}
|
||||
onClick={() => openSupportContact(supportConfig)}
|
||||
>
|
||||
{t('support.writeButton', 'Написать')}
|
||||
</Button>
|
||||
|
||||
@@ -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
|
||||
|
||||
69
src/utils/supportContact.test.ts
Normal file
69
src/utils/supportContact.test.ts
Normal file
@@ -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>): 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');
|
||||
});
|
||||
});
|
||||
36
src/utils/supportContact.ts
Normal file
36
src/utils/supportContact.ts
Normal file
@@ -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}` };
|
||||
}
|
||||
Reference in New Issue
Block a user