visual changes

This commit is contained in:
2026-04-30 14:51:32 +04:00
parent d6e1b30554
commit d9b0c221f1
16 changed files with 266 additions and 50 deletions

3
.gitignore vendored
View File

@@ -4,6 +4,9 @@
/tmp /tmp
/out-tsc /out-tsc
/bazel-out /bazel-out
/dist
changes.txt
api.txt
# Node # Node
/node_modules /node_modules

150
BACKEND_CHANGES.md Normal file
View File

@@ -0,0 +1,150 @@
# Fastcheck Backend — изменения сверх api.txt
Документ с **дельтой** — что нужно добавить/изменить в спеке `public/api.txt`,
чтобы фронт полноценно заработал. Базовая спека остаётся в силе, здесь только
правки.
> Полный референс с примерами — см. `BACKEND.md` в этом же репозитории.
---
## 1. `POST /fastcheck` (создание) — расширить тело
Добавить поля `orderId`, `note`, `returnUrl` (все опциональные):
```diff
POST /fastcheck
HEADER: Authorization: {"sessionID": "..."}
Body:
{
"amount": 158000,
"currency": "RUB",
+ "orderId": "merchant-order-uuid", // id заказа на стороне мерчанта
+ "note": "Оплата заказа №123", // комментарий, видит получатель
+ "returnUrl": "https://shop.example.com/thanks" // куда вернуть юзера после оплаты
}
Response:
{
"fastcheck": "1234-5678-0001",
"expiration": "2026-07-07T09:08:18Z",
"code": "5864",
"Status": true,
+ "orderId": "merchant-order-uuid" // эхо обратно
}
```
`note` также возвращать в `GET /fastcheck`, чтобы получатель видел причину
платежа перед приёмом.
---
## 2. Зафиксировать единицу `amount`
В `api.txt` пример `"amount": 158000` неоднозначен. Зафиксировать:
> `amount` — **целое число в основной единице валюты** (для RUB — рубли,
> не копейки). Минимум 1.
Если бэкенд считает в копейках — сообщить, фронт изменит формат.
---
## 3. Merchant webhook (новое)
После успешного `POST /fastcheck` (accept), сервер шлёт `POST` на
`webhookUrl`, привязанный к `orderId`/мерчанту:
```
POST <merchant_webhook_url>
Headers:
Content-Type: application/json
X-Fastcheck-Signature: <hmac-sha256 от тела с секретом мерчанта>
Body:
{
"event": "fastcheck.paid",
"fastcheck": "1234-5678-0001",
"orderId": "merchant-order-uuid",
"amount": 158000,
"currency": "RUB",
"paidAt": "2026-04-30T12:34:56Z"
}
```
Мерчант проверяет подпись и помечает заказ оплаченным. Ретраи (минимум
3 попытки с экспоненциальной задержкой) при не-2xx ответах.
---
## 4. Развести create и accept на разные пути (рекомендация)
Сейчас `POST /fastcheck` делает оба действия — отличается только формой тела
(`amount` vs `code`). Это хрупко.
```diff
- POST /fastcheck (создание)
- POST /fastcheck (приём)
+ POST /fastcheck (создание)
+ POST /fastcheck/accept (приём)
```
Фронт правится в одну строку. Если оставляете один путь — оставляем как есть.
---
## 5. Гранулярные ошибки `POST /fastcheck` (accept)
В `api.txt` любая ошибка = `404 { "message": "not authorized" }`. Юзер не
понимает, что пошло не так. Различать:
```
401 { "message": "not authorized" } — нет/невалидная сессия
404 { "message": "fastcheck not found" } — нет такого номера
403 { "message": "wrong code" } — код неверный
410 { "message": "already used" } — чек уже погашен
410 { "message": "expired" } — просрочен
```
---
## 6. CORS + HTTPS + DNS (блокер)
Сейчас `https://api.fastcheck.store` даёт `ERR_NAME_NOT_RESOLVED`
домен не резолвится. Без этого тестировать нечего.
Минимально:
- Поднять DNS A-запись на `api.fastcheck.store`.
- Валидный TLS-сертификат (Let's Encrypt подойдёт).
- На все эндпоинты + `OPTIONS` отвечать заголовками:
```
Access-Control-Allow-Origin: https://<домен-фронта>
Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
```
`OPTIONS` → `204 No Content` без тела.
Подробности — `BACKEND.md` §1.2.
---
## Что **не меняется**
- `GET /ping`
- `GET /websession`, `GET /websession/:id`, `DELETE /websession/:id`
- `GET /fastcheck`
- Формат заголовка `Authorization: {"sessionID":"..."}`
- Telegram-логин через бот `@DexarSupport_bot` с `?start=<sessionId>`
---
## Чеклист для бэкенда
- [ ] DNS + HTTPS + CORS (блокер)
- [ ] `orderId`, `note`, `returnUrl` в `POST /fastcheck` (create)
- [ ] `note` возвращается в `GET /fastcheck`
- [ ] Зафиксировать `amount` в основной единице (рубли)
- [ ] Webhook на `fastcheck.paid` с HMAC-подписью
- [ ] Гранулярные ошибки accept
- [ ] (опц.) Развести create / accept на разные пути

View File

@@ -1,4 +1,30 @@
--------------------------------------------------------------------------------
Package: @angular/forms
License: "MIT"
The MIT License
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
Package: @angular/core Package: @angular/core
License: "MIT" License: "MIT"
@@ -327,29 +353,3 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
Package: @angular/forms
License: "MIT"
The MIT License
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--------------------------------------------------------------------------------

BIN
dist/favicon.ico vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

4
dist/favicon.svg vendored
View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="7" fill="#2563eb"/>
<text x="16" y="23" font-family="Arial, sans-serif" font-size="20" font-weight="bold" fill="white" text-anchor="middle"></text>
</svg>

Before

Width:  |  Height:  |  Size: 256 B

2
dist/index.html vendored
View File

@@ -13,5 +13,5 @@
<style>*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body{height:100%}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:#1e40af}</style><link rel="stylesheet" href="styles-4STSJS4C.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-4STSJS4C.css"></noscript></head> <style>*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body{height:100%}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:#1e40af}</style><link rel="stylesheet" href="styles-4STSJS4C.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-4STSJS4C.css"></noscript></head>
<body> <body>
<app-root></app-root> <app-root></app-root>
<script src="main-S3GKETUH.js" type="module"></script></body> <link rel="modulepreload" href="chunk-FBABAKVO.js"><script src="main-XKBOOIAP.js" type="module"></script></body>
</html> </html>

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/logo_big.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
public/logo_small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -8,7 +8,10 @@
<path d="M15 18l-6-6 6-6" /> <path d="M15 18l-6-6 6-6" />
</svg> </svg>
</a> </a>
<h1 class="card__title">Новый Фастчек</h1> <h1 class="card__title">
Новый
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
</h1>
<p class="card__subtitle">Укажите сумму для пополнения</p> <p class="card__subtitle">Укажите сумму для пополнения</p>
</div> </div>
@@ -111,7 +114,12 @@
<path d="M12 5v14M5 12h14" /> <path d="M12 5v14M5 12h14" />
</svg> </svg>
</span> </span>
{{ loading() ? 'Создание…' : 'Создать Фастчек' }} @if (loading()) {
Создание…
} @else {
Создать&nbsp;
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
}
</button> </button>
</div> </div>

View File

@@ -82,12 +82,12 @@ export class CreatePage {
}); });
this.router.navigate(['/']); this.router.navigate(['/']);
} else { } else {
this.error.set('Не удалось создать Фастчек.'); this.error.set('Не удалось создать платёж.');
} }
}, },
error: () => { error: () => {
this.loading.set(false); this.loading.set(false);
this.error.set('Ошибка при создании Фастчека. Попробуйте ещё раз.'); this.error.set('Ошибка при создании платежа. Попробуйте ещё раз.');
} }
}); });
} }

