import { Component, computed, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Router, RouterLink } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { FastcheckService } from '../../fastcheck.service'; import { FASTCHECK_API } from '../../api'; interface WebSessionResponse { sessionId: string; userId: string; expires: string; userSessionId: string; Status: boolean; } interface CheckFastcheckResponse { fastcheck: string; amount?: number; expiration: string; Status: boolean; } @Component({ selector: 'app-fastcheck-page', imports: [FormsModule, RouterLink], templateUrl: './fastcheck-page.html', styleUrl: './fastcheck-page.scss' }) export class FastcheckPage { private http = inject(HttpClient); private store = inject(FastcheckService); private router = inject(Router); // Telegram bot used for the sign-in deep link. private readonly telegramBot = 'DexarSupport_bot'; fastcheckNumber = signal(''); fastcheckAmount = signal(null); fastcheckCode = signal(''); error = signal(''); amountLoading = signal(false); popupOpen = signal(false); popupLoading = signal(false); popupError = signal(''); webSessionId = signal(''); paid = signal(false); private pollHandle: ReturnType | null = null; private lastLookedUpNumber = ''; canPay = computed(() => { const digits = this.fastcheckNumber().replace(/\D/g, ''); const codeDigits = this.fastcheckCode().replace(/\D/g, ''); return digits.length === 12 && codeDigits.length === 5 && !this.amountLoading(); }); telegramLink = computed(() => { const sid = this.webSessionId(); return sid ? `https://t.me/${this.telegramBot}?start=${encodeURIComponent(sid)}` : `https://t.me/${this.telegramBot}`; }); qrUrl = computed(() => { const link = this.telegramLink(); return `https://api.qrserver.com/v1/create-qr-code/?size=240x240&margin=8&data=${encodeURIComponent(link)}`; }); constructor() { // Pull autofill data left over by the create page. const created = this.store.consume(); if (created) { this.fastcheckNumber.set(created.fastcheck); this.fastcheckAmount.set(created.amount); this.fastcheckCode.set(created.code); } } pay(): void { if (!this.canPay()) { return; } this.error.set(''); this.openPopup(); } private openPopup(): void { this.popupOpen.set(true); this.popupError.set(''); this.paid.set(false); this.popupLoading.set(true); this.http.get(`${FASTCHECK_API}/websession`).subscribe({ next: (res) => { this.popupLoading.set(false); this.webSessionId.set(res.sessionId); this.startPolling(res.sessionId); }, error: () => { this.popupLoading.set(false); this.popupError.set('Не удалось создать сессию. Попробуйте ещё раз.'); } }); } closePopup(): void { this.popupOpen.set(false); this.stopPolling(); if (this.webSessionId()) { // Best-effort logout; ignore errors. this.http .request('DELETE', `${FASTCHECK_API}/websession/${this.webSessionId()}`, { body: { sessionId: this.webSessionId() } }) .subscribe({ error: () => undefined }); } this.webSessionId.set(''); } private startPolling(sessionId: string): void { this.stopPolling(); this.pollHandle = setInterval(() => { this.http .get(`${FASTCHECK_API}/websession/${sessionId}`) .subscribe({ next: (res) => { if (res?.Status) { this.stopPolling(); this.acceptFastcheck(sessionId); } }, error: () => undefined }); }, 3000); } private stopPolling(): void { if (this.pollHandle !== null) { clearInterval(this.pollHandle); this.pollHandle = null; } } private acceptFastcheck(sessionId: string): void { this.popupLoading.set(true); this.http .post( `${FASTCHECK_API}/fastcheck`, { fastcheck: this.fastcheckNumber().trim(), code: this.fastcheckCode().trim() }, { headers: { Authorization: JSON.stringify({ sessionID: sessionId }) } } ) .subscribe({ next: () => { this.popupLoading.set(false); this.paid.set(true); // Fire-and-forget merchant callback if a return_url is on the page. this.fireMerchantCallback(); }, error: () => { this.popupLoading.set(false); this.popupError.set('Не удалось принять платёж.'); } }); } private fireMerchantCallback(): void { const params = new URLSearchParams(window.location.search); const returnUrl = params.get('return_url'); if (returnUrl) { setTimeout(() => { window.location.href = `${returnUrl}${returnUrl.includes('?') ? '&' : '?'}fastcheck=${encodeURIComponent( this.fastcheckNumber() )}&status=ok`; }, 1500); } } onAmountChange(value: number | null): void { this.fastcheckAmount.set(value); } /** Mask fastcheck number as XXXX-XXXX-XXXX, allow only digits. */ onNumberChange(raw: string): void { const digits = (raw ?? '').replace(/\D/g, '').slice(0, 12); const groups: string[] = []; for (let i = 0; i < digits.length; i += 4) { groups.push(digits.slice(i, i + 4)); } const masked = groups.join('-'); this.fastcheckNumber.set(masked); this.error.set(''); // If number became incomplete, drop the previously fetched amount so the // user doesn't see a stale value tied to a different (now-edited) number. if (digits.length < 12 && this.lastLookedUpNumber) { this.fastcheckAmount.set(null); this.lastLookedUpNumber = ''; } // Auto-lookup when 12 digits are entered (and we haven't already looked it up). if (digits.length === 12 && masked !== this.lastLookedUpNumber) { this.lookupFastcheck(masked); } } /** Allow only digits, max 5, in the code field. */ onCodeChange(raw: string): void { const digits = (raw ?? '').replace(/\D/g, '').slice(0, 5); this.fastcheckCode.set(digits); this.error.set(''); } private lookupFastcheck(number: string): void { this.lastLookedUpNumber = number; this.amountLoading.set(true); this.fastcheckAmount.set(null); // GET /fastcheck — body in GET is non-standard; many HTTP libs strip it. // The backend should accept ?fastcheck= as a query param too. We send both. this.http .request('GET', `${FASTCHECK_API}/fastcheck`, { body: { fastcheck: number }, params: { fastcheck: number } }) .subscribe({ next: (res) => { this.amountLoading.set(false); if (res?.Status && typeof res.amount === 'number') { this.fastcheckAmount.set(res.amount); } else if (res?.Status === false) { this.error.set('Платёж не найден или просрочен.'); } }, error: () => { this.amountLoading.set(false); this.error.set('Не удалось проверить номер. Попробуйте ещё раз.'); this.lastLookedUpNumber = ''; } }); } }