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

242 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Component, computed, 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 } from '../../api';
interface WebSessionResponse {
sessionId: string;
userId: string;
expires: string;
userSessionId: string;
Status: boolean;
}
interface CheckFastcheckResponse {
fastcheck: string;
amount?: number;
expiration: string;
Status: boolean;
}
@Component({
selector: 'app-fastcheck-page',
imports: [FormsModule, RouterLink],
templateUrl: './fastcheck-page.html',
styleUrl: './fastcheck-page.scss'
})
export class FastcheckPage {
private http = inject(HttpClient);
private store = inject(FastcheckService);
private router = inject(Router);
// 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>('');
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 === 12 && codeDigits.length === 5 && !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)}`;
});
constructor() {
// Pull autofill data left over by the create page.
const created = this.store.consume();
if (created) {
this.fastcheckNumber.set(created.fastcheck);
this.fastcheckAmount.set(created.amount);
this.fastcheckCode.set(created.code);
}
}
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);
this.startPolling(res.sessionId);
},
error: () => {
this.popupLoading.set(false);
this.popupError.set('Не удалось создать сессию. Попробуйте ещё раз.');
}
});
}
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-and-forget merchant callback if a return_url is on the page.
this.fireMerchantCallback();
},
error: () => {
this.popupLoading.set(false);
this.popupError.set('Не удалось принять платёж.');
}
});
}
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 XXXX-XXXX-XXXX, allow only digits. */
onNumberChange(raw: string): void {
const digits = (raw ?? '').replace(/\D/g, '').slice(0, 12);
const groups: string[] = [];
for (let i = 0; i < digits.length; i += 4) {
groups.push(digits.slice(i, i + 4));
}
const masked = groups.join('-');
this.fastcheckNumber.set(masked);
this.error.set('');
// If number became incomplete, drop the previously fetched amount so the
// user doesn't see a stale value tied to a different (now-edited) number.
if (digits.length < 12 && this.lastLookedUpNumber) {
this.fastcheckAmount.set(null);
this.lastLookedUpNumber = '';
}
// Auto-lookup when 12 digits are entered (and we haven't already looked it up).
if (digits.length === 12 && masked !== this.lastLookedUpNumber) {
this.lookupFastcheck(masked);
}
}
/** Allow only digits, max 5, in the code field. */
onCodeChange(raw: string): void {
const digits = (raw ?? '').replace(/\D/g, '').slice(0, 5);
this.fastcheckCode.set(digits);
this.error.set('');
}
private lookupFastcheck(number: string): void {
this.lastLookedUpNumber = number;
this.amountLoading.set(true);
this.fastcheckAmount.set(null);
// GET /fastcheck — body in GET is non-standard; many HTTP libs strip it.
// The backend should accept ?fastcheck= as a query param too. We send both.
this.http
.request<CheckFastcheckResponse>('GET', `${FASTCHECK_API}/fastcheck`, {
body: { fastcheck: number },
params: { fastcheck: number }
})
.subscribe({
next: (res) => {
this.amountLoading.set(false);
if (res?.Status && typeof res.amount === 'number') {
this.fastcheckAmount.set(res.amount);
} else if (res?.Status === false) {
this.error.set('Платёж не найден или просрочен.');
}
},
error: () => {
this.amountLoading.set(false);
this.error.set('Не удалось проверить номер. Попробуйте ещё раз.');
this.lastLookedUpNumber = '';
}
});
}
}