import { Component, 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, QR_VITANOVA_API } from '../../api'; import { TranslatePipe } from '../../translate/translate.pipe'; import { TranslationService } from '../../translate/translation.service'; type PaymentMethod = 'sbp'; type Currency = 'RUB'; interface SettingsResponse { minAmount?: number; maxAmount?: number; [key: string]: unknown; } interface CreateQrResponse { qrId?: string; nspkID?: string; Payload?: string; // per API doc (capital P) nspkurl?: string; // actual field name in real responses qrUrl?: string; status?: string; // e.g. "REGISTERED" [key: string]: unknown; } interface QrStatusResponse { status?: string; // "REGISTERED" | "NEW" | "APPROVED" | "REJECTED" | "COMPLETED" nspkurl?: string; nspkID?: string; [key: string]: unknown; } interface CreateFastcheckResponse { id?: string; // real field name from server fastcheck?: string; // per API doc fallback expiration?: string; code?: string; amount?: number; Status?: boolean; } /** Generate a v4-like UUID without crypto dependency. */ function generateUUID(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); }); } @Component({ selector: 'app-create-page', imports: [FormsModule, RouterLink, TranslatePipe], templateUrl: './create-page.html', styleUrl: './create-page.scss' }) export class CreatePage { private http = inject(HttpClient); private store = inject(FastcheckService); private router = inject(Router); private i18n = inject(TranslationService); private t(key: string): string { return this.i18n.translate(key); } // Limits – updated from settings API on init. minAmount = signal(30); maxAmount = signal(200_000); amount = signal(null); note = signal(''); error = signal(''); loading = signal(false); settingsLoaded = signal(false); currency = signal('RUB'); payment = signal('sbp'); selectPayment(method: PaymentMethod, enabled: boolean): void { if (!enabled) return; this.payment.set(method); } selectCurrency(c: Currency, enabled: boolean): void { if (!enabled) return; this.currency.set(c); } // QR display state qrImageUrl = signal(null); qrPolling = signal(false); qrStatus = signal(''); private pollHandle: ReturnType | null = null; private activeQrId = ''; /** Auth credentials passed by the host page as URL params. */ private get authKey(): string { return new URLSearchParams(window.location.search).get('authorization-key') ?? ''; } private get userId(): string { return new URLSearchParams(window.location.search).get('userid-value') ?? ''; } private get sessionId(): string { return new URLSearchParams(window.location.search).get('session') ?? ''; } private get reference(): string { return new URLSearchParams(window.location.search).get('ref') ?? window.location.hostname; } get isMobile(): boolean { return window.innerWidth < 768; } constructor() { this.loadSettings(); } private loadSettings(): void { this.http.get(`${QR_VITANOVA_API}/settings`).subscribe({ next: (s) => { if (typeof s?.minAmount === 'number') this.minAmount.set(s.minAmount); if (typeof s?.maxAmount === 'number') this.maxAmount.set(s.maxAmount); this.settingsLoaded.set(true); }, error: () => this.settingsLoaded.set(true) // proceed with defaults }); } createCheck(): void { const val = this.amount(); if (val !== null && val < this.minAmount()) { this.error.set(`${this.t('errors.invalid_amount')} (мин. ${this.minAmount()} ₽)`); return; } if (val !== null && val > this.maxAmount()) { this.error.set(`${this.t('errors.invalid_amount')} (макс. ${this.maxAmount().toLocaleString('ru')} ₽)`); return; } this.error.set(''); this.loading.set(true); const headers: Record = {}; if (this.authKey) headers['authorization-key'] = this.authKey; if (this.userId) headers['userid-value'] = this.userId; const partnerqrID = generateUUID(); this.http .post( `${QR_VITANOVA_API}/qr`, { qrtype: 'QRDynamic', ...(val !== null ? { amount: val } : {}), currency: this.currency(), partnerqrID, qrDescription: this.note().trim(), Userid: this.userId, Reference: this.reference }, { headers } ) .subscribe({ next: (res) => { this.loading.set(false); const qrId = res?.qrId ?? res?.nspkID ?? ''; // Real API uses 'nspkurl'; doc says 'Payload' — try both const nspkUrl = res?.nspkurl ?? res?.Payload; this.qrStatus.set(res?.status ?? ''); if (nspkUrl && this.isMobile) { window.location.href = nspkUrl; return; } if (qrId || nspkUrl) { this.activeQrId = qrId; const qrData = nspkUrl ? `https://api.qrserver.com/v1/create-qr-code/?size=256x256&margin=8&data=${encodeURIComponent(nspkUrl)}` : (res.qrUrl ?? null); this.qrImageUrl.set(qrData); if (qrId) this.startPolling(qrId); } else { this.error.set(this.t('errors.payment_failed')); } }, error: (err) => { this.loading.set(false); const msg: string | undefined = err?.error?.message; this.error.set(msg ?? this.t('errors.lookup_failed')); } }); } private startPolling(qrId: string): void { this.stopPolling(); this.qrPolling.set(true); this.pollHandle = setInterval(() => { this.http.get(`${QR_VITANOVA_API}/qr/dynamic/${qrId}`) .subscribe({ next: (res) => { const st = res?.status ?? ''; this.qrStatus.set(st); if (st === 'COMPLETED' || st === 'APPROVED') { this.stopPolling(); this.createFastcheck(); } else if (st === 'REJECTED') { this.stopPolling(); this.error.set(this.t('errors.payment_failed')); this.qrImageUrl.set(null); } // REGISTERED / NEW / '' — keep polling }, error: () => undefined }); }, 5000); } private stopPolling(): void { if (this.pollHandle !== null) { clearInterval(this.pollHandle); this.pollHandle = null; } this.qrPolling.set(false); } private createFastcheck(): void { const headers: Record = {}; if (this.sessionId) headers['Authorization'] = JSON.stringify({ sessionID: this.sessionId }); this.http .post( `${FASTCHECK_API}/fastcheck`, { amount: this.amount(), currency: this.currency() }, { headers } ) .subscribe({ next: (res) => { const fcNumber = res?.id ?? res?.fastcheck ?? ''; const payload = { fastcheck: fcNumber, code: res?.code ?? '', amount: res?.amount ?? this.amount() ?? null, expiration: res?.expiration }; if (fcNumber) { this.store.setCreated(payload); } this.router.navigate(['/'], { state: fcNumber ? payload : {} }); }, error: () => this.router.navigate(['/']) }); } onAmountChange(value: number | null): void { this.amount.set(value || null); if (value && value > 0) this.error.set(''); } onNoteChange(value: string): void { this.note.set(value); } closeQr(): void { this.qrImageUrl.set(null); this.qrPolling.set(false); this.qrStatus.set(''); if (this.pollHandle !== null) { clearInterval(this.pollHandle); this.pollHandle = null; } } }