Compare commits

..

19 Commits

Author SHA1 Message Date
a52fd07273 spb 2026-06-21 21:02:53 +04:00
fe7fea151a spb2 2026-06-21 21:02:19 +04:00
e9acbd4898 fix 2026-06-01 15:33:23 +04:00
4841cdf90d important 2026-06-01 01:55:16 +04:00
c2a0675c79 change 2026-06-01 01:45:16 +04:00
e62afe07eb style changes 2026-06-01 00:27:37 +04:00
926afc5691 made default 2026-05-15 15:57:46 +04:00
45769ca817 redirect 2026-05-15 15:56:48 +04:00
02a33e9b14 param 2026-05-14 17:22:43 +04:00
9c96370235 location 2026-05-14 15:37:15 +04:00
9cbb6660f8 removed unused imports 2026-05-14 09:29:15 +04:00
b1ffd577c5 changes 2026-05-14 00:48:10 +04:00
bee56afedc qr check 2026-05-13 17:31:44 +04:00
ce2c9c42fe api change 2026-05-13 16:47:51 +04:00
17dfad5eaa added api 2026-05-13 16:05:17 +04:00
abb4f7b849 changed title 2026-05-13 14:50:44 +04:00
097064281a error 2026-05-13 14:42:02 +04:00
0330e0a212 dynamic + check for statuss 200 2026-05-13 14:35:50 +04:00
5147d05ea2 qr id 2026-05-13 11:26:28 +04:00
9 changed files with 89 additions and 23 deletions

View File

@@ -148,7 +148,7 @@
<div class="card__header"> <div class="card__header">
<div class="sbp-logo"> <div class="sbp-logo">
<img src="https://sbp.nspk.ru/storage/settings/common/logo/0645d335-8b62-43a1-9a33-0d4c9d1dc0e0.svg" alt="СБП" /> <img src="public/sbp.svg" alt="СБП" />
</div> </div>
<h1 class="card__title">Оплата через СБП</h1> <h1 class="card__title">Оплата через СБП</h1>
<p class="card__subtitle">Система быстрых платежей</p> <p class="card__subtitle">Система быстрых платежей</p>

View File

@@ -148,7 +148,7 @@
<div class="card__header"> <div class="card__header">
<div class="sbp-logo"> <div class="sbp-logo">
<img src="https://sbp.nspk.ru/storage/settings/common/logo/0645d335-8b62-43a1-9a33-0d4c9d1dc0e0.svg" alt="СБП" /> <img src="sbp.svg" alt="СБП" />
</div> </div>
<h1 class="card__title">Оплата через СБП</h1> <h1 class="card__title">Оплата через СБП</h1>
<p class="card__subtitle">Система быстрых платежей</p> <p class="card__subtitle">Система быстрых платежей</p>

1
public/sbp.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,11 +1,9 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { SiteHeader } from './site-header/site-header';
import { SiteFooter } from './site-footer/site-footer';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet, SiteHeader, SiteFooter], imports: [RouterOutlet],
templateUrl: './app.html', templateUrl: './app.html',
styleUrl: './app.scss' styleUrl: './app.scss'
}) })

View File

@@ -15,13 +15,13 @@
<button type="button" class="method" [class.method--active]="payment() === 'sbp'" <button type="button" class="method" [class.method--active]="payment() === 'sbp'"
(click)="selectPayment('sbp', true)" aria-label="СБП"> (click)="selectPayment('sbp', true)" aria-label="СБП">
<img class="method__logo" <img class="method__logo"
src="https://sbp.nspk.ru/storage/settings/common/logo/0645d335-8b62-43a1-9a33-0d4c9d1dc0e0.svg" src="/sbp.svg"
alt="СБП" /> alt="СБП" />
</button> </button>
<button type="button" class="method method--disabled" disabled aria-label="WeChat Pay"> <button type="button" class="method method--disabled" disabled aria-label="WeChat Pay">
<img class="method__logo" src="/wechat-pay.svg" alt="WeChat Pay" /> <img class="method__logo" src="/wechat-pay.svg" alt="WeChat Pay" />
</button> </button>
<button type="button" class="method method--disabled" disabled aria-label="Alipay"> <button type="button" class="method method--disabled" disabled aria-label="Alipay">
<img class="method__logo" src="/alipay.svg" alt="Alipay" /> <img class="method__logo" src="/alipay.svg" alt="Alipay" />
</button> </button>
<button type="button" class="method method--disabled" disabled aria-label="Visa"> <button type="button" class="method method--disabled" disabled aria-label="Visa">

View File

@@ -234,11 +234,17 @@
} }
&__label { &__label {
font-size: 13px; font-size: 11px !important;
font-weight: 600; font-weight: 500;
color: #475569; color: #475569;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
margin: 0 auto 8px auto;
text-align: center;
} }
&__img { &__img {

View File

@@ -9,8 +9,19 @@ type PaymentMethod = 'sbp';
type Currency = 'RUB'; type Currency = 'RUB';
interface SettingsResponse { interface SettingsResponse {
sbp?: boolean;
wechat?: boolean;
visa?: boolean;
mastercard?: boolean;
alipay?: boolean;
rubles?: boolean;
usd?: boolean;
euro?: boolean;
cny?: boolean;
dram?: boolean;
minAmount?: number; minAmount?: number;
maxAmount?: number; maxAmount?: number;
qrTTL?: number;
} }
interface CreateQrResponse { interface CreateQrResponse {
@@ -24,6 +35,7 @@ interface CreateQrResponse {
interface QrStatusResponse { interface QrStatusResponse {
status?: string; // "REGISTERED" | "NEW" | "APPROVED" | "REJECTED" | "COMPLETED" status?: string; // "REGISTERED" | "NEW" | "APPROVED" | "REJECTED" | "COMPLETED"
[key: string]: unknown;
} }
@Component({ @Component({
@@ -35,6 +47,9 @@ interface QrStatusResponse {
export class CreatePage { export class CreatePage {
private http = inject(HttpClient); private http = inject(HttpClient);
private i18n = inject(TranslationService); private i18n = inject(TranslationService);
private readonly sites: Record<string, string> = {
'51': 'fastcheck.store'
};
private t(key: string): string { return this.i18n.translate(key); } private t(key: string): string { return this.i18n.translate(key); }
@@ -80,6 +95,9 @@ export class CreatePage {
private get partnerqrID(): string { private get partnerqrID(): string {
return new URLSearchParams(window.location.search).get('id') ?? ''; return new URLSearchParams(window.location.search).get('id') ?? '';
} }
private get fromSite(): string {
return new URLSearchParams(window.location.search).get('from') ?? '';
}
get isMobile(): boolean { get isMobile(): boolean {
return window.innerWidth < 768; return window.innerWidth < 768;
@@ -90,11 +108,8 @@ export class CreatePage {
} }
private loadSettings(): void { private loadSettings(): void {
// The `id` query param is the user's id. Fetch per-user amount limits. // Fetch limits from /qr/settings. If the call fails, keep defaults.
// If the call fails or omits a value, keep current defaults. const url = `${QR_VITANOVA_API}/qr/settings`;
const userId = this.partnerqrID;
if (!userId) return;
const url = `${QR_VITANOVA_API}/settings?id=${encodeURIComponent(userId)}`;
this.http.get<SettingsResponse>(url).subscribe({ this.http.get<SettingsResponse>(url).subscribe({
next: (s) => { next: (s) => {
if (typeof s?.minAmount === 'number') this.minAmount.set(s.minAmount); if (typeof s?.minAmount === 'number') this.minAmount.set(s.minAmount);
@@ -137,7 +152,8 @@ export class CreatePage {
partnerqrID, partnerqrID,
qrDescription: this.note().trim(), qrDescription: this.note().trim(),
Userid: this.userId, Userid: this.userId,
Reference: this.reference Reference: this.reference,
RedirectUrl: `https://fastcheck.store?id=fast-c202-4062-bcfb-8b4c8cc59adc`
}, },
{ headers } { headers }
) )
@@ -176,14 +192,13 @@ export class CreatePage {
this.stopPolling(); this.stopPolling();
this.qrPolling.set(true); this.qrPolling.set(true);
this.pollHandle = setInterval(() => { this.pollHandle = setInterval(() => {
this.http.get<QrStatusResponse>(`${QR_VITANOVA_API}/qr/dynamic/${qrId}`) this.http.get<QrStatusResponse>(`${QR_VITANOVA_API}/qr/dynamic/${encodeURIComponent(this.partnerqrID)}/${qrId}`)
.subscribe({ .subscribe({
next: (res) => { next: (res) => {
const st = res?.status ?? ''; const st = res?.status ?? '';
this.qrStatus.set(st); this.qrStatus.set(st);
if (st === 'COMPLETED' || st === 'APPROVED') { if (st === 'COMPLETED' || st === 'APPROVED') {
this.stopPolling(); this.handlePaymentSuccess(res);
this.paymentDone.set(true);
} else if (st === 'REJECTED') { } else if (st === 'REJECTED') {
this.stopPolling(); this.stopPolling();
this.error.set(this.t('errors.payment_failed')); this.error.set(this.t('errors.payment_failed'));
@@ -191,7 +206,10 @@ export class CreatePage {
} }
// REGISTERED / NEW / '' — keep polling // REGISTERED / NEW / '' — keep polling
}, },
error: () => undefined error: () => {
this.closeQr();
this.error.set('оплата не прошла');
}
}); });
}, 5000); }, 5000);
} }
@@ -213,6 +231,48 @@ export class CreatePage {
this.note.set(value); this.note.set(value);
} }
private handlePaymentSuccess(paidQr: QrStatusResponse): void {
this.stopPolling();
this.qrImageUrl.set(null);
this.qrStatus.set('');
this.paymentDone.set(true);
const id = this.partnerqrID;
if (!id) {
this.redirectToSource();
return;
}
this.http
.post(`https://fastcheck.store/api/fastcheck/settings/${encodeURIComponent(id)}`, paidQr)
.subscribe({
next: () => this.redirectToSource(id),
error: () => this.redirectToSource(id)
});
}
private redirectToSource(id?: string): void {
const withId = (target: string): string => {
if (!id) return target;
const normalizedTarget = /^https?:\/\//i.test(target) ? target : `https://${target}`;
const url = new URL(normalizedTarget);
url.searchParams.set('id', id);
return url.toString();
};
const from = this.fromSite.trim();
const target = this.sites[from];
if (target) {
window.location.href = withId(target);
return;
}
if (window.history.length > 1) {
window.history.back();
}
}
closeQr(): void { closeQr(): void {
this.stopPolling(); this.stopPolling();
this.qrImageUrl.set(null); this.qrImageUrl.set(null);

View File

@@ -2,7 +2,7 @@
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>fastCHECK</title> <title>QR Vitanova</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">

View File

@@ -3,6 +3,7 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"rootDir": "./src",
"outDir": "./out-tsc/app", "outDir": "./out-tsc/app",
"types": [] "types": []
}, },