2026-05-06 23:26:00 +04:00
|
|
|
|
import { Component, computed, inject, signal } from '@angular/core';
|
|
|
|
|
|
import { FormsModule } from '@angular/forms';
|
2026-05-07 00:31:14 +04:00
|
|
|
|
import { Router } from '@angular/router';
|
2026-05-06 23:26:00 +04:00
|
|
|
|
import { HttpClient } from '@angular/common/http';
|
|
|
|
|
|
import { FastcheckService } from '../../fastcheck.service';
|
|
|
|
|
|
import { FASTCHECK_API } from '../../api';
|
|
|
|
|
|
import { TranslatePipe } from '../../translate/translate.pipe';
|
|
|
|
|
|
import { TranslationService } from '../../translate/translation.service';
|
|
|
|
|
|
|
|
|
|
|
|
interface WebSessionResponse {
|
|
|
|
|
|
sessionId: string;
|
|
|
|
|
|
userId: string;
|
|
|
|
|
|
expires: string;
|
|
|
|
|
|
userSessionId: string;
|
|
|
|
|
|
Status: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface CheckFastcheckResponse {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
code: string;
|
|
|
|
|
|
owneID: string;
|
|
|
|
|
|
amount: number;
|
|
|
|
|
|
currency: string;
|
|
|
|
|
|
createdAt: string;
|
|
|
|
|
|
creattransactionID: string;
|
|
|
|
|
|
firedAT: string;
|
|
|
|
|
|
firetransactionID: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
|
selector: 'app-fastcheck-page',
|
2026-05-07 00:31:14 +04:00
|
|
|
|
imports: [FormsModule, TranslatePipe],
|
2026-05-06 23:26:00 +04:00
|
|
|
|
templateUrl: './fastcheck-page.html',
|
|
|
|
|
|
styleUrl: './fastcheck-page.scss'
|
|
|
|
|
|
})
|
|
|
|
|
|
export class FastcheckPage {
|
|
|
|
|
|
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); }
|
|
|
|
|
|
|
|
|
|
|
|
// Telegram bot used for the sign-in deep link.
|
|
|
|
|
|
private readonly telegramBot = 'DexarSupport_bot';
|
|
|
|
|
|
|
|
|
|
|
|
fastcheckNumber = signal<string>('');
|
|
|
|
|
|
fastcheckAmount = signal<number | null>(null);
|
|
|
|
|
|
fastcheckCode = signal<string>('');
|
|
|
|
|
|
codeEnabled = signal<boolean>(false);
|
|
|
|
|
|
error = signal<string>('');
|
|
|
|
|
|
amountLoading = signal<boolean>(false);
|
|
|
|
|
|
|
|
|
|
|
|
popupOpen = signal<boolean>(false);
|
|
|
|
|
|
popupLoading = signal<boolean>(false);
|
|
|
|
|
|
popupError = signal<string>('');
|
|
|
|
|
|
webSessionId = signal<string>('');
|
|
|
|
|
|
paid = signal<boolean>(false);
|
|
|
|
|
|
private pollHandle: ReturnType<typeof setInterval> | null = null;
|
|
|
|
|
|
private lastLookedUpNumber = '';
|
|
|
|
|
|
|
|
|
|
|
|
canPay = computed(() => {
|
|
|
|
|
|
const digits = this.fastcheckNumber().replace(/\D/g, '');
|
|
|
|
|
|
const codeDigits = this.fastcheckCode().replace(/\D/g, '');
|
|
|
|
|
|
return digits.length === 18 && codeDigits.length === 6
|
|
|
|
|
|
&& this.codeEnabled() && !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)}`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
get isMobile(): boolean {
|
|
|
|
|
|
return typeof window !== 'undefined' && window.innerWidth < 768;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
// Pull autofill data: prefer router navigation state, fall back to service.
|
|
|
|
|
|
const navState = typeof window !== 'undefined' ? (window.history?.state ?? {}) : {};
|
|
|
|
|
|
const created = (navState?.fastcheck)
|
|
|
|
|
|
? { fastcheck: navState.fastcheck, code: navState.code ?? '', amount: navState.amount ?? null, expiration: navState.expiration }
|
|
|
|
|
|
: this.store.consume();
|
|
|
|
|
|
|
|
|
|
|
|
if (created) {
|
|
|
|
|
|
this.fastcheckNumber.set(created.fastcheck);
|
|
|
|
|
|
this.fastcheckAmount.set(created.amount);
|
|
|
|
|
|
this.fastcheckCode.set(created.code);
|
|
|
|
|
|
this.codeEnabled.set(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ?iid=xxxxxx-xxxxxx-xxxxxx — auto-fill and trigger lookup
|
|
|
|
|
|
const iidParam = new URLSearchParams(window.location.search).get('iid') ?? '';
|
|
|
|
|
|
if (iidParam && !created) {
|
|
|
|
|
|
const digits = iidParam.replace(/\D/g, '').slice(0, 18);
|
|
|
|
|
|
const groups: string[] = [];
|
|
|
|
|
|
for (let i = 0; i < digits.length; i += 6) groups.push(digits.slice(i, i + 6));
|
|
|
|
|
|
const masked = groups.join('-');
|
|
|
|
|
|
this.fastcheckNumber.set(masked);
|
|
|
|
|
|
if (digits.length === 18) this.lookupFastcheck(masked);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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<WebSessionResponse>(`${FASTCHECK_API}/websession`).subscribe({
|
|
|
|
|
|
next: (res) => {
|
|
|
|
|
|
this.popupLoading.set(false);
|
|
|
|
|
|
this.webSessionId.set(res.sessionId);
|
|
|
|
|
|
if (this.isMobile) {
|
|
|
|
|
|
window.location.href = `https://t.me/${this.telegramBot}?start=${encodeURIComponent(res.sessionId)}`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.startPolling(res.sessionId);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
error: () => {
|
|
|
|
|
|
this.popupLoading.set(false);
|
|
|
|
|
|
this.popupError.set(this.t('errors.session_failed'));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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<WebSessionResponse>(`${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 DELETE to mark fastcheck as consumed on the merchant side.
|
|
|
|
|
|
this.http
|
|
|
|
|
|
.delete(`${FASTCHECK_API}/fastcheck/${encodeURIComponent(this.fastcheckNumber())}`)
|
|
|
|
|
|
.subscribe({ error: () => undefined });
|
|
|
|
|
|
this.fireMerchantCallback();
|
|
|
|
|
|
},
|
|
|
|
|
|
error: () => {
|
|
|
|
|
|
this.popupLoading.set(false);
|
|
|
|
|
|
this.popupError.set(this.t('errors.payment_failed'));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 XXXXXX-XXXXXX-XXXXXX, allow only digits. */
|
|
|
|
|
|
onNumberChange(raw: string): void {
|
|
|
|
|
|
const digits = (raw ?? '').replace(/\D/g, '').slice(0, 18);
|
|
|
|
|
|
const groups: string[] = [];
|
|
|
|
|
|
for (let i = 0; i < digits.length; i += 6) {
|
|
|
|
|
|
groups.push(digits.slice(i, i + 6));
|
|
|
|
|
|
}
|
|
|
|
|
|
const masked = groups.join('-');
|
|
|
|
|
|
this.fastcheckNumber.set(masked);
|
|
|
|
|
|
this.error.set('');
|
|
|
|
|
|
|
|
|
|
|
|
if (digits.length < 18 && this.lastLookedUpNumber) {
|
|
|
|
|
|
this.fastcheckAmount.set(null);
|
|
|
|
|
|
this.codeEnabled.set(false);
|
|
|
|
|
|
this.lastLookedUpNumber = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (digits.length === 18 && masked !== this.lastLookedUpNumber) {
|
|
|
|
|
|
this.lookupFastcheck(masked);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Allow only digits, max 6, in the code field. */
|
|
|
|
|
|
onCodeChange(raw: string): void {
|
|
|
|
|
|
const digits = (raw ?? '').replace(/\D/g, '').slice(0, 6);
|
|
|
|
|
|
this.fastcheckCode.set(digits);
|
|
|
|
|
|
this.error.set('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private lookupFastcheck(number: string): void {
|
|
|
|
|
|
this.lastLookedUpNumber = number;
|
|
|
|
|
|
this.amountLoading.set(true);
|
|
|
|
|
|
this.fastcheckAmount.set(null);
|
|
|
|
|
|
this.codeEnabled.set(false);
|
|
|
|
|
|
|
|
|
|
|
|
// API doc: GET /fastcheck/<id>
|
|
|
|
|
|
this.http
|
|
|
|
|
|
.get<CheckFastcheckResponse>(`${FASTCHECK_API}/fastcheck/${number}`)
|
|
|
|
|
|
.subscribe({
|
|
|
|
|
|
next: (res) => {
|
|
|
|
|
|
this.amountLoading.set(false);
|
|
|
|
|
|
if (res?.id) {
|
|
|
|
|
|
this.fastcheckAmount.set(typeof res.amount === 'number' ? res.amount : null);
|
|
|
|
|
|
this.codeEnabled.set(true);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.error.set(this.t('errors.not_found'));
|
|
|
|
|
|
this.lastLookedUpNumber = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
error: (err) => {
|
|
|
|
|
|
this.amountLoading.set(false);
|
|
|
|
|
|
const serverMsg: string | undefined = err?.error?.message;
|
|
|
|
|
|
this.error.set(serverMsg ?? this.t('errors.lookup_failed'));
|
|
|
|
|
|
this.lastLookedUpNumber = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
shareByEmail(): void {
|
|
|
|
|
|
const num = this.fastcheckNumber();
|
|
|
|
|
|
const amount = this.fastcheckAmount();
|
|
|
|
|
|
const subject = encodeURIComponent('fastCHECK');
|
|
|
|
|
|
const body = encodeURIComponent(`Номер: ${num}\nСумма: ${amount} ₽\nhttps://qr.vitanova.network/`);
|
|
|
|
|
|
window.open(`mailto:?subject=${subject}&body=${body}`, '_blank');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
shareByTelegram(): void {
|
|
|
|
|
|
const num = this.fastcheckNumber();
|
|
|
|
|
|
const amount = this.fastcheckAmount();
|
|
|
|
|
|
const text = encodeURIComponent(`fastCHECK: ${num} — ${amount} ₽`);
|
|
|
|
|
|
window.open(`https://t.me/share/url?url=https%3A%2F%2Fqr.vitanova.network%2F&text=${text}`, '_blank');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|