Files
qr_vitanova/src/app/pages/create-page/create-page.ts

256 lines
7.7 KiB
TypeScript
Raw Normal View History

2026-04-30 01:17:17 +04:00
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';
2026-05-05 00:52:03 +04:00
import { FASTCHECK_API, QR_VITANOVA_API } from '../../api';
2026-05-04 23:56:38 +04:00
import { TranslatePipe } from '../../translate/translate.pipe';
2026-05-05 00:52:03 +04:00
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;
qrStatus?: string;
2026-05-05 00:52:03 +04:00
[key: string]: unknown;
}
interface QrStatusResponse {
status?: string; // "NEW" | "APPROVED" | "REJECTED" | "COMPLETED"
nspkurl?: string;
nspkID?: string;
2026-05-05 00:52:03 +04:00
[key: string]: unknown;
}
2026-04-30 01:17:17 +04:00
interface CreateFastcheckResponse {
fastcheck: string;
expiration: string;
code: string;
Status: boolean;
}
2026-05-05 00:52:03 +04:00
/** 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);
});
}
2026-04-30 01:17:17 +04:00
@Component({
selector: 'app-create-page',
2026-05-04 23:56:38 +04:00
imports: [FormsModule, RouterLink, TranslatePipe],
2026-04-30 01:17:17 +04:00
templateUrl: './create-page.html',
styleUrl: './create-page.scss'
})
export class CreatePage {
private http = inject(HttpClient);
private store = inject(FastcheckService);
private router = inject(Router);
2026-05-05 00:52:03 +04:00
private i18n = inject(TranslationService);
private t(key: string): string { return this.i18n.translate(key); }
2026-04-30 01:17:17 +04:00
2026-05-05 00:52:03 +04:00
// Limits updated from settings API on init.
minAmount = signal<number>(30);
maxAmount = signal<number>(200_000);
amount = signal<number | null>(null);
2026-04-30 01:17:17 +04:00
note = signal<string>('');
error = signal<string>('');
loading = signal<boolean>(false);
2026-05-05 00:52:03 +04:00
settingsLoaded = signal<boolean>(false);
2026-04-30 01:17:17 +04:00
currency = signal<Currency>('RUB');
2026-05-05 00:52:03 +04:00
payment = signal<PaymentMethod>('sbp');
2026-04-30 01:17:17 +04:00
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);
}
2026-05-05 00:52:03 +04:00
// QR display state
qrImageUrl = signal<string | null>(null);
qrPolling = signal<boolean>(false);
private pollHandle: ReturnType<typeof setInterval> | 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') ?? '';
2026-05-05 00:52:03 +04:00
}
private get userId(): string {
return new URLSearchParams(window.location.search).get('userid-value') ?? '';
2026-05-05 00:52:03 +04:00
}
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;
}
constructor() {
this.loadSettings();
}
private loadSettings(): void {
this.http.get<SettingsResponse>(`${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
});
}
2026-04-30 01:17:17 +04:00
createCheck(): void {
const val = this.amount();
if (val !== null && val < this.minAmount()) {
2026-05-05 00:52:03 +04:00
this.error.set(`${this.t('errors.invalid_amount')} (мин. ${this.minAmount()} ₽)`);
return;
}
if (val !== null && val > this.maxAmount()) {
2026-05-05 00:52:03 +04:00
this.error.set(`${this.t('errors.invalid_amount')} (макс. ${this.maxAmount().toLocaleString('ru')} ₽)`);
2026-04-30 01:17:17 +04:00
return;
}
this.error.set('');
this.loading.set(true);
const headers: Record<string, string> = {};
2026-05-05 00:52:03 +04:00
if (this.authKey) headers['authorization-key'] = this.authKey;
if (this.userId) headers['userid-value'] = this.userId;
const partnerqrID = generateUUID();
this.http
.post<CreateQrResponse>(
`${QR_VITANOVA_API}/qr`,
{
qrtype: 'QRDynamic',
...(val !== null ? { amount: val } : {}),
2026-05-05 00:52:03 +04:00
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;
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);
2026-05-05 00:52:03 +04:00
this.qrImageUrl.set(qrData);
if (qrId) this.startPolling(qrId);
2026-05-05 00:52:03 +04:00
} 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<QrStatusResponse>(`${QR_VITANOVA_API}/qr/dynamic/${qrId}`)
.subscribe({
next: (res) => {
// API returns status: "NEW" | "APPROVED" | "REJECTED" | "COMPLETED"
if (res?.status === 'COMPLETED' || res?.status === 'APPROVED') {
2026-05-05 00:52:03 +04:00
this.stopPolling();
this.createFastcheck();
} else if (res?.status === 'REJECTED') {
this.stopPolling();
this.error.set(this.t('errors.payment_failed'));
this.qrImageUrl.set(null);
2026-05-05 00:52:03 +04:00
}
},
error: () => undefined
});
}, 3000);
}
private stopPolling(): void {
if (this.pollHandle !== null) {
clearInterval(this.pollHandle);
this.pollHandle = null;
2026-04-30 01:17:17 +04:00
}
2026-05-05 00:52:03 +04:00
this.qrPolling.set(false);
}
private createFastcheck(): void {
const headers: Record<string, string> = {};
if (this.sessionId) headers['Authorization'] = JSON.stringify({ sessionID: this.sessionId });
2026-04-30 01:17:17 +04:00
this.http
.post<CreateFastcheckResponse>(
`${FASTCHECK_API}/fastcheck`,
2026-05-05 00:52:03 +04:00
{ amount: this.amount(), currency: this.currency() },
2026-04-30 01:17:17 +04:00
{ headers }
)
.subscribe({
next: (res) => {
if (res?.fastcheck) {
this.store.setCreated({
fastcheck: res.fastcheck,
code: res.code,
amount: this.amount() ?? null,
2026-04-30 01:17:17 +04:00
expiration: res.expiration
});
}
2026-05-05 00:52:03 +04:00
this.router.navigate(['/']);
2026-04-30 01:17:17 +04:00
},
2026-05-05 00:52:03 +04:00
error: () => this.router.navigate(['/'])
2026-04-30 01:17:17 +04:00
});
}
onAmountChange(value: number | null): void {
this.amount.set(value || null);
if (value && value > 0) this.error.set('');
2026-04-30 01:17:17 +04:00
}
onNoteChange(value: string): void {
this.note.set(value);
}
closeQr(): void {
this.qrImageUrl.set(null);
this.qrPolling.set(false);
if (this.pollHandle !== null) {
clearInterval(this.pollHandle);
this.pollHandle = null;
}
}
2026-04-30 01:17:17 +04:00
}