283 lines
8.1 KiB
TypeScript
283 lines
8.1 KiB
TypeScript
import { Component, inject, signal } from '@angular/core';
|
||
import { FormsModule } from '@angular/forms';
|
||
import { HttpClient } from '@angular/common/http';
|
||
import { 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 {
|
||
sbp?: boolean;
|
||
wechat?: boolean;
|
||
visa?: boolean;
|
||
mastercard?: boolean;
|
||
alipay?: boolean;
|
||
rubles?: boolean;
|
||
usd?: boolean;
|
||
euro?: boolean;
|
||
cny?: boolean;
|
||
dram?: boolean;
|
||
minAmount?: number;
|
||
maxAmount?: number;
|
||
qrTTL?: number;
|
||
}
|
||
|
||
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"
|
||
}
|
||
|
||
interface QrStatusResponse {
|
||
status?: string; // "REGISTERED" | "NEW" | "APPROVED" | "REJECTED" | "COMPLETED"
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
@Component({
|
||
selector: 'app-create-page',
|
||
imports: [FormsModule, TranslatePipe],
|
||
templateUrl: './create-page.html',
|
||
styleUrl: './create-page.scss'
|
||
})
|
||
export class CreatePage {
|
||
private http = inject(HttpClient);
|
||
private i18n = inject(TranslationService);
|
||
private readonly sites: Record<string, string> = {
|
||
'51': 'fastcheck.store'
|
||
};
|
||
|
||
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);
|
||
|
||
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>('');
|
||
paymentDone = signal<boolean>(false);
|
||
private pollHandle: ReturnType<typeof setInterval> | null = null;
|
||
|
||
/** 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 reference(): string {
|
||
return new URLSearchParams(window.location.search).get('ref') ?? window.location.hostname;
|
||
}
|
||
private get partnerqrID(): string {
|
||
return new URLSearchParams(window.location.search).get('id') ?? '';
|
||
}
|
||
private get fromSite(): string {
|
||
return new URLSearchParams(window.location.search).get('from') ?? '';
|
||
}
|
||
|
||
get isMobile(): boolean {
|
||
return window.innerWidth < 768;
|
||
}
|
||
|
||
constructor() {
|
||
this.loadSettings();
|
||
}
|
||
|
||
private loadSettings(): void {
|
||
// Fetch limits from /qr/settings. If the call fails, keep defaults.
|
||
const url = `${QR_VITANOVA_API}/qr/settings`;
|
||
this.http.get<SettingsResponse>(url).subscribe({
|
||
next: (s) => {
|
||
if (typeof s?.minAmount === 'number') this.minAmount.set(s.minAmount);
|
||
if (typeof s?.maxAmount === 'number') this.maxAmount.set(s.maxAmount);
|
||
}
|
||
});
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
const partnerqrID = this.partnerqrID;
|
||
if (!partnerqrID) {
|
||
this.error.set(this.t('errors.lookup_failed'));
|
||
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;
|
||
|
||
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,
|
||
RedirectUrl: `https://fastcheck.store?id=${partnerqrID}`
|
||
},
|
||
{ 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) {
|
||
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/${encodeURIComponent(this.partnerqrID)}/${qrId}`)
|
||
.subscribe({
|
||
next: (res) => {
|
||
const st = res?.status ?? '';
|
||
this.qrStatus.set(st);
|
||
if (st === 'COMPLETED' || st === 'APPROVED') {
|
||
this.handlePaymentSuccess(res);
|
||
} else if (st === 'REJECTED') {
|
||
this.stopPolling();
|
||
this.error.set(this.t('errors.payment_failed'));
|
||
this.qrImageUrl.set(null);
|
||
}
|
||
// REGISTERED / NEW / '' — keep polling
|
||
},
|
||
error: () => {
|
||
this.closeQr();
|
||
this.error.set('оплата не прошла');
|
||
}
|
||
});
|
||
}, 5000);
|
||
}
|
||
|
||
private stopPolling(): void {
|
||
if (this.pollHandle !== null) {
|
||
clearInterval(this.pollHandle);
|
||
this.pollHandle = null;
|
||
}
|
||
this.qrPolling.set(false);
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
private handlePaymentSuccess(paidQr: QrStatusResponse): void {
|
||
this.stopPolling();
|
||
this.qrImageUrl.set(null);
|
||
this.qrStatus.set('');
|
||
this.paymentDone.set(true);
|
||
|
||
const id = this.partnerqrID;
|
||
if (!id) {
|
||
this.redirectToSource();
|
||
return;
|
||
}
|
||
|
||
this.http
|
||
.post(`https://fastcheck.store/api/fastcheck/settings/${encodeURIComponent(id)}`, paidQr)
|
||
.subscribe({
|
||
next: () => this.redirectToSource(id),
|
||
error: () => this.redirectToSource(id)
|
||
});
|
||
}
|
||
|
||
private redirectToSource(id?: string): void {
|
||
const withId = (target: string): string => {
|
||
if (!id) return target;
|
||
|
||
const normalizedTarget = /^https?:\/\//i.test(target) ? target : `https://${target}`;
|
||
const url = new URL(normalizedTarget);
|
||
url.searchParams.set('id', id);
|
||
return url.toString();
|
||
};
|
||
|
||
const from = this.fromSite.trim();
|
||
const target = this.sites[from];
|
||
if (target) {
|
||
window.location.href = withId(target);
|
||
return;
|
||
}
|
||
|
||
if (window.history.length > 1) {
|
||
window.history.back();
|
||
}
|
||
}
|
||
|
||
closeQr(): void {
|
||
this.stopPolling();
|
||
this.qrImageUrl.set(null);
|
||
this.qrStatus.set('');
|
||
this.paymentDone.set(false);
|
||
}
|
||
}
|