From cdb9760033aebd0351505f351343a313df173abf Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Thu, 7 May 2026 00:31:14 +0400 Subject: [PATCH] changes --- src/app/api.ts | 10 - src/app/app.routes.ts | 5 - src/app/pages/create-page/create-page.html | 158 ---------- src/app/pages/create-page/create-page.scss | 266 ----------------- src/app/pages/create-page/create-page.ts | 274 ------------------ .../pages/fastcheck-page/fastcheck-page.html | 2 +- .../pages/fastcheck-page/fastcheck-page.ts | 4 +- 7 files changed, 3 insertions(+), 716 deletions(-) delete mode 100644 src/app/pages/create-page/create-page.html delete mode 100644 src/app/pages/create-page/create-page.scss delete mode 100644 src/app/pages/create-page/create-page.ts diff --git a/src/app/api.ts b/src/app/api.ts index a64c274..db24c04 100644 --- a/src/app/api.ts +++ b/src/app/api.ts @@ -9,13 +9,3 @@ export const FASTCHECK_API = isDevMode() ? '/proxy/fastcheck' : 'https://api.fastcheck.store'; - -// Legacy QR endpoint kept for the SBP amount → payload redirect flow. -export const QR_API = isDevMode() - ? '/proxy/legacy-qr/qr' - : 'https://qr.vitanova.network:567/qr'; - -// New QR Vitanova API (dynamic QR, settings, polling). -export const QR_VITANOVA_API = isDevMode() - ? '/proxy/qr-vitanova/api' - : 'https://qr.vitanova.network/api'; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 8ac1161..65a662e 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -12,11 +12,6 @@ export const routes: Routes = [ : import('./pages/fastcheck-page/fastcheck-page').then((m) => m.FastcheckPage); } }, - { - path: 'new', - loadComponent: () => - import('./pages/create-page/create-page').then((m) => m.CreatePage) - }, { path: 'about', loadComponent: () => diff --git a/src/app/pages/create-page/create-page.html b/src/app/pages/create-page/create-page.html deleted file mode 100644 index f28fc67..0000000 --- a/src/app/pages/create-page/create-page.html +++ /dev/null @@ -1,158 +0,0 @@ -
-
- -
- - - - - -

- {{ 'create.title' | translate }}  - fastCHECK -

-

{{ 'create.subtitle' | translate }}

