diff --git a/BACKEND_CHANGES.md b/BACKEND_CHANGES.md index cac0f13..965ef6d 100644 --- a/BACKEND_CHANGES.md +++ b/BACKEND_CHANGES.md @@ -39,6 +39,33 @@ Response: --- +## 1.1. `GET /fastcheck` — добавить `amount` в ответ + +Фронт автоматически делает `GET /fastcheck` после ввода полного номера +(`xxxx-xxxx-xxxx`), чтобы показать получателю сумму до ввода кода. Сейчас в +`api.txt` ответ содержит только `fastcheck`, `expiration`, `Status` — суммы нет. + +Добавить: + +```diff +GET /fastcheck +Body: { "fastcheck": "1234-5678-0001" } +Response: +{ + "fastcheck": "1234-5678-0001", + "expiration": "2026-07-07T09:08:18Z", ++ "amount": 158000, ++ "currency": "RUB", ++ "note": "За кофе", + "Status": true +} +``` + +Также: GET с телом — нестандарт, многие HTTP-клиенты его выкидывают. **Принимать +`?fastcheck=...` как query-параметр** (фронт шлёт оба варианта одновременно). + +--- + ## 2. Зафиксировать единицу `amount` В `api.txt` пример `"amount": 158000` неоднозначен. Зафиксировать: @@ -144,6 +171,8 @@ Body: - [ ] DNS + HTTPS + CORS (блокер) - [ ] `orderId`, `note`, `returnUrl` в `POST /fastcheck` (create) - [ ] `note` возвращается в `GET /fastcheck` +- [ ] `amount` (+ currency) возвращается в `GET /fastcheck` +- [ ] `GET /fastcheck` принимает `?fastcheck=` как query-param - [ ] Зафиксировать `amount` в основной единице (рубли) - [ ] Webhook на `fastcheck.paid` с HMAC-подписью - [ ] Гранулярные ошибки accept diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 783e2be..dbccfc8 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -3,8 +3,14 @@ export const routes: Routes = [ { path: '', - loadComponent: () => - import('./pages/fastcheck-page/fastcheck-page').then((m) => m.FastcheckPage) + loadComponent: () => { + // Branch: ?id= means legacy SBP merchant flow. + const hasLegacyId = typeof window !== 'undefined' + && new URLSearchParams(window.location.search).has('id'); + return hasLegacyId + ? import('./pages/legacy-pay-page/legacy-pay-page').then((m) => m.LegacyPayPage) + : import('./pages/fastcheck-page/fastcheck-page').then((m) => m.FastcheckPage); + } }, { path: 'new', diff --git a/src/app/pages/fastcheck-page/fastcheck-page.html b/src/app/pages/fastcheck-page/fastcheck-page.html index bac2f41..5f64f61 100644 --- a/src/app/pages/fastcheck-page/fastcheck-page.html +++ b/src/app/pages/fastcheck-page/fastcheck-page.html @@ -25,10 +25,11 @@ type="text" class="input" [ngModel]="fastcheckNumber()" - (ngModelChange)="fastcheckNumber.set($event)" + (ngModelChange)="onNumberChange($event)" placeholder="1234-5678-0001" inputmode="numeric" autocomplete="off" + maxlength="14" /> Новый @@ -49,8 +50,12 @@ step="1" inputmode="numeric" placeholder="0" + [readonly]="amountLoading() || fastcheckAmount() !== null" /> + @if (amountLoading()) { + Проверяем… + } @@ -61,10 +66,10 @@ type="text" class="input" [ngModel]="fastcheckCode()" - (ngModelChange)="fastcheckCode.set($event)" - placeholder="0000" + (ngModelChange)="onCodeChange($event)" + placeholder="00000" inputmode="numeric" - maxlength="8" + maxlength="5" autocomplete="one-time-code" /> @if (error()) { @@ -72,7 +77,7 @@ } - + + + + + diff --git a/src/app/pages/legacy-pay-page/legacy-pay-page.scss b/src/app/pages/legacy-pay-page/legacy-pay-page.scss new file mode 100644 index 0000000..498d511 --- /dev/null +++ b/src/app/pages/legacy-pay-page/legacy-pay-page.scss @@ -0,0 +1,60 @@ +@use './../../../shared' as *; + +.sbp-logo { + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.15); + backdrop-filter: blur(8px); + border-radius: 16px; + padding: 12px 20px; + border: 1px solid rgba(255, 255, 255, 0.25); + margin-bottom: 14px; + + img { + height: 40px; + display: block; + + @media (max-width: 480px) { + height: 34px; + } + } +} + +.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; } +} + +.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; + } +} diff --git a/src/app/pages/legacy-pay-page/legacy-pay-page.ts b/src/app/pages/legacy-pay-page/legacy-pay-page.ts new file mode 100644 index 0000000..0800580 --- /dev/null +++ b/src/app/pages/legacy-pay-page/legacy-pay-page.ts @@ -0,0 +1,91 @@ +import { Component, computed, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { HttpClient } from '@angular/common/http'; + +interface LegacyPayResponse { + payload?: string; +} + +/** + * Legacy SBP merchant payment flow. + * Activated when the root URL has `?id=`. + * Mirrors public/payment.html behaviour: + * POST https://qr.vitanova.network:567/qr + * { payment, amount, currency, id, note } -> { payload: '' } + * then window.location.href = payload. + */ +@Component({ + selector: 'app-legacy-pay-page', + imports: [FormsModule], + templateUrl: './legacy-pay-page.html', + styleUrl: './legacy-pay-page.scss' +}) +export class LegacyPayPage { + private http = inject(HttpClient); + private route = inject(ActivatedRoute); + + private readonly LEGACY_API = 'https://qr.vitanova.network:567/qr'; + + amount = signal(null); + note = signal(''); + error = signal(''); + loading = signal(false); + + paymentId = signal(''); + + canPay = computed(() => { + const a = this.amount(); + return !!this.paymentId() && a !== null && a > 0 && !this.loading(); + }); + + constructor() { + const id = this.route.snapshot.queryParamMap.get('id') ?? ''; + this.paymentId.set(id); + } + + onAmountChange(value: number | null): void { + this.amount.set(value); + if (this.error()) this.error.set(''); + } + + onNoteChange(value: string): void { + this.note.set(value); + } + + pay(): void { + if (!this.canPay()) { + if (!this.paymentId()) { + this.error.set('Не указан идентификатор платежа (параметр id)'); + } else { + this.error.set('Введите корректную сумму'); + } + return; + } + this.error.set(''); + this.loading.set(true); + + const body = { + payment: 'sbp', + amount: this.amount(), + currency: 'rub', + id: this.paymentId(), + note: this.note().trim() + }; + + this.http.post(this.LEGACY_API, body).subscribe({ + next: (res) => { + this.loading.set(false); + if (res?.payload) { + window.location.href = res.payload; + } else { + this.error.set('Сервер не вернул ссылку для оплаты.'); + } + }, + error: () => { + this.loading.set(false); + this.error.set('Ошибка при создании платежа. Попробуйте ещё раз.'); + } + }); + } +} diff --git a/src/shared.scss b/src/shared.scss index 7d6922e..f6b3fa5 100644 --- a/src/shared.scss +++ b/src/shared.scss @@ -114,6 +114,14 @@ color: #ef4444; font-weight: 500; } + + &__hint { + display: block; + margin-top: 6px; + font-size: 13px; + color: #64748b; + font-weight: 500; + } } .input-wrap { @@ -191,6 +199,7 @@ transition: opacity 0.15s, transform 0.1s, box-shadow 0.15s; box-shadow: 0 6px 20px rgba(37, 99, 235, 0.38); font-family: inherit; + appearance: none; -webkit-appearance: none; &:hover { opacity: 0.92; box-shadow: 0 8px 28px rgba(37, 99, 235, 0.45); } @@ -212,17 +221,18 @@ } // ─── Brand wordmark: "fastCHECK" inline ───────────────────────────────────── -// "fast" is rendered half the size of "CHECK". +// "fast" sits a bit smaller and lighter than "CHECK". .brand { display: inline-flex; align-items: baseline; - font-weight: 800; + font-weight: inherit; letter-spacing: -0.02em; white-space: nowrap; + font-size: calc(1em + 3px); &__fast { - font-size: 0.5em; - font-weight: 700; + font-size: 0.72em; + font-weight: 400; text-transform: lowercase; margin-right: 0.05em; opacity: 0.85; @@ -230,6 +240,7 @@ &__check { font-size: 1em; + font-weight: 500; text-transform: uppercase; letter-spacing: 0.02em; }