Compare commits
2 Commits
742b2665e9
...
976eb33492
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
976eb33492 | ||
|
|
cdb9760033 |
@@ -47,7 +47,9 @@
|
|||||||
"modal_paid_title": "Paid",
|
"modal_paid_title": "Paid",
|
||||||
"modal_paid_sub": "fastCHECK successfully accepted.",
|
"modal_paid_sub": "fastCHECK successfully accepted.",
|
||||||
"share_email": "Send by email",
|
"share_email": "Send by email",
|
||||||
"share_tg": "Send via Telegram"
|
"share_tg": "Send via Telegram",
|
||||||
|
"modal_loggedin_title": "Signed in",
|
||||||
|
"modal_loggedin_sub": "You are now signed in to fastCHECK."
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "New",
|
"title": "New",
|
||||||
|
|||||||
@@ -47,7 +47,9 @@
|
|||||||
"modal_paid_title": "Վճարված է",
|
"modal_paid_title": "Վճարված է",
|
||||||
"modal_paid_sub": "fastCHECK-ը հաջողությամբ ընդունված է:",
|
"modal_paid_sub": "fastCHECK-ը հաջողությամբ ընդունված է:",
|
||||||
"share_email": "Ուղարկել էլ. նամակով",
|
"share_email": "Ուղարկել էլ. նամակով",
|
||||||
"share_tg": "Ուղարկել Telegram-ով"
|
"share_tg": "Ուղարկել Telegram-ով",
|
||||||
|
"modal_loggedin_title": "Մուտք գործել",
|
||||||
|
"modal_loggedin_sub": "Դուք մուտք գործել եք fastCHECK:"
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "Նոր",
|
"title": "Նոր",
|
||||||
|
|||||||
@@ -47,7 +47,9 @@
|
|||||||
"modal_paid_title": "Оплачено",
|
"modal_paid_title": "Оплачено",
|
||||||
"modal_paid_sub": "fastCHECK успешно принят.",
|
"modal_paid_sub": "fastCHECK успешно принят.",
|
||||||
"share_email": "Отправить на почту",
|
"share_email": "Отправить на почту",
|
||||||
"share_tg": "Отправить в Telegram"
|
"share_tg": "Отправить в Telegram",
|
||||||
|
"modal_loggedin_title": "Вы вошли",
|
||||||
|
"modal_loggedin_sub": "Вы авторизованы в fastCHECK."
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "Новый",
|
"title": "Новый",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 375 KiB After Width: | Height: | Size: 123 KiB |
@@ -9,13 +9,3 @@
|
|||||||
export const FASTCHECK_API = isDevMode()
|
export const FASTCHECK_API = isDevMode()
|
||||||
? '/proxy/fastcheck'
|
? '/proxy/fastcheck'
|
||||||
: 'https://api.fastcheck.store';
|
: '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';
|
|
||||||
|
|||||||
@@ -12,11 +12,6 @@ export const routes: Routes = [
|
|||||||
: import('./pages/fastcheck-page/fastcheck-page').then((m) => m.FastcheckPage);
|
: 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',
|
path: 'about',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
<div class="page">
|
|
||||||
<div class="card">
|
|
||||||
|
|
||||||
<div class="card__header">
|
|
||||||
<a class="back" routerLink="/" [attr.aria-label]="'create.back_label' | translate">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M15 18l-6-6 6-6" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<h1 class="card__title">
|
|
||||||
{{ 'create.title' | translate }}
|
|
||||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
|
||||||
</h1>
|
|
||||||
<p class="card__subtitle">{{ 'create.subtitle' | translate }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card__body">
|
|
||||||
|
|
||||||
<!-- Payment methods -->
|
|
||||||
<div class="field">
|
|
||||||
<span class="field__label">{{ 'create.payment_label' | translate }}</span>
|
|
||||||
<div class="methods">
|
|
||||||
<button type="button" class="method" [class.method--active]="payment() === 'sbp'"
|
|
||||||
(click)="selectPayment('sbp', true)" aria-label="СБП">
|
|
||||||
<img class="method__logo"
|
|
||||||
src="https://sbp.nspk.ru/storage/settings/common/logo/0645d335-8b62-43a1-9a33-0d4c9d1dc0e0.svg"
|
|
||||||
alt="СБП" />
|
|
||||||
</button>
|
|
||||||
<button type="button" class="method method--disabled" disabled aria-label="WeChat Pay">
|
|
||||||
<img class="method__logo" src="/wechat-pay.svg" alt="WeChat Pay" />
|
|
||||||
</button>
|
|
||||||
<button type="button" class="method method--disabled" disabled aria-label="Alipay">
|
|
||||||
<img class="method__logo" src="/alipay.svg" alt="Alipay" />
|
|
||||||
</button>
|
|
||||||
<button type="button" class="method method--disabled" disabled aria-label="Visa">
|
|
||||||
<img class="method__logo" src="/visa.svg" alt="Visa" />
|
|
||||||
</button>
|
|
||||||
<button type="button" class="method method--disabled" disabled aria-label="MasterCard">
|
|
||||||
<img class="method__logo" src="/mastercard.svg" alt="Mastercard" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Currencies -->
|
|
||||||
<div class="field">
|
|
||||||
<span class="field__label">{{ 'create.currency_label' | translate }}</span>
|
|
||||||
<div class="currencies">
|
|
||||||
<button type="button" class="chip" [class.chip--active]="currency() === 'RUB'"
|
|
||||||
(click)="selectCurrency('RUB', true)">
|
|
||||||
<span class="chip__sign">₽</span>
|
|
||||||
<span class="chip__code">RUB</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="chip chip--disabled" disabled>
|
|
||||||
<span class="chip__sign">¥</span>
|
|
||||||
<span class="chip__code">CNY</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="chip chip--disabled" disabled>
|
|
||||||
<span class="chip__sign">$</span>
|
|
||||||
<span class="chip__code">USD</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="chip chip--disabled" disabled>
|
|
||||||
<span class="chip__sign">€</span>
|
|
||||||
<span class="chip__code">EUR</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="chip chip--disabled" disabled>
|
|
||||||
<span class="chip__sign">֏</span>
|
|
||||||
<span class="chip__code">AMD</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="field__label" for="amount">{{ 'create.amount_label' | translate }}</label>
|
|
||||||
<div class="input-wrap" [class.input-wrap--error]="error()">
|
|
||||||
<span class="input-wrap__prefix">₽</span>
|
|
||||||
<input
|
|
||||||
id="amount"
|
|
||||||
type="number"
|
|
||||||
class="input-wrap__input"
|
|
||||||
[ngModel]="amount()"
|
|
||||||
(ngModelChange)="onAmountChange($event)"
|
|
||||||
[min]="minAmount()"
|
|
||||||
[max]="maxAmount()"
|
|
||||||
step="1"
|
|
||||||
inputmode="numeric"
|
|
||||||
placeholder="0"
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span class="field__hint">{{ 'create.amount_hint' | translate }} {{ minAmount() }}–{{ maxAmount().toLocaleString('ru') }} ₽</span>
|
|
||||||
@if (error()) {
|
|
||||||
<span class="field__error">{{ error() }}</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="field__label" for="note">{{ 'create.note_label' | translate }}</label>
|
|
||||||
<textarea
|
|
||||||
id="note"
|
|
||||||
class="note-input"
|
|
||||||
[ngModel]="note()"
|
|
||||||
(ngModelChange)="onNoteChange($event)"
|
|
||||||
[placeholder]="'create.note_placeholder' | translate"
|
|
||||||
rows="3"
|
|
||||||
maxlength="500"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="pay-btn" type="button" (click)="createCheck()" [disabled]="loading() || qrImageUrl() !== null">
|
|
||||||
<span class="pay-btn__icon">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M12 5v14M5 12h14" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
@if (loading()) {
|
|
||||||
{{ 'create.creating' | translate }}
|
|
||||||
} @else {
|
|
||||||
{{ 'create.create_btn' | translate }}
|
|
||||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- QR popup overlay -->
|
|
||||||
@if (qrImageUrl()) {
|
|
||||||
<div class="qr-overlay" (click)="closeQr()">
|
|
||||||
<div class="qr-modal" (click)="$event.stopPropagation()">
|
|
||||||
<button class="qr-modal__close" type="button" (click)="closeQr()" aria-label="Close">
|
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M18 6L6 18M6 6l12 12"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<p class="qr-modal__label">{{ 'create.qr_label' | translate }}</p>
|
|
||||||
<img class="qr-modal__img" [src]="qrImageUrl()!" width="260" height="260" alt="QR" />
|
|
||||||
@if (qrStatus()) {
|
|
||||||
<span class="qr-modal__status">{{ qrStatus() }}</span>
|
|
||||||
}
|
|
||||||
@if (qrPolling()) {
|
|
||||||
<p class="qr-modal__hint">{{ 'create.qr_waiting' | translate }}</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card__footer">
|
|
||||||
<span class="secure-badge">
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
||||||
</svg>
|
|
||||||
{{ 'common.secure' | translate }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
@@ -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<number>(30);
|
|
||||||
maxAmount = signal<number>(200_000);
|
|
||||||
|
|
||||||
amount = signal<number | null>(null);
|
|
||||||
note = signal<string>('');
|
|
||||||
error = signal<string>('');
|
|
||||||
loading = signal<boolean>(false);
|
|
||||||
settingsLoaded = signal<boolean>(false);
|
|
||||||
|
|
||||||
currency = signal<Currency>('RUB');
|
|
||||||
payment = signal<PaymentMethod>('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<string | null>(null);
|
|
||||||
qrPolling = signal<boolean>(false);
|
|
||||||
qrStatus = signal<string>('');
|
|
||||||
private pollHandle: ReturnType<typeof setInterval> | 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<SettingsResponse>(`${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<string, string> = {};
|
|
||||||
if (this.authKey) headers['authorization-key'] = this.authKey;
|
|
||||||
if (this.userId) headers['userid-value'] = this.userId;
|
|
||||||
|
|
||||||
const partnerqrID = generateUUID();
|
|
||||||
|
|
||||||
this.http
|
|
||||||
.post<CreateQrResponse>(
|
|
||||||
`${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<QrStatusResponse>(`${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<string, string> = {};
|
|
||||||
if (this.sessionId) headers['Authorization'] = JSON.stringify({ sessionID: this.sessionId });
|
|
||||||
|
|
||||||
this.http
|
|
||||||
.post<CreateFastcheckResponse>(
|
|
||||||
`${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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
/>
|
/>
|
||||||
<a class="btn btn--ghost" routerLink="/new" aria-label="Создать новый fastCHECK">{{ 'fastcheck.number_new' | translate }}</a>
|
<a class="btn btn--ghost" href="https://qr.vitanova.network/" target="_blank" rel="noopener" aria-label="Создать новый fastCHECK">{{ 'fastcheck.number_new' | translate }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -133,11 +133,16 @@
|
|||||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M20 6L9 17l-5-5" />
|
<path d="M20 6L9 17l-5-5" />
|
||||||
</svg>
|
</svg>
|
||||||
<h2 class="modal__title">{{ 'fastcheck.modal_paid_title' | translate }}</h2>
|
@if (loginOnly()) {
|
||||||
<p class="modal__sub">
|
<h2 class="modal__title">{{ 'fastcheck.modal_loggedin_title' | translate }}</h2>
|
||||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
<p class="modal__sub">{{ 'fastcheck.modal_loggedin_sub' | translate }}</p>
|
||||||
{{ 'fastcheck.modal_paid_sub' | translate }}
|
} @else {
|
||||||
</p>
|
<h2 class="modal__title">{{ 'fastcheck.modal_paid_title' | translate }}</h2>
|
||||||
|
<p class="modal__sub">
|
||||||
|
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
||||||
|
{{ 'fastcheck.modal_paid_sub' | translate }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<img class="brand-logo brand-logo--small" src="/logo_small.png"
|
<img class="brand-logo brand-logo--small" src="/logo_small.png"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, computed, inject, signal } from '@angular/core';
|
import { Component, computed, inject, signal } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { Router, RouterLink } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { FastcheckService } from '../../fastcheck.service';
|
import { FastcheckService } from '../../fastcheck.service';
|
||||||
import { FASTCHECK_API } from '../../api';
|
import { FASTCHECK_API } from '../../api';
|
||||||
@@ -29,7 +29,7 @@ interface CheckFastcheckResponse {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-fastcheck-page',
|
selector: 'app-fastcheck-page',
|
||||||
imports: [FormsModule, RouterLink, TranslatePipe],
|
imports: [FormsModule, TranslatePipe],
|
||||||
templateUrl: './fastcheck-page.html',
|
templateUrl: './fastcheck-page.html',
|
||||||
styleUrl: './fastcheck-page.scss'
|
styleUrl: './fastcheck-page.scss'
|
||||||
})
|
})
|
||||||
@@ -56,6 +56,8 @@ export class FastcheckPage {
|
|||||||
popupError = signal<string>('');
|
popupError = signal<string>('');
|
||||||
webSessionId = signal<string>('');
|
webSessionId = signal<string>('');
|
||||||
paid = signal<boolean>(false);
|
paid = signal<boolean>(false);
|
||||||
|
loginOnly = signal<boolean>(false);
|
||||||
|
sessionToken = signal<string>(localStorage.getItem('fc_session') ?? '');
|
||||||
private pollHandle: ReturnType<typeof setInterval> | null = null;
|
private pollHandle: ReturnType<typeof setInterval> | null = null;
|
||||||
private lastLookedUpNumber = '';
|
private lastLookedUpNumber = '';
|
||||||
|
|
||||||
@@ -113,6 +115,7 @@ export class FastcheckPage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.error.set('');
|
this.error.set('');
|
||||||
|
this.loginOnly.set(false);
|
||||||
this.openPopup();
|
this.openPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +125,32 @@ export class FastcheckPage {
|
|||||||
this.paid.set(false);
|
this.paid.set(false);
|
||||||
this.popupLoading.set(true);
|
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({
|
this.http.get<WebSessionResponse>(`${FASTCHECK_API}/websession`).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.popupLoading.set(false);
|
this.popupLoading.set(false);
|
||||||
@@ -142,13 +171,20 @@ export class FastcheckPage {
|
|||||||
closePopup(): void {
|
closePopup(): void {
|
||||||
this.popupOpen.set(false);
|
this.popupOpen.set(false);
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
if (this.webSessionId()) {
|
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.
|
// Best-effort logout; ignore errors.
|
||||||
this.http
|
this.http
|
||||||
.request('DELETE', `${FASTCHECK_API}/websession/${this.webSessionId()}`, {
|
.request('DELETE', `${FASTCHECK_API}/websession/${this.webSessionId()}`, {
|
||||||
body: { sessionId: this.webSessionId() }
|
body: { sessionId: this.webSessionId() }
|
||||||
})
|
})
|
||||||
.subscribe({ error: () => undefined });
|
.subscribe({ error: () => undefined });
|
||||||
|
localStorage.removeItem('fc_session');
|
||||||
|
this.sessionToken.set('');
|
||||||
}
|
}
|
||||||
this.webSessionId.set('');
|
this.webSessionId.set('');
|
||||||
}
|
}
|
||||||
@@ -162,7 +198,11 @@ export class FastcheckPage {
|
|||||||
next: (res) => {
|
next: (res) => {
|
||||||
if (res?.Status) {
|
if (res?.Status) {
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
this.acceptFastcheck(sessionId);
|
if (this.loginOnly()) {
|
||||||
|
this.paid.set(true);
|
||||||
|
} else {
|
||||||
|
this.acceptFastcheck(sessionId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: () => undefined
|
error: () => undefined
|
||||||
@@ -285,9 +325,7 @@ export class FastcheckPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shareByTelegram(): void {
|
shareByTelegram(): void {
|
||||||
const num = this.fastcheckNumber();
|
this.loginOnly.set(true);
|
||||||
const amount = this.fastcheckAmount();
|
this.openPopup();
|
||||||
const text = encodeURIComponent(`fastCHECK: ${num} — ${amount} ₽`);
|
|
||||||
window.open(`https://t.me/share/url?url=https%3A%2F%2Fqr.vitanova.network%2F&text=${text}`, '_blank');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user