-
- -
- - -
- {{ 'create.payment_label' | translate }} -
- - - - - -
-
- - -
- {{ 'create.currency_label' | translate }} -
- - - - - -
-
- -
- -
- - -
- {{ 'create.amount_hint' | translate }} {{ minAmount() }}–{{ maxAmount().toLocaleString('ru') }} ₽ - @if (error()) { - {{ error() }} - } -
- -
- - -
- - - - -@if (qrImageUrl()) { -
-
- -

{{ 'create.qr_label' | translate }}

- QR - @if (qrStatus()) { - {{ qrStatus() }} - } - @if (qrPolling()) { -

{{ 'create.qr_waiting' | translate }}

- } -
-
-} -
- - -
-
diff --git a/src/app/pages/create-page/create-page.scss b/src/app/pages/create-page/create-page.scss deleted file mode 100644 index d4621ea..0000000 --- a/src/app/pages/create-page/create-page.scss +++ /dev/null @@ -1,266 +0,0 @@ -@use './../../../shared' as *; - -.card__header { - position: relative; -} - -.back { - position: absolute; - top: 14px; - left: 14px; - width: 44px; - height: 44px; - border-radius: 50%; - display: inline-flex; - align-items: center; - justify-content: center; - color: #475569; - background: #f1f5f9; - border: 1px solid #e2e8f0; - text-decoration: none; - transition: background 0.15s, color 0.15s; - z-index: 1; - - &:hover { background: #e2e8f0; color: #0f172a; } - &:active { background: #cbd5e1; } -} - -.currency-badge { - display: flex; - align-items: center; - gap: 10px; - background: #f1f5f9; - border-radius: 12px; - padding: 12px 16px; - margin-bottom: 18px; - - &__flag { font-size: 22px; line-height: 1; } - &__code { font-size: 15px; font-weight: 700; color: #0f172a; } - &__name { font-size: 13px; color: #64748b; margin-left: auto; } -} - -// ─── Methods row ──────────────────────────────────────────────────────────── -.methods { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px; - - @media (max-width: 360px) { - gap: 6px; - } -} - -.method { - display: flex; - align-items: center; - justify-content: center; - height: 56px; - padding: 8px; - border-radius: 12px; - border: 2px solid #e2e8f0; - background: #fff; - cursor: pointer; - transition: border-color .15s, background .15s, transform .1s, box-shadow .15s; - -webkit-appearance: none; - font-family: inherit; - - @media (max-width: 360px) { - height: 52px; - padding: 6px; - } - - &__logo { - max-width: 100%; - max-height: 28px; - object-fit: contain; - display: block; - pointer-events: none; - } - - &:hover:not(:disabled):not(.method--disabled) { - border-color: #cbd5e1; - } - - &:active:not(:disabled) { transform: scale(.97); } - - &--active { - border-color: #2563eb; - background: rgba(37, 99, 235, .06); - box-shadow: 0 0 0 3px rgba(37, 99, 235, .1); - } - - &--disabled, - &:disabled { - cursor: not-allowed; - background: #f8fafc; - - .method__logo { - filter: grayscale(1); - opacity: .45; - } - } -} - -// ─── Currency chips ───────────────────────────────────────────────────────── -.currencies { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.chip { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 0 14px; - height: 44px; - border-radius: 999px; - border: 2px solid #e2e8f0; - background: #f8fafc; - color: #475569; - font-family: inherit; - font-size: 14px; - font-weight: 700; - cursor: pointer; - transition: border-color .15s, background .15s, color .15s; - -webkit-appearance: none; - - &__flag { font-size: 16px; line-height: 1; } - &__sign { - font-size: 15px; - font-weight: 800; - color: #1e40af; - line-height: 1; - } - &__code { letter-spacing: .3px; } - - &--active { - border-color: #2563eb; - background: rgba(37, 99, 235, .08); - color: #1e40af; - } - - &--disabled, - &:disabled { - opacity: .45; - cursor: not-allowed; - color: #94a3b8; - - .chip__sign { color: #94a3b8; } - } -} - -.note-input { - width: 100%; - border: 2px solid #e2e8f0; - border-radius: 14px; - background: #f8fafc; - padding: 14px 16px; - font-size: 15px; - font-weight: 500; - color: #0f172a; - font-family: inherit; - resize: vertical; - outline: none; - transition: border-color 0.2s, box-shadow 0.2s, background 0.2s; - line-height: 1.5; - - &::placeholder { color: #cbd5e1; font-weight: 400; } - - &:focus { - border-color: #2563eb; - box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12); - background: #fff; - } -} - -// ─── QR section ───────────────────────────────────────────────────────────── -// ─── QR popup ─────────────────────────────────────────────────────────────── -.qr-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.55); - z-index: 1000; - display: flex; - align-items: center; - justify-content: center; - animation: overlay-in 0.2s ease; -} - -.qr-modal { - position: relative; - background: #fff; - border-radius: 20px; - padding: 32px 28px 24px; - display: flex; - flex-direction: column; - align-items: center; - gap: 14px; - box-shadow: 0 20px 60px rgba(0,0,0,0.25); - animation: modal-in 0.22s cubic-bezier(.34,1.56,.64,1); - max-width: 340px; - width: 90vw; - - &__close { - position: absolute; - top: 12px; - right: 12px; - width: 36px; - height: 36px; - border-radius: 50%; - border: none; - background: #f1f5f9; - color: #475569; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.15s; - &:hover { background: #e2e8f0; } - } - - &__label { - font-size: 13px; - font-weight: 600; - color: #475569; - text-transform: uppercase; - letter-spacing: 0.05em; - } - - &__img { - border-radius: 12px; - box-shadow: 0 2px 12px rgba(0,0,0,0.1); - } - - &__hint { - font-size: 13px; - color: #64748b; - animation: pulse 1.6s ease-in-out infinite; - } - - &__status { - font-size: 11px; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; - padding: 3px 10px; - border-radius: 20px; - background: #f1f5f9; - color: #475569; - } -} - -@keyframes overlay-in { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes modal-in { - from { opacity: 0; transform: scale(0.85); } - to { opacity: 1; transform: scale(1); } -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.45; } -} diff --git a/src/app/pages/create-page/create-page.ts b/src/app/pages/create-page/create-page.ts deleted file mode 100644 index 9346e6f..0000000 --- a/src/app/pages/create-page/create-page.ts +++ /dev/null @@ -1,274 +0,0 @@ -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(30); - maxAmount = signal(200_000); - - amount = signal(null); - note = signal(''); - error = signal(''); - loading = signal(false); - settingsLoaded = signal(false); - - currency = signal('RUB'); - payment = signal('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(null); - qrPolling = signal(false); - qrStatus = signal(''); - private pollHandle: ReturnType | 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(`${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 = {}; - if (this.authKey) headers['authorization-key'] = this.authKey; - if (this.userId) headers['userid-value'] = this.userId; - - const partnerqrID = generateUUID(); - - this.http - .post( - `${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(`${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 = {}; - if (this.sessionId) headers['Authorization'] = JSON.stringify({ sessionID: this.sessionId }); - - this.http - .post( - `${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; - } - } -} diff --git a/src/app/pages/fastcheck-page/fastcheck-page.html b/src/app/pages/fastcheck-page/fastcheck-page.html index baa0ac4..614d6d4 100644 --- a/src/app/pages/fastcheck-page/fastcheck-page.html +++ b/src/app/pages/fastcheck-page/fastcheck-page.html @@ -28,7 +28,7 @@ autocomplete="off" maxlength="20" /> - {{ 'fastcheck.number_new' | translate }} + {{ 'fastcheck.number_new' | translate }} diff --git a/src/app/pages/fastcheck-page/fastcheck-page.ts b/src/app/pages/fastcheck-page/fastcheck-page.ts index 90b5f0d..554eb33 100644 --- a/src/app/pages/fastcheck-page/fastcheck-page.ts +++ b/src/app/pages/fastcheck-page/fastcheck-page.ts @@ -1,6 +1,6 @@ import { Component, computed, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { Router, RouterLink } from '@angular/router'; +import { Router } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { FastcheckService } from '../../fastcheck.service'; import { FASTCHECK_API } from '../../api'; @@ -29,7 +29,7 @@ interface CheckFastcheckResponse { @Component({ selector: 'app-fastcheck-page', - imports: [FormsModule, RouterLink, TranslatePipe], + imports: [FormsModule, TranslatePipe], templateUrl: './fastcheck-page.html', styleUrl: './fastcheck-page.scss' })