View File

@@ -2,15 +2,23 @@
<div class="card"> <div class="card">
<div class="card__header"> <div class="card__header">
<h1 class="card__title">Оплата Фастчеком</h1> <img class="card__brand" src="/logo_big.png"
<p class="card__subtitle">Введите данные Фастчека или создайте новый</p> alt="fastCHECK" width="220" height="60" />
<p class="card__subtitle">
Введите данные
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
или создайте новый
</p>
</div> </div>
<div class="card__body"> <div class="card__body">
<!-- Fastcheck number + new --> <!-- Fastcheck number + new -->
<div class="field"> <div class="field">
<label class="field__label" for="fcNumber">Номер Фастчека</label> <label class="field__label" for="fcNumber">
Номер
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
</label>
<div class="row"> <div class="row">
<input <input
id="fcNumber" id="fcNumber"
@@ -22,7 +30,7 @@
inputmode="numeric" inputmode="numeric"
autocomplete="off" autocomplete="off"
/> />
<a class="btn btn--ghost" routerLink="/new" aria-label="Создать новый Фастчек">Новый</a> <a class="btn btn--ghost" routerLink="/new" aria-label="Создать новый fastCHECK">Новый</a>
</div> </div>
</div> </div>
@@ -101,9 +109,14 @@
<path d="M20 6L9 17l-5-5" /> <path d="M20 6L9 17l-5-5" />
</svg> </svg>
<h2 class="modal__title">Оплачено</h2> <h2 class="modal__title">Оплачено</h2>
<p class="modal__sub">Фастчек успешно принят.</p> <p class="modal__sub">
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
успешно принят.
</p>
</div> </div>
} @else { } @else {
<img class="brand-logo brand-logo--small" src="/logo_small.png"
alt="fastCHECK" width="32" height="32" />
<h2 class="modal__title">Войти через Telegram</h2> <h2 class="modal__title">Войти через Telegram</h2>
<p class="modal__sub">Отсканируйте QR или откройте ссылку</p> <p class="modal__sub">Отсканируйте QR или откройте ссылку</p>

View File

@@ -63,11 +63,11 @@ export class FastcheckPage {
pay(): void { pay(): void {
if (!this.fastcheckNumber().trim()) { if (!this.fastcheckNumber().trim()) {
this.error.set('Введите номер Фастчека'); this.error.set('Введите номер');
return; return;
} }
if (!this.fastcheckCode().trim()) { if (!this.fastcheckCode().trim()) {
this.error.set('Введите код Фастчека'); this.error.set('Введите код');
return; return;
} }
this.error.set(''); this.error.set('');
@@ -148,7 +148,7 @@ export class FastcheckPage {
}, },
error: () => { error: () => {
this.popupLoading.set(false); this.popupLoading.set(false);
this.popupError.set('Не удалось принять Фастчек.'); this.popupError.set('Не удалось принять платёж.');
} }
}); });
} }

