Compare commits

..

47 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
14d9642568 remove footer 2026-05-13 00:38:51 +04:00
6e7527cf1e some improvements 2026-05-08 23:55:07 +04:00
957321ae1e id api 2026-05-08 23:42:42 +04:00
11ea0793ba removed director 2026-05-08 23:28:21 +04:00
ea291525e9 partnerqrid 2026-05-08 18:58:25 +04:00
76e02e5ca6 styles 2026-05-07 23:33:02 +04:00
d37ca14f69 qr only 2026-05-07 00:13:45 +04:00
e0df81c071 link
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 18:21:22 +04:00
98423be0c3 link
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 17:37:55 +04:00
1d19ddd47c link
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 17:27:18 +04:00
c0b7ac08fb link
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 17:22:52 +04:00
889f289489 fast link
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 17:21:35 +04:00
be4e44d102 link 2026-05-06 17:11:42 +04:00
9fc7fbbae2 link change 2026-05-06 13:17:54 +04:00
f853a498ce response 2026-05-05 15:56:39 +04:00
fb6e00c00b autofill 2026-05-05 14:43:09 +04:00
5eb7b2dcba status check 2026-05-05 11:53:52 +04:00
b4ad6da49e changes
Co-authored-by: Copilot <copilot@github.com>
2026-05-05 11:31:51 +04:00
679e404dc8 removed default
Co-authored-by: Copilot <copilot@github.com>
2026-05-05 11:23:07 +04:00
138b774073 header
Co-authored-by: Copilot <copilot@github.com>
2026-05-05 10:46:49 +04:00
b238282569 changes
Co-authored-by: Copilot <copilot@github.com>
2026-05-05 01:45:46 +04:00
28a1d1be8b icons 2026-05-05 01:43:32 +04:00
390367abda some changes 2026-05-05 01:13:13 +04:00
504e0c87fb placeholders 2026-05-05 01:09:00 +04:00
0a86aaf4f6 chore: untrack dist from git 2026-05-05 01:00:04 +04:00
59bda137e5 changes 2026-05-05 00:52:03 +04:00
cf634f766f language 2026-05-04 23:56:38 +04:00
f97f4b5d96 added header and footer
Co-authored-by: Copilot <copilot@github.com>
2026-05-04 23:49:27 +04:00
56 changed files with 2463 additions and 1352 deletions

4
.gitignore vendored
View File

@@ -5,8 +5,10 @@
/out-tsc
/bazel-out
/dist
# Local-only docs and scratch (not for publishing)
/docs/
changes.txt
api.txt
# Node
/node_modules

View File

@@ -68,7 +68,8 @@
"buildTarget": "qr_vitanova:build:production"
},
"development": {
"buildTarget": "qr_vitanova:build:development"
"buildTarget": "qr_vitanova:build:development",
"proxyConfig": "proxy.conf.json"
}
},
"defaultConfiguration": "development"

View File

@@ -1,355 +0,0 @@
--------------------------------------------------------------------------------
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
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: rxjs
License: "Apache-2.0"
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--------------------------------------------------------------------------------
Package: tslib
License: "0BSD"
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
--------------------------------------------------------------------------------
Package: @angular/common
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/platform-browser
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/router
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: 4.2 KiB

17
dist/index.html vendored
View File

@@ -1,17 +0,0 @@
<!doctype html>
<html lang="ru" data-beasties-container>
<head>
<meta charset="utf-8">
<title>Оплата через СБП</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="theme-color" content="#2563eb">
<meta name="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">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<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>
<app-root></app-root>
<link rel="modulepreload" href="chunk-FBABAKVO.js"><script src="main-XKBOOIAP.js" type="module"></script></body>
</html>

View File

@@ -1,3 +0,0 @@
{
"routes": {}
}

View File

@@ -1 +0,0 @@
*,*: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}

View File

@@ -148,7 +148,7 @@
<div class="card__header">
<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>
<h1 class="card__title">Оплата через СБП</h1>
<p class="card__subtitle">Система быстрых платежей</p>
@@ -187,7 +187,7 @@
id="note"
class="note-input"
placeholder="Причина платежа..."
rows="3"
rows="2"
maxlength="500"
></textarea>
</div>

23
proxy.conf.json Normal file
View File

@@ -0,0 +1,23 @@
{
"/proxy/legacy-qr": {
"target": "https://qr.vitanova.network:567",
"secure": false,
"changeOrigin": true,
"pathRewrite": { "^/proxy/legacy-qr": "" },
"logLevel": "debug"
},
"/proxy/fastcheck": {
"target": "https://api.fastcheck.store",
"secure": true,
"changeOrigin": true,
"pathRewrite": { "^/proxy/fastcheck": "" },
"logLevel": "debug"
},
"/proxy/qr-vitanova": {
"target": "https://qr.vitanova.network",
"secure": true,
"changeOrigin": true,
"pathRewrite": { "^/proxy/qr-vitanova": "" },
"logLevel": "debug"
}
}

5
public/alipay.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<rect width="48" height="48" rx="8" fill="#1677FF"/>
<text x="24" y="34" font-family="Arial,Helvetica,sans-serif" font-size="26" font-weight="900"
text-anchor="middle" fill="#fff">A</text>
</svg>

After

Width:  |  Height:  |  Size: 269 B

26
public/example.json Normal file
View File

@@ -0,0 +1,26 @@
{
"nspkID": "AD10001UFDAT5FDD9LVPEJVDHN75UHHK",
"amount": 100,
"currency": "RUB",
"order": "",
"paymentDetails": "",
"qrType": "QRDynamic",
"qrExpirationDate": "",
"qrExecutionDate": "",
"sbpBank": "",
"sbpMerchant": "",
"sbpMerchantId": "",
"sbpOperationId": 633730903,
"status": "",
"nspkurl": "https://qr.nspk.ru/AD10001UFDAT5FDD9LVPEJVDHN75UHHK",
"statusurl": "https://pay.payworld.online/webapi/SbpPaymentStatus?id=3426680585\u0026sbpOperationId=633730903\u0026sector=24819\u0026signature=NjZkZjMyZDBjZTI1Zjc1NWUyZjYxNDRkM2ZjN2ViZWQ2YzcwYTc0NWM1ZWRlMGE1YzQyZWNlOTVjNWY1ODUxYg%3D%3D",
"redirectUrl": "",
"qrDescription": "Payment for shop 00897f30-414f-4b89-b1af-25596b15411c",
"additionalInfo": "",
"TTL": 0,
"callbackUrl": "",
"retry": 0,
"partnerID": "",
"partnerqrID": "00897f30-414f-4b89-b1af-25596b15411c",
"requestIP": ""
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

1
public/flags/arm.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="#102f9b" d="M1 11H31V21H1z"></path><path d="M5,4H27c2.208,0,4,1.792,4,4v4H1v-4c0-2.208,1.792-4,4-4Z" fill="#c82a20"></path><path d="M5,20H27c2.208,0,4,1.792,4,4v4H1v-4c0-2.208,1.792-4,4-4Z" transform="rotate(180 16 24)" fill="#e8ad3b"></path><path d="M27,4H5c-2.209,0-4,1.791-4,4V24c0,2.209,1.791,4,4,4H27c2.209,0,4-1.791,4-4V8c0-2.209-1.791-4-4-4Zm3,20c0,1.654-1.346,3-3,3H5c-1.654,0-3-1.346-3-3V8c0-1.654,1.346-3,3-3H27c1.654,0,3,1.346,3,3V24Z" opacity=".15"></path><path d="M27,5H5c-1.657,0-3,1.343-3,3v1c0-1.657,1.343-3,3-3H27c1.657,0,3,1.343,3,3v-1c0-1.657-1.343-3-3-3Z" fill="#fff" opacity=".2"></path></svg>

After

Width:  |  Height:  |  Size: 709 B

1
public/flags/en.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect x="1" y="4" width="30" height="24" rx="4" ry="4" fill="#fff"></rect><path d="M1.638,5.846H30.362c-.711-1.108-1.947-1.846-3.362-1.846H5c-1.414,0-2.65,.738-3.362,1.846Z" fill="#a62842"></path><path d="M2.03,7.692c-.008,.103-.03,.202-.03,.308v1.539H31v-1.539c0-.105-.022-.204-.03-.308H2.03Z" fill="#a62842"></path><path fill="#a62842" d="M2 11.385H31V13.231H2z"></path><path fill="#a62842" d="M2 15.077H31V16.923000000000002H2z"></path><path fill="#a62842" d="M1 18.769H31V20.615H1z"></path><path d="M1,24c0,.105,.023,.204,.031,.308H30.969c.008-.103,.031-.202,.031-.308v-1.539H1v1.539Z" fill="#a62842"></path><path d="M30.362,26.154H1.638c.711,1.108,1.947,1.846,3.362,1.846H27c1.414,0,2.65-.738,3.362-1.846Z" fill="#a62842"></path><path d="M5,4h11v12.923H1V8c0-2.208,1.792-4,4-4Z" fill="#102d5e"></path><path d="M27,4H5c-2.209,0-4,1.791-4,4V24c0,2.209,1.791,4,4,4H27c2.209,0,4-1.791,4-4V8c0-2.209-1.791-4-4-4Zm3,20c0,1.654-1.346,3-3,3H5c-1.654,0-3-1.346-3-3V8c0-1.654,1.346-3,3-3H27c1.654,0,3,1.346,3,3V24Z" opacity=".15"></path><path d="M27,5H5c-1.657,0-3,1.343-3,3v1c0-1.657,1.343-3,3-3H27c1.657,0,3,1.343,3,3v-1c0-1.657-1.343-3-3-3Z" fill="#fff" opacity=".2"></path><path fill="#fff" d="M4.601 7.463L5.193 7.033 4.462 7.033 4.236 6.338 4.01 7.033 3.279 7.033 3.87 7.463 3.644 8.158 4.236 7.729 4.827 8.158 4.601 7.463z"></path><path fill="#fff" d="M7.58 7.463L8.172 7.033 7.441 7.033 7.215 6.338 6.989 7.033 6.258 7.033 6.849 7.463 6.623 8.158 7.215 7.729 7.806 8.158 7.58 7.463z"></path><path fill="#fff" d="M10.56 7.463L11.151 7.033 10.42 7.033 10.194 6.338 9.968 7.033 9.237 7.033 9.828 7.463 9.603 8.158 10.194 7.729 10.785 8.158 10.56 7.463z"></path><path fill="#fff" d="M6.066 9.283L6.658 8.854 5.927 8.854 5.701 8.158 5.475 8.854 4.744 8.854 5.335 9.283 5.109 9.979 5.701 9.549 6.292 9.979 6.066 9.283z"></path><path fill="#fff" d="M9.046 9.283L9.637 8.854 8.906 8.854 8.68 8.158 8.454 8.854 7.723 8.854 8.314 9.283 8.089 9.979 8.68 9.549 9.271 9.979 9.046 9.283z"></path><path fill="#fff" d="M12.025 9.283L12.616 8.854 11.885 8.854 11.659 8.158 11.433 8.854 10.702 8.854 11.294 9.283 11.068 9.979 11.659 9.549 12.251 9.979 12.025 9.283z"></path><path fill="#fff" d="M6.066 12.924L6.658 12.494 5.927 12.494 5.701 11.799 5.475 12.494 4.744 12.494 5.335 12.924 5.109 13.619 5.701 13.19 6.292 13.619 6.066 12.924z"></path><path fill="#fff" d="M9.046 12.924L9.637 12.494 8.906 12.494 8.68 11.799 8.454 12.494 7.723 12.494 8.314 12.924 8.089 13.619 8.68 13.19 9.271 13.619 9.046 12.924z"></path><path fill="#fff" d="M12.025 12.924L12.616 12.494 11.885 12.494 11.659 11.799 11.433 12.494 10.702 12.494 11.294 12.924 11.068 13.619 11.659 13.19 12.251 13.619 12.025 12.924z"></path><path fill="#fff" d="M13.539 7.463L14.13 7.033 13.399 7.033 13.173 6.338 12.947 7.033 12.216 7.033 12.808 7.463 12.582 8.158 13.173 7.729 13.765 8.158 13.539 7.463z"></path><path fill="#fff" d="M4.601 11.104L5.193 10.674 4.462 10.674 4.236 9.979 4.01 10.674 3.279 10.674 3.87 11.104 3.644 11.799 4.236 11.369 4.827 11.799 4.601 11.104z"></path><path fill="#fff" d="M7.58 11.104L8.172 10.674 7.441 10.674 7.215 9.979 6.989 10.674 6.258 10.674 6.849 11.104 6.623 11.799 7.215 11.369 7.806 11.799 7.58 11.104z"></path><path fill="#fff" d="M10.56 11.104L11.151 10.674 10.42 10.674 10.194 9.979 9.968 10.674 9.237 10.674 9.828 11.104 9.603 11.799 10.194 11.369 10.785 11.799 10.56 11.104z"></path><path fill="#fff" d="M13.539 11.104L14.13 10.674 13.399 10.674 13.173 9.979 12.947 10.674 12.216 10.674 12.808 11.104 12.582 11.799 13.173 11.369 13.765 11.799 13.539 11.104z"></path><path fill="#fff" d="M4.601 14.744L5.193 14.315 4.462 14.315 4.236 13.619 4.01 14.315 3.279 14.315 3.87 14.744 3.644 15.44 4.236 15.01 4.827 15.44 4.601 14.744z"></path><path fill="#fff" d="M7.58 14.744L8.172 14.315 7.441 14.315 7.215 13.619 6.989 14.315 6.258 14.315 6.849 14.744 6.623 15.44 7.215 15.01 7.806 15.44 7.58 14.744z"></path><path fill="#fff" d="M10.56 14.744L11.151 14.315 10.42 14.315 10.194 13.619 9.968 14.315 9.237 14.315 9.828 14.744 9.603 15.44 10.194 15.01 10.785 15.44 10.56 14.744z"></path><path fill="#fff" d="M13.539 14.744L14.13 14.315 13.399 14.315 13.173 13.619 12.947 14.315 12.216 14.315 12.808 14.744 12.582 15.44 13.173 15.01 13.765 15.44 13.539 14.744z"></path></svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

1
public/flags/ru.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="#1435a1" d="M1 11H31V21H1z"></path><path d="M5,4H27c2.208,0,4,1.792,4,4v4H1v-4c0-2.208,1.792-4,4-4Z" fill="#fff"></path><path d="M5,20H27c2.208,0,4,1.792,4,4v4H1v-4c0-2.208,1.792-4,4-4Z" transform="rotate(180 16 24)" fill="#c53a28"></path><path d="M27,4H5c-2.209,0-4,1.791-4,4V24c0,2.209,1.791,4,4,4H27c2.209,0,4-1.791,4-4V8c0-2.209-1.791-4-4-4Zm3,20c0,1.654-1.346,3-3,3H5c-1.654,0-3-1.346-3-3V8c0-1.654,1.346-3,3-3H27c1.654,0,3,1.346,3,3V24Z" opacity=".15"></path><path d="M27,5H5c-1.657,0-3,1.343-3,3v1c0-1.657,1.343-3,3-3H27c1.657,0,3,1.343,3,3v-1c0-1.657-1.343-3-3-3Z" fill="#fff" opacity=".2"></path></svg>

After

Width:  |  Height:  |  Size: 706 B

131
public/i18n/en.json Normal file
View File

@@ -0,0 +1,131 @@
{
"header": {
"nav_about": "About",
"nav_contacts": "Contacts",
"nav_partners": "Partners",
"nav_support": "Support",
"aria_nav": "Navigation",
"aria_menu": "Mobile menu",
"aria_burger": "Menu",
"aria_close": "Close menu"
},
"footer": {
"desc": "An innovative virtual check service for individuals. Create digital checks online and cash them out at partner bank ATMs 24/7.",
"contacts_heading": "Contacts",
"russia": "Russia",
"armenia": "Armenia",
"support_label": "Tech support",
"support_hours": "24/7",
"questions_label": "Questions",
"questions_hours": "10:0019:00 MSK",
"legal_heading": "Legal details",
"legal_company": "LLC «VIAEXPORT»",
"legal_inn_ru": "TIN (RU): 9909675800",
"legal_inn_am": "TIN (AM): 01051049",
"legal_kpp": "KPP: 770287001",
"legal_ogrn": "OGRN: 282.110.1296681",
"legal_address": "Armenia, 0201, Yerevan, Minskaya St. 21-23, apt. 44",
"rights": "LLC «VIAEXPORT». All rights reserved.",
"director": "Director: Amirkhanyan Sargis Artashesovich"
},
"fastcheck": {
"subtitle": "Enter fastCHECK details or create a new one",
"number_label": "fastCHECK number",
"number_placeholder": "123456-123456-123456",
"number_new": "New",
"amount_label": "Amount",
"amount_checking": "Checking…",
"code_label": "Code",
"code_placeholder": "000000",
"pay_btn": "Pay",
"modal_title": "Sign in via Telegram",
"modal_sub": "Scan QR or open the link",
"modal_loading": "Loading…",
"modal_open_tg": "Open in Telegram",
"modal_confirming": "Confirming payment…",
"modal_waiting": "Waiting for sign-in…",
"modal_paid_title": "Paid",
"modal_paid_sub": "fastCHECK successfully accepted.",
"share_email": "Send by email",
"share_tg": "Send via Telegram"
},
"create": {
"title": "New",
"subtitle": "Enter the amount to top up",
"back_label": "Back",
"payment_label": "Payment method",
"currency_label": "Currency",
"amount_label": "Payment amount",
"note_label": "Note",
"note_placeholder": "Reason for payment...",
"creating": "Creating…",
"create_btn": "Create",
"amount_hint": "Allowed amount:",
"qr_label": "Scan QR to pay",
"qr_waiting": "Waiting for payment confirmation…",
"payment_done": "Payment completed successfully."
},
"sbp": {
"title": "Pay via SBP",
"subtitle": "Fast Payment System",
"amount_label": "Payment amount",
"currency_name": "Russian ruble",
"note_label": "Note",
"note_placeholder": "Reason for payment...",
"pay_loading": "Please wait...",
"pay_btn": "Proceed to payment"
},
"about": {
"title": "About the service",
"lead": "fastCHECK is an innovative virtual check service for individuals, available 24/7.",
"what_title": "What is fastCHECK?",
"what_text": "fastCHECK is a digital check you create online and cash out at partner bank ATMs at any time of day. No queues, no offices — just your phone and the nearest ATM.",
"how_title": "How does it work?",
"step1": "Log in and create a new fastCHECK with the required amount.",
"step2": "Save the check number and 5-digit code.",
"step3": "Enter the details on the site and confirm via Telegram.",
"step4": "Receive the funds in a convenient way.",
"why_title": "Why fastCHECK?",
"why1": "Available 24/7 — including weekends and holidays.",
"why2": "Secure authorisation via Telegram.",
"why3": "Supports SBP and other popular payment methods.",
"why4": "Fast processing — from seconds to a few minutes.",
"company_title": "About the company",
"company_text": "The service is developed by LLC VIAEXPORT (TIN 9909675800). The company is registered in Russia and Armenia. Legal address: Armenia, 0201, Yerevan, Minskaya St. 21-23, apt. 44."
},
"contacts": {
"title": "Contacts",
"lead": "We are available 24/7. Choose your preferred way to reach us.",
"ru_label": "Phone — Russia",
"am_label": "Phone — Armenia",
"email_label": "Email",
"tg_label": "Telegram bot",
"hours_title": "Working hours"
},
"errors": {
"not_found": "Payment not found or expired.",
"lookup_failed": "Could not verify the number. Please try again.",
"session_failed": "Could not create a session. Please try again.",
"payment_failed": "Could not process the payment. Check the code and try again.",
"invalid_code": "Invalid code. Please check and try again.",
"invalid_amount": "Please enter a valid amount."
},
"common": {
"secure": "Secure connection"
},
"partners": {
"title": "Partners",
"lead": "Stores, services and companies accepting fastCHECK as a payment method.",
"cat_finance": "Finance",
"cat_retail": "Retail",
"cat_hotels": "Hotels",
"cat_services": "Services",
"p1_desc": "Currency exchange and transfers across Armenia.",
"p2_desc": "Forex broker supporting fastCHECK for account top-ups.",
"p3_desc": "Online retailer with delivery across Russia and CIS.",
"p4_desc": "Hotel booking and payment via fastCHECK.",
"cta_title": "Want to become a partner?",
"cta_text": "Connect fastCHECK to your business — fast, with minimal paperwork.",
"cta_btn": "Contact us"
}
}

131
public/i18n/hy.json Normal file
View File

@@ -0,0 +1,131 @@
{
"header": {
"nav_about": "Ծառայության մասին",
"nav_contacts": "Կապ",
"nav_partners": "Գործընկերներ",
"nav_support": "Աջակցություն",
"aria_nav": "Նավիգացիա",
"aria_menu": "Բջջային ընտրացանկ",
"aria_burger": "Ընտրացանկ",
"aria_close": "Փակել ընտրացանկը"
},
"footer": {
"desc": "Ֆիզիկական անձանց համար վիրտուալ չեկերի նորարարական ծառայություն: Ստեղծեք թվային չեկեր առցանց և կանխիկացրեք դրանք գործընկեր բանկերի բանկոմատներում 24/7:",
"contacts_heading": "Կապ",
"russia": "Ռուսաստան",
"armenia": "Հայաստան",
"support_label": "Տեխ. աջակցություն",
"support_hours": "24/7",
"questions_label": "Հարցեր",
"questions_hours": "10:0019:00 MSK",
"legal_heading": "Իրավաբանական տվյալներ",
"legal_company": "ООО «ВИАЭКСПОРТ»",
"legal_inn_ru": "ИНН (РФ): 9909675800",
"legal_inn_am": "ИНН (AM): 01051049",
"legal_kpp": "КПП: 770287001",
"legal_ogrn": "ОГРН: 282.110.1296681",
"legal_address": "Հայաստան, 0201, Երևան, Մինսկայա փ. 21-23, բն. 44",
"rights": "ООО «ВИАЭКСПОРТ»: Բոլոր իրավունքները պաշտպանված են:",
"director": "Տնօրեն՝ Ամիրխանյան Սարգիս Արտաշեսի"
},
"fastcheck": {
"subtitle": "Մուտքագրեք fastCHECK տվյալները կամ ստեղծեք նորը",
"number_label": "fastCHECK համար",
"number_placeholder": "123456-123456-123456",
"number_new": "Նոր",
"amount_label": "Գումար",
"amount_checking": "Ստուգվում է…",
"code_label": "Կոդ",
"code_placeholder": "000000",
"pay_btn": "Վճարել",
"modal_title": "Մուտք գործել Telegram-ով",
"modal_sub": "Սկանավորեք QR կամ բացեք հղումը",
"modal_loading": "Բեռնվում է…",
"modal_open_tg": "Բացել Telegram-ում",
"modal_confirming": "Վճարման հաստատում…",
"modal_waiting": "Սպասում ենք մուտքի…",
"modal_paid_title": "Վճարված է",
"modal_paid_sub": "fastCHECK-ը հաջողությամբ ընդունված է:",
"share_email": "Ուղարկել էլ. նամակով",
"share_tg": "Ուղարկել Telegram-ով"
},
"create": {
"title": "Նոր",
"subtitle": "Նշեք համալրման գումարը",
"back_label": "Հետ",
"payment_label": "Վճարման եղանակ",
"currency_label": "Արժույթ",
"amount_label": "Վճարման գումար",
"note_label": "Նշում",
"note_placeholder": "Վճարման պատճառ...",
"creating": "Ստեղծվում է…",
"create_btn": "Ստեղծել",
"amount_hint": "Թույլատրելի գումար՝",
"qr_label": "Սկանավորեք QR-կոդը վճարելու համար",
"qr_waiting": "Սպասում ենք վճարման հաստատման…",
"payment_done": "Վճարումը հաջողությամբ ավարտվեց."
},
"sbp": {
"title": "Վճարել SBP-ով",
"subtitle": "Արագ վճարումների համակարգ",
"amount_label": "Վճարման գումար",
"currency_name": "Ռուսական ռուբլի",
"note_label": "Նշում",
"note_placeholder": "Վճարման պատճառ...",
"pay_loading": "Սպասեք...",
"pay_btn": "Անցնել վճարմանը"
},
"about": {
"title": "Ծառայության մասին",
"lead": "fastCHECK-ը ֆիզիկական անձանց համար վիրտուալ չեկերի նորարարական ծառայություն է, հասանելի 24/7:",
"what_title": "Ի՞նչ է fastCHECK-ը",
"what_text": "fastCHECK-ը թվային չեկ է, որը ստեղծում եք առցանց և կանխիկացնում գործընկեր բանկերի բանկոմատներում: Հերթեր չկան, գրասենյակներ չկան — միայն հեռախոս և ամենամոտ բանկոմատ:",
"how_title": "Ինչպե՞ս է դա աշխատում",
"step1": "Մուտք գործեք և ստեղծեք նոր fastCHECK անհրաժեշտ գումարով:",
"step2": "Պահպանեք չեկի համարն ու 5-նիշ կոդը:",
"step3": "Մուտքագրեք տվյալները կայքում և հաստատեք Telegram-ի միջոցով:",
"step4": "Ստացեք գումարն ձեզ հարմար ձևով:",
"why_title": "Ինչու՞ fastCHECK",
"why1": "Հասանելի 24/7 — ներառյալ հանգստյան և տոն օրերը:",
"why2": "Անվտանգ թույլտվություն Telegram-ի միջոցով:",
"why3": "Աջակցում է ՍԲՊ-ին և այլ հայտնի վճարման եղանակներ:",
"why4": "Արագ մշակում — վայրկյաններից մինչև մի քանի րոպե:",
"company_title": "Ընկերության մասին",
"company_text": "Ծառայությունը մշակվել է ООО «ВИАЭКСПОРТ»-ի կողմից (ИНН 9909675800): Ընկերությունը գրանցված է Ռուսաստանում և Հայաստանում: Իրավաբանական հասցե՝ Հայաստան, 0201, Երևան, Մինսկայա փ. 21-23, բն. 44:"
},
"contacts": {
"title": "Կապ",
"lead": "Մենք կապի մեջ ենք 24/7: Ընտրեք կապի հարմար եղանակ:",
"ru_label": "Հեռախոս — Ռուսաստան",
"am_label": "Հեռախոս — Հայաստան",
"email_label": "Էլ. փոստ",
"tg_label": "Telegram-բոտ",
"hours_title": "Աշխատանքային ժամեր"
},
"errors": {
"not_found": "Վճարումը չի գտնվել կամ ժամկետն անցել է:",
"lookup_failed": "Չհաջողվեց ստուգել համարը: Կրկին փորձեք:",
"session_failed": "Չհաջողվեց ստեղծել նիստ: Կրկին փորձեք:",
"payment_failed": "Չհաջողվեց մշակել վճարումը: Ստուգեք կոդը և կրկին փորձեք:",
"invalid_code": "Սխալ կոդ: Ստուգեք և կրկին մուտքագրեք:",
"invalid_amount": "Մուտքագրեք ճիշտ գումար:"
},
"common": {
"secure": "Անվտանգ կապ"
},
"partners": {
"title": "Գործընկերներ",
"lead": "Խանութներ, ծառայություններ և ընկերություններ, որոնք ընդունում են fastCHECK-ը:",
"cat_finance": "Ֆինանսներ",
"cat_retail": "Ռիթեյլ",
"cat_hotels": "Հյուրանոցներ",
"cat_services": "Ծառայություններ",
"p1_desc": "Արժույթի փոխանակում և փոխանցումներ ամբողջ Հայաստանում:",
"p2_desc": "Ֆորեքս բրոքեր fastCHECK-ով հաշիվ համալրման համար:",
"p3_desc": "Առցանց ռիթեյլ՝ Ռուսաստանով և ԱՊՀ-ով առաքմամբ:",
"p4_desc": "Հյուրանոցի ամրագրում և վճարում fastCHECK-ի միջոցով:",
"cta_title": "Ցանկանու՞մ եք դառնալ գործընկեր",
"cta_text": "Միացրեք fastCHECK-ը ձեր բիզնեսին — արագ, նվազ փաստաթղթերով:",
"cta_btn": "Կապվեք մեզ հետ"
}
}

131
public/i18n/ru.json Normal file
View File

@@ -0,0 +1,131 @@
{
"header": {
"nav_about": "О сервисе",
"nav_contacts": "Контакты",
"nav_partners": "Партнёры",
"nav_support": "Поддержка",
"aria_nav": "Навигация",
"aria_menu": "Мобильное меню",
"aria_burger": "Меню",
"aria_close": "Закрыть меню"
},
"footer": {
"desc": "Инновационный сервис виртуальных чеков для физических лиц. Создавайте цифровые чеки онлайн и обналичивайте их через банкоматы банков-партнёров 24/7.",
"contacts_heading": "Контакты",
"russia": "Россия",
"armenia": "Армения",
"support_label": "Техподдержка",
"support_hours": "24/7",
"questions_label": "Вопросы",
"questions_hours": "10:0019:00 МСК",
"legal_heading": "Реквизиты",
"legal_company": "ООО «ВИАЭКСПОРТ»",
"legal_inn_ru": "ИНН (РФ): 9909675800",
"legal_inn_am": "ИНН (AM): 01051049",
"legal_kpp": "КПП: 770287001",
"legal_ogrn": "ОГРН: 282.110.1296681",
"legal_address": "Армения, 0201, Ереван, ул. Минская, дом 21-23, кв. 44",
"rights": "ООО «ВИАЭКСПОРТ». Все права защищены.",
"director": "Директор: Амирханян Саргис Арташесович"
},
"fastcheck": {
"subtitle": "Введите данные fastCHECK или создайте новый",
"number_label": "Номер fastCHECK",
"number_placeholder": "123456-123456-123456",
"number_new": "Новый",
"amount_label": "Сумма",
"amount_checking": "Проверяем…",
"code_label": "Код",
"code_placeholder": "000000",
"pay_btn": "Оплатить",
"modal_title": "Войти через Telegram",
"modal_sub": "Отсканируйте QR или откройте ссылку",
"modal_loading": "Загрузка…",
"modal_open_tg": "Открыть в Telegram",
"modal_confirming": "Подтверждение оплаты…",
"modal_waiting": "Ожидание входа…",
"modal_paid_title": "Оплачено",
"modal_paid_sub": "fastCHECK успешно принят.",
"share_email": "Отправить на почту",
"share_tg": "Отправить в Telegram"
},
"create": {
"title": "Новый",
"subtitle": "Укажите сумму для пополнения",
"back_label": "Назад",
"payment_label": "Способ оплаты",
"currency_label": "Валюта",
"amount_label": "Сумма платежа",
"note_label": "Примечание",
"note_placeholder": "Причина платежа...",
"creating": "Создание…",
"create_btn": "Создать",
"amount_hint": "Допустимая сумма:",
"qr_label": "Отсканируйте QR для оплаты",
"qr_waiting": "Ожидаем подтверждения оплаты…",
"payment_done": "Оплата успешно завершена."
},
"sbp": {
"title": "Оплата через СБП",
"subtitle": "Система быстрых платежей",
"amount_label": "Сумма платежа",
"currency_name": "Российский рубль",
"note_label": "Примечание",
"note_placeholder": "Причина платежа...",
"pay_loading": "Подождите...",
"pay_btn": "Перейти к оплате"
},
"about": {
"title": "О сервисе",
"lead": "fastCHECK — инновационный сервис виртуальных чеков для физических лиц, доступный 24/7.",
"what_title": "Что такое fastCHECK?",
"what_text": "fastCHECK — это цифровой чек, который вы создаёте онлайн и обналичиваете через банкоматы банков-партнёров в любое время суток. Никакой очереди, никаких офисов — только телефон и ближайший банкомат.",
"how_title": "Как это работает?",
"step1": "Зайдите в личный кабинет и создайте новый fastCHECK с нужной суммой.",
"step2": "Запомните или сохраните номер чека и 5-значный код.",
"step3": "Введите данные на сайте и подтвердите операцию через Telegram.",
"step4": "Получите средства удобным вам способом.",
"why_title": "Почему fastCHECK?",
"why1": "Работает 24/7 — включая выходные и праздники.",
"why2": "Безопасная авторизация через Telegram.",
"why3": "Поддержка СБП и других популярных методов оплаты.",
"why4": "Быстрое обслуживание — от секунд до нескольких минут.",
"company_title": "О компании",
"company_text": "Сервис разработан ООО «ВИАЭКСПОРТ» (ИНН 9909675800). Компания зарегистрирована в России и Армении, юридический адрес: Армения, 0201, Ереван, ул. Минская, дом 21-23, кв. 44."
},
"contacts": {
"title": "Контакты",
"lead": "Мы на связи 24/7. Выберите удобный способ связи.",
"ru_label": "Телефон — Россия",
"am_label": "Телефон — Армения",
"email_label": "Электронная почта",
"tg_label": "Telegram-бот",
"hours_title": "Часы работы"
},
"errors": {
"not_found": "Платёж не найден или просрочен.",
"lookup_failed": "Не удалось проверить номер. Попробуйте ещё раз.",
"session_failed": "Не удалось создать сессию. Попробуйте ещё раз.",
"payment_failed": "Не удалось принять платёж. Проверьте код и попробуйте снова.",
"invalid_code": "Неверный код. Проверьте и введите снова.",
"invalid_amount": "Введите корректную сумму."
},
"common": {
"secure": "Защищённое соединение"
},
"partners": {
"title": "Партнёры",
"lead": "Магазины, сервисы и компании, принимающие fastCHECK как способ оплаты.",
"cat_finance": "Финансы",
"cat_retail": "Ритейл",
"cat_hotels": "Отели",
"cat_services": "Услуги",
"p1_desc": "Обмен валют и переводы по всей Армении.",
"p2_desc": "Форекс-брокер с поддержкой fastCHECK для пополнения счёта.",
"p3_desc": "Онлайн-ритейлер с доставкой по России и СНГ.",
"p4_desc": "Бронирование и оплата проживания через fastCHECK.",
"cta_title": "Хотите стать партнёром?",
"cta_text": "Подключите fastCHECK к своему бизнесу — быстро, без лишних документов.",
"cta_btn": "Связаться с нами"
}
}

302
public/index (4).html Normal file
View File

@@ -0,0 +1,302 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Оплата через СБП</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="theme-color" content="#2563eb">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<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;
background: #1e40af;
}
.page {
min-height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: linear-gradient(135deg, #1e40af 0%, #2563eb 40%, #0ea5e9 100%);
}
@media (max-width: 480px) {
.page { align-items: flex-end; padding: 0; }
}
.card {
background: #fff;
border-radius: 24px;
width: 100%;
max-width: 400px;
box-shadow: 0 24px 60px rgba(0,0,0,.18);
overflow: hidden;
}
@media (max-width: 480px) {
.card { border-radius: 24px 24px 0 0; max-width: 100%; box-shadow: 0 -8px 40px rgba(0,0,0,.15); }
}
.card__header {
background: linear-gradient(135deg, #1e40af 0%, #2563eb 100%);
padding: 32px 28px 28px;
text-align: center;
}
@media (max-width: 480px) { .card__header { padding: 28px 24px 24px; } }
.card__title { color: #fff; font-size: 22px; font-weight: 700; margin: 14px 0 4px; letter-spacing: -.3px; }
.card__subtitle { color: rgba(255,255,255,.7); font-size: 13px; }
.card__body { padding: 28px 28px 20px; }
@media (max-width: 480px) { .card__body { padding: 24px 20px 16px; } }
.card__footer { padding: 0 28px 24px; display: flex; justify-content: center; }
@media (max-width: 480px) { .card__footer { padding: 0 20px 32px; } }
.sbp-logo {
display: inline-flex; align-items: center; justify-content: center;
background: rgba(255,255,255,.15); backdrop-filter: blur(8px);
border-radius: 16px; padding: 12px 20px;
border: 1px solid rgba(255,255,255,.25);
}
.sbp-logo img { height: 40px; display: block; }
@media (max-width: 480px) { .sbp-logo img { height: 34px; } }
.field { margin-bottom: 16px; }
.field__label {
display: block; font-size: 13px; font-weight: 600; color: #64748b;
margin-bottom: 8px; text-transform: uppercase; letter-spacing: .6px;
}
.field__error { display: block; margin-top: 6px; font-size: 13px; color: #ef4444; font-weight: 500; }
.field__error:empty { display: none; }
.input-wrap {
display: flex; align-items: center;
border: 2px solid #e2e8f0; border-radius: 14px;
background: #f8fafc;
transition: border-color .2s, box-shadow .2s, background .2s;
}
.input-wrap:focus-within {
border-color: #2563eb;
box-shadow: 0 0 0 4px rgba(37,99,235,.12);
background: #fff;
}
.input-wrap--error { border-color: #ef4444; box-shadow: 0 0 0 4px rgba(239,68,68,.1); }
.input-wrap__prefix { padding: 0 4px 0 18px; font-size: 26px; font-weight: 700; color: #2563eb; user-select: none; line-height: 1; }
.input-wrap__input {
flex: 1; border: none; background: transparent;
padding: 16px 16px 16px 8px; font-size: 32px; font-weight: 700;
color: #0f172a; outline: none; min-width: 0; font-family: inherit;
appearance: textfield; -moz-appearance: textfield;
}
.input-wrap__input::placeholder { color: #cbd5e1; }
.input-wrap__input::-webkit-outer-spin-button,
.input-wrap__input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
@media (max-width: 480px) { .input-wrap__input { font-size: 28px; padding: 14px 14px 14px 6px; } }
.currency-badge {
display: flex; align-items: center; gap: 10px;
background: #f1f5f9; border-radius: 12px; padding: 12px 16px; margin-bottom: 20px;
}
.currency-badge__flag { font-size: 22px; line-height: 1; }
.currency-badge__code { font-size: 15px; font-weight: 700; color: #0f172a; }
.currency-badge__name { font-size: 13px; color: #64748b; margin-left: auto; }
.pay-btn {
width: 100%; display: flex; align-items: center; justify-content: center;
gap: 10px; padding: 17px 24px;
background: linear-gradient(135deg, #1e40af 0%, #2563eb 100%);
color: #fff; border: none; border-radius: 14px;
font-size: 17px; font-weight: 700; letter-spacing: .2px;
cursor: pointer; font-family: inherit;
transition: opacity .15s, transform .1s, box-shadow .15s;
box-shadow: 0 6px 20px rgba(37,99,235,.38);
}
.pay-btn:hover { opacity: .92; box-shadow: 0 8px 28px rgba(37,99,235,.45); }
.pay-btn:active { transform: scale(.98); opacity: .88; }
.pay-btn:disabled { opacity: .6; cursor: not-allowed; transform: none; }
.pay-btn__icon { display: flex; align-items: center; }
@media (max-width: 480px) { .pay-btn { padding: 16px 24px; font-size: 16px; } }
.secure-badge {
display: inline-flex; align-items: center; gap: 6px;
font-size: 12px; color: #94a3b8; font-weight: 500;
}
.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 .2s, box-shadow .2s, background .2s;
line-height: 1.5;
}
.note-input::placeholder { color: #cbd5e1; font-weight: 400; }
.note-input:focus {
border-color: #2563eb;
box-shadow: 0 0 0 4px rgba(37,99,235,.12);
background: #fff;
}
</style>
</head>
<body>
<div class="page">
<div class="card">
<div class="card__header">
<div class="sbp-logo">
<img src="sbp.svg" alt="СБП" />
</div>
<h1 class="card__title">Оплата через СБП</h1>
<p class="card__subtitle">Система быстрых платежей</p>
</div>
<div class="card__body">
<div class="field">
<label class="field__label" for="amount">Сумма платежа</label>
<div class="input-wrap" id="inputWrap">
<span class="input-wrap__prefix"></span>
<input
id="amount"
type="number"
class="input-wrap__input"
min="1"
step="1"
inputmode="numeric"
placeholder="0"
autofocus
value="50"
/>
</div>
<span class="field__error" id="error"></span>
</div>
<div class="currency-badge">
<span class="currency-badge__flag">🇷🇺</span>
<span class="currency-badge__code">RUB</span>
<span class="currency-badge__name">Российский рубль</span>
</div>
<div class="field">
<label class="field__label" for="note">Примечание</label>
<textarea
id="note"
class="note-input"
placeholder="Причина платежа..."
rows="3"
maxlength="500"
></textarea>
</div>
<button class="pay-btn" id="payBtn" onclick="goToPayment()">
<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">
<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/>
<line x1="1" y1="10" x2="23" y2="10"/>
</svg>
</span>
<span id="btnText">Перейти к оплате</span>
</button>
</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>
Защищённое соединение
</span>
</div>
</div>
</div>
<script>
const API_URL = 'https://qr.vitanova.network:567/qr';
const amountInput = document.getElementById('amount');
const noteInput = document.getElementById('note');
const errorEl = document.getElementById('error');
const payBtn = document.getElementById('payBtn');
const btnText = document.getElementById('btnText');
const inputWrap = document.getElementById('inputWrap');
amountInput.addEventListener('input', function () {
if (Number(this.value) > 0) {
errorEl.textContent = '';
inputWrap.classList.remove('input-wrap--error');
}
});
function getPaymentId() {
return new URLSearchParams(window.location.search).get('id');
}
function setLoading(loading) {
payBtn.disabled = loading;
btnText.textContent = loading
? 'Подождите...'
: 'Перейти к оплате';
}
function showError(msg) {
errorEl.textContent = msg;
inputWrap.classList.add('input-wrap--error');
}
async function goToPayment() {
const amount = Number(amountInput.value);
if (!amount || amount <= 0) {
showError('Введите корректную сумму');
return;
}
const id = getPaymentId();
if (!id) {
showError('Не указан идентификатор платежа (параметр id)');
return;
}
errorEl.textContent = '';
inputWrap.classList.remove('input-wrap--error');
setLoading(true);
try {
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
qrtype: 'QRDynamic',
amount: amount,
currency: 'RUB',
partnerqrID: id,
qrDescription: noteInput.value.trim()
})
});
const data = await response.json();
if (data.nspkurl) {
window.location.href = data.nspkurl;
} else {
showError("nspkurl не найден в ответе сервера");
setLoading(false);
}
} catch (error) {
console.error(error);
showError("Ошибка запроса");
setLoading(false);
}
}
</script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 184 KiB

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,8 +1,6 @@
/**
* Endpoint constants for the Fastcheck backend (see public/api.txt).
* Centralised so they can be swapped in one place.
*/
export const FASTCHECK_API = 'https://api.fastcheck.store';
import { isDevMode } from '@angular/core';
// Legacy QR endpoint kept for the SBP amount → payload redirect flow.
export const QR_API = 'https://qr.vitanova.network:567/qr';
// QR Vitanova API (dynamic QR, settings, polling).
export const QR_VITANOVA_API = isDevMode()
? '/proxy/qr-vitanova/api'
: 'https://qr.vitanova.network/api';

View File

@@ -1 +1,5 @@
<router-outlet />
<!-- <app-site-header /> -->
<main class="app-main">
<router-outlet />
</main>
<!-- <app-site-footer /> -->

View File

@@ -3,19 +3,23 @@
export const routes: Routes = [
{
path: '',
loadComponent: () => {
// Branch: ?id=<orderId> 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',
loadComponent: () =>
import('./pages/create-page/create-page').then((m) => m.CreatePage)
},
{
path: 'about',
loadComponent: () =>
import('./pages/about-page/about-page').then((m) => m.AboutPage)
},
{
path: 'contacts',
loadComponent: () =>
import('./pages/contacts-page/contacts-page').then((m) => m.ContactsPage)
},
{
path: 'partners',
loadComponent: () =>
import('./pages/partners-page/partners-page').then((m) => m.PartnersPage)
},
{ path: '**', redirectTo: '' }
];

View File

@@ -1,2 +1,12 @@
:host { display: block; min-height: 100dvh; }
:host {
display: flex;
flex-direction: column;
min-height: 100dvh;
}
.app-main {
flex: 1;
display: flex;
flex-direction: column;
}

View File

@@ -1,28 +0,0 @@
import { Injectable, signal } from '@angular/core';
export interface FastcheckData {
fastcheck: string;
amount: number;
code: string;
expiration?: string;
}
/**
* Shared state between the home (Fastcheck) page and the create-new page.
* When a new fastcheck is created via POST /fastcheck, the create page stores
* the returned data here and the home page reads it to autofill its fields.
*/
@Injectable({ providedIn: 'root' })
export class FastcheckService {
readonly created = signal<FastcheckData | null>(null);
setCreated(data: FastcheckData): void {
this.created.set(data);
}
consume(): FastcheckData | null {
const value = this.created();
this.created.set(null);
return value;
}
}

View File

@@ -0,0 +1,40 @@
<div class="info-page">
<div class="info-page__hero">
<h1 class="info-page__title">{{ 'about.title' | translate }}</h1>
<p class="info-page__lead">{{ 'about.lead' | translate }}</p>
</div>
<div class="info-page__body">
<section class="info-section">
<h2 class="info-section__title">{{ 'about.what_title' | translate }}</h2>
<p class="info-section__text">{{ 'about.what_text' | translate }}</p>
</section>
<section class="info-section">
<h2 class="info-section__title">{{ 'about.how_title' | translate }}</h2>
<ol class="info-section__steps">
<li>{{ 'about.step1' | translate }}</li>
<li>{{ 'about.step2' | translate }}</li>
<li>{{ 'about.step3' | translate }}</li>
<li>{{ 'about.step4' | translate }}</li>
</ol>
</section>
<section class="info-section">
<h2 class="info-section__title">{{ 'about.why_title' | translate }}</h2>
<ul class="info-section__list">
<li>{{ 'about.why1' | translate }}</li>
<li>{{ 'about.why2' | translate }}</li>
<li>{{ 'about.why3' | translate }}</li>
<li>{{ 'about.why4' | translate }}</li>
</ul>
</section>
<section class="info-section">
<h2 class="info-section__title">{{ 'about.company_title' | translate }}</h2>
<p class="info-section__text">{{ 'about.company_text' | translate }}</p>
</section>
</div>
</div>

View File

@@ -0,0 +1,75 @@
:host {
display: block;
background: #f8fafc;
min-height: 100vh;
}
// Shared info page layout — used by AboutPage and ContactsPage
.info-page {
max-width: 760px;
margin: 0 auto;
padding: 48px 24px 72px;
@media (max-width: 600px) {
padding: 32px 16px 56px;
}
&__hero {
margin-bottom: 48px;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 32px;
}
&__title {
font-size: 32px;
font-weight: 800;
color: #0f172a;
margin: 0 0 12px;
letter-spacing: -0.5px;
@media (max-width: 600px) { font-size: 26px; }
}
&__lead {
font-size: 17px;
line-height: 1.7;
color: #475569;
margin: 0;
}
&__body {
display: flex;
flex-direction: column;
gap: 48px;
}
}
.info-section {
&__title {
font-size: 20px;
font-weight: 700;
color: #1e293b;
margin: 0 0 14px;
}
&__text {
font-size: 15.5px;
line-height: 1.75;
color: #475569;
margin: 0;
}
&__steps, &__list {
padding-left: 22px;
margin: 0;
display: flex;
flex-direction: column;
gap: 10px;
li {
font-size: 15.5px;
line-height: 1.65;
color: #475569;
}
}
}

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { TranslatePipe } from '../../translate/translate.pipe';
@Component({
selector: 'app-about-page',
imports: [TranslatePipe],
templateUrl: './about-page.html',
styleUrl: './about-page.scss'
})
export class AboutPage {}

View File

@@ -0,0 +1,66 @@
<div class="info-page">
<div class="info-page__hero">
<h1 class="info-page__title">{{ 'contacts.title' | translate }}</h1>
<p class="info-page__lead">{{ 'contacts.lead' | translate }}</p>
</div>
<div class="info-page__body">
<div class="contacts-grid">
<!-- Phone Russia -->
<a class="contact-card" href="tel:+79299037443">
<div class="contact-card__icon">🇷🇺</div>
<div class="contact-card__body">
<span class="contact-card__label">{{ 'contacts.ru_label' | translate }}</span>
<span class="contact-card__value">+7 (929) 903-74-43</span>
</div>
</a>
<!-- Phone Armenia -->
<a class="contact-card" href="tel:+37498632421">
<div class="contact-card__icon">🇦🇲</div>
<div class="contact-card__body">
<span class="contact-card__label">{{ 'contacts.am_label' | translate }}</span>
<span class="contact-card__value">+374 98 632421</span>
</div>
</a>
<!-- Email -->
<a class="contact-card" href="mailto:info@viaexport.store">
<div class="contact-card__icon">✉️</div>
<div class="contact-card__body">
<span class="contact-card__label">{{ 'contacts.email_label' | translate }}</span>
<span class="contact-card__value">info@viaexport.store</span>
</div>
</a>
<!-- Telegram -->
<a class="contact-card" href="https://t.me/DexarSupport_bot" target="_blank" rel="noopener">
<div class="contact-card__icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="#2b9fd0"><path d="M9.04 15.65l-.36 4.06c.51 0 .73-.22.99-.48l2.38-2.27 4.93 3.6c.9.5 1.55.24 1.79-.83l3.24-15.18h.01c.29-1.34-.48-1.86-1.36-1.54L1.13 9.66c-1.32.5-1.3 1.23-.22 1.56l4.92 1.53L17.27 5.6c.54-.34 1.03-.15.62.19"/></svg>
</div>
<div class="contact-card__body">
<span class="contact-card__label">{{ 'contacts.tg_label' | translate }}</span>
<span class="contact-card__value">@DexarSupport_bot</span>
</div>
</a>
</div>
<section class="info-section">
<h2 class="info-section__title">{{ 'contacts.hours_title' | translate }}</h2>
<div class="hours-table">
<div class="hours-row">
<span class="hours-row__label">{{ 'footer.support_label' | translate }}</span>
<span class="hours-row__value hours-row__value--green">24/7</span>
</div>
<div class="hours-row">
<span class="hours-row__label">{{ 'footer.questions_label' | translate }}</span>
<span class="hours-row__value">10:0019:00 МСК</span>
</div>
</div>
</section>
</div>
</div>

View File

@@ -0,0 +1,146 @@
:host {
display: block;
background: #f8fafc;
min-height: 100vh;
}
.info-page {
max-width: 760px;
margin: 0 auto;
padding: 48px 24px 72px;
@media (max-width: 600px) {
padding: 32px 16px 56px;
}
&__hero {
margin-bottom: 48px;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 32px;
}
&__title {
font-size: 32px;
font-weight: 800;
color: #0f172a;
margin: 0 0 12px;
letter-spacing: -0.5px;
@media (max-width: 600px) { font-size: 26px; }
}
&__lead {
font-size: 17px;
line-height: 1.7;
color: #475569;
margin: 0;
}
&__body {
display: flex;
flex-direction: column;
gap: 48px;
}
}
.info-section {
&__title {
font-size: 20px;
font-weight: 700;
color: #1e293b;
margin: 0 0 14px;
}
}
.contacts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
@media (max-width: 540px) {
grid-template-columns: 1fr;
}
}
.contact-card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
border-radius: 16px;
border: 1px solid #e2e8f0;
background: #fff;
text-decoration: none;
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
&:hover {
border-color: #93c5fd;
box-shadow: 0 4px 16px rgba(30, 64, 175, 0.08);
background: #f8fbff;
}
&__icon {
font-size: 28px;
line-height: 1;
flex-shrink: 0;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
&__body {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
&__label {
font-size: 11.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #94a3b8;
}
&__value {
font-size: 14.5px;
font-weight: 600;
color: #0f172a;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.hours-table {
display: flex;
flex-direction: column;
gap: 12px;
}
.hours-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-radius: 10px;
background: #f8fafc;
border: 1px solid #e2e8f0;
&__label {
font-size: 14px;
color: #475569;
font-weight: 500;
}
&__value {
font-size: 14px;
font-weight: 700;
color: #0f172a;
&--green { color: #16a34a; }
}
}

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { TranslatePipe } from '../../translate/translate.pipe';
@Component({
selector: 'app-contacts-page',
imports: [TranslatePipe],
templateUrl: './contacts-page.html',
styleUrl: './contacts-page.scss'
})
export class ContactsPage {}

View File

@@ -2,34 +2,28 @@
<div class="card">
<div class="card__header">
<a class="back" routerLink="/" aria-label="Назад">
<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">
Новый
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
</h1>
<p class="card__subtitle">Укажите сумму для пополнения</p>
<h1 class="card__title">Оплата через СБП</h1>
<p class="card__subtitle">{{ 'create.subtitle' | translate }}</p>
</div>
<div class="card__body">
<!-- Payment methods -->
<div class="field">
<span class="field__label">Способ оплаты</span>
<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"
src="/sbp.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>
@@ -41,31 +35,26 @@
<!-- Currencies -->
<div class="field">
<span class="field__label">Валюта</span>
<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__flag">🇷🇺</span> -->
<span class="chip__sign"></span>
<span class="chip__code">RUB</span>
</button>
<button type="button" class="chip chip--disabled" disabled>
<!-- <span class="chip__flag">🇨🇳</span> -->
<span class="chip__sign">¥</span>
<span class="chip__code">CNY</span>
</button>
<button type="button" class="chip chip--disabled" disabled>
<!-- <span class="chip__flag">🇺🇸</span> -->
<span class="chip__sign">$</span>
<span class="chip__code">USD</span>
</button>
<button type="button" class="chip chip--disabled" disabled>
<!-- <span class="chip__flag">🇪🇺</span> -->
<span class="chip__sign"></span>
<span class="chip__code">EUR</span>
</button>
<button type="button" class="chip chip--disabled" disabled>
<!-- <span class="chip__flag">🇦🇲</span> -->
<span class="chip__sign">֏</span>
<span class="chip__code">AMD</span>
</button>
@@ -73,7 +62,7 @@
</div>
<div class="field">
<label class="field__label" for="amount">Сумма платежа</label>
<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
@@ -82,32 +71,34 @@
class="input-wrap__input"
[ngModel]="amount()"
(ngModelChange)="onAmountChange($event)"
min="1"
[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">Примечание</label>
<label class="field__label" for="note">{{ 'create.note_label' | translate }}</label>
<textarea
id="note"
class="note-input"
[ngModel]="note()"
(ngModelChange)="onNoteChange($event)"
placeholder="Причина платежа..."
rows="3"
[placeholder]="'create.note_placeholder' | translate"
rows="2"
maxlength="500"
></textarea>
</div>
<button class="pay-btn" type="button" (click)="createCheck()" [disabled]="loading()">
<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">
@@ -115,12 +106,36 @@
</svg>
</span>
@if (loading()) {
Создание…
{{ 'create.creating' | translate }}
} @else {
Создать&nbsp;
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
{{ 'create.create_btn' | translate }}
}
</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>
}
@if (paymentDone()) {
<p class="qr-modal__hint qr-modal__hint--done">{{ 'create.payment_done' | translate }}</p>
}
</div>
</div>
}
</div>
<div class="card__footer">
@@ -129,7 +144,7 @@
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>

View File

@@ -41,9 +41,15 @@
// ─── Methods row ────────────────────────────────────────────────────────────
.methods {
display: grid;
grid-template-columns: repeat(4, 1fr);
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
gap: 8px;
padding-bottom: 4px;
&::-webkit-scrollbar { display: none; }
@media (max-width: 360px) {
gap: 6px;
@@ -63,6 +69,8 @@
transition: border-color .15s, background .15s, transform .1s, box-shadow .15s;
-webkit-appearance: none;
font-family: inherit;
flex: 0 0 72px;
width: 72px;
@media (max-width: 360px) {
height: 52px;
@@ -104,8 +112,14 @@
// ─── Currency chips ─────────────────────────────────────────────────────────
.currencies {
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
gap: 8px;
padding-bottom: 4px;
&::-webkit-scrollbar { display: none; }
}
.chip {
@@ -173,3 +187,100 @@
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: 11px !important;
font-weight: 500;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.05em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
margin: 0 auto 8px auto;
text-align: center;
}
&__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; }
}

View File

@@ -1,43 +1,69 @@
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 } from '../../api';
import { QR_VITANOVA_API } from '../../api';
import { TranslatePipe } from '../../translate/translate.pipe';
import { TranslationService } from '../../translate/translation.service';
interface CreateFastcheckResponse {
fastcheck: string;
expiration: string;
code: string;
Status: boolean;
type PaymentMethod = 'sbp';
type Currency = 'RUB';
interface SettingsResponse {
sbp?: boolean;
wechat?: boolean;
visa?: boolean;
mastercard?: boolean;
alipay?: boolean;
rubles?: boolean;
usd?: boolean;
euro?: boolean;
cny?: boolean;
dram?: boolean;
minAmount?: number;
maxAmount?: number;
qrTTL?: number;
}
type PaymentMethod = 'sbp' | 'wechat' | 'visa' | 'master';
type Currency = 'RUB' | 'CNY' | 'USD' | 'EUR' | 'AMD';
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"
}
interface QrStatusResponse {
status?: string; // "REGISTERED" | "NEW" | "APPROVED" | "REJECTED" | "COMPLETED"
[key: string]: unknown;
}
@Component({
selector: 'app-create-page',
imports: [FormsModule, RouterLink],
imports: [FormsModule, 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 readonly sites: Record<string, string> = {
'51': 'fastcheck.store'
};
amount = signal<number>(10);
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);
payment = signal<PaymentMethod>('sbp');
currency = signal<Currency>('RUB');
/** sessionID for the Authorization header. Comes from ?session=... or websession. */
private get sessionId(): string {
return new URLSearchParams(window.location.search).get('session') ?? '';
}
payment = signal<PaymentMethod>('sbp');
selectPayment(method: PaymentMethod, enabled: boolean): void {
if (!enabled) return;
@@ -49,10 +75,63 @@ export class CreatePage {
this.currency.set(c);
}
// QR display state
qrImageUrl = signal<string | null>(null);
qrPolling = signal<boolean>(false);
qrStatus = signal<string>('');
paymentDone = signal<boolean>(false);
private pollHandle: ReturnType<typeof setInterval> | null = null;
/** 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 reference(): string {
return new URLSearchParams(window.location.search).get('ref') ?? window.location.hostname;
}
private get partnerqrID(): string {
return new URLSearchParams(window.location.search).get('id') ?? '';
}
private get fromSite(): string {
return new URLSearchParams(window.location.search).get('from') ?? '';
}
get isMobile(): boolean {
return window.innerWidth < 768;
}
constructor() {
this.loadSettings();
}
private loadSettings(): void {
// Fetch limits from /qr/settings. If the call fails, keep defaults.
const url = `${QR_VITANOVA_API}/qr/settings`;
this.http.get<SettingsResponse>(url).subscribe({
next: (s) => {
if (typeof s?.minAmount === 'number') this.minAmount.set(s.minAmount);
if (typeof s?.maxAmount === 'number') this.maxAmount.set(s.maxAmount);
}
});
}
createCheck(): void {
const val = this.amount();
if (!val || val <= 0) {
this.error.set('Введите корректную сумму');
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;
}
const partnerqrID = this.partnerqrID;
if (!partnerqrID) {
this.error.set(this.t('errors.lookup_failed'));
return;
}
@@ -60,44 +139,144 @@ export class CreatePage {
this.loading.set(true);
const headers: Record<string, string> = {};
if (this.sessionId) {
headers['Authorization'] = JSON.stringify({ sessionID: this.sessionId });
}
if (this.authKey) headers['authorization-key'] = this.authKey;
if (this.userId) headers['userid-value'] = this.userId;
this.http
.post<CreateFastcheckResponse>(
`${FASTCHECK_API}/fastcheck`,
{ amount: val, currency: this.currency() },
.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,
RedirectUrl: `https://fastcheck.store?id=fast-c202-4062-bcfb-8b4c8cc59adc`
},
{ headers }
)
.subscribe({
next: (res) => {
this.loading.set(false);
if (res?.fastcheck) {
this.store.setCreated({
fastcheck: res.fastcheck,
code: res.code,
amount: val,
expiration: res.expiration
});
this.router.navigate(['/']);
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) {
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.error.set(this.t('errors.payment_failed'));
}
},
error: () => {
error: (err) => {
this.loading.set(false);
this.error.set('Ошибка при создании платежа. Попробуйте ещё раз.');
const msg: string | undefined = err?.error?.message;
this.error.set(msg ?? this.t('errors.lookup_failed'));
}
});
}
onAmountChange(value: number): void {
this.amount.set(value);
if (value > 0) this.error.set('');
private startPolling(qrId: string): void {
this.stopPolling();
this.qrPolling.set(true);
this.pollHandle = setInterval(() => {
this.http.get<QrStatusResponse>(`${QR_VITANOVA_API}/qr/dynamic/${encodeURIComponent(this.partnerqrID)}/${qrId}`)
.subscribe({
next: (res) => {
const st = res?.status ?? '';
this.qrStatus.set(st);
if (st === 'COMPLETED' || st === 'APPROVED') {
this.handlePaymentSuccess(res);
} else if (st === 'REJECTED') {
this.stopPolling();
this.error.set(this.t('errors.payment_failed'));
this.qrImageUrl.set(null);
}
// REGISTERED / NEW / '' — keep polling
},
error: () => {
this.closeQr();
this.error.set('оплата не прошла');
}
});
}, 5000);
}
private stopPolling(): void {
if (this.pollHandle !== null) {
clearInterval(this.pollHandle);
this.pollHandle = null;
}
this.qrPolling.set(false);
}
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);
}
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 {
this.stopPolling();
this.qrImageUrl.set(null);
this.qrStatus.set('');
this.paymentDone.set(false);
}
}

View File

@@ -1,157 +0,0 @@
<div class="page">
<div class="card">
<div class="card__header">
<img class="card__brand" src="/logo_big.png"
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 class="card__body">
<!-- Fastcheck number + new -->
<div class="field">
<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">
<input
id="fcNumber"
type="text"
class="input"
[ngModel]="fastcheckNumber()"
(ngModelChange)="onNumberChange($event)"
placeholder="1234-5678-0001"
inputmode="numeric"
autocomplete="off"
maxlength="14"
/>
<a class="btn btn--ghost" routerLink="/new" aria-label="Создать новый fastCHECK">Новый</a>
</div>
</div>
<!-- Amount -->
<div class="field">
<label class="field__label" for="fcAmount">Сумма</label>
<div class="input-wrap">
<span class="input-wrap__prefix"></span>
<input
id="fcAmount"
type="number"
class="input-wrap__input"
[ngModel]="fastcheckAmount()"
(ngModelChange)="onAmountChange($event)"
min="1"
step="1"
inputmode="numeric"
placeholder="0"
[readonly]="amountLoading() || fastcheckAmount() !== null"
/>
</div>
@if (amountLoading()) {
<span class="field__hint">Проверяем…</span>
}
</div>
<!-- Code -->
<div class="field">
<label class="field__label" for="fcCode">Код</label>
<input
id="fcCode"
type="text"
class="input"
[ngModel]="fastcheckCode()"
(ngModelChange)="onCodeChange($event)"
placeholder="00000"
inputmode="numeric"
maxlength="5"
autocomplete="one-time-code"
/>
@if (error()) {
<span class="field__error">{{ error() }}</span>
}
</div>
<button class="pay-btn" type="button" (click)="pay()" [disabled]="!canPay()">
<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">
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
<line x1="1" y1="10" x2="23" y2="10" />
</svg>
</span>
Оплатить
</button>
</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>
Защищённое соединение
</span>
</div>
</div>
</div>
<!-- Telegram sign-in popup -->
@if (popupOpen()) {
<div class="modal" (click)="closePopup()">
<div class="modal__card" (click)="$event.stopPropagation()">
<button class="modal__close" type="button" (click)="closePopup()" aria-label="Закрыть">×</button>
@if (paid()) {
<div class="modal__success">
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="#16a34a"
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6L9 17l-5-5" />
</svg>
<h2 class="modal__title">Оплачено</h2>
<p class="modal__sub">
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
успешно принят.
</p>
</div>
} @else {
<img class="brand-logo brand-logo--small" src="/logo_small.png"
alt="fastCHECK" width="32" height="32" />
<h2 class="modal__title">Войти через Telegram</h2>
<p class="modal__sub">Отсканируйте QR или откройте ссылку</p>
<div class="qr">
@if (popupLoading() && !webSessionId()) {
<div class="qr__placeholder">Загрузка…</div>
} @else if (webSessionId()) {
<img [src]="qrUrl()" width="240" height="240" alt="QR Telegram" />
}
</div>
@if (webSessionId()) {
<a class="tg-link" [href]="telegramLink()" target="_blank" rel="noopener">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M9.04 15.65l-.36 4.06c.51 0 .73-.22.99-.48l2.38-2.27 4.93 3.6c.9.5 1.55.24 1.79-.83l3.24-15.18h.01c.29-1.34-.48-1.86-1.36-1.54L1.13 9.66c-1.32.5-1.3 1.23-.22 1.56l4.92 1.53L17.27 5.6c.54-.34 1.03-.15.62.19" />
</svg>
Открыть в Telegram
</a>
}
@if (popupLoading() && webSessionId()) {
<p class="modal__hint">Подтверждение оплаты…</p>
} @else if (webSessionId()) {
<p class="modal__hint">Ожидание входа…</p>
}
@if (popupError()) {
<p class="modal__error">{{ popupError() }}</p>
}
}
</div>
</div>
}

View File

@@ -1,220 +0,0 @@
@use './../../../shared' as *;
.row {
display: flex;
gap: 8px;
align-items: stretch;
.input { flex: 1; min-width: 0; }
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 16px;
height: 48px;
min-width: 64px;
border-radius: 12px;
font-size: 14px;
font-weight: 700;
text-decoration: none;
border: 2px solid transparent;
cursor: pointer;
font-family: inherit;
white-space: nowrap;
transition: opacity .15s, transform .1s, background .15s;
-webkit-appearance: none;
&--ghost {
background: #f1f5f9;
color: #2563eb;
border-color: #e2e8f0;
&:hover { background: #e2e8f0; }
&:active { transform: scale(.97); }
}
}
.input {
width: 100%;
border: 2px solid #e2e8f0;
border-radius: 12px;
background: #f8fafc;
padding: 0 14px;
height: 48px;
font-size: 16px;
font-weight: 600;
color: #0f172a;
font-family: inherit;
outline: none;
transition: border-color .2s, box-shadow .2s, background .2s;
&::placeholder { color: #cbd5e1; font-weight: 500; }
&:focus {
border-color: #2563eb;
box-shadow: 0 0 0 4px rgba(37,99,235,.12);
background: #fff;
}
}
// ─── Modal (Telegram QR popup) ──────────────────────────────────────────────
.modal {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(15, 23, 42, .55);
backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
animation: fade-in .15s ease-out;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
@media (max-width: 480px) {
align-items: stretch;
padding: 0;
}
&__card {
position: relative;
background: #fff;
border-radius: 24px;
width: 100%;
max-width: 360px;
padding: 28px 24px 24px;
text-align: center;
box-shadow: 0 24px 60px rgba(0,0,0,.25);
animation: pop-in .2s ease-out;
margin: auto;
@media (max-width: 480px) {
max-width: 100%;
border-radius: 0;
box-shadow: none;
padding: calc(28px + env(safe-area-inset-top)) 20px calc(28px + env(safe-area-inset-bottom));
margin: 0;
min-height: 100dvh;
display: flex;
flex-direction: column;
justify-content: center;
}
}
&__close {
position: absolute;
top: 8px;
right: 8px;
width: 44px;
height: 44px;
border-radius: 50%;
border: none;
background: #f1f5f9;
color: #475569;
font-size: 24px;
line-height: 1;
cursor: pointer;
font-family: inherit;
transition: background .15s;
-webkit-appearance: none;
&:hover { background: #e2e8f0; }
}
&__title {
font-size: 20px;
font-weight: 700;
color: #0f172a;
margin: 4px 0 6px;
}
&__sub {
font-size: 14px;
color: #64748b;
margin: 0 0 18px;
}
&__hint {
font-size: 13px;
color: #94a3b8;
margin: 14px 0 0;
}
&__error {
font-size: 13px;
color: #ef4444;
font-weight: 500;
margin: 12px 0 0;
}
&__success {
padding: 12px 0 4px;
svg { display: block; margin: 0 auto 10px; }
}
}
.qr {
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 16px;
padding: 12px;
width: 264px;
height: 264px;
max-width: 100%;
margin: 0 auto;
@media (max-width: 380px) {
width: min(264px, 70vw);
height: auto;
aspect-ratio: 1;
}
&__placeholder {
color: #94a3b8;
font-size: 14px;
}
img {
width: 100%;
height: auto;
max-width: 240px;
display: block;
}
}
.tg-link {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 16px;
padding: 14px 22px;
min-height: 48px;
border-radius: 12px;
background: #229ED9;
color: #fff;
font-size: 15px;
font-weight: 700;
text-decoration: none;
transition: opacity .15s;
&:hover { opacity: .9; }
&:active { transform: scale(.97); }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes pop-in {
from { transform: translateY(12px) scale(.98); opacity: 0; }
to { transform: translateY(0) scale(1); opacity: 1; }
}

View File

@@ -1,241 +0,0 @@
import { Component, computed, 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 } from '../../api';
interface WebSessionResponse {
sessionId: string;
userId: string;
expires: string;
userSessionId: string;
Status: boolean;
}
interface CheckFastcheckResponse {
fastcheck: string;
amount?: number;
expiration: string;
Status: boolean;
}
@Component({
selector: 'app-fastcheck-page',
imports: [FormsModule, RouterLink],
templateUrl: './fastcheck-page.html',
styleUrl: './fastcheck-page.scss'
})
export class FastcheckPage {
private http = inject(HttpClient);
private store = inject(FastcheckService);
private router = inject(Router);
// Telegram bot used for the sign-in deep link.
private readonly telegramBot = 'DexarSupport_bot';
fastcheckNumber = signal<string>('');
fastcheckAmount = signal<number | null>(null);
fastcheckCode = signal<string>('');
error = signal<string>('');
amountLoading = signal<boolean>(false);
popupOpen = signal<boolean>(false);
popupLoading = signal<boolean>(false);
popupError = signal<string>('');
webSessionId = signal<string>('');
paid = signal<boolean>(false);
private pollHandle: ReturnType<typeof setInterval> | null = null;
private lastLookedUpNumber = '';
canPay = computed(() => {
const digits = this.fastcheckNumber().replace(/\D/g, '');
const codeDigits = this.fastcheckCode().replace(/\D/g, '');
return digits.length === 12 && codeDigits.length === 5 && !this.amountLoading();
});
telegramLink = computed(() => {
const sid = this.webSessionId();
return sid
? `https://t.me/${this.telegramBot}?start=${encodeURIComponent(sid)}`
: `https://t.me/${this.telegramBot}`;
});
qrUrl = computed(() => {
const link = this.telegramLink();
return `https://api.qrserver.com/v1/create-qr-code/?size=240x240&margin=8&data=${encodeURIComponent(link)}`;
});
constructor() {
// Pull autofill data left over by the create page.
const created = this.store.consume();
if (created) {
this.fastcheckNumber.set(created.fastcheck);
this.fastcheckAmount.set(created.amount);
this.fastcheckCode.set(created.code);
}
}
pay(): void {
if (!this.canPay()) {
return;
}
this.error.set('');
this.openPopup();
}
private openPopup(): void {
this.popupOpen.set(true);
this.popupError.set('');
this.paid.set(false);
this.popupLoading.set(true);
this.http.get<WebSessionResponse>(`${FASTCHECK_API}/websession`).subscribe({
next: (res) => {
this.popupLoading.set(false);
this.webSessionId.set(res.sessionId);
this.startPolling(res.sessionId);
},
error: () => {
this.popupLoading.set(false);
this.popupError.set('Не удалось создать сессию. Попробуйте ещё раз.');
}
});
}
closePopup(): void {
this.popupOpen.set(false);
this.stopPolling();
if (this.webSessionId()) {
// Best-effort logout; ignore errors.
this.http
.request('DELETE', `${FASTCHECK_API}/websession/${this.webSessionId()}`, {
body: { sessionId: this.webSessionId() }
})
.subscribe({ error: () => undefined });
}
this.webSessionId.set('');
}
private startPolling(sessionId: string): void {
this.stopPolling();
this.pollHandle = setInterval(() => {
this.http
.get<WebSessionResponse>(`${FASTCHECK_API}/websession/${sessionId}`)
.subscribe({
next: (res) => {
if (res?.Status) {
this.stopPolling();
this.acceptFastcheck(sessionId);
}
},
error: () => undefined
});
}, 3000);
}
private stopPolling(): void {
if (this.pollHandle !== null) {
clearInterval(this.pollHandle);
this.pollHandle = null;
}
}
private acceptFastcheck(sessionId: string): void {
this.popupLoading.set(true);
this.http
.post(
`${FASTCHECK_API}/fastcheck`,
{ fastcheck: this.fastcheckNumber().trim(), code: this.fastcheckCode().trim() },
{ headers: { Authorization: JSON.stringify({ sessionID: sessionId }) } }
)
.subscribe({
next: () => {
this.popupLoading.set(false);
this.paid.set(true);
// Fire-and-forget merchant callback if a return_url is on the page.
this.fireMerchantCallback();
},
error: () => {
this.popupLoading.set(false);
this.popupError.set('Не удалось принять платёж.');
}
});
}
private fireMerchantCallback(): void {
const params = new URLSearchParams(window.location.search);
const returnUrl = params.get('return_url');
if (returnUrl) {
setTimeout(() => {
window.location.href = `${returnUrl}${returnUrl.includes('?') ? '&' : '?'}fastcheck=${encodeURIComponent(
this.fastcheckNumber()
)}&status=ok`;
}, 1500);
}
}
onAmountChange(value: number | null): void {
this.fastcheckAmount.set(value);
}
/** Mask fastcheck number as XXXX-XXXX-XXXX, allow only digits. */
onNumberChange(raw: string): void {
const digits = (raw ?? '').replace(/\D/g, '').slice(0, 12);
const groups: string[] = [];
for (let i = 0; i < digits.length; i += 4) {
groups.push(digits.slice(i, i + 4));
}
const masked = groups.join('-');
this.fastcheckNumber.set(masked);
this.error.set('');
// If number became incomplete, drop the previously fetched amount so the
// user doesn't see a stale value tied to a different (now-edited) number.
if (digits.length < 12 && this.lastLookedUpNumber) {
this.fastcheckAmount.set(null);
this.lastLookedUpNumber = '';
}
// Auto-lookup when 12 digits are entered (and we haven't already looked it up).
if (digits.length === 12 && masked !== this.lastLookedUpNumber) {
this.lookupFastcheck(masked);
}
}
/** Allow only digits, max 5, in the code field. */
onCodeChange(raw: string): void {
const digits = (raw ?? '').replace(/\D/g, '').slice(0, 5);
this.fastcheckCode.set(digits);
this.error.set('');
}
private lookupFastcheck(number: string): void {
this.lastLookedUpNumber = number;
this.amountLoading.set(true);
this.fastcheckAmount.set(null);
// GET /fastcheck — body in GET is non-standard; many HTTP libs strip it.
// The backend should accept ?fastcheck= as a query param too. We send both.
this.http
.request<CheckFastcheckResponse>('GET', `${FASTCHECK_API}/fastcheck`, {
body: { fastcheck: number },
params: { fastcheck: number }
})
.subscribe({
next: (res) => {
this.amountLoading.set(false);
if (res?.Status && typeof res.amount === 'number') {
this.fastcheckAmount.set(res.amount);
} else if (res?.Status === false) {
this.error.set('Платёж не найден или просрочен.');
}
},
error: () => {
this.amountLoading.set(false);
this.error.set('Не удалось проверить номер. Попробуйте ещё раз.');
this.lastLookedUpNumber = '';
}
});
}
}

View File

@@ -1,78 +0,0 @@
<div class="page">
<div class="card">
<div class="card__header">
<div class="sbp-logo">
<img src="https://sbp.nspk.ru/storage/settings/common/logo/0645d335-8b62-43a1-9a33-0d4c9d1dc0e0.svg"
alt="СБП" />
</div>
<h1 class="card__title">Оплата через СБП</h1>
<p class="card__subtitle">Система быстрых платежей</p>
</div>
<div class="card__body">
<div class="field">
<label class="field__label" for="amount">Сумма платежа</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="1"
step="1"
inputmode="numeric"
placeholder="0"
autofocus
/>
</div>
@if (error()) {
<span class="field__error">{{ error() }}</span>
}
</div>
<div class="currency-badge">
<span class="currency-badge__flag">🇷🇺</span>
<span class="currency-badge__code">RUB</span>
<span class="currency-badge__name">Российский рубль</span>
</div>
<div class="field">
<label class="field__label" for="note">Примечание</label>
<textarea
id="note"
class="note-input"
[ngModel]="note()"
(ngModelChange)="onNoteChange($event)"
placeholder="Причина платежа..."
rows="3"
maxlength="500"
></textarea>
</div>
<button class="pay-btn" type="button" (click)="pay()" [disabled]="loading()">
<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">
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
<line x1="1" y1="10" x2="23" y2="10" />
</svg>
</span>
{{ loading() ? 'Подождите...' : 'Перейти к оплате' }}
</button>
</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>
Защищённое соединение
</span>
</div>
</div>
</div>

View File

@@ -1,60 +0,0 @@
@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;
}
}

View File

@@ -1,91 +0,0 @@
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=<orderId>`.
* Mirrors public/payment.html behaviour:
* POST https://qr.vitanova.network:567/qr
* { payment, amount, currency, id, note } -> { payload: '<sbp-deep-link>' }
* 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<number | null>(null);
note = signal<string>('');
error = signal<string>('');
loading = signal<boolean>(false);
paymentId = signal<string>('');
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<LegacyPayResponse>(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('Ошибка при создании платежа. Попробуйте ещё раз.');
}
});
}
}

View File

@@ -0,0 +1,26 @@
<div class="info-page">
<div class="info-page__hero">
<h1 class="info-page__title">{{ 'partners.title' | translate }}</h1>
<p class="info-page__lead">{{ 'partners.lead' | translate }}</p>
</div>
<div class="partners-grid">
@for (p of partners; track p.name) {
<div class="partner-card">
<div class="partner-card__logo">{{ p.logo }}</div>
<div class="partner-card__body">
<span class="partner-card__cat">{{ p.category | translate }}</span>
<h3 class="partner-card__name">{{ p.name }}</h3>
<p class="partner-card__city">📍 {{ p.city }}</p>
<p class="partner-card__desc">{{ p.desc | translate }}</p>
</div>
</div>
}
</div>
<div class="partners-cta">
<h2 class="partners-cta__title">{{ 'partners.cta_title' | translate }}</h2>
<p class="partners-cta__text">{{ 'partners.cta_text' | translate }}</p>
<a class="partners-cta__btn" routerLink="/contacts">{{ 'partners.cta_btn' | translate }}</a>
</div>
</div>

View File

@@ -0,0 +1,146 @@
:host {
display: block;
background: #f8fafc;
min-height: 100vh;
}
.info-page {
max-width: 900px;
margin: 0 auto;
padding: 48px 24px 72px;
@media (max-width: 600px) {
padding: 32px 16px 56px;
}
&__hero {
margin-bottom: 40px;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 32px;
}
&__title {
font-size: 32px;
font-weight: 800;
color: #0f172a;
margin: 0 0 12px;
letter-spacing: -0.5px;
@media (max-width: 600px) { font-size: 26px; }
}
&__lead {
font-size: 17px;
line-height: 1.7;
color: #475569;
margin: 0;
}
}
.partners-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 20px;
margin-bottom: 56px;
}
.partner-card {
display: flex;
gap: 16px;
padding: 22px 20px;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 16px;
transition: border-color 0.15s, box-shadow 0.15s;
&:hover {
border-color: #93c5fd;
box-shadow: 0 4px 16px rgba(30, 64, 175, 0.08);
}
&__logo {
font-size: 36px;
line-height: 1;
flex-shrink: 0;
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
background: #f1f5f9;
border-radius: 12px;
}
&__body {
display: flex;
flex-direction: column;
gap: 4px;
}
&__cat {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #1e40af;
}
&__name {
font-size: 16px;
font-weight: 700;
color: #0f172a;
margin: 0;
}
&__city {
font-size: 13px;
color: #64748b;
margin: 0;
}
&__desc {
font-size: 13.5px;
line-height: 1.6;
color: #475569;
margin: 4px 0 0;
}
}
.partners-cta {
text-align: center;
padding: 40px 24px;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
border-radius: 20px;
border: 1px solid #bfdbfe;
&__title {
font-size: 22px;
font-weight: 800;
color: #1e3a8a;
margin: 0 0 12px;
}
&__text {
font-size: 15px;
line-height: 1.7;
color: #3b5998;
margin: 0 0 24px;
max-width: 480px;
margin-inline: auto;
margin-bottom: 24px;
}
&__btn {
display: inline-block;
padding: 12px 28px;
background: #1e40af;
color: #fff;
border-radius: 10px;
font-size: 15px;
font-weight: 700;
text-decoration: none;
transition: background 0.15s;
&:hover { background: #1d3a9f; }
}
}

View File

@@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { TranslatePipe } from '../../translate/translate.pipe';
interface Partner {
name: string;
category: string;
city: string;
logo: string; // emoji placeholder until real logos are provided
desc: string;
}
@Component({
selector: 'app-partners-page',
imports: [RouterLink, TranslatePipe],
templateUrl: './partners-page.html',
styleUrl: './partners-page.scss'
})
export class PartnersPage {
partners: Partner[] = [
{ name: 'Vitanova Exchange', category: 'partners.cat_finance', city: 'Ереван', logo: '🏦', desc: 'partners.p1_desc' },
{ name: 'ForEx.am', category: 'partners.cat_finance', city: 'Ереван', logo: '💱', desc: 'partners.p2_desc' },
{ name: 'Dexar Market', category: 'partners.cat_retail', city: 'Москва', logo: '🛒', desc: 'partners.p3_desc' },
{ name: 'City Hotel Yerevan', category: 'partners.cat_hotels', city: 'Ереван', logo: '🏨', desc: 'partners.p4_desc' },
];
}

View File

@@ -0,0 +1,57 @@
<footer class="site-footer">
<div class="site-footer__inner">
<!-- Brand + about -->
<div class="site-footer__col site-footer__col--brand">
<a class="site-footer__brand" href="/">
<!-- <img src="/logo_big.png" alt="fastCHECK" width="28" height="28" /> -->
<span class="site-footer__wordmark">
<span class="wm-fast">fast</span><span class="wm-check">CHECK</span>
</span>
</a>
<p class="site-footer__desc" id="about">{{ 'footer.desc' | translate }}</p>
</div>
<!-- Contacts -->
<div class="site-footer__col" id="contacts">
<h3 class="site-footer__heading">{{ 'footer.contacts_heading' | translate }}</h3>
<ul class="site-footer__list">
<li>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.07 10.5a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21 16.92z"/></svg>
<a href="tel:+79299037443">+7 (929) 903-74-43</a> <span class="site-footer__note">{{ 'footer.russia' | translate }}</span>
</li>
<li>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.07 10.5a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21 16.92z"/></svg>
<a href="tel:+37498632421">+374 98 632421</a> <span class="site-footer__note">{{ 'footer.armenia' | translate }}</span>
</li>
<li>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
<a href="mailto:info@viaexport.store">info@viaexport.store</a>
</li>
</ul>
<div class="site-footer__hours">
<p><strong>{{ 'footer.support_label' | translate }}:</strong> {{ 'footer.support_hours' | translate }}</p>
<p><strong>{{ 'footer.questions_label' | translate }}:</strong> {{ 'footer.questions_hours' | translate }}</p>
</div>
</div>
<!-- Legal -->
<div class="site-footer__col">
<h3 class="site-footer__heading">{{ 'footer.legal_heading' | translate }}</h3>
<ul class="site-footer__list site-footer__list--legal">
<li>{{ 'footer.legal_company' | translate }}</li>
<li>{{ 'footer.legal_inn_ru' | translate }}</li>
<li>{{ 'footer.legal_inn_am' | translate }}</li>
<li>{{ 'footer.legal_kpp' | translate }}</li>
<li>{{ 'footer.legal_ogrn' | translate }}</li>
<li class="site-footer__address">{{ 'footer.legal_address' | translate }}</li>
</ul>
</div>
</div>
<div class="site-footer__bottom">
<p>© {{ year }} {{ 'footer.rights' | translate }}</p>
<!-- <p>{{ 'footer.director' | translate }}</p> -->
</div>
</footer>

View File

@@ -0,0 +1,156 @@
:host { display: block; }
.site-footer {
background: #0f172a;
color: #94a3b8;
&__inner {
max-width: 1100px;
margin: 0 auto;
padding: 48px 24px 32px;
display: grid;
grid-template-columns: 2fr 1fr 1fr;
gap: 40px;
@media (max-width: 860px) {
grid-template-columns: 1fr 1fr;
}
@media (max-width: 560px) {
grid-template-columns: 1fr;
gap: 32px;
padding: 36px 20px 24px;
}
}
&__col {
&--brand {
@media (max-width: 860px) {
grid-column: 1 / -1;
}
}
}
&__brand {
display: inline-flex;
align-items: center;
gap: 10px;
text-decoration: none;
margin-bottom: 14px;
img {
width: 28px;
height: 28px;
object-fit: contain;
filter: brightness(0) invert(1);
opacity: 0.9;
}
}
&__wordmark {
font-size: 18px;
letter-spacing: -0.02em;
line-height: 1;
}
&__desc {
font-size: 13.5px;
line-height: 1.65;
color: #64748b;
max-width: 380px;
}
&__heading {
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
color: #e2e8f0;
margin-bottom: 16px;
}
&__list {
list-style: none;
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 18px;
li {
display: flex;
align-items: center;
gap: 8px;
font-size: 13.5px;
svg { flex-shrink: 0; opacity: 0.5; }
}
a {
color: #94a3b8;
text-decoration: none;
transition: color 0.15s;
&:hover { color: #e2e8f0; }
}
&--legal {
li {
display: block;
font-size: 12.5px;
color: #64748b;
gap: 0;
}
}
}
&__note {
font-size: 11px;
color: #475569;
margin-left: 4px;
}
&__hours {
font-size: 12.5px;
color: #64748b;
line-height: 1.7;
}
&__address {
color: #475569;
font-size: 12px !important;
line-height: 1.5;
margin-top: 4px;
}
&__bottom {
border-top: 1px solid #1e293b;
max-width: 1100px;
margin: 0 auto;
padding: 16px 24px;
display: flex;
flex-wrap: wrap;
gap: 6px 24px;
justify-content: space-between;
font-size: 12px;
color: #475569;
@media (max-width: 560px) {
flex-direction: column;
padding: 14px 20px;
}
}
}
.wm-fast {
font-weight: 400;
font-size: 0.72em;
color: #64748b;
margin-right: 0.04em;
}
.wm-check {
font-weight: 700;
font-size: 1em;
color: #93c5fd;
text-transform: uppercase;
letter-spacing: 0.03em;
}

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
import { TranslatePipe } from '../translate/translate.pipe';
@Component({
selector: 'app-site-footer',
imports: [TranslatePipe],
templateUrl: './site-footer.html',
styleUrl: './site-footer.scss'
})
export class SiteFooter {
year = new Date().getFullYear();
}

View File

@@ -0,0 +1,95 @@
<header class="site-header">
<div class="site-header__inner">
<!-- Brand -->
<a class="site-header__brand" routerLink="/" (click)="closeMenu()">
<img src="/logo_small.png" alt="SBP Pay" width="22" height="22" />
</a>
<!-- Desktop nav -->
<nav class="site-header__nav" [attr.aria-label]="'header.aria_nav' | translate">
<a class="site-header__link" routerLink="/about">{{ 'header.nav_about' | translate }}</a>
<a class="site-header__link" routerLink="/partners">{{ 'header.nav_partners' | translate }}</a>
<a class="site-header__link" routerLink="/contacts">{{ 'header.nav_contacts' | translate }}</a>
<a class="site-header__link" href="mailto:info@viaexport.store">{{ 'header.nav_support' | translate }}</a>
</nav>
<!-- Language dropdown -->
<div class="lang-select" [class.lang-select--open]="langOpen()">
<button type="button" class="lang-select__trigger" (click)="toggleLang()">
<img class="lang-select__flag" [src]="activeLang.flag" [alt]="activeLang.label" width="20" height="20" />
<span class="lang-select__code">{{ activeLang.code | uppercase }}</span>
<svg class="lang-select__chevron" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
@if (langOpen()) {
<div class="lang-select__dropdown">
@for (lang of langs; track lang.code) {
<button type="button" class="lang-select__option"
[class.lang-select__option--active]="currentLang() === lang.code"
(click)="setLang(lang.code)">
<img class="lang-select__flag" [src]="lang.flag" [alt]="lang.label" width="20" height="20" />
<span class="lang-select__name">{{ lang.label }}</span>
@if (currentLang() === lang.code) {
<svg class="lang-select__check" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
<path d="M20 6L9 17l-5-5"/>
</svg>
}
</button>
}
</div>
}
</div>
<!-- Mobile hamburger -->
<button class="site-header__burger" type="button"
[attr.aria-expanded]="menuOpen()"
[attr.aria-label]="'header.aria_burger' | translate"
(click)="toggleMenu()">
@if (menuOpen()) {
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2.5" stroke-linecap="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
} @else {
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2.5" stroke-linecap="round">
<line x1="3" y1="7" x2="21" y2="7"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="17" x2="21" y2="17"/>
</svg>
}
</button>
</div>
<!-- Mobile overlay + drawer -->
@if (menuOpen()) {
<div class="mobile-overlay" (click)="closeMenu()">
<nav class="mobile-panel" (click)="$event.stopPropagation()" [attr.aria-label]="'header.aria_menu' | translate">
<div class="mobile-panel__header">
<span class="mobile-panel__title">SBP Pay</span>
<button type="button" class="mobile-panel__close" (click)="closeMenu()" [attr.aria-label]="'header.aria_close' | translate">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<a class="mobile-panel__link" routerLink="/about" (click)="closeMenu()">{{ 'header.nav_about' | translate }}</a>
<a class="mobile-panel__link" routerLink="/partners" (click)="closeMenu()">{{ 'header.nav_partners' | translate }}</a>
<a class="mobile-panel__link" routerLink="/contacts" (click)="closeMenu()">{{ 'header.nav_contacts' | translate }}</a>
<a class="mobile-panel__link" href="mailto:info@viaexport.store" (click)="closeMenu()">{{ 'header.nav_support' | translate }}</a>
<div class="mobile-panel__langs">
@for (lang of langs; track lang.code) {
<button type="button" class="site-header__lang"
[class.site-header__lang--active]="currentLang() === lang.code"
(click)="setLang(lang.code); closeMenu()">
<img [src]="lang.flag" [alt]="lang.label" width="20" height="20" />
<span>{{ lang.code | uppercase }}</span>
</button>
}
</div>
</nav>
</div>
}
</header>

View File

@@ -0,0 +1,324 @@
:host {
display: block;
position: sticky;
top: 0;
z-index: 900;
}
.site-header {
background: #fff;
border-bottom: 1px solid #e2e8f0;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.06);
&__inner {
max-width: 1100px;
margin: 0 auto;
padding: 0 24px;
height: 60px;
display: flex;
align-items: center;
gap: 32px;
@media (max-width: 600px) {
padding: 0 16px;
}
}
&__brand {
display: inline-flex;
align-items: center;
gap: 10px;
text-decoration: none;
flex-shrink: 0;
img {
width: 22px;
height: 22px;
object-fit: contain;
}
}
&__wordmark {
font-size: 18px;
letter-spacing: -0.02em;
white-space: nowrap;
line-height: 1;
}
&__nav {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
@media (max-width: 600px) {
display: none;
}
}
&__link {
padding: 8px 14px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
color: #475569;
text-decoration: none;
transition: background 0.15s, color 0.15s;
&:hover {
background: #f1f5f9;
color: #0f172a;
}
}
&__lang {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 6px;
border: none;
background: transparent;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
color: #94a3b8;
cursor: pointer;
transition: background 0.15s, color 0.15s;
font-family: inherit;
&:hover { background: #f1f5f9; color: #475569; }
&--active {
background: #eff6ff;
color: #1e40af;
}
}
&__mobile-langs {
display: flex;
gap: 4px;
padding: 8px 14px 4px;
border-top: 1px solid #f1f5f9;
margin-top: 4px;
}
&__burger {
display: none;
margin-left: auto;
width: 40px;
height: 40px;
border-radius: 8px;
border: none;
background: transparent;
color: #475569;
cursor: pointer;
align-items: center;
justify-content: center;
transition: background 0.15s;
-webkit-appearance: none;
font-family: inherit;
&:hover { background: #f1f5f9; }
@media (max-width: 600px) {
display: inline-flex;
}
}
&__mobile-menu { display: none; } // replaced by .mobile-overlay / .mobile-panel
&__mobile-link { display: none; }
}
// Wordmark colours
.wm-fast {
font-weight: 400;
font-size: 0.72em;
color: #64748b;
margin-right: 0.04em;
}
.wm-check {
font-weight: 700;
font-size: 1em;
color: #1e40af;
text-transform: uppercase;
letter-spacing: 0.03em;
}
// Language dropdown
.lang-select {
position: relative;
flex-shrink: 0;
@media (max-width: 600px) {
display: none;
}
&__trigger {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 10px;
border-radius: 8px;
border: 1px solid #e2e8f0;
background: #fff;
font-size: 13px;
font-weight: 600;
color: #334155;
cursor: pointer;
font-family: inherit;
transition: border-color 0.15s, background 0.15s;
white-space: nowrap;
&:hover { background: #f8fafc; border-color: #cbd5e1; }
}
&--open &__trigger {
background: #f8fafc;
border-color: #94a3b8;
}
&__flag { width: 20px; height: 20px; object-fit: cover; border-radius: 2px; flex-shrink: 0; }
&__code { font-size: 12px; font-weight: 700; letter-spacing: 0.05em; }
&__chevron {
color: #94a3b8;
transition: transform 0.2s;
}
&--open &__chevron { transform: rotate(180deg); }
&__dropdown {
position: absolute;
top: calc(100% + 6px);
right: 0;
min-width: 160px;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
overflow: hidden;
z-index: 1000;
animation: dropdown-in 0.12s ease;
}
&__option {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 11px 14px;
border: none;
background: transparent;
font-family: inherit;
font-size: 14px;
font-weight: 500;
color: #334155;
cursor: pointer;
text-align: left;
transition: background 0.12s;
&:hover { background: #f8fafc; }
&--active { color: #1e40af; background: #eff6ff; }
}
&__name { flex: 1; }
&__check { color: #1e40af; margin-left: auto; flex-shrink: 0; }
}
@keyframes dropdown-in {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
// ── Mobile overlay + drawer ──────────────────────────────────────
.mobile-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 998;
animation: overlay-in 0.2s ease;
}
.mobile-panel {
position: fixed;
top: 0;
right: 0;
height: 100%;
width: min(300px, 85vw);
background: #fff;
z-index: 999;
display: flex;
flex-direction: column;
gap: 2px;
overflow-y: auto;
animation: panel-in 0.22s cubic-bezier(0.4, 0, 0.2, 1);
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 20px 16px;
border-bottom: 1px solid #e2e8f0;
flex-shrink: 0;
}
&__title {
font-size: 16px;
font-weight: 700;
color: #1e40af;
letter-spacing: 0.02em;
}
&__close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
border: none;
background: transparent;
color: #64748b;
cursor: pointer;
transition: background 0.15s, color 0.15s;
font-family: inherit;
&:hover { background: #f1f5f9; color: #0f172a; }
}
&__link {
display: block;
padding: 14px 20px;
font-size: 15px;
font-weight: 500;
color: #0f172a;
text-decoration: none;
transition: background 0.12s;
border-radius: 0;
&:hover { background: #f8fafc; }
}
&__langs {
display: flex;
gap: 6px;
padding: 12px 20px 16px;
border-top: 1px solid #f1f5f9;
margin-top: auto;
}
}
@keyframes overlay-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes panel-in {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}

View File

@@ -0,0 +1,48 @@
import { UpperCasePipe } from '@angular/common';
import { Component, HostListener, inject, signal } from '@angular/core';
import { RouterLink } from '@angular/router';
import { TranslatePipe } from '../translate/translate.pipe';
import { TranslationService, Lang } from '../translate/translation.service';
interface LangOption { code: Lang; label: string; flag: string; }
@Component({
selector: 'app-site-header',
imports: [RouterLink, TranslatePipe, UpperCasePipe],
templateUrl: './site-header.html',
styleUrl: './site-header.scss'
})
export class SiteHeader {
private i18n = inject(TranslationService);
menuOpen = signal(false);
langOpen = signal(false);
currentLang = this.i18n.currentLang;
langs: LangOption[] = [
{ code: 'ru', label: 'Русский', flag: '/flags/ru.svg' },
{ code: 'en', label: 'English', flag: '/flags/en.svg' },
{ code: 'hy', label: 'Հայերեն', flag: '/flags/arm.svg' },
];
get activeLang(): LangOption {
return this.langs.find(l => l.code === this.currentLang()) ?? this.langs[0];
}
toggleMenu(): void { this.menuOpen.update(v => !v); }
closeMenu(): void { this.menuOpen.set(false); }
toggleLang(): void { this.langOpen.update(v => !v); }
closeLang(): void { this.langOpen.set(false); }
setLang(lang: Lang): void {
this.i18n.setLanguage(lang);
this.langOpen.set(false);
}
@HostListener('document:click', ['$event.target'])
onDocClick(target: EventTarget | null): void {
if (!(target instanceof HTMLElement) || !target.closest('.lang-select')) {
this.langOpen.set(false);
}
}
}

View File

@@ -0,0 +1,11 @@
import { Pipe, PipeTransform, inject } from '@angular/core';
import { TranslationService } from './translation.service';
@Pipe({ name: 'translate', pure: false, standalone: true })
export class TranslatePipe implements PipeTransform {
private svc = inject(TranslationService);
transform(key: string): string {
return this.svc.translate(key);
}
}

View File

@@ -0,0 +1,36 @@
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export type Lang = 'ru' | 'en' | 'hy';
type Translations = Record<string, Record<string, string>>;
@Injectable({ providedIn: 'root' })
export class TranslationService {
private http = inject(HttpClient);
currentLang = signal<Lang>('ru');
private translations = signal<Translations>({});
constructor() {
this.load('ru');
}
setLanguage(lang: Lang): void {
this.currentLang.set(lang);
this.load(lang);
}
private load(lang: Lang): void {
this.http.get<Translations>(`/i18n/${lang}.json`).subscribe({
next: data => this.translations.set(data),
});
}
translate(key: string): string {
const dot = key.indexOf('.');
if (dot === -1) return key;
const section = key.slice(0, dot);
const k = key.slice(dot + 1);
return this.translations()[section]?.[k] ?? key;
}
}

View File

@@ -2,7 +2,7 @@
<html lang="ru">
<head>
<meta charset="utf-8">
<title>fastCHECK</title>
<title>QR Vitanova</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#2563eb">

View File

@@ -2,7 +2,7 @@
// Imported via @use './../../../shared' as *;
.page {
min-height: 100dvh;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
@@ -29,7 +29,7 @@
border-radius: 0;
max-width: 100%;
box-shadow: none;
min-height: 100dvh;
flex: 1;
display: flex;
flex-direction: column;
}

View File

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