mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 10:27:49 +00:00
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:
96
src/utils/qrScanner.ts
Normal file
96
src/utils/qrScanner.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
isQrScannerSupported,
|
||||
openQrScanner,
|
||||
retrieveLaunchParams,
|
||||
} from '@telegram-apps/sdk-react';
|
||||
|
||||
// Сканирование QR внутри Telegram: WebView не даёт нормального доступа к камере,
|
||||
// поэтому на мобильных клиентах используется НАТИВНЫЙ сканер Telegram, а на вебе
|
||||
// — html5-qrcode с CDN. Логика вынесена из TvQuickConnect, чтобы её мог
|
||||
// переиспользовать любой экран (подарки и т.д.).
|
||||
|
||||
const TG_MOBILE_PLATFORMS = new Set(['ios', 'android', 'android_x', 'ios_x']);
|
||||
|
||||
export const HTML5_QRCODE_CDN = 'https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js';
|
||||
|
||||
export interface Html5QrcodeInstance {
|
||||
start: (
|
||||
camera: { facingMode: string },
|
||||
config: { fps: number; qrbox: { width: number; height: number } },
|
||||
onSuccess: (decoded: string) => void,
|
||||
onError: () => void,
|
||||
) => Promise<void>;
|
||||
stop: () => Promise<void>;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
type WindowWithHtml5 = Window & { Html5Qrcode?: new (id: string) => Html5QrcodeInstance };
|
||||
|
||||
export function isTelegramMobile(): boolean {
|
||||
try {
|
||||
return TG_MOBILE_PLATFORMS.has(retrieveLaunchParams().tgWebAppPlatform);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** true — доступен нативный сканер Telegram (мобильный клиент). */
|
||||
export function canUseTelegramScanner(): boolean {
|
||||
try {
|
||||
return isTelegramMobile() && isQrScannerSupported() && openQrScanner.isAvailable();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Открывает нативный сканер Telegram. Возвращает распознанную строку либо null,
|
||||
* если пользователь закрыл сканер (или он недоступен).
|
||||
*/
|
||||
export async function scanWithTelegram(
|
||||
text: string,
|
||||
capture?: (value: string) => boolean,
|
||||
): Promise<string | null> {
|
||||
const result = await openQrScanner({ text, capture: capture ?? (() => true) });
|
||||
return result ?? null;
|
||||
}
|
||||
|
||||
/** Подгружает html5-qrcode с CDN (в бандл не тянем — нужен редко). */
|
||||
export async function loadHtml5Qrcode(): Promise<WindowWithHtml5['Html5Qrcode'] | undefined> {
|
||||
const w = window as WindowWithHtml5;
|
||||
if (w.Html5Qrcode) return w.Html5Qrcode;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = HTML5_QRCODE_CDN;
|
||||
document.head.appendChild(script);
|
||||
await new Promise<void>((resolve) => {
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => resolve();
|
||||
});
|
||||
return w.Html5Qrcode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Достаёт код подарка из отсканированной строки.
|
||||
*
|
||||
* Один и тот же подарок распространяется тремя способами, и скан должен понимать
|
||||
* все: deep-link бота (``?start=GIFT_<code>``), ссылка кабинета
|
||||
* (``/gift?tab=activate&code=<code>``) и просто код (``GIFT-xxxx`` или голый).
|
||||
*/
|
||||
export function parseGiftCode(raw: string | null | undefined): string | null {
|
||||
const value = (raw || '').trim();
|
||||
if (!value) return null;
|
||||
|
||||
const startParam = value.match(/[?&]start=GIFT_([A-Za-z0-9_-]+)/i);
|
||||
if (startParam) return startParam[1];
|
||||
|
||||
const cabinetParam = value.match(/[?&]code=([A-Za-z0-9_-]+)/i);
|
||||
if (cabinetParam) return cabinetParam[1];
|
||||
|
||||
// Голый код: допускаем префикс GIFT- и разделители, но не произвольный текст —
|
||||
// иначе сканер «распознал» бы любую ссылку и подставил мусор в поле.
|
||||
const bare = value.replace(/^GIFT[-_]/i, '');
|
||||
if (/^[A-Za-z0-9]{6,64}$/.test(bare)) return bare;
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user