275 lines
8.2 KiB
TypeScript
275 lines
8.2 KiB
TypeScript
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<number>(30);
|
||
maxAmount = signal<number>(200_000);
|
||
|
||
amount = signal<number | null>(null);
|
||
note = signal<string>('');
|
||
error = signal<string>('');
|
||
loading = signal<boolean>(false);
|
||
settingsLoaded = signal<boolean>(false);
|
||
|
||
currency = signal<Currency>('RUB');
|
||
payment = signal<PaymentMethod>('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<string | null>(null);
|
||
qrPolling = signal<boolean>(false);
|
||
qrStatus = signal<string>('');
|
||
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') ?? '';
|
||
}
|
||
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<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
|
||
});
|
||
}
|
||
|
||
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<string, string> = {};
|
||
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 } : {}),
|
||
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<QrStatusResponse>(`${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<string, string> = {};
|
||
if (this.sessionId) headers['Authorization'] = JSON.stringify({ sessionID: this.sessionId });
|
||
|
||
this.http
|
||
.post<CreateFastcheckResponse>(
|
||
`${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;
|
||
}
|
||
}
|
||
}
|