Files
FastCheck/src/app/pages/fastcheck-page/fastcheck-page.ts
sdarbinyan 976eb33492 changes
2026-05-07 00:51:09 +04:00

332 lines
10 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 } from '@angular/router';
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',
imports: [FormsModule, TranslatePipe],
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);
loginOnly = signal<boolean>(false);
sessionToken = signal<string>(localStorage.getItem('fc_session') ?? '');
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.loginOnly.set(false);
this.openPopup();
}
private openPopup(): void {
this.popupOpen.set(true);
this.popupError.set('');
this.paid.set(false);
this.popupLoading.set(true);
const existing = this.sessionToken();
if (existing) {
this.http.get<WebSessionResponse>(`${FASTCHECK_API}/websession/${existing}`).subscribe({
next: (res) => {
if (res?.Status) {
this.popupLoading.set(false);
this.webSessionId.set(existing);
if (this.loginOnly()) {
this.paid.set(true);
} else {
this.acceptFastcheck(existing);
}
} else {
this.sessionToken.set('');
this.createNewSession();
}
},
error: () => { this.sessionToken.set(''); this.createNewSession(); }
});
return;
}
this.createNewSession();
}
private createNewSession(): void {
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.loginOnly() && this.paid()) {
// Keep session alive — user is logged in, preserve token for next action.
const tok = this.webSessionId();
localStorage.setItem('fc_session', tok);
this.sessionToken.set(tok);
} else if (this.webSessionId()) {
// Best-effort logout; ignore errors.
this.http
.request('DELETE', `${FASTCHECK_API}/websession/${this.webSessionId()}`, {
body: { sessionId: this.webSessionId() }
})
.subscribe({ error: () => undefined });
localStorage.removeItem('fc_session');
this.sessionToken.set('');
}
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();
if (this.loginOnly()) {
this.paid.set(true);
} else {
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 {
this.loginOnly.set(true);
this.openPopup();
}
}