View File

@@ -2,14 +2,15 @@
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Оплата через СБП</title> <title>fastCHECK</title>
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#2563eb"> <meta name="theme-color" content="#2563eb">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="apple-mobile-web-app-status-bar-style" content="default">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/png" href="logo_small.png">
<link rel="apple-touch-icon" href="logo_small.png">
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>

View File

@@ -58,6 +58,18 @@
margin: 0; margin: 0;
} }
&__brand {
display: block;
margin: 0 auto 10px;
max-width: 220px;
height: auto;
object-fit: contain;
@media (max-width: 480px) {
max-width: 200px;
}
}
&__body { &__body {
padding: 24px 22px 18px; padding: 24px 22px 18px;
@@ -198,3 +210,41 @@
svg { flex-shrink: 0; } svg { flex-shrink: 0; }
} }
// ─── Brand wordmark: "fastCHECK" inline ─────────────────────────────────────
// "fast" is rendered half the size of "CHECK".
.brand {
display: inline-flex;
align-items: baseline;
font-weight: 800;
letter-spacing: -0.02em;
white-space: nowrap;
&__fast {
font-size: 0.5em;
font-weight: 700;
text-transform: lowercase;
margin-right: 0.05em;
opacity: 0.85;
}
&__check {
font-size: 1em;
text-transform: uppercase;
letter-spacing: 0.02em;
}
}
// Standalone logo image (used inside modal/header)
.brand-logo {
display: block;
height: auto;
object-fit: contain;
user-select: none;
-webkit-user-drag: none;
&--small {
max-height: 32px;
margin: 0 auto 8px;
}
}