feat(gift): QR подарка и сканирование в кабинете

Получателю раньше оставалось только скопировать ссылку или код руками.

- QR на экране готового подарка: кодируется bot-ссылка (активация в боте —
  основной путь), при отсутствии username бота — ссылка кабинета. Использован
  уже имеющийся qrcode.react, на белой подложке ради контраста в тёмной теме.
- Кнопка «Отсканировать QR» в форме активации. В Telegram используется
  НАТИВНЫЙ сканер (в WebView камера через getUserMedia работает ненадёжно —
  ровно то, из-за чего скан «в тг» и был проблемой), в вебе — html5-qrcode с
  CDN, с фолбэком на фронтальную камеру и понятной ошибкой, если камеры нет.
- src/utils/qrScanner.ts: логика сканера вынесена из TvQuickConnect, чтобы её
  мог переиспользовать любой экран. parseGiftCode понимает все три способа
  распространения подарка (deep-link бота, ссылка кабинета, голый код с
  префиксом GIFT- и без) и ОТКЛОНЯЕТ посторонние QR — иначе сканер подставил
  бы в поле мусор с любой ссылки или Wi-Fi-кода.
- Веб-сканер гасится при уходе с вкладки и размонтировании, иначе индикатор
  камеры остаётся гореть.

Локали ru/en/zh/fa, +4 теста на парсер.
This commit is contained in:
Fringg
2026-07-29 22:11:04 +03:00
parent 390da33914
commit 0e4dc43335
8 changed files with 281 additions and 5 deletions

View File

@@ -3,6 +3,7 @@ import { useSearchParams, useNavigate } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { motion } from 'framer-motion';
import { QRCodeSVG } from 'qrcode.react';
import { giftApi } from '../api/gift';
import { brandingApi, type TelegramWidgetConfig } from '../api/branding';
import { Spinner } from '@/components/ui/Spinner';
@@ -121,6 +122,14 @@ function CodeOnlySuccessState({
<p className="select-all font-mono text-lg font-bold text-accent-400">{giftCode}</p>
</div>
{/* QR — получателю проще отсканировать, чем копировать ссылку.
Кодируем bot-ссылку, если она есть: активация в боте — основной путь,
иначе ссылку кабинета. */}
<div className="flex w-full flex-col items-center gap-2 rounded-xl border border-dark-700/30 bg-white p-4">
<QRCodeSVG value={botLink ?? cabinetLink} size={180} level="M" includeMargin={false} />
</div>
<p className="-mt-3 text-xs text-dark-400">{t('gift.qrHint', 'Scan to activate the gift')}</p>
{/* Share message preview */}
<div className="w-full rounded-xl border border-dark-700/30 bg-dark-800/40 p-4 text-left">
<p className="mb-3 text-sm font-medium text-dark